Files
zeekay f21b520d17 fix: Correct snippet component reference and fix syntax highlighting
- Update snippet.mdx to reference correct component name (code-snippet-demo)
- Create proper working demo for CodeSnippet component with TypeScript example
- Fix syntax highlighting by updating to Shiki v3 API with bundledLanguages
- Add language validation to prevent unsupported language errors
- Improve error handling with HTML escaping fallback
- Rebuild registry with updated component
2025-11-06 13:45:30 -08:00

14 lines
11 KiB
JSON

{
"name": "code-snippet",
"description": "Inline code snippet with syntax highlighting",
"dependencies": [
"prism-react-renderer"
],
"files": [
{
"name": "code-snippet.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Check, Copy, FileText, Maximize2, Minimize2 } from \"lucide-react\"\nimport { bundledLanguages, codeToHtml } from \"shiki\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/registry/default/ui/badge\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport { Skeleton } from \"@/registry/default/ui/skeleton\"\n\nconst codeSnippetVariants = cva(\"relative group\", {\n variants: {\n variant: {\n default: \"rounded-lg border bg-muted/50\",\n inline: \"inline-block rounded-md bg-muted px-2 py-1\",\n minimal: \"bg-transparent\",\n },\n theme: {\n dark: \"bg-slate-950 border-slate-800\",\n light: \"bg-slate-50 border-slate-200\",\n github: \"bg-white border-slate-200\",\n \"github-dark\": \"bg-slate-900 border-slate-700\",\n terminal: \"bg-black border-green-500/30 text-green-400\",\n retro: \"bg-amber-50 border-amber-200 text-amber-900\",\n },\n size: {\n xs: \"text-xs\",\n sm: \"text-sm\",\n default: \"text-sm\",\n lg: \"text-base\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n theme: \"dark\",\n size: \"default\",\n },\n})\n\nexport interface CodeSnippetProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof codeSnippetVariants> {\n code: string\n language?: string\n filename?: string\n showLineNumbers?: boolean\n highlightLines?: number[]\n showCopyButton?: boolean\n showLanguageBadge?: boolean\n showHeader?: boolean\n maxHeight?: string\n expandable?: boolean\n collapsedHeight?: string\n wrapLines?: boolean\n startLineNumber?: number\n}\n\nconst CodeSnippet = React.forwardRef<HTMLDivElement, CodeSnippetProps>(\n (\n {\n className,\n variant,\n theme,\n size,\n code,\n language = \"text\",\n filename,\n showLineNumbers = false,\n highlightLines = [],\n showCopyButton = true,\n showLanguageBadge = true,\n showHeader = true,\n maxHeight,\n expandable = false,\n collapsedHeight = \"200px\",\n wrapLines = false,\n startLineNumber = 1,\n ...props\n },\n ref\n ) => {\n const [highlightedCode, setHighlightedCode] = React.useState<string>(\"\")\n const [copied, setCopied] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const [isExpanded, setIsExpanded] = React.useState(!expandable)\n\n const themeMap = {\n dark: \"github-dark\",\n light: \"github-light\",\n github: \"github-light\",\n \"github-dark\": \"github-dark\",\n terminal: \"github-dark\",\n retro: \"github-light\",\n }\n\n React.useEffect(() => {\n const highlightCode = async () => {\n setIsLoading(true)\n try {\n // Check if language is supported\n const supportedLang = Object.keys(bundledLanguages).includes(language)\n\n const highlighted = await codeToHtml(code, {\n lang: supportedLang ? language : \"text\",\n theme: themeMap[theme || \"dark\"],\n })\n\n // Extract just the inner HTML content\n const tempDiv = document.createElement(\"div\")\n tempDiv.innerHTML = highlighted\n const codeElement = tempDiv.querySelector(\"code\")\n setHighlightedCode(codeElement?.innerHTML || highlighted)\n } catch (error) {\n console.error(\"Failed to highlight code:\", error)\n // Fallback to plain text with basic HTML escaping\n const escaped = code\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n setHighlightedCode(escaped)\n } finally {\n setIsLoading(false)\n }\n }\n\n highlightCode()\n }, [code, language, theme])\n\n const copyToClipboard = React.useCallback(async () => {\n try {\n await navigator.clipboard.writeText(code)\n setCopied(true)\n setTimeout(() => setCopied(false), 2000)\n } catch (error) {\n console.error(\"Failed to copy code:\", error)\n }\n }, [code])\n\n const lines = highlightedCode.split(\"\\n\").filter((line, index, array) => {\n // Keep empty lines except for the last one if it's empty\n return line.trim() !== \"\" || index < array.length - 1\n })\n\n const renderInline = () => (\n <code\n className={cn(\n codeSnippetVariants({ variant, theme, size }),\n \"font-mono\",\n className\n )}\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n />\n )\n\n const renderBlock = () => (\n <div\n ref={ref}\n className={cn(\n codeSnippetVariants({ variant, theme, size }),\n \"overflow-hidden\",\n className\n )}\n {...props}\n >\n {/* Header */}\n {showHeader &&\n (filename || language || showCopyButton || expandable) && (\n <div className=\"flex items-center justify-between border-b px-4 py-2 bg-muted/30\">\n <div className=\"flex items-center gap-2\">\n {filename && (\n <div className=\"flex items-center gap-1.5\">\n <FileText className=\"h-3.5 w-3.5 text-muted-foreground\" />\n <span className=\"text-sm font-medium\">{filename}</span>\n </div>\n )}\n {showLanguageBadge && language && (\n <Badge variant=\"secondary\" className=\"text-xs\">\n {language}\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-1\">\n {expandable && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setIsExpanded(!isExpanded)}\n className=\"h-8 w-8 p-0\"\n >\n {isExpanded ? (\n <Minimize2 className=\"h-3.5 w-3.5\" />\n ) : (\n <Maximize2 className=\"h-3.5 w-3.5\" />\n )}\n <span className=\"sr-only\">\n {isExpanded ? \"Collapse\" : \"Expand\"}\n </span>\n </Button>\n )}\n {showCopyButton && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={copyToClipboard}\n className=\"h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n {copied ? (\n <Check className=\"h-3.5 w-3.5 text-green-500\" />\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n <span className=\"sr-only\">\n {copied ? \"Copied\" : \"Copy code\"}\n </span>\n </Button>\n )}\n </div>\n </div>\n )}\n\n {/* Content */}\n <div\n className={cn(\"overflow-auto\", !wrapLines && \"overflow-x-auto\")}\n style={{\n maxHeight: isExpanded ? maxHeight : collapsedHeight,\n }}\n >\n {isLoading ? (\n <div className=\"p-4 space-y-2\">\n {Array.from({ length: Math.min(lines.length || 5, 10) }).map(\n (_, i) => (\n <div key={i} className=\"flex items-center gap-4\">\n {showLineNumbers && <Skeleton className=\"h-4 w-8\" />}\n <Skeleton className=\"h-4 flex-1\" />\n </div>\n )\n )}\n </div>\n ) : (\n <div className=\"font-mono\">\n {lines.map((line, index) => {\n const lineNumber = index + startLineNumber\n const isHighlighted = highlightLines.includes(lineNumber)\n\n return (\n <div\n key={index}\n className={cn(\n \"group relative flex min-h-[1.5rem] items-center px-4 py-0.5\",\n isHighlighted &&\n \"bg-blue-500/10 border-l-2 border-l-blue-500\",\n !wrapLines && \"whitespace-nowrap\"\n )}\n >\n {showLineNumbers && (\n <span className=\"mr-4 inline-block w-8 select-none text-right text-xs text-muted-foreground\">\n {lineNumber}\n </span>\n )}\n <div\n className={cn(\n \"flex-1\",\n wrapLines\n ? \"whitespace-pre-wrap break-words\"\n : \"overflow-x-auto\"\n )}\n dangerouslySetInnerHTML={{ __html: line }}\n />\n </div>\n )\n })}\n </div>\n )}\n </div>\n\n {/* Expand indicator */}\n {expandable && !isExpanded && (\n <div className=\"absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-background to-transparent pointer-events-none\" />\n )}\n\n {/* Copy button for minimal variant */}\n {variant === \"minimal\" && showCopyButton && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={copyToClipboard}\n className=\"absolute top-2 right-2 h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n {copied ? (\n <Check className=\"h-3.5 w-3.5 text-green-500\" />\n ) : (\n <Copy className=\"h-3.5 w-3.5\" />\n )}\n </Button>\n )}\n </div>\n )\n\n if (variant === \"inline\") {\n return renderInline()\n }\n\n return renderBlock()\n }\n)\n\nCodeSnippet.displayName = \"CodeSnippet\"\n\n// Helper component for inline code\nconst InlineCode = React.forwardRef<\n HTMLElement,\n React.HTMLAttributes<HTMLElement> & {\n children: string\n language?: string\n }\n>(({ children, language, className, ...props }, ref) => {\n return (\n <CodeSnippet\n ref={ref as any}\n code={children}\n language={language}\n variant=\"inline\"\n showCopyButton={false}\n showLanguageBadge={false}\n showHeader={false}\n className={className}\n {...props}\n />\n )\n})\n\nInlineCode.displayName = \"InlineCode\"\n\nexport { CodeSnippet, InlineCode, codeSnippetVariants }\n"
}
],
"type": "components:code"
}