{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "code-snippet", "title": "Code Snippet", "description": "Self-highlighting code block with six color schemes, line numbers, line highlighting, collapsing, wrapping and PNG export. The highlighter is local, so there is no async theme load and no flash of unstyled code. A refused clipboard write tells the reader to select the code instead, and reports through onCopyError.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "@duck/theme", "@duck/copy-button" ], "files": [ { "path": "registry/duck/ui/code-snippet.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n ChevronsDownUp,\n Clipboard,\n ImageDown,\n WrapText,\n type LucideIcon,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { CopyButton } from \"@/components/ui/copy-button\";\nimport {\n codeSnippetSchemes,\n detectLanguage,\n getCodeScheme,\n languageLabels,\n tokenizeCode,\n type CodeLanguage,\n type CodeLine,\n type CodePalette,\n type CodeScheme,\n type CodeSchemeName,\n type TokenKind,\n} from \"@/components/ui/code-highlight\";\n\n/**\n * CodeSnippet — a code block that highlights itself, wears one of six color\n * schemes, and exports itself as a PNG.\n *\n * The highlighter is local (see code-highlight.ts), so there is no async theme\n * load and no flash of unstyled code. Both palettes of a scheme are written to\n * the element as CSS variables and the active one is picked by the `dark`\n * variant, which means the server render is already correct in either mode and\n * the PNG can read the resolved colors straight back out of the DOM.\n *\n * Syntax colors are the one place duck/ui uses raw values instead of semantic\n * tokens — a syntax palette is data, like a Shiki theme. The frame, the header\n * and every control around the code stay on the theme.\n */\n\n/* ================================================================\n Palette plumbing\n\n Each scheme writes `--cs--light` and `--cs--dark` through\n the style attribute; the static pairs below collapse those into\n `--cs-` for whichever mode is live. Static because Tailwind has\n to see every class it generates — never build these strings.\n ================================================================ */\n\nconst PALETTE_VARS = [\n \"[--cs-bg:var(--cs-bg-light)] dark:[--cs-bg:var(--cs-bg-dark)]\",\n \"[--cs-fg:var(--cs-fg-light)] dark:[--cs-fg:var(--cs-fg-dark)]\",\n \"[--cs-muted:var(--cs-muted-light)] dark:[--cs-muted:var(--cs-muted-dark)]\",\n \"[--cs-band:var(--cs-band-light)] dark:[--cs-band:var(--cs-band-dark)]\",\n \"[--cs-accent:var(--cs-accent-light)] dark:[--cs-accent:var(--cs-accent-dark)]\",\n \"[--cs-comment:var(--cs-comment-light)] dark:[--cs-comment:var(--cs-comment-dark)]\",\n \"[--cs-string:var(--cs-string-light)] dark:[--cs-string:var(--cs-string-dark)]\",\n \"[--cs-number:var(--cs-number-light)] dark:[--cs-number:var(--cs-number-dark)]\",\n \"[--cs-keyword:var(--cs-keyword-light)] dark:[--cs-keyword:var(--cs-keyword-dark)]\",\n \"[--cs-fn:var(--cs-fn-light)] dark:[--cs-fn:var(--cs-fn-dark)]\",\n \"[--cs-type:var(--cs-type-light)] dark:[--cs-type:var(--cs-type-dark)]\",\n \"[--cs-attribute:var(--cs-attribute-light)] dark:[--cs-attribute:var(--cs-attribute-dark)]\",\n \"[--cs-tag:var(--cs-tag-light)] dark:[--cs-tag:var(--cs-tag-dark)]\",\n \"[--cs-insert:var(--cs-insert-light)] dark:[--cs-insert:var(--cs-insert-dark)]\",\n \"[--cs-delete:var(--cs-delete-light)] dark:[--cs-delete:var(--cs-delete-dark)]\",\n].join(\" \");\n\nfunction schemeVars(scheme: CodeScheme) {\n const vars: Record = {};\n for (const [key, value] of Object.entries(scheme.light)) vars[`--cs-${key}-light`] = value;\n for (const [key, value] of Object.entries(scheme.dark)) vars[`--cs-${key}-dark`] = value;\n return vars as React.CSSProperties;\n}\n\n/**\n * One row per token kind: the class the DOM uses and the palette key the\n * canvas paints with. Keep the two halves in agreement or the PNG stops\n * matching the block it came from.\n */\nconst tokenStyles: Record = {\n plain: { className: \"text-[var(--cs-fg)]\", color: \"fg\" },\n variable: { className: \"text-[var(--cs-fg)]\", color: \"fg\" },\n comment: { className: \"text-[var(--cs-comment)] italic\", color: \"comment\" },\n string: { className: \"text-[var(--cs-string)]\", color: \"string\" },\n number: { className: \"text-[var(--cs-number)]\", color: \"number\" },\n constant: { className: \"text-[var(--cs-number)]\", color: \"number\" },\n keyword: { className: \"text-[var(--cs-keyword)]\", color: \"keyword\" },\n function: { className: \"text-[var(--cs-fn)]\", color: \"fn\" },\n type: { className: \"text-[var(--cs-type)]\", color: \"type\" },\n property: { className: \"text-[var(--cs-type)]\", color: \"type\" },\n tag: { className: \"text-[var(--cs-tag)]\", color: \"tag\" },\n attribute: { className: \"text-[var(--cs-attribute)]\", color: \"attribute\" },\n operator: { className: \"text-[var(--cs-muted)]\", color: \"muted\" },\n punctuation: { className: \"text-[var(--cs-muted)]\", color: \"muted\" },\n meta: { className: \"text-[var(--cs-muted)]\", color: \"muted\" },\n insert: { className: \"text-[var(--cs-insert)]\", color: \"insert\" },\n delete: { className: \"text-[var(--cs-delete)]\", color: \"delete\" },\n};\n\nconst paletteKeys = [\n \"bg\",\n \"fg\",\n \"muted\",\n \"band\",\n \"accent\",\n \"comment\",\n \"string\",\n \"number\",\n \"keyword\",\n \"fn\",\n \"type\",\n \"attribute\",\n \"tag\",\n \"insert\",\n \"delete\",\n] as const satisfies readonly (keyof CodePalette)[];\n\n/** The resolved palette, straight from the element the browser just styled. */\nfunction readPalette(element: HTMLElement): CodePalette {\n const styles = getComputedStyle(element);\n const palette = {} as CodePalette;\n for (const key of paletteKeys) {\n palette[key] = styles.getPropertyValue(`--cs-${key}`).trim() || \"#808080\";\n }\n return palette;\n}\n\n/* ================================================================\n Helpers\n ================================================================ */\n\n/** `\"1,4-6\"` or `[1, 4, 5, 6]` — both end up as a set of line numbers. */\nfunction parseLines(spec: string | number[] | undefined): Set {\n if (!spec) return new Set();\n if (Array.isArray(spec)) return new Set(spec);\n\n const numbers = new Set();\n for (const part of spec.split(\",\")) {\n const [from, to] = part.split(\"-\").map((value) => Number.parseInt(value.trim(), 10));\n if (Number.isNaN(from)) continue;\n // `\"2\"` splits to a single part, so `to` is undefined rather than NaN.\n const end = to === undefined || Number.isNaN(to) ? from : to;\n for (let line = Math.min(from, end); line <= Math.max(from, end); line += 1) {\n numbers.add(line);\n }\n }\n return numbers;\n}\n\nfunction lineLength(line: CodeLine) {\n return line.tokens.reduce((total, token) => total + token.text.length, 0);\n}\n\nfunction fileNameFor(title: string | undefined, override: string | undefined) {\n if (override) return override.endsWith(\".png\") ? override : `${override}.png`;\n const base = (title ?? \"snippet\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n return `${base || \"snippet\"}.png`;\n}\n\nfunction roundRectPath(\n context: CanvasRenderingContext2D,\n x: number,\n y: number,\n width: number,\n height: number,\n radius: number\n) {\n context.beginPath();\n if (typeof context.roundRect === \"function\") {\n context.roundRect(x, y, width, height, radius);\n return;\n }\n context.moveTo(x + radius, y);\n context.arcTo(x + width, y, x + width, y + height, radius);\n context.arcTo(x + width, y + height, x, y + height, radius);\n context.arcTo(x, y + height, x, y, radius);\n context.arcTo(x, y, x + width, y, radius);\n context.closePath();\n}\n\n/* ================================================================\n Component\n ================================================================ */\n\ntype CodeSnippetProps = Omit, \"children\"> & {\n code: string;\n /** `\"auto\"` reads the extension in `title` first, the code itself second. */\n lang?: CodeLanguage;\n /** Filename in the header bar. Also names the exported PNG. */\n title?: string;\n scheme?: CodeSchemeName;\n frame?: \"sticker\" | \"holo\" | \"plain\";\n chrome?: \"dots\" | \"plain\" | \"none\";\n lineNumbers?: boolean;\n startLine?: number;\n /** Lines to wash with the accent color: `\"3,7-9\"` or `[3, 7, 8, 9]`. */\n highlight?: string | number[];\n wrap?: boolean;\n /** Collapse to this many lines behind a \"show all\" control. */\n maxLines?: number;\n languageBadge?: boolean;\n copyable?: boolean;\n /** Show the PNG controls: download, and copy to the clipboard where allowed. */\n exportable?: boolean;\n /** Let the reader switch scheme. `scheme` stays the starting point. */\n schemePicker?: boolean;\n wrapToggle?: boolean;\n /** Pixel density of the PNG. Clamped to 1–4. */\n exportScale?: number;\n /** What sits behind the card in the PNG. */\n exportBackdrop?: \"holo\" | \"scheme\" | \"none\";\n /** Small credit painted into the PNG only. */\n watermark?: string;\n /** Override the download filename. */\n fileName?: string;\n onCopied?: (value: string) => void;\n /**\n * The clipboard refused — plain HTTP, an embedded browser, a denied prompt.\n * The block already announces it in its own live region; this is for the page\n * that wants to say more than \"Copy failed\".\n */\n onCopyError?: (error: unknown) => void;\n};\n\nfunction CodeSnippet({\n code,\n lang = \"auto\",\n title,\n scheme: schemeName = \"duck\",\n frame = \"sticker\",\n chrome = \"dots\",\n lineNumbers = true,\n startLine = 1,\n highlight,\n wrap = false,\n maxLines,\n languageBadge = true,\n copyable = true,\n exportable = true,\n schemePicker = false,\n wrapToggle = false,\n exportScale = 2,\n exportBackdrop = \"holo\",\n watermark,\n fileName,\n onCopied,\n onCopyError,\n className,\n ...props\n}: CodeSnippetProps) {\n const rootRef = React.useRef(null);\n const preRef = React.useRef(null);\n\n // Props stay authoritative until the reader touches a control.\n const [pickedScheme, setPickedScheme] = React.useState(null);\n const [pickedWrap, setPickedWrap] = React.useState(null);\n const [expanded, setExpanded] = React.useState(false);\n const [busy, setBusy] = React.useState(false);\n const [status, setStatus] = React.useState(\"\");\n const [canCopyImage, setCanCopyImage] = React.useState(false);\n\n const scheme = getCodeScheme(pickedScheme ?? schemeName);\n const wrapped = pickedWrap ?? wrap;\n\n const language = React.useMemo(\n () => (lang === \"auto\" ? detectLanguage(code, title) : lang),\n [lang, code, title]\n );\n const lines = React.useMemo(() => tokenizeCode(code, language), [code, language]);\n const marked = React.useMemo(() => parseLines(highlight), [highlight]);\n\n const total = lines.length;\n const collapsible = maxLines !== undefined && total > maxLines;\n const visible = collapsible && !expanded ? lines.slice(0, maxLines) : lines;\n const gutterWidth = String(startLine + total - 1).length;\n\n // ClipboardItem is missing in Firefox, so the control only appears where it\n // would actually work.\n React.useEffect(() => {\n setCanCopyImage(\n typeof ClipboardItem !== \"undefined\" && typeof navigator.clipboard?.write === \"function\"\n );\n }, []);\n\n React.useEffect(() => {\n if (!status) return;\n const timer = window.setTimeout(() => setStatus(\"\"), 2400);\n return () => window.clearTimeout(timer);\n }, [status]);\n\n /* ---- PNG ---- */\n\n /**\n * Paint the whole snippet — never the collapsed slice, never wrapped — from\n * the same tokens the DOM rendered, in the colors the DOM resolved.\n */\n const paint = React.useCallback(async () => {\n const root = rootRef.current;\n const pre = preRef.current;\n if (!root || !pre) throw new Error(\"CodeSnippet is not mounted\");\n\n await document.fonts?.ready;\n\n const palette = readPalette(root);\n const preStyles = getComputedStyle(pre);\n const fontSize = Math.round(Number.parseFloat(preStyles.fontSize) || 13);\n const fontFamily = preStyles.fontFamily || \"monospace\";\n const font = `${fontSize}px ${fontFamily}`;\n\n const canvas = document.createElement(\"canvas\");\n const measure = canvas.getContext(\"2d\");\n if (!measure) throw new Error(\"Canvas is unavailable\");\n\n measure.font = font;\n const charWidth = measure.measureText(\"0\".repeat(20)).width / 20;\n const lineHeight = Math.round(fontSize * 1.7);\n const pad = Math.round(fontSize * 1.5);\n const header = chrome === \"none\" ? 0 : Math.round(fontSize * 2.8);\n const gutter = lineNumbers ? gutterWidth * charWidth + fontSize : 0;\n const footer = watermark ? lineHeight : 0;\n const columns = lines.reduce((widest, line) => Math.max(widest, lineLength(line)), 0);\n\n const cardWidth = Math.max(pad * 2 + gutter + columns * charWidth, fontSize * 24);\n const cardHeight = header + pad * 2 + total * lineHeight + footer;\n const inset = exportBackdrop === \"none\" ? 0 : Math.round(fontSize * 2.6);\n const scale = Math.min(Math.max(exportScale, 1), 4);\n\n canvas.width = Math.ceil((cardWidth + inset * 2) * scale);\n canvas.height = Math.ceil((cardHeight + inset * 2) * scale);\n\n const context = canvas.getContext(\"2d\");\n if (!context) throw new Error(\"Canvas is unavailable\");\n context.scale(scale, scale);\n context.textBaseline = \"middle\";\n context.font = font;\n\n // Backdrop\n if (exportBackdrop !== \"none\") {\n if (exportBackdrop === \"holo\") {\n const gradient = context.createLinearGradient(\n 0,\n 0,\n cardWidth + inset * 2,\n cardHeight + inset * 2\n );\n gradient.addColorStop(0, palette.accent);\n gradient.addColorStop(0.55, palette.type);\n gradient.addColorStop(1, palette.tag);\n context.fillStyle = gradient;\n } else {\n context.fillStyle = palette.band;\n }\n context.fillRect(0, 0, cardWidth + inset * 2, cardHeight + inset * 2);\n }\n\n // Card\n const left = inset;\n const top = inset;\n const radius = Math.round(fontSize);\n context.save();\n context.shadowColor = \"rgba(0, 0, 0, 0.32)\";\n context.shadowBlur = fontSize * 2;\n context.shadowOffsetY = fontSize * 0.8;\n roundRectPath(context, left, top, cardWidth, cardHeight, radius);\n context.fillStyle = palette.bg;\n context.fill();\n context.restore();\n\n roundRectPath(context, left, top, cardWidth, cardHeight, radius);\n context.save();\n context.clip();\n\n // Header bar\n if (header > 0) {\n context.fillStyle = palette.band;\n context.fillRect(left, top, cardWidth, header);\n context.save();\n context.globalAlpha = 0.3;\n context.fillStyle = palette.muted;\n context.fillRect(left, top + header - 1, cardWidth, 1);\n context.restore();\n\n let dotX = left + pad;\n if (chrome === \"dots\") {\n const dotRadius = fontSize * 0.32;\n for (const color of [palette.delete, palette.number, palette.insert]) {\n context.beginPath();\n context.arc(dotX + dotRadius, top + header / 2, dotRadius, 0, Math.PI * 2);\n context.fillStyle = color;\n context.fill();\n dotX += dotRadius * 3.2;\n }\n dotX += fontSize * 0.4;\n }\n if (title) {\n context.font = `${Math.round(fontSize * 0.92)}px ${fontFamily}`;\n context.fillStyle = palette.muted;\n context.fillText(title, dotX, top + header / 2);\n }\n }\n\n // Lines\n const contentTop = top + header + pad;\n lines.forEach((line, index) => {\n const rowTop = contentTop + index * lineHeight;\n const middle = rowTop + lineHeight / 2;\n\n const wash =\n line.change === \"insert\"\n ? palette.insert\n : line.change === \"delete\"\n ? palette.delete\n : marked.has(startLine + index)\n ? palette.accent\n : null;\n\n if (wash) {\n context.save();\n context.globalAlpha = 0.14;\n context.fillStyle = wash;\n context.fillRect(left, rowTop, cardWidth, lineHeight);\n context.restore();\n context.fillStyle = wash;\n context.fillRect(left, rowTop, 2, lineHeight);\n }\n\n if (lineNumbers) {\n context.font = font;\n context.fillStyle = palette.muted;\n context.textAlign = \"right\";\n context.fillText(\n String(startLine + index),\n left + pad + gutter - fontSize,\n middle\n );\n context.textAlign = \"left\";\n }\n\n let x = left + pad + gutter;\n for (const token of line.tokens) {\n context.font = token.kind === \"comment\" ? `italic ${font}` : font;\n context.fillStyle = palette[tokenStyles[token.kind].color];\n context.fillText(token.text, x, middle);\n x += token.text.length * charWidth;\n }\n });\n\n if (watermark) {\n context.font = `${Math.round(fontSize * 0.85)}px ${fontFamily}`;\n context.textAlign = \"right\";\n context.save();\n context.globalAlpha = 0.6;\n context.fillStyle = palette.muted;\n context.fillText(watermark, left + cardWidth - pad, top + cardHeight - pad - footer / 2);\n context.restore();\n context.textAlign = \"left\";\n }\n\n context.restore();\n return canvas;\n }, [\n chrome,\n exportBackdrop,\n exportScale,\n gutterWidth,\n lineNumbers,\n lines,\n marked,\n startLine,\n title,\n total,\n watermark,\n ]);\n\n const toBlob = React.useCallback(async () => {\n const canvas = await paint();\n const blob = await new Promise((resolve) =>\n canvas.toBlob(resolve, \"image/png\")\n );\n if (!blob) throw new Error(\"Could not encode the PNG\");\n return blob;\n }, [paint]);\n\n const download = async () => {\n setBusy(true);\n try {\n const blob = await toBlob();\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = fileNameFor(title, fileName);\n link.click();\n URL.revokeObjectURL(url);\n setStatus(\"PNG saved\");\n } catch {\n setStatus(\"Export failed\");\n } finally {\n setBusy(false);\n }\n };\n\n const copyImage = async () => {\n setBusy(true);\n try {\n // Built before the first await: Safari drops the write once the user\n // gesture has expired, but it will wait on a promise handed to it now.\n const item = new ClipboardItem({ \"image/png\": toBlob() });\n await navigator.clipboard.write([item]);\n setStatus(\"Image copied\");\n } catch (error) {\n setStatus(\"Copy failed\");\n // Same refusal, same channel: a page that handles one wants both.\n onCopyError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n /* ---- Chrome ---- */\n\n const showHeader = chrome !== \"none\";\n const actions = (\n <>\n {schemePicker && (\n setPickedScheme(event.target.value as CodeSchemeName)}\n aria-label=\"Color scheme\"\n className={cn(\n \"h-7 cursor-pointer rounded-md border border-transparent bg-transparent px-1 font-mono text-[11px]\",\n \"text-[var(--cs-muted)] transition-colors hover:text-[var(--cs-fg)]\",\n \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n )}\n >\n {codeSnippetSchemes.map((option) => (\n \n ))}\n \n )}\n {wrapToggle && (\n setPickedWrap(!wrapped)}\n />\n )}\n {copyable && (\n {\n // The code is on the page and selectable, so the honest fallback is\n // to say so rather than to leave the button looking broken.\n setStatus(\"Copy failed — select the code and copy it manually\");\n onCopyError?.(error);\n }}\n className=\"size-7 border-transparent bg-transparent text-[var(--cs-muted)] hover:border-transparent hover:text-[var(--cs-fg)]\"\n />\n )}\n {exportable && (\n \n )}\n {exportable && canCopyImage && (\n \n )}\n \n );\n\n return (\n \n {showHeader ? (\n
\n {chrome === \"dots\" && (\n \n \n \n \n \n )}\n {title && (\n {title}\n )}\n {languageBadge && (\n \n {languageLabels[language]}\n \n )}\n {actions}\n
\n ) : (\n \n {actions}\n \n )}\n\n
\n \n \n \n {visible.map((line, index) => {\n const number = startLine + index;\n const change = line.change;\n return (\n \n {lineNumbers && (\n \n {number}\n \n )}\n \n {line.tokens.length === 0\n ? // A zero-width space, or an empty line has no height.\n \"\\u200b\"\n : line.tokens.map((token, position) => (\n \n {token.text}\n \n ))}\n \n \n );\n })}\n \n \n
\n\n {collapsible && !expanded && (\n <>\n \n setExpanded(true)}\n className=\"absolute bottom-2 left-1/2 -translate-x-1/2 cursor-pointer rounded-lg border border-[color-mix(in_oklab,var(--cs-accent)_45%,transparent)] bg-[var(--cs-band)] px-3 py-1 font-mono text-[11px] text-[var(--cs-accent)] transition-transform duration-200 ease-[var(--ease-duck)] hover:-translate-y-px focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n >\n Show all {total} lines\n \n \n )}\n \n\n {collapsible && expanded && (\n
\n setExpanded(false)}\n className=\"inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-3 py-1 font-mono text-[11px] text-[var(--cs-muted)] transition-colors hover:text-[var(--cs-fg)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n >\n \n Collapse to {maxLines} lines\n \n
\n )}\n\n \n {status}\n \n \n );\n}\n\nfunction SnippetAction({\n icon: Icon,\n label,\n pressed,\n className,\n ...props\n}: React.ComponentProps<\"button\"> & { icon: LucideIcon; label: string; pressed?: boolean }) {\n return (\n \n \n \n );\n}\n\nexport { CodeSnippet, codeSnippetSchemes };\n", "type": "registry:ui" }, { "path": "registry/duck/ui/code-highlight.ts", "content": "/**\n * code-highlight — the grammar and the palettes behind .\n *\n * Not a parser. A snippet is a few dozen lines of already-correct code, so an\n * ordered scan of sticky regexes over a small context stack colors it\n * convincingly at a fraction of a real highlighter's weight: no async load, no\n * WASM, no theme fetch, and it runs in a client component or on the server.\n * Reach for Shiki when you need a whole MDX pipeline; reach for this when you\n * need a snippet in a card.\n *\n * The palettes are the one place in duck/ui that holds raw hex. Syntax colors\n * are content, not chrome — the same reason a Shiki theme is a data file. The\n * frame, the toolbar and every control around the code stay on semantic\n * tokens; only what sits inside reads from these.\n */\n\n/* ================================================================\n Tokens\n ================================================================ */\n\nexport type TokenKind =\n | \"plain\"\n | \"comment\"\n | \"string\"\n | \"number\"\n | \"keyword\"\n | \"constant\"\n | \"function\"\n | \"type\"\n | \"property\"\n | \"tag\"\n | \"attribute\"\n | \"variable\"\n | \"operator\"\n | \"punctuation\"\n | \"meta\"\n | \"insert\"\n | \"delete\";\n\nexport interface CodeToken {\n kind: TokenKind;\n text: string;\n}\n\nexport interface CodeLine {\n tokens: CodeToken[];\n /** Set for diff input, so a line can be washed green or red as a whole. */\n change?: \"insert\" | \"delete\" | \"meta\";\n}\n\n/* ================================================================\n Grammars\n\n Every rule carries a sticky regex, tried in order at the current\n index; the first hit wins and nothing backtracks. `push` and `pop`\n move a context stack, which is what makes JSX work: a tag opens a\n context where bare identifiers are attributes, `{` re-enters code,\n and `>` closes it again.\n ================================================================ */\n\ninterface Rule {\n kind: TokenKind;\n /** Must carry the `y` flag — the scanner matches at an exact index. */\n re: RegExp;\n /** Only apply when the previous meaningful token was one of these. */\n after?: TokenKind[];\n /** Enter this context after a match. */\n push?: string;\n /** Leave the current context after a match. */\n pop?: boolean;\n}\n\ntype Grammar = Record;\n\nconst whitespace: Rule = { kind: \"plain\", re: /[ \\t\\n]+/y };\n\n/* ---- JavaScript family: js, jsx, ts, tsx ---- */\n\nconst jsKeywords =\n /\\b(?:abstract|as|asserts|async|await|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|finally|for|from|function|get|global|if|implements|import|in|infer|instanceof|interface|is|keyof|let|namespace|new|of|override|package|private|protected|public|readonly|return|satisfies|set|static|super|switch|this|throw|try|type|typeof|unique|var|void|while|with|yield)\\b/y;\n\nconst jsCode: Rule[] = [\n whitespace,\n { kind: \"comment\", re: /\\/\\/[^\\n]*/y },\n { kind: \"comment\", re: /\\/\\*[\\s\\S]*?(?:\\*\\/|$)/y },\n { kind: \"string\", re: /\"(?:\\\\.|[^\"\\\\\\n])*\"/y },\n { kind: \"string\", re: /'(?:\\\\.|[^'\\\\\\n])*'/y },\n { kind: \"string\", re: /`(?:\\\\.|[^`\\\\])*`/y },\n // A slash only starts a regex where a value is expected. After an\n // identifier or a closing bracket it is division.\n {\n kind: \"string\",\n re: /\\/(?![*/])(?:\\\\.|\\[(?:\\\\.|[^\\]\\\\\\n])*\\]|[^/\\\\\\n])+\\/[dgimsuvy]*/y,\n after: [\"operator\", \"keyword\", \"punctuation\"],\n },\n { kind: \"attribute\", re: /@[A-Za-z_$][\\w$.]*/y },\n {\n kind: \"number\",\n re: /0[xXbBoO][0-9a-fA-F_]+n?|(?:\\d[\\d_]*)?\\.?\\d[\\d_]*(?:[eE][+-]?\\d+)?n?/y,\n },\n // ``, where the previous token is a\n // type or an identifier rather than punctuation.\n {\n kind: \"tag\",\n re: /<\\/?[A-Za-z][\\w.:-]*/y,\n after: [\"punctuation\", \"operator\", \"keyword\"],\n push: \"tag\",\n },\n { kind: \"constant\", re: /\\b(?:true|false|null|undefined|NaN|Infinity)\\b/y },\n { kind: \"keyword\", re: jsKeywords },\n { kind: \"function\", re: /\\b[A-Za-z_$][\\w$]*(?=\\s*\\()/y },\n { kind: \"constant\", re: /\\b[A-Z][A-Z0-9_]{2,}\\b/y },\n { kind: \"type\", re: /\\b[A-Z][\\w$]*\\b/y },\n { kind: \"property\", re: /\\b[A-Za-z_$][\\w$]*(?=\\s*:)/y },\n { kind: \"variable\", re: /[A-Za-z_$][\\w$]*/y },\n { kind: \"punctuation\", re: /\\{/y, push: \"code\" },\n { kind: \"punctuation\", re: /\\}/y, pop: true },\n { kind: \"punctuation\", re: /[()[\\];,.]/y },\n { kind: \"operator\", re: /[+\\-*/%=&|!<>?:^~]+/y },\n];\n\n/** Inside `<... >`: bare words are attributes and `{` re-enters code. */\nconst jsTag: Rule[] = [\n whitespace,\n { kind: \"string\", re: /\"(?:\\\\.|[^\"\\\\\\n])*\"/y },\n { kind: \"string\", re: /'(?:\\\\.|[^'\\\\\\n])*'/y },\n { kind: \"punctuation\", re: /\\{/y, push: \"code\" },\n { kind: \"punctuation\", re: /\\/?>/y, pop: true },\n { kind: \"operator\", re: /=/y },\n { kind: \"attribute\", re: /[A-Za-z_$][\\w$:.-]*/y },\n { kind: \"punctuation\", re: /[^\\s]/y },\n];\n\nconst jsGrammar: Grammar = { code: jsCode, tag: jsTag };\n\n/* ---- HTML and SVG ---- */\n\nconst htmlGrammar: Grammar = {\n code: [\n { kind: \"comment\", re: /|$)/y },\n { kind: \"meta\", re: /]*>/y },\n { kind: \"tag\", re: /<\\/?[A-Za-z][\\w:-]*/y, push: \"tag\" },\n { kind: \"constant\", re: /&#?[\\w]+;/y },\n { kind: \"plain\", re: /[^<&]+/y },\n ],\n tag: [\n whitespace,\n { kind: \"string\", re: /\"(?:\\\\.|[^\"\\\\])*\"/y },\n { kind: \"string\", re: /'(?:\\\\.|[^'\\\\])*'/y },\n { kind: \"punctuation\", re: /\\/?>/y, pop: true },\n { kind: \"operator\", re: /=/y },\n { kind: \"attribute\", re: /[A-Za-z_@:][\\w:.-]*/y },\n { kind: \"punctuation\", re: /[^\\s]/y },\n ],\n};\n\n/* ---- CSS ---- */\n\nconst cssGrammar: Grammar = {\n code: [\n whitespace,\n { kind: \"comment\", re: /\\/\\*[\\s\\S]*?(?:\\*\\/|$)/y },\n { kind: \"keyword\", re: /@[\\w-]+|!important/y },\n { kind: \"string\", re: /\"(?:\\\\.|[^\"\\\\\\n])*\"|'(?:\\\\.|[^'\\\\\\n])*'/y },\n { kind: \"constant\", re: /#[0-9a-fA-F]{3,8}\\b/y },\n { kind: \"property\", re: /[\\w-]+(?=\\s*:)/y },\n // Before the selector rule below, or `.5rem` reads as a class name.\n {\n kind: \"number\",\n re: /-?(?:\\d*\\.)?\\d+(?:e[+-]?\\d+)?(?:%|[a-zA-Z]+)?/y,\n },\n { kind: \"type\", re: /::?[\\w-]+|[.#][\\w-]+/y },\n { kind: \"function\", re: /[\\w-]+(?=\\()/y },\n { kind: \"variable\", re: /--[\\w-]+|[\\w-]+/y },\n { kind: \"punctuation\", re: /[{}()[\\];:,]/y },\n { kind: \"operator\", re: /[>+~*/=]/y },\n ],\n};\n\n/* ---- JSON and JSONC ---- */\n\nconst jsonGrammar: Grammar = {\n code: [\n whitespace,\n { kind: \"comment\", re: /\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?(?:\\*\\/|$)/y },\n { kind: \"property\", re: /\"(?:\\\\.|[^\"\\\\])*\"(?=\\s*:)/y },\n { kind: \"string\", re: /\"(?:\\\\.|[^\"\\\\])*\"/y },\n { kind: \"constant\", re: /\\b(?:true|false|null)\\b/y },\n { kind: \"number\", re: /-?(?:\\d+)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/y },\n { kind: \"punctuation\", re: /[{}[\\],:]/y },\n ],\n};\n\n/* ---- Shell ---- */\n\nconst bashGrammar: Grammar = {\n code: [\n whitespace,\n { kind: \"comment\", re: /#[^\\n]*/y },\n { kind: \"string\", re: /\"(?:\\\\.|[^\"\\\\])*\"|'[^']*'/y },\n { kind: \"variable\", re: /\\$\\{[^}\\n]*\\}|\\$[\\w@#?!*$-]+/y },\n {\n kind: \"keyword\",\n re: /\\b(?:if|then|elif|else|fi|for|while|until|do|done|case|esac|in|function|return|exit|export|local|readonly|source|set|unset|shift|trap|eval)\\b/y,\n },\n // The first word of a command, and anything after a pipe or a separator.\n { kind: \"function\", re: /(?:^|(?<=[|&;]\\s*))[\\w.\\/-]+/my },\n { kind: \"attribute\", re: /(?<=\\s)--?[A-Za-z][\\w-]*/y },\n { kind: \"number\", re: /\\b\\d+\\b/y },\n { kind: \"operator\", re: /[|&;<>]+|=/y },\n { kind: \"punctuation\", re: /[(){}[\\],]/y },\n { kind: \"plain\", re: /[^\\s|&;<>(){}[\\],=]+/y },\n ],\n};\n\n/* ---- Python ---- */\n\nconst pythonGrammar: Grammar = {\n code: [\n whitespace,\n { kind: \"comment\", re: /#[^\\n]*/y },\n { kind: \"string\", re: /[rbfuRBFU]{0,2}(\"\"\"|''')[\\s\\S]*?(?:\\1|$)/y },\n {\n kind: \"string\",\n re: /[rbfuRBFU]{0,2}(?:\"(?:\\\\.|[^\"\\\\\\n])*\"|'(?:\\\\.|[^'\\\\\\n])*')/y,\n },\n { kind: \"attribute\", re: /@[\\w.]+/y },\n { kind: \"constant\", re: /\\b(?:True|False|None|self|cls)\\b/y },\n {\n kind: \"keyword\",\n re: /\\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|raise|return|try|while|with|yield)\\b/y,\n },\n { kind: \"number\", re: /\\b(?:0[xXbBoO][0-9a-fA-F_]+|(?:\\d[\\d_]*)?\\.?\\d[\\d_]*(?:[eE][+-]?\\d+)?j?)\\b/y },\n { kind: \"function\", re: /\\b\\w+(?=\\s*\\()/y },\n { kind: \"type\", re: /\\b[A-Z]\\w*\\b/y },\n { kind: \"property\", re: /\\b\\w+(?=\\s*=(?!=))/y },\n { kind: \"variable\", re: /\\b\\w+\\b/y },\n { kind: \"punctuation\", re: /[()[\\]{},:.]/y },\n { kind: \"operator\", re: /[+\\-*/%=&|!<>^~]+/y },\n ],\n};\n\n/* ---- SQL ---- */\n\nconst sqlGrammar: Grammar = {\n code: [\n whitespace,\n { kind: \"comment\", re: /--[^\\n]*|\\/\\*[\\s\\S]*?(?:\\*\\/|$)/y },\n { kind: \"string\", re: /'(?:''|[^'])*'/y },\n {\n kind: \"keyword\",\n re: /\\b(?:add|all|alter|and|as|asc|between|by|case|cast|column|constraint|create|cross|default|delete|desc|distinct|drop|else|end|exists|foreign|from|full|group|having|if|in|index|inner|insert|into|is|join|key|left|like|limit|not|null|offset|on|or|order|outer|primary|references|returning|right|select|set|table|then|union|unique|update|using|values|view|when|where|with)\\b/iy,\n },\n {\n kind: \"type\",\n re: /\\b(?:bigint|boolean|bytea|char|date|decimal|double|float|int|integer|json|jsonb|numeric|real|serial|smallint|text|time|timestamp|uuid|varchar)\\b/iy,\n },\n { kind: \"constant\", re: /\\b(?:true|false|null|current_timestamp|now)\\b/iy },\n { kind: \"number\", re: /\\b\\d+(?:\\.\\d+)?\\b/y },\n { kind: \"function\", re: /\\b\\w+(?=\\s*\\()/y },\n { kind: \"variable\", re: /\"[^\"]*\"|`[^`]*`|\\b\\w+\\b/y },\n { kind: \"punctuation\", re: /[()[\\],;.]/y },\n { kind: \"operator\", re: /[+\\-*/%=<>!|]+/y },\n ],\n};\n\n/* ---- YAML ---- */\n\nconst yamlGrammar: Grammar = {\n code: [\n whitespace,\n { kind: \"comment\", re: /#[^\\n]*/y },\n { kind: \"meta\", re: /^(?:---|\\.\\.\\.)$/my },\n { kind: \"property\", re: /[\\w.$/-]+(?=\\s*:(?:\\s|$))/y },\n { kind: \"attribute\", re: /[&*][\\w-]+|![\\w!/:-]+/y },\n { kind: \"string\", re: /\"(?:\\\\.|[^\"\\\\])*\"|'(?:''|[^'])*'/y },\n { kind: \"constant\", re: /\\b(?:true|false|null|yes|no|on|off|~)\\b/iy },\n { kind: \"number\", re: /-?\\b\\d+(?:\\.\\d+)?\\b/y },\n { kind: \"punctuation\", re: /-(?=\\s)|[:[\\]{},]|[|>][-+]?$/my },\n { kind: \"plain\", re: /[^\\s]+/y },\n ],\n};\n\nconst grammars: Record = {\n js: jsGrammar,\n html: htmlGrammar,\n css: cssGrammar,\n json: jsonGrammar,\n bash: bashGrammar,\n python: pythonGrammar,\n sql: sqlGrammar,\n yaml: yamlGrammar,\n};\n\n/* ================================================================\n Languages\n ================================================================ */\n\nexport const codeSnippetLanguages = [\n \"auto\",\n \"tsx\",\n \"ts\",\n \"jsx\",\n \"js\",\n \"json\",\n \"jsonc\",\n \"css\",\n \"html\",\n \"bash\",\n \"python\",\n \"sql\",\n \"yaml\",\n \"diff\",\n \"text\",\n] as const;\n\nexport type CodeLanguage = (typeof codeSnippetLanguages)[number];\n\n/** What each language is called in the header badge. */\nexport const languageLabels: Record = {\n auto: \"auto\",\n tsx: \"tsx\",\n ts: \"ts\",\n jsx: \"jsx\",\n js: \"js\",\n json: \"json\",\n jsonc: \"jsonc\",\n css: \"css\",\n html: \"html\",\n bash: \"bash\",\n python: \"py\",\n sql: \"sql\",\n yaml: \"yaml\",\n diff: \"diff\",\n text: \"txt\",\n};\n\nconst grammarNames: Record = {\n auto: \"js\",\n tsx: \"js\",\n ts: \"js\",\n jsx: \"js\",\n js: \"js\",\n json: \"json\",\n jsonc: \"json\",\n css: \"css\",\n html: \"html\",\n bash: \"bash\",\n python: \"python\",\n sql: \"sql\",\n yaml: \"yaml\",\n diff: null,\n text: null,\n};\n\nconst extensions: Record = {\n tsx: \"tsx\",\n ts: \"ts\",\n mts: \"ts\",\n cts: \"ts\",\n jsx: \"jsx\",\n js: \"js\",\n mjs: \"js\",\n cjs: \"js\",\n json: \"json\",\n jsonc: \"jsonc\",\n css: \"css\",\n scss: \"css\",\n less: \"css\",\n html: \"html\",\n htm: \"html\",\n svg: \"html\",\n vue: \"html\",\n sh: \"bash\",\n bash: \"bash\",\n zsh: \"bash\",\n env: \"bash\",\n py: \"python\",\n sql: \"sql\",\n yml: \"yaml\",\n yaml: \"yaml\",\n diff: \"diff\",\n patch: \"diff\",\n txt: \"text\",\n md: \"text\",\n};\n\n/**\n * Guess a language from the filename first — it is the one signal that is\n * never wrong — and from the shape of the code only as a fallback.\n */\nexport function detectLanguage(code: string, title?: string): CodeLanguage {\n const extension = title?.toLowerCase().match(/\\.([a-z0-9]+)\\s*$/)?.[1];\n if (extension && extension in extensions) return extensions[extension];\n\n const source = code.trim();\n if (/^(?:diff --git|@@ |[-+]{3} )/m.test(source)) return \"diff\";\n if (/^[{[]/.test(source) && /[\"\\d}\\]]\\s*$/.test(source)) return \"json\";\n if (/^(?: {\n const last = tokens[tokens.length - 1];\n if (last && last.kind === kind) last.text += text;\n else tokens.push({ kind, text });\n if (text.trim()) previous = kind;\n };\n\n while (index < source.length) {\n const rules = grammar[stack[stack.length - 1]] ?? grammar.code;\n let length = 0;\n\n for (const rule of rules) {\n if (rule.after && previous && !rule.after.includes(previous)) continue;\n rule.re.lastIndex = index;\n const match = rule.re.exec(source);\n if (!match?.[0]) continue;\n\n emit(rule.kind, match[0]);\n length = match[0].length;\n if (rule.push) stack.push(rule.push);\n else if (rule.pop && stack.length > 1) stack.pop();\n break;\n }\n\n // No rule matched: take one character so the scan always advances.\n if (length === 0) {\n emit(\"plain\", source[index]);\n length = 1;\n }\n index += length;\n }\n\n return tokens;\n}\n\nfunction splitLines(tokens: CodeToken[]): CodeLine[] {\n const lines: CodeLine[] = [{ tokens: [] }];\n for (const token of tokens) {\n const parts = token.text.split(\"\\n\");\n parts.forEach((part, index) => {\n if (index > 0) lines.push({ tokens: [] });\n if (part) lines[lines.length - 1].tokens.push({ kind: token.kind, text: part });\n });\n }\n return lines;\n}\n\n/** Diffs are line-based, so the marker decides the whole line. */\nfunction splitDiff(source: string): CodeLine[] {\n return source.split(\"\\n\").map((text) => {\n const change =\n /^(?:@@|diff |index |[-+]{3} )/.test(text)\n ? (\"meta\" as const)\n : text.startsWith(\"+\")\n ? (\"insert\" as const)\n : text.startsWith(\"-\")\n ? (\"delete\" as const)\n : undefined;\n const kind: TokenKind = change ?? \"plain\";\n return { tokens: text ? [{ kind, text }] : [], change };\n });\n}\n\n/**\n * Turn source into lines of colored tokens. Tabs become two spaces and CRLF\n * becomes LF, so what the DOM shows and what the PNG paints are the same\n * string measured the same way.\n */\nexport function tokenizeCode(code: string, language: CodeLanguage): CodeLine[] {\n const source = code.replace(/\\r\\n?/g, \"\\n\").replace(/\\t/g, \" \");\n if (language === \"diff\") return splitDiff(source);\n\n const grammar = grammars[grammarNames[language] ?? \"\"];\n if (!grammar) {\n return source\n .split(\"\\n\")\n .map((text) => ({ tokens: text ? [{ kind: \"plain\" as TokenKind, text }] : [] }));\n }\n return splitLines(scan(source, grammar));\n}\n\n/* ================================================================\n Palettes\n\n Fifteen colors per mode. Everything else is derived in the\n component, so adding a scheme stays a small job.\n ================================================================ */\n\nexport interface CodePalette {\n /** Code surface. */\n bg: string;\n /** Default text, identifiers, punctuation-adjacent plain runs. */\n fg: string;\n /** Gutter, operators, brackets — everything that should recede. */\n muted: string;\n /** Header bar and the collapsed-fade base. */\n band: string;\n /** Caret, focus, badge, line-highlight wash. */\n accent: string;\n comment: string;\n string: string;\n number: string;\n keyword: string;\n /** Function and method names. */\n fn: string;\n type: string;\n attribute: string;\n tag: string;\n insert: string;\n delete: string;\n}\n\nexport interface CodeScheme {\n name: string;\n label: string;\n light: CodePalette;\n dark: CodePalette;\n}\n\nexport const codeSnippetSchemes = [\n {\n name: \"duck\",\n label: \"Duck\",\n dark: {\n bg: \"#1c1c20\",\n fg: \"#e8e8ec\",\n muted: \"#82828d\",\n band: \"#26262c\",\n accent: \"#cbe86a\",\n comment: \"#82828d\",\n string: \"#c3e86a\",\n number: \"#f0b45f\",\n keyword: \"#c58cf5\",\n fn: \"#6ad4bd\",\n type: \"#7cbef8\",\n attribute: \"#f0b45f\",\n tag: \"#ff9095\",\n insert: \"#7fd88f\",\n delete: \"#ff8b8b\",\n },\n light: {\n bg: \"#ffffff\",\n fg: \"#24242a\",\n muted: \"#6c6c76\",\n band: \"#f5f6f0\",\n accent: \"#5d7f14\",\n comment: \"#77777f\",\n string: \"#4d7a10\",\n number: \"#9a5b06\",\n keyword: \"#7b3bc4\",\n fn: \"#08736a\",\n type: \"#1863b8\",\n attribute: \"#9a5b06\",\n tag: \"#b32b3a\",\n insert: \"#227a33\",\n delete: \"#b3242f\",\n },\n },\n {\n name: \"pond\",\n label: \"Pond\",\n dark: {\n bg: \"#0e1b21\",\n fg: \"#d7e9ee\",\n muted: \"#6d8d96\",\n band: \"#16272f\",\n accent: \"#58cfe0\",\n comment: \"#64848d\",\n string: \"#86e0c4\",\n number: \"#f2c68a\",\n keyword: \"#66c9f2\",\n fn: \"#a8dff0\",\n type: \"#b6b0ff\",\n attribute: \"#f2c68a\",\n tag: \"#6fe0d0\",\n insert: \"#7ad6a8\",\n delete: \"#ff9a9a\",\n },\n light: {\n bg: \"#f9fdff\",\n fg: \"#17323b\",\n muted: \"#4f7480\",\n band: \"#e7f4f8\",\n accent: \"#0f7f96\",\n comment: \"#5c8290\",\n string: \"#0d6b57\",\n number: \"#8a5310\",\n keyword: \"#0b6a91\",\n fn: \"#116c86\",\n type: \"#4b45c9\",\n attribute: \"#8a5310\",\n tag: \"#0a7b6e\",\n insert: \"#1c7a4d\",\n delete: \"#ab2f2f\",\n },\n },\n {\n name: \"sunset\",\n label: \"Sunset\",\n dark: {\n bg: \"#241a1f\",\n fg: \"#f6e6e2\",\n muted: \"#a2848a\",\n band: \"#30222a\",\n accent: \"#ff9d76\",\n comment: \"#9a7d84\",\n string: \"#ffc38a\",\n number: \"#ffd76b\",\n keyword: \"#ff8fa3\",\n fn: \"#ffb37c\",\n type: \"#f2a5ff\",\n attribute: \"#ffd76b\",\n tag: \"#ff9d76\",\n insert: \"#b7e08a\",\n delete: \"#ff8080\",\n },\n light: {\n bg: \"#fffaf6\",\n fg: \"#3a2226\",\n muted: \"#8a6a6f\",\n band: \"#fdeee2\",\n accent: \"#cf5b28\",\n comment: \"#8a6a6f\",\n string: \"#a4560d\",\n number: \"#8f5a00\",\n keyword: \"#c2325a\",\n fn: \"#b04a12\",\n type: \"#8b3fb0\",\n attribute: \"#8f5a00\",\n tag: \"#b3441c\",\n insert: \"#2f7a33\",\n delete: \"#b52626\",\n },\n },\n {\n name: \"neon\",\n label: \"Neon\",\n dark: {\n bg: \"#0b0b16\",\n fg: \"#e9e9ff\",\n muted: \"#6f6f9c\",\n band: \"#15152b\",\n accent: \"#ff5ed2\",\n comment: \"#5f5f8f\",\n string: \"#6cffc7\",\n number: \"#ffe066\",\n keyword: \"#ff5ed2\",\n fn: \"#5fe6ff\",\n type: \"#b57cff\",\n attribute: \"#ffb14d\",\n tag: \"#ff5e7a\",\n insert: \"#5dff9b\",\n delete: \"#ff5e7a\",\n },\n light: {\n bg: \"#fdfbff\",\n fg: \"#1d1b33\",\n muted: \"#6a6690\",\n band: \"#f4eefe\",\n accent: \"#b3007f\",\n comment: \"#6a6690\",\n string: \"#0a7a5c\",\n number: \"#8a6100\",\n keyword: \"#b3007f\",\n fn: \"#0a6f8a\",\n type: \"#6a2fb5\",\n attribute: \"#9a5a00\",\n tag: \"#b8143a\",\n insert: \"#0f7a44\",\n delete: \"#b8143a\",\n },\n },\n {\n name: \"paper\",\n label: \"Paper\",\n dark: {\n bg: \"#221f1a\",\n fg: \"#eee7d9\",\n muted: \"#9a9182\",\n band: \"#2c2822\",\n accent: \"#d8a273\",\n comment: \"#948b7c\",\n string: \"#b9cc8a\",\n number: \"#e0b978\",\n keyword: \"#e8998f\",\n fn: \"#9dc0d6\",\n type: \"#c0aee8\",\n attribute: \"#e0b978\",\n tag: \"#e8998f\",\n insert: \"#a4d6a4\",\n delete: \"#e89b9b\",\n },\n light: {\n bg: \"#fbf7ef\",\n fg: \"#2f2a24\",\n muted: \"#8a8073\",\n band: \"#f2ebdc\",\n accent: \"#8a5a2f\",\n comment: \"#8a8073\",\n string: \"#4f6b2a\",\n number: \"#8a5a00\",\n keyword: \"#8a2f2f\",\n fn: \"#2f5f7a\",\n type: \"#5b4a8a\",\n attribute: \"#8a5a00\",\n tag: \"#8a2f2f\",\n insert: \"#3f7a3f\",\n delete: \"#a33a3a\",\n },\n },\n {\n name: \"mono\",\n label: \"Mono\",\n dark: {\n bg: \"#1a1a1a\",\n fg: \"#e6e6e6\",\n muted: \"#8c8c8c\",\n band: \"#252525\",\n accent: \"#bdbdbd\",\n comment: \"#7a7a7a\",\n string: \"#cfcfcf\",\n number: \"#d6d6d6\",\n keyword: \"#ffffff\",\n fn: \"#c8c8c8\",\n type: \"#dcdcdc\",\n attribute: \"#b4b4b4\",\n tag: \"#ffffff\",\n insert: \"#d0d0d0\",\n delete: \"#9a9a9a\",\n },\n light: {\n bg: \"#ffffff\",\n fg: \"#1f1f1f\",\n muted: \"#757575\",\n band: \"#f2f2f2\",\n accent: \"#4a4a4a\",\n comment: \"#6f6f6f\",\n string: \"#3d3d3d\",\n number: \"#454545\",\n keyword: \"#000000\",\n fn: \"#2a2a2a\",\n type: \"#333333\",\n attribute: \"#4f4f4f\",\n tag: \"#000000\",\n insert: \"#2f2f2f\",\n delete: \"#767676\",\n },\n },\n] as const satisfies readonly CodeScheme[];\n\nexport type CodeSchemeName = (typeof codeSnippetSchemes)[number][\"name\"];\n\nexport function getCodeScheme(name: CodeSchemeName): CodeScheme {\n return codeSnippetSchemes.find((scheme) => scheme.name === name) ?? codeSnippetSchemes[0];\n}\n", "type": "registry:ui" } ], "type": "registry:ui" }