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
15 KiB
JSON
14 lines
15 KiB
JSON
{
|
|
"name": "code-preview",
|
|
"description": "Live code preview with editing",
|
|
"dependencies": [
|
|
"react-live"
|
|
],
|
|
"files": [
|
|
{
|
|
"name": "code-preview.tsx",
|
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport {\n AlertTriangle,\n Check,\n Copy,\n Eye,\n EyeOff,\n Play,\n RefreshCw,\n Settings,\n Square,\n Terminal,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/registry/default/ui/badge\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport { CodeSnippet } from \"@/registry/default/ui/code-snippet\"\nimport { Separator } from \"@/registry/default/ui/separator\"\nimport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from \"@/registry/default/ui/tabs\"\nimport { Toggle } from \"@/registry/default/ui/toggle\"\n\nconst codePreviewVariants = cva(\n \"relative overflow-hidden rounded-lg border bg-background\",\n {\n variants: {\n size: {\n sm: \"text-xs\",\n default: \"text-sm\",\n lg: \"text-base\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n)\n\nexport interface PreviewFile {\n filename: string\n content: string\n language: string\n type: \"html\" | \"css\" | \"js\" | \"jsx\" | \"ts\" | \"tsx\"\n}\n\nexport interface PreviewResult {\n type: \"output\" | \"error\" | \"log\"\n content: string\n timestamp: number\n}\n\nexport interface CodePreviewProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof codePreviewVariants> {\n files: PreviewFile[]\n defaultFile?: string\n showPreview?: boolean\n showConsole?: boolean\n autoRun?: boolean\n maxHeight?: string\n previewHeight?: string\n consoleHeight?: string\n allowFullscreen?: boolean\n customRunner?: (files: PreviewFile[]) => Promise<string>\n}\n\n// Simple HTML/CSS/JS runner\nconst runHTMLCode = (files: PreviewFile[]): string => {\n const htmlFile = files.find((f) => f.type === \"html\")\n const cssFiles = files.filter((f) => f.type === \"css\")\n const jsFiles = files.filter((f) => f.type === \"js\")\n\n let html = htmlFile?.content || \"<div>No HTML file provided</div>\"\n\n // Inject CSS\n if (cssFiles.length > 0) {\n const cssContent = cssFiles.map((f) => f.content).join(\"\\n\")\n html = `<style>${cssContent}</style>\\n${html}`\n }\n\n // Inject JS\n if (jsFiles.length > 0) {\n const jsContent = jsFiles.map((f) => f.content).join(\"\\n\")\n html = `${html}\\n<script>${jsContent}</script>`\n }\n\n return html\n}\n\n// Console capture script\nconst consoleScript = `\n<script>\n const originalConsole = window.console;\n const logs = [];\n\n ['log', 'warn', 'error', 'info'].forEach(method => {\n window.console[method] = function(...args) {\n logs.push({\n type: method,\n content: args.map(arg =>\n typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)\n ).join(' '),\n timestamp: Date.now()\n });\n\n // Also call original console method\n originalConsole[method].apply(originalConsole, arguments);\n\n // Send to parent\n window.parent.postMessage({\n type: 'console',\n data: logs[logs.length - 1]\n }, '*');\n };\n });\n\n window.addEventListener('error', (e) => {\n window.parent.postMessage({\n type: 'error',\n data: {\n type: 'error',\n content: e.message + ' at ' + e.filename + ':' + e.lineno,\n timestamp: Date.now()\n }\n }, '*');\n });\n</script>\n`\n\nconst CodePreview = React.forwardRef<HTMLDivElement, CodePreviewProps>(\n (\n {\n className,\n size,\n files,\n defaultFile,\n showPreview = true,\n showConsole = true,\n autoRun = false,\n maxHeight = \"600px\",\n previewHeight = \"300px\",\n consoleHeight = \"150px\",\n allowFullscreen = true,\n customRunner,\n ...props\n },\n ref\n ) => {\n const [activeFile, setActiveFile] = React.useState(\n defaultFile || files[0]?.filename || \"\"\n )\n const [previewContent, setPreviewContent] = React.useState(\"\")\n const [isRunning, setIsRunning] = React.useState(false)\n const [results, setResults] = React.useState<PreviewResult[]>([])\n const [isPreviewVisible, setIsPreviewVisible] = React.useState(showPreview)\n const [isConsoleVisible, setIsConsoleVisible] = React.useState(showConsole)\n const [isFullscreen, setIsFullscreen] = React.useState(false)\n const [copied, setCopied] = React.useState<Record<string, boolean>>({})\n\n const iframeRef = React.useRef<HTMLIFrameElement>(null)\n\n const currentFile = files.find((f) => f.filename === activeFile) || files[0]\n\n // Handle messages from iframe\n React.useEffect(() => {\n const handleMessage = (event: MessageEvent) => {\n if (event.data?.type === \"console\" || event.data?.type === \"error\") {\n setResults((prev) => [...prev, event.data.data])\n }\n }\n\n window.addEventListener(\"message\", handleMessage)\n return () => window.removeEventListener(\"message\", handleMessage)\n }, [])\n\n const runCode = React.useCallback(async () => {\n setIsRunning(true)\n setResults([])\n\n try {\n let output: string\n\n if (customRunner) {\n output = await customRunner(files)\n } else {\n output = runHTMLCode(files)\n }\n\n // Add console capture\n output = consoleScript + output\n\n setPreviewContent(output)\n\n // Update iframe\n if (iframeRef.current) {\n const doc = iframeRef.current.contentDocument\n if (doc) {\n doc.open()\n doc.write(output)\n doc.close()\n }\n }\n } catch (error) {\n setResults([\n {\n type: \"error\",\n content: error instanceof Error ? error.message : String(error),\n timestamp: Date.now(),\n },\n ])\n } finally {\n setIsRunning(false)\n }\n }, [files, customRunner])\n\n // Auto-run on file changes\n React.useEffect(() => {\n if (autoRun) {\n const timer = setTimeout(runCode, 1000)\n return () => clearTimeout(timer)\n }\n }, [files, autoRun, runCode])\n\n const copyFileContent = React.useCallback(\n async (filename: string) => {\n const file = files.find((f) => f.filename === filename)\n if (!file) return\n\n try {\n await navigator.clipboard.writeText(file.content)\n setCopied((prev) => ({ ...prev, [filename]: true }))\n setTimeout(() => {\n setCopied((prev) => ({ ...prev, [filename]: false }))\n }, 2000)\n } catch (error) {\n console.error(\"Failed to copy code:\", error)\n }\n },\n [files]\n )\n\n const clearConsole = () => {\n setResults([])\n }\n\n const renderConsole = () => (\n <div className=\"flex flex-col h-full\">\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 <Terminal className=\"h-4 w-4\" />\n <span className=\"text-sm font-medium\">Console</span>\n {results.length > 0 && (\n <Badge variant=\"secondary\" className=\"text-xs\">\n {results.length}\n </Badge>\n )}\n </div>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={clearConsole}\n className=\"h-8 w-8 p-0\"\n >\n <X className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n <div className=\"flex-1 overflow-auto p-2 space-y-1 font-mono text-xs\">\n {results.length === 0 ? (\n <div className=\"text-muted-foreground italic\">No output</div>\n ) : (\n results.map((result, index) => (\n <div\n key={index}\n className={cn(\n \"flex items-start gap-2 p-2 rounded\",\n result.type === \"error\" &&\n \"bg-red-500/10 text-red-600 dark:text-red-400\",\n result.type === \"log\" &&\n \"bg-blue-500/10 text-blue-600 dark:text-blue-400\",\n result.type === \"output\" &&\n \"bg-green-500/10 text-green-600 dark:text-green-400\"\n )}\n >\n <span className=\"text-xs text-muted-foreground\">\n {new Date(result.timestamp).toLocaleTimeString()}\n </span>\n <pre className=\"flex-1 whitespace-pre-wrap break-words\">\n {result.content}\n </pre>\n </div>\n ))\n )}\n </div>\n </div>\n )\n\n const layout = isFullscreen ? \"fixed inset-0 z-50 bg-background\" : \"\"\n\n return (\n <div\n ref={ref}\n className={cn(codePreviewVariants({ size }), layout, className)}\n style={{ maxHeight: isFullscreen ? \"100vh\" : maxHeight }}\n {...props}\n >\n {/* Header */}\n <div className=\"flex items-center justify-between border-b px-4 py-2\">\n <Tabs\n value={activeFile}\n onValueChange={setActiveFile}\n className=\"flex-1\"\n >\n <TabsList className=\"h-auto p-0 bg-transparent\">\n {files.map((file) => (\n <TabsTrigger\n key={file.filename}\n value={file.filename}\n className=\"relative flex items-center gap-2 data-[state=active]:bg-muted\"\n >\n <span>{file.filename}</span>\n <Badge variant=\"outline\" className=\"text-xs\">\n {file.language}\n </Badge>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation()\n copyFileContent(file.filename)\n }}\n className=\"h-6 w-6 p-0 opacity-0 group-hover:opacity-100\"\n >\n {copied[file.filename] ? (\n <Check className=\"h-3 w-3 text-green-500\" />\n ) : (\n <Copy className=\"h-3 w-3\" />\n )}\n </Button>\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n\n <div className=\"flex items-center gap-2\">\n <Toggle\n pressed={isPreviewVisible}\n onPressedChange={setIsPreviewVisible}\n aria-label=\"Toggle preview\"\n size=\"sm\"\n >\n {isPreviewVisible ? (\n <Eye className=\"h-4 w-4\" />\n ) : (\n <EyeOff className=\"h-4 w-4\" />\n )}\n </Toggle>\n\n <Toggle\n pressed={isConsoleVisible}\n onPressedChange={setIsConsoleVisible}\n aria-label=\"Toggle console\"\n size=\"sm\"\n >\n <Terminal className=\"h-4 w-4\" />\n </Toggle>\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={runCode}\n disabled={isRunning}\n className=\"gap-2\"\n >\n {isRunning ? (\n <RefreshCw className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Play className=\"h-4 w-4\" />\n )}\n Run\n </Button>\n\n {allowFullscreen && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setIsFullscreen(!isFullscreen)}\n className=\"h-8 w-8 p-0\"\n >\n {isFullscreen ? (\n <Square className=\"h-4 w-4\" />\n ) : (\n <Settings className=\"h-4 w-4\" />\n )}\n </Button>\n )}\n </div>\n </div>\n\n {/* Content */}\n <div className=\"flex flex-col h-full\">\n {/* Code Editor */}\n <div className=\"flex-1 min-h-0\">\n <Tabs value={activeFile} onValueChange={setActiveFile}>\n {files.map((file) => (\n <TabsContent\n key={file.filename}\n value={file.filename}\n className=\"m-0 h-full\"\n >\n <CodeSnippet\n code={file.content}\n language={file.language}\n showLineNumbers\n showCopyButton={false}\n showHeader={false}\n maxHeight=\"100%\"\n className=\"border-0 rounded-none\"\n />\n </TabsContent>\n ))}\n </Tabs>\n </div>\n\n {/* Preview and Console */}\n {(isPreviewVisible || isConsoleVisible) && (\n <>\n <Separator />\n <div className=\"flex flex-col lg:flex-row min-h-0\">\n {/* Preview */}\n {isPreviewVisible && (\n <div\n className=\"flex-1 min-h-0 border-r last:border-r-0\"\n style={{ height: previewHeight }}\n >\n <div className=\"flex items-center gap-2 border-b px-4 py-2 bg-muted/30\">\n <Eye className=\"h-4 w-4\" />\n <span className=\"text-sm font-medium\">Preview</span>\n {results.filter((r) => r.type === \"error\").length > 0 && (\n <AlertTriangle className=\"h-4 w-4 text-red-500\" />\n )}\n </div>\n <iframe\n ref={iframeRef}\n className=\"w-full h-full bg-white\"\n sandbox=\"allow-scripts allow-same-origin\"\n srcDoc={previewContent}\n />\n </div>\n )}\n\n {/* Console */}\n {isConsoleVisible && (\n <div\n className=\"flex-1 min-h-0\"\n style={{ height: consoleHeight }}\n >\n {renderConsole()}\n </div>\n )}\n </div>\n </>\n )}\n </div>\n\n {/* Fullscreen overlay */}\n {isFullscreen && (\n <div\n className=\"absolute inset-0 bg-black/50\"\n onClick={() => setIsFullscreen(false)}\n />\n )}\n </div>\n )\n }\n)\n\nCodePreview.displayName = \"CodePreview\"\n\nexport { CodePreview, codePreviewVariants }\n"
|
|
}
|
|
],
|
|
"type": "components:code"
|
|
} |