{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "code-snippet", "title": "Code Snippet", "description": "A read-only syntax-highlighted code viewer powered by Shiki with CSS variable theming.", "dependencies": [ "shiki", "class-variance-authority", "lucide-react" ], "registryDependencies": [ "https://ds.formance.com/r/code-themes.json" ], "files": [ { "path": "registry/default/ui/code/code-snippet.tsx", "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Check, Copy } from 'lucide-react';\nimport { type ReactNode, useEffect, useState } from 'react';\n\nimport { cn } from '@/lib/utils';\nimport { Button } from '@/registry/default/ui/button';\nimport {\n cssVarsTheme,\n getHighlighter,\n type TCodeLanguage,\n} from '@/registry/default/ui/code/code-themes';\nimport {\n renderHast,\n type HastNode,\n} from '@/registry/default/ui/code/render-hast';\n\n// ---------------------------------------------------------------------------\n// Variants\n// ---------------------------------------------------------------------------\n\nconst codeSnippetVariants = cva(\n 'not-prose overflow-hidden rounded-lg font-mono [&>pre]:overflow-x-scroll [&>pre]:[scrollbar-width:none] [&>pre::-webkit-scrollbar]:hidden [&_code]:font-mono',\n {\n variants: {\n size: {\n sm: 'text-sm [&>pre]:p-3',\n md: 'text-base [&>pre]:p-4',\n lg: 'text-lg [&>pre]:p-6',\n },\n bordered: {\n true: 'border border-border',\n false: '',\n },\n isSingleLine: {\n true: '[&>pre]:whitespace-nowrap',\n false: '',\n },\n },\n defaultVariants: {\n size: 'sm',\n bordered: true,\n isSingleLine: false,\n },\n }\n);\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\ntype TCodeSnippetProps = {\n /** Source code to highlight */\n code: string;\n /** Language for syntax highlighting */\n language?: TCodeLanguage;\n /** Show line numbers gutter */\n showLineNumbers?: boolean;\n /** Show copy-to-clipboard button */\n canCopy?: boolean;\n /**\n * Render a top bar showing the language label on the left and the copy\n * button on the right, instead of the floating hover copy button.\n */\n showHeader?: boolean;\n /**\n * Extra actions rendered alongside the copy button — in the header\n * (when `showHeader`) or in the floating hover cluster otherwise.\n */\n headerActions?: ReactNode;\n /** Force a specific theme instead of inheriting from the document */\n isDark?: boolean;\n /** Additional class names on the outer wrapper */\n className?: string;\n} & VariantProps;\n\nfunction CodeSnippet({\n code,\n language = 'typescript',\n showLineNumbers = false,\n canCopy = true,\n showHeader = false,\n headerActions,\n size,\n bordered,\n isDark,\n isSingleLine,\n className,\n}: TCodeSnippetProps) {\n const [highlighted, setHighlighted] = useState(null);\n const [copied, setCopied] = useState(false);\n\n useEffect(() => {\n let cancelled = false;\n\n (async () => {\n if (!code) {\n setHighlighted(null);\n\n return;\n }\n\n const highlighter = await getHighlighter();\n // Fall back to plaintext for any language the highlighter didn't load,\n // so an unsupported `language` renders unhighlighted instead of throwing.\n const safeLang = highlighter.getLoadedLanguages().includes(language)\n ? language\n : 'plaintext';\n const result = highlighter.codeToHast(code, {\n lang: safeLang,\n theme: cssVarsTheme.name!,\n transformers: showLineNumbers\n ? [\n {\n name: 'line-numbers',\n line(node, line) {\n node.properties['data-line'] = line;\n node.children.unshift({\n type: 'element',\n tagName: 'span',\n properties: {\n class: 'line-number',\n style:\n 'color: var(--shiki-token-comment); margin-right: 1rem; user-select: none; display: inline-block; width: 2em; text-align: right;',\n },\n children: [{ type: 'text', value: String(line) }],\n });\n },\n },\n ]\n : [],\n });\n\n if (!cancelled) setHighlighted(renderHast(result as HastNode));\n })();\n\n return () => {\n cancelled = true;\n };\n }, [code, language, showLineNumbers]);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(code.trim());\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const dataShikiTheme =\n isDark !== undefined\n ? { 'data-shiki-theme': isDark ? 'dark' : 'light' }\n : {};\n\n const codeAreaClassName = cn(\n codeSnippetVariants({\n size,\n bordered: showHeader ? false : bordered,\n isSingleLine,\n }),\n showHeader && 'rounded-none'\n );\n\n const codeArea = highlighted ? (\n
{highlighted}
\n ) : (\n
\n
\n        {code}\n      
\n
\n );\n\n const copyButton = canCopy && (\n \n {copied ? : }\n \n );\n\n if (showHeader) {\n return (\n \n
\n \n {language}\n \n
\n {headerActions}\n {copyButton}\n
\n
\n {codeArea}\n \n );\n }\n\n return (\n \n {codeArea}\n {(copyButton || headerActions) && (\n
\n {headerActions}\n {copyButton}\n
\n )}\n \n );\n}\n\nexport { CodeSnippet, codeSnippetVariants, type TCodeSnippetProps };\n", "type": "registry:ui", "target": "components/code/code-snippet.tsx" }, { "path": "registry/default/ui/code/render-hast.tsx", "content": "import { Fragment } from 'react';\nimport type { CSSProperties, ElementType, ReactNode } from 'react';\n\nconst ALLOWED_TAG_NAMES = new Set(['pre', 'code', 'span', 'div', 'br']);\n\nexport type HastNode = {\n type: string;\n tagName?: string;\n value?: string;\n properties?: Record;\n children?: HastNode[];\n};\n\nfunction toCamelCase(property: string) {\n if (property.startsWith('--')) {\n return property;\n }\n\n return property.replace(/-([a-z])/g, (_, letter: string) =>\n letter.toUpperCase()\n );\n}\n\nfunction parseStyle(style: unknown): CSSProperties | undefined {\n if (typeof style !== 'string') {\n return undefined;\n }\n\n return style.split(';').reduce>((styles, rule) => {\n const [property, ...valueParts] = rule.split(':');\n const value = valueParts.join(':').trim();\n\n if (property && value) {\n styles[toCamelCase(property.trim())] = value;\n }\n\n return styles;\n }, {}) as CSSProperties;\n}\n\nfunction normalizePropertyValue(value: unknown) {\n return Array.isArray(value) ? value.join(' ') : value;\n}\n\n/**\n * Properties come from the syntax highlighter, never from the highlighted\n * source, so they cannot carry a sink — but the invariant is enforced here\n * rather than assumed, since these keys would let a hast node opt back out of\n * React's escaping or hijack the element identity.\n */\nconst FORBIDDEN_PROPERTIES = new Set(['dangerouslySetInnerHTML', 'ref', 'key']);\n\nfunction getReactProps(properties: Record = {}) {\n return Object.entries(properties).reduce>(\n (props, [key, value]) => {\n if (FORBIDDEN_PROPERTIES.has(key)) {\n return props;\n }\n\n if (key === 'class' || key === 'className') {\n props.className = normalizePropertyValue(value);\n } else if (key === 'style') {\n props.style = parseStyle(value);\n } else if (key === 'tabindex') {\n props.tabIndex = Number(value);\n } else {\n props[key] = normalizePropertyValue(value);\n }\n\n return props;\n },\n {}\n );\n}\n\nfunction renderHastNode(node: HastNode, key: string): ReactNode {\n if (node.type === 'text') {\n return node.value ?? '';\n }\n\n const children = node.children?.map((child, index) =>\n renderHastNode(child, `${key}-${index}`)\n );\n\n if (node.type === 'root') {\n return <>{children};\n }\n\n if (node.type === 'element' && node.tagName) {\n if (!ALLOWED_TAG_NAMES.has(node.tagName)) {\n return {children};\n }\n\n const Tag = node.tagName as ElementType;\n\n return (\n \n {children}\n \n );\n }\n\n return null;\n}\n\nexport function renderHast(node: HastNode): ReactNode {\n return renderHastNode(node, 'root');\n}\n", "type": "registry:ui", "target": "components/code/render-hast.tsx" } ], "type": "registry:ui" }