{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "code-block", "type": "registry:component", "title": "Code Block", "description": "A line-numbered listing and a unified diff.", "registryDependencies": [ "https://www.beautifului.dev/r/foundation.json" ], "files": [ { "path": "components/primitives/CodeBlock.tsx", "content": "\"use client\";\n\nimport { useCallback, useState, type ReactNode } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * CODE BLOCK\n * A light editor panel with two versions (switch in the card):\n * · Code — a line-numbered listing\n * · Diff — a unified diff: old/new gutters, a green/red accent\n * bar and row tint, plus word-level add/del highlights.\n * Both share syntax coloring, insets, and wrapping behavior.\n * ───────────────────────────────────────────────────────── */\n\nconst FILE = \"churn.ts\";\n\nconst CODE_LINES = [\n \"export async function churnBatch() {\",\n ' const flavor = await getFlavor(\"pistachio\");',\n \" const base = await dairy.fetch({ flavor });\",\n ' await freezer.store(base, { temp: \"-16C\" });',\n \" if (!base.approved) return null;\",\n \" return base.gallons;\",\n \"}\",\n];\n\n/* A single run of code within a diff row; `change` tints it as an add/del. */\nexport type CodePiece = { text: string; change?: \"add\" | \"del\" };\n/* One row of a unified diff: old/new line numbers, its kind, and its pieces. */\nexport type DiffRow = {\n old: number | null;\n cur: number | null;\n type: \"ctx\" | \"add\" | \"del\";\n pieces: CodePiece[];\n};\n/* Prominent copy strings on the code block. */\nexport type CodeBlockLabels = { copy: string; copied: string };\n\n// Back-compat internal aliases for the local component signatures.\ntype Piece = CodePiece;\ntype Row = DiffRow;\n\nconst DIFF: Row[] = [\n { old: 1, cur: 1, type: \"ctx\", pieces: [{ text: \"export async function churnBatch() {\" }] },\n { old: 2, cur: 2, type: \"ctx\", pieces: [{ text: ' const flavor = await getFlavor(\"pistachio\");' }] },\n { old: 3, cur: 3, type: \"ctx\", pieces: [{ text: \" const base = await dairy.fetch({ flavor });\" }] },\n { old: 4, cur: null, type: \"del\", pieces: [{ text: \" await freezer.store(base, { temp: \" }, { text: '\"-14C\"', change: \"del\" }, { text: \" });\" }] },\n { old: null, cur: 4, type: \"add\", pieces: [{ text: \" await freezer.store(base, { temp: \" }, { text: '\"-16C\"', change: \"add\" }, { text: \" });\" }] },\n { old: null, cur: 5, type: \"add\", pieces: [{ text: \" if (!base.approved) return null;\" }] },\n { old: 5, cur: 6, type: \"ctx\", pieces: [{ text: \" return base.gallons;\" }] },\n { old: 6, cur: 7, type: \"ctx\", pieces: [{ text: \"}\" }] },\n];\n\nconst HATCH = \"repeating-linear-gradient(45deg, var(--red) 0, var(--red) 1.5px, transparent 1.5px, transparent 3px)\";\n\n/* light syntax coloring — keywords/imports/conditionals, functions, strings & numbers */\nconst KEYWORDS = new Set([\"import\", \"from\", \"export\", \"default\", \"async\", \"function\", \"const\", \"let\", \"var\", \"await\", \"return\", \"if\", \"else\", \"for\", \"while\", \"new\", \"throw\", \"try\", \"catch\", \"null\", \"true\", \"false\", \"undefined\"]);\nconst TOKEN = /(\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|`[^`]*`|\\b\\d+(?:\\.\\d+)?\\b|\\b(?:import|from|export|default|async|function|const|let|var|await|return|if|else|for|while|new|throw|try|catch|null|true|false|undefined)\\b|[A-Za-z_$][\\w$]*(?=\\s*\\())/g;\n\nfunction highlight(text: string): ReactNode[] {\n const nodes: ReactNode[] = [];\n let last = 0;\n let k = 0;\n for (const m of text.matchAll(TOKEN)) {\n const idx = m.index ?? 0;\n const t = m[0];\n if (idx > last) nodes.push({text.slice(last, idx)});\n let color: string;\n let weight: number | undefined;\n if (/^[\"'`]/.test(t) || /^\\d/.test(t)) color = \"var(--orange)\"; // string / number\n else if (KEYWORDS.has(t)) color = \"var(--accent-ink)\"; // keyword / import / conditional\n else { color = \"var(--ink)\"; weight = 500; } // function call\n nodes.push({t});\n last = idx + t.length;\n }\n if (last < text.length) nodes.push({text.slice(last)});\n return nodes;\n}\n\nfunction Pieces({ pieces }: { pieces: Piece[] }) {\n return (\n <>\n {pieces.map((p, i) => {\n if (p.change) {\n const add = p.change === \"add\";\n return (\n \n {highlight(p.text)}\n \n );\n }\n return {highlight(p.text)};\n })}\n \n );\n}\n\nfunction FileIcon() {\n return (\n \n \n \n );\n}\n\nconst DEFAULT_LABELS: CodeBlockLabels = { copy: \"Copy\", copied: \"Copied\" };\n\nexport type CodeBlockProps = {\n /** Which view to render — \"Code\" (line-numbered listing) or \"Diff\". */\n variant?: string;\n /** The lines shown in the Code view. */\n lines?: string[];\n /** Raw text placed on the clipboard by Copy. Defaults to `lines` joined. */\n code?: string;\n /** The unified-diff rows shown in the Diff view. */\n diff?: DiffRow[];\n /** Filename shown in the header. */\n filename?: string;\n /** Prominent copy strings. */\n labels?: Partial;\n /** Called with the copied text after a successful copy. */\n onCopy?: (text: string) => void;\n};\n\nexport default function CodeBlock({\n variant = \"Code\",\n lines = CODE_LINES,\n code,\n diff = DIFF,\n filename = FILE,\n labels,\n onCopy,\n}: CodeBlockProps) {\n const [copied, setCopied] = useState(false);\n const isDiff = variant === \"Diff\";\n const text = { ...DEFAULT_LABELS, ...labels };\n const raw = code ?? lines.join(\"\\n\");\n\n const copy = useCallback(() => {\n navigator.clipboard.writeText(raw).then(() => {\n setCopied(true);\n onCopy?.(raw);\n setTimeout(() => setCopied(false), 1500);\n });\n }, [raw, onCopy]);\n\n const added = diff.filter((r) => r.type === \"add\").length;\n const removed = diff.filter((r) => r.type === \"del\").length;\n\n return (\n
\n {/* header — file · (diff stat | copy) */}\n
\n \n \n {filename}\n \n\n {isDiff ? (\n \n +{added}\n -{removed}\n \n ) : (\n \n {copied ? (\n \n ) : (\n \n )}\n {copied ? text.copied : text.copy}\n \n )}\n
\n\n {/* body — equal 12px inset on top / left / right; lines wrap */}\n
\n {isDiff ? (\n
\n \n {diff.map((r, i) => {\n const add = r.type === \"add\";\n const del = r.type === \"del\";\n // one gutter column: removals keep the old number, additions/context show the new one\n const num = del ? r.old : r.cur;\n return (\n \n {(add || del) && (\n \n )}\n {num ?? \"\"}\n \n \n \n
\n );\n })}\n
\n ) : (\n
\n \n {lines.map((line, i) => (\n
\n {i + 1}\n {highlight(line)}\n
\n ))}\n
\n )}\n
\n \n );\n}\n", "type": "registry:component", "target": "components/primitives/CodeBlock.tsx" } ] }