Files
inkreach-agent/docs/ui-ref/beautifului/code-block.json
T

18 lines
11 KiB
JSON

{
"$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(<span key={k++}>{text.slice(last, idx)}</span>);\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(<span key={k++} style={{ color, fontWeight: weight }}>{t}</span>);\n last = idx + t.length;\n }\n if (last < text.length) nodes.push(<span key={k++}>{text.slice(last)}</span>);\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 <span\n key={i}\n className=\"rounded-[3px]\"\n style={{\n background: `color-mix(in srgb, var(--${add ? \"green\" : \"red\"}) 18%, transparent)`,\n padding: \"0 2px\",\n margin: \"0 -1px\",\n boxDecorationBreak: \"clone\",\n WebkitBoxDecorationBreak: \"clone\",\n }}\n >\n {highlight(p.text)}\n </span>\n );\n }\n return <span key={i}>{highlight(p.text)}</span>;\n })}\n </>\n );\n}\n\nfunction FileIcon() {\n return (\n <svg aria-hidden width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"shrink-0 text-ink-3\">\n <path d=\"M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5\" />\n </svg>\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<CodeBlockLabels>;\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 <div className=\"w-full max-w-105 overflow-hidden rounded-card bg-surface shadow-card\">\n {/* header — file · (diff stat | copy) */}\n <div className=\"flex h-11 items-center gap-2 border-b border-line px-4 text-[12.5px]\">\n <span className=\"inline-flex min-w-0 items-center gap-[7px]\">\n <FileIcon />\n <span className=\"truncate font-mono leading-none text-ink\">{filename}</span>\n </span>\n\n {isDiff ? (\n <span className=\"ml-auto inline-flex items-center gap-2 font-mono text-[12px] leading-none tabular-nums\">\n <span className=\"text-green\">+{added}</span>\n <span className=\"text-red\">-{removed}</span>\n </span>\n ) : (\n <button\n type=\"button\"\n aria-label=\"Copy code\"\n onClick={copy}\n className={`-mr-1 ml-auto flex h-6 items-center gap-1 rounded-[6px] px-1.5 text-[12px]\n font-medium transition-colors duration-100 hover:bg-hover\n ${copied ? \"text-green\" : \"text-ink-3 hover:text-ink\"}`}\n >\n {copied ? (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n ) : (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"12\" height=\"12\" rx=\"2.5\" /><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" /></svg>\n )}\n {copied ? text.copied : text.copy}\n </button>\n )}\n </div>\n\n {/* body — equal 12px inset on top / left / right; lines wrap */}\n <div className=\"py-3 font-mono text-[12.5px] leading-[1.65] text-ink-2\">\n {isDiff ? (\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-line\" />\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 <div\n key={i}\n className={`relative grid grid-cols-[20px_minmax(0,1fr)] items-start\n ${add ? \"bg-green-tint\" : del ? \"bg-red-tint\" : \"\"}`}\n >\n {(add || del) && (\n <span className=\"absolute inset-y-0 left-0 w-[3px]\" style={{ background: add ? \"var(--green)\" : HATCH }} />\n )}\n <span className={`select-none text-center text-[11px] ${add ? \"text-green\" : del ? \"text-red\" : \"text-ink-3\"}`}>{num ?? \"\"}</span>\n <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">\n <Pieces pieces={r.pieces} />\n </code>\n </div>\n );\n })}\n </div>\n ) : (\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-line\" />\n {lines.map((line, i) => (\n <div key={i} className=\"grid grid-cols-[20px_minmax(0,1fr)] items-start\">\n <span className=\"select-none text-center text-[11px] text-ink-3\">{i + 1}</span>\n <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">{highlight(line)}</code>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n",
"type": "registry:component",
"target": "components/primitives/CodeBlock.tsx"
}
]
}