mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-11-03 03:18:52 +00:00
- Updated build-registry.mts to include all component types (ui, ai, 3d, animation, code, etc.) - Fixed missing components issue by processing all components that start with 'components:' - Now properly handles components that only exist in one theme variant - Registry now contains 363 components (up from 117) - AI, 3D, animation, and code components are now accessible via the registry
14 lines
18 KiB
JSON
14 lines
18 KiB
JSON
{
|
|
"name": "code-diff",
|
|
"description": "Code difference viewer with highlighting",
|
|
"dependencies": [
|
|
"diff"
|
|
],
|
|
"files": [
|
|
{
|
|
"name": "code-diff.tsx",
|
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport {\n Check,\n Copy,\n GitCompare,\n Minus,\n Plus,\n SplitSquareHorizontal,\n Square,\n} from \"lucide-react\"\nimport { codeToHtml } from \"shiki\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/registry/default/ui/badge\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport { Separator } from \"@/registry/default/ui/separator\"\nimport { Skeleton } from \"@/registry/default/ui/skeleton\"\nimport {\n ToggleGroup,\n ToggleGroupItem,\n} from \"@/registry/default/ui/toggle-group\"\n\nconst codeDiffVariants = cva(\n \"relative overflow-hidden rounded-lg border bg-muted/50\",\n {\n variants: {\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 },\n size: {\n sm: \"text-xs\",\n default: \"text-sm\",\n lg: \"text-base\",\n },\n },\n defaultVariants: {\n theme: \"dark\",\n size: \"default\",\n },\n }\n)\n\nexport interface DiffChange {\n type: \"add\" | \"remove\" | \"unchanged\"\n lineNumber: number\n content: string\n oldLineNumber?: number\n newLineNumber?: number\n}\n\nexport interface CodeDiffProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof codeDiffVariants> {\n oldCode: string\n newCode: string\n language?: string\n filename?: string\n oldFilename?: string\n newFilename?: string\n showLineNumbers?: boolean\n showCopyButton?: boolean\n maxHeight?: string\n defaultView?: \"unified\" | \"split\"\n collapseUnchanged?: boolean\n contextLines?: number\n}\n\ninterface DiffLineProps {\n change: DiffChange\n showLineNumbers: boolean\n isUnified?: boolean\n highlightedContent?: string\n}\n\nconst DiffLine = React.memo(\n ({\n change,\n showLineNumbers,\n isUnified = false,\n highlightedContent,\n }: DiffLineProps) => {\n const getLineClasses = () => {\n const base = \"group relative flex min-h-[1.5rem] items-center py-0.5\"\n\n if (isUnified) {\n switch (change.type) {\n case \"add\":\n return cn(base, \"bg-green-500/10 border-l-2 border-l-green-500\")\n case \"remove\":\n return cn(base, \"bg-red-500/10 border-l-2 border-l-red-500\")\n default:\n return cn(base, \"hover:bg-muted/50\")\n }\n }\n\n return cn(base, \"hover:bg-muted/50\")\n }\n\n const getLinePrefix = () => {\n if (!isUnified) return null\n\n switch (change.type) {\n case \"add\":\n return <Plus className=\"h-3 w-3 text-green-500 mr-2 flex-shrink-0\" />\n case \"remove\":\n return <Minus className=\"h-3 w-3 text-red-500 mr-2 flex-shrink-0\" />\n default:\n return <span className=\"w-3 mr-2 flex-shrink-0\" />\n }\n }\n\n return (\n <div className={getLineClasses()}>\n <div className=\"flex items-center px-4 w-full\">\n {showLineNumbers && (\n <div className=\"flex items-center gap-2 mr-4\">\n {!isUnified ? (\n <>\n <span className=\"inline-block w-8 select-none text-right text-xs text-muted-foreground\">\n {change.oldLineNumber || \"\"}\n </span>\n <span className=\"inline-block w-8 select-none text-right text-xs text-muted-foreground\">\n {change.newLineNumber || \"\"}\n </span>\n </>\n ) : (\n <span className=\"inline-block w-8 select-none text-right text-xs text-muted-foreground\">\n {change.lineNumber}\n </span>\n )}\n </div>\n )}\n {getLinePrefix()}\n <div\n className=\"flex-1 overflow-x-auto font-mono\"\n dangerouslySetInnerHTML={{\n __html: highlightedContent || change.content,\n }}\n />\n </div>\n </div>\n )\n }\n)\n\nDiffLine.displayName = \"DiffLine\"\n\n// Simple diff algorithm to generate changes\nfunction generateDiff(oldCode: string, newCode: string): DiffChange[] {\n const oldLines = oldCode.split(\"\\n\")\n const newLines = newCode.split(\"\\n\")\n const changes: DiffChange[] = []\n\n let oldIndex = 0\n let newIndex = 0\n let lineNumber = 1\n\n while (oldIndex < oldLines.length || newIndex < newLines.length) {\n const oldLine = oldLines[oldIndex]\n const newLine = newLines[newIndex]\n\n if (oldIndex >= oldLines.length) {\n // Only new lines left\n changes.push({\n type: \"add\",\n lineNumber: lineNumber++,\n content: newLine,\n newLineNumber: newIndex + 1,\n })\n newIndex++\n } else if (newIndex >= newLines.length) {\n // Only old lines left\n changes.push({\n type: \"remove\",\n lineNumber: lineNumber++,\n content: oldLine,\n oldLineNumber: oldIndex + 1,\n })\n oldIndex++\n } else if (oldLine === newLine) {\n // Lines are the same\n changes.push({\n type: \"unchanged\",\n lineNumber: lineNumber++,\n content: oldLine,\n oldLineNumber: oldIndex + 1,\n newLineNumber: newIndex + 1,\n })\n oldIndex++\n newIndex++\n } else {\n // Lines are different - for simplicity, treat as remove + add\n changes.push({\n type: \"remove\",\n lineNumber: lineNumber++,\n content: oldLine,\n oldLineNumber: oldIndex + 1,\n })\n changes.push({\n type: \"add\",\n lineNumber: lineNumber++,\n content: newLine,\n newLineNumber: newIndex + 1,\n })\n oldIndex++\n newIndex++\n }\n }\n\n return changes\n}\n\nconst CodeDiff = React.forwardRef<HTMLDivElement, CodeDiffProps>(\n (\n {\n className,\n theme,\n size,\n oldCode,\n newCode,\n language = \"text\",\n filename,\n oldFilename,\n newFilename,\n showLineNumbers = true,\n showCopyButton = true,\n maxHeight = \"500px\",\n defaultView = \"unified\",\n collapseUnchanged = false,\n contextLines = 3,\n ...props\n },\n ref\n ) => {\n const [view, setView] = React.useState<\"unified\" | \"split\">(defaultView)\n const [copied, setCopied] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const [highlightedOldCode, setHighlightedOldCode] =\n React.useState<string>(\"\")\n const [highlightedNewCode, setHighlightedNewCode] =\n React.useState<string>(\"\")\n const [changes, setChanges] = React.useState<DiffChange[]>([])\n\n const themeMap = {\n dark: \"github-dark\",\n light: \"github-light\",\n github: \"github-light\",\n \"github-dark\": \"github-dark\",\n }\n\n React.useEffect(() => {\n const highlightCode = async () => {\n setIsLoading(true)\n try {\n const [oldHighlighted, newHighlighted] = await Promise.all([\n codeToHtml(oldCode, {\n lang: language,\n theme: themeMap[theme || \"dark\"],\n }),\n codeToHtml(newCode, {\n lang: language,\n theme: themeMap[theme || \"dark\"],\n }),\n ])\n\n // Extract just the inner HTML content\n const extractContent = (html: string) => {\n const tempDiv = document.createElement(\"div\")\n tempDiv.innerHTML = html\n const codeElement = tempDiv.querySelector(\"code\")\n return codeElement?.innerHTML || html\n }\n\n setHighlightedOldCode(extractContent(oldHighlighted))\n setHighlightedNewCode(extractContent(newHighlighted))\n\n // Generate diff changes\n const diffChanges = generateDiff(oldCode, newCode)\n setChanges(diffChanges)\n } catch (error) {\n console.error(\"Failed to highlight code:\", error)\n setHighlightedOldCode(oldCode)\n setHighlightedNewCode(newCode)\n setChanges(generateDiff(oldCode, newCode))\n } finally {\n setIsLoading(false)\n }\n }\n\n highlightCode()\n }, [oldCode, newCode, language, theme])\n\n const copyToClipboard = React.useCallback(async () => {\n try {\n const diffText =\n view === \"unified\"\n ? changes\n .map((change) => {\n const prefix =\n change.type === \"add\"\n ? \"+\"\n : change.type === \"remove\"\n ? \"-\"\n : \" \"\n return `${prefix}${change.content}`\n })\n .join(\"\\n\")\n : `--- ${oldFilename || \"old\"}\\n+++ ${newFilename || \"new\"}\\n${oldCode}\\n\\n${newCode}`\n\n await navigator.clipboard.writeText(diffText)\n setCopied(true)\n setTimeout(() => setCopied(false), 2000)\n } catch (error) {\n console.error(\"Failed to copy diff:\", error)\n }\n }, [changes, view, oldCode, newCode, oldFilename, newFilename])\n\n const filteredChanges = React.useMemo(() => {\n if (!collapseUnchanged) return changes\n\n const result: DiffChange[] = []\n let i = 0\n\n while (i < changes.length) {\n const change = changes[i]\n\n if (change.type !== \"unchanged\") {\n result.push(change)\n i++\n continue\n }\n\n // Find consecutive unchanged lines\n const unchangedStart = i\n while (i < changes.length && changes[i].type === \"unchanged\") {\n i++\n }\n\n const unchangedCount = i - unchangedStart\n\n if (unchangedCount <= contextLines * 2) {\n // Include all unchanged lines if there aren't many\n for (let j = unchangedStart; j < i; j++) {\n result.push(changes[j])\n }\n } else {\n // Include context lines at the beginning\n for (let j = unchangedStart; j < unchangedStart + contextLines; j++) {\n result.push(changes[j])\n }\n\n // Add collapse indicator\n result.push({\n type: \"unchanged\",\n lineNumber: -1,\n content: `... ${unchangedCount - contextLines * 2} unchanged lines ...`,\n })\n\n // Include context lines at the end\n for (let j = i - contextLines; j < i; j++) {\n result.push(changes[j])\n }\n }\n }\n\n return result\n }, [changes, collapseUnchanged, contextLines])\n\n const stats = React.useMemo(() => {\n const additions = changes.filter((c) => c.type === \"add\").length\n const deletions = changes.filter((c) => c.type === \"remove\").length\n return { additions, deletions }\n }, [changes])\n\n const renderUnifiedView = () => {\n const oldLines = highlightedOldCode.split(\"\\n\")\n const newLines = highlightedNewCode.split(\"\\n\")\n\n return (\n <div className=\"font-mono\">\n {filteredChanges.map((change, index) => {\n if (change.lineNumber === -1) {\n // Collapse indicator\n return (\n <div\n key={index}\n className=\"flex items-center justify-center py-2 text-muted-foreground bg-muted/30\"\n >\n <span className=\"text-xs\">{change.content}</span>\n </div>\n )\n }\n\n let highlightedContent = change.content\n if (change.type === \"add\" && change.newLineNumber) {\n highlightedContent =\n newLines[change.newLineNumber - 1] || change.content\n } else if (change.type === \"remove\" && change.oldLineNumber) {\n highlightedContent =\n oldLines[change.oldLineNumber - 1] || change.content\n } else if (change.type === \"unchanged\" && change.oldLineNumber) {\n highlightedContent =\n oldLines[change.oldLineNumber - 1] || change.content\n }\n\n return (\n <DiffLine\n key={index}\n change={change}\n showLineNumbers={showLineNumbers}\n isUnified={true}\n highlightedContent={highlightedContent}\n />\n )\n })}\n </div>\n )\n }\n\n const renderSplitView = () => {\n const oldLines = highlightedOldCode.split(\"\\n\")\n const newLines = highlightedNewCode.split(\"\\n\")\n\n return (\n <div className=\"grid grid-cols-2 gap-0\">\n {/* Old Code */}\n <div className=\"border-r\">\n <div className=\"border-b px-4 py-2 bg-red-500/10\">\n <span className=\"text-sm font-medium text-red-600 dark:text-red-400\">\n {oldFilename || filename || \"Old\"}\n </span>\n </div>\n <div className=\"font-mono\">\n {oldLines.map((line, index) => (\n <DiffLine\n key={`old-${index}`}\n change={{\n type: \"remove\",\n lineNumber: index + 1,\n content: line,\n oldLineNumber: index + 1,\n }}\n showLineNumbers={showLineNumbers}\n highlightedContent={line}\n />\n ))}\n </div>\n </div>\n\n {/* New Code */}\n <div>\n <div className=\"border-b px-4 py-2 bg-green-500/10\">\n <span className=\"text-sm font-medium text-green-600 dark:text-green-400\">\n {newFilename || filename || \"New\"}\n </span>\n </div>\n <div className=\"font-mono\">\n {newLines.map((line, index) => (\n <DiffLine\n key={`new-${index}`}\n change={{\n type: \"add\",\n lineNumber: index + 1,\n content: line,\n newLineNumber: index + 1,\n }}\n showLineNumbers={showLineNumbers}\n highlightedContent={line}\n />\n ))}\n </div>\n </div>\n </div>\n )\n }\n\n return (\n <div\n ref={ref}\n className={cn(codeDiffVariants({ theme, size, className }))}\n {...props}\n >\n {/* Header */}\n <div className=\"flex items-center justify-between border-b px-4 py-2\">\n <div className=\"flex items-center gap-4\">\n <div className=\"flex items-center gap-2\">\n <GitCompare className=\"h-4 w-4 text-muted-foreground\" />\n {filename && (\n <span className=\"text-sm font-medium\">{filename}</span>\n )}\n {language && (\n <Badge variant=\"secondary\" className=\"text-xs\">\n {language}\n </Badge>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span className=\"flex items-center gap-1\">\n <Plus className=\"h-3 w-3 text-green-500\" />\n {stats.additions}\n </span>\n <span className=\"flex items-center gap-1\">\n <Minus className=\"h-3 w-3 text-red-500\" />\n {stats.deletions}\n </span>\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <ToggleGroup\n type=\"single\"\n value={view}\n onValueChange={(value) =>\n value && setView(value as \"unified\" | \"split\")\n }\n size=\"sm\"\n >\n <ToggleGroupItem value=\"unified\" aria-label=\"Unified view\">\n <Square className=\"h-3 w-3\" />\n </ToggleGroupItem>\n <ToggleGroupItem value=\"split\" aria-label=\"Split view\">\n <SplitSquareHorizontal className=\"h-3 w-3\" />\n </ToggleGroupItem>\n </ToggleGroup>\n\n {showCopyButton && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={copyToClipboard}\n className=\"h-8 w-8 p-0\"\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 diff\"}\n </span>\n </Button>\n )}\n </div>\n </div>\n\n {/* Content */}\n <div className=\"overflow-auto\" style={{ maxHeight }}>\n {isLoading ? (\n <div className=\"p-4 space-y-2\">\n {Array.from({ length: 10 }).map((_, 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 </div>\n ) : view === \"unified\" ? (\n renderUnifiedView()\n ) : (\n renderSplitView()\n )}\n </div>\n </div>\n )\n }\n)\n\nCodeDiff.displayName = \"CodeDiff\"\n\nexport { CodeDiff, codeDiffVariants }\n"
|
|
}
|
|
],
|
|
"type": "components:code"
|
|
} |