Files
zeekay b836586a64 fix: Include all component types in registry build
- 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
2025-10-16 22:33:38 -07:00

14 lines
12 KiB
JSON

{
"name": "code-explorer",
"description": "File tree and code explorer interface",
"dependencies": [
"@monaco-editor/react"
],
"files": [
{
"name": "code-explorer.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport {\n ChevronDown,\n ChevronRight,\n File,\n FileCode,\n FileImage,\n FileText,\n Folder,\n FolderOpen,\n Search,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport { Input } from \"@/registry/default/ui/input\"\nimport { ScrollArea } from \"@/registry/default/ui/scroll-area\"\n\nconst codeExplorerVariants = cva(\n \"relative flex flex-col 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 FileTreeNode {\n id: string\n name: string\n type: \"file\" | \"folder\"\n path: string\n size?: number\n modified?: Date\n children?: FileTreeNode[]\n content?: string\n language?: string\n isExpanded?: boolean\n}\n\nexport interface CodeExplorerProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof codeExplorerVariants> {\n files: FileTreeNode[]\n onFileSelect?: (file: FileTreeNode) => void\n onFolderToggle?: (folder: FileTreeNode) => void\n selectedFile?: string\n showSearch?: boolean\n showFileIcons?: boolean\n showFileSize?: boolean\n showModifiedDate?: boolean\n height?: string\n defaultExpanded?: string[]\n searchPlaceholder?: string\n emptyMessage?: string\n}\n\n// File type icons mapping\nconst getFileIcon = (fileName: string, isFolder: boolean = false) => {\n if (isFolder) return Folder\n\n const ext = fileName.split(\".\").pop()?.toLowerCase()\n\n switch (ext) {\n case \"js\":\n case \"jsx\":\n case \"ts\":\n case \"tsx\":\n case \"vue\":\n case \"svelte\":\n return FileCode\n case \"json\":\n case \"xml\":\n case \"yaml\":\n case \"yml\":\n case \"toml\":\n case \"ini\":\n return FileText\n case \"png\":\n case \"jpg\":\n case \"jpeg\":\n case \"gif\":\n case \"svg\":\n case \"webp\":\n case \"ico\":\n return FileImage\n default:\n return File\n }\n}\n\n// Format file size\nconst formatFileSize = (bytes?: number): string => {\n if (!bytes) return \"\"\n\n const units = [\"B\", \"KB\", \"MB\", \"GB\"]\n let size = bytes\n let unitIndex = 0\n\n while (size >= 1024 && unitIndex < units.length - 1) {\n size /= 1024\n unitIndex++\n }\n\n return `${size.toFixed(1)} ${units[unitIndex]}`\n}\n\n// Format modified date\nconst formatModifiedDate = (date?: Date): string => {\n if (!date) return \"\"\n\n const now = new Date()\n const diff = now.getTime() - date.getTime()\n const days = Math.floor(diff / (1000 * 60 * 60 * 24))\n\n if (days === 0) return \"Today\"\n if (days === 1) return \"Yesterday\"\n if (days < 7) return `${days} days ago`\n if (days < 30) return `${Math.floor(days / 7)} weeks ago`\n if (days < 365) return `${Math.floor(days / 30)} months ago`\n return `${Math.floor(days / 365)} years ago`\n}\n\nconst CodeExplorer = React.forwardRef<HTMLDivElement, CodeExplorerProps>(\n (\n {\n className,\n size,\n files,\n onFileSelect,\n onFolderToggle,\n selectedFile,\n showSearch = true,\n showFileIcons = true,\n showFileSize = false,\n showModifiedDate = false,\n height = \"400px\",\n defaultExpanded = [],\n searchPlaceholder = \"Search files...\",\n emptyMessage = \"No files found\",\n ...props\n },\n ref\n ) => {\n const [searchTerm, setSearchTerm] = React.useState(\"\")\n const [expandedFolders, setExpandedFolders] = React.useState<Set<string>>(\n new Set(defaultExpanded)\n )\n\n // Build file tree with expansion state\n const buildFileTree = React.useCallback(\n (nodes: FileTreeNode[]): FileTreeNode[] => {\n return nodes.map((node) => ({\n ...node,\n isExpanded: expandedFolders.has(node.id),\n children: node.children ? buildFileTree(node.children) : undefined,\n }))\n },\n [expandedFolders]\n )\n\n // Filter files based on search term\n const filterFiles = React.useCallback(\n (nodes: FileTreeNode[], term: string): FileTreeNode[] => {\n if (!term) return nodes\n\n const filtered: FileTreeNode[] = []\n\n for (const node of nodes) {\n if (node.name.toLowerCase().includes(term.toLowerCase())) {\n filtered.push(node)\n } else if (node.type === \"folder\" && node.children) {\n const filteredChildren = filterFiles(node.children, term)\n if (filteredChildren.length > 0) {\n filtered.push({\n ...node,\n children: filteredChildren,\n isExpanded: true, // Auto-expand folders with matches\n })\n }\n }\n }\n\n return filtered\n },\n []\n )\n\n const processedFiles = React.useMemo(() => {\n const withExpansion = buildFileTree(files)\n return searchTerm ? filterFiles(withExpansion, searchTerm) : withExpansion\n }, [files, searchTerm, buildFileTree, filterFiles])\n\n const toggleFolder = React.useCallback(\n (folder: FileTreeNode) => {\n setExpandedFolders((prev) => {\n const newSet = new Set(prev)\n if (newSet.has(folder.id)) {\n newSet.delete(folder.id)\n } else {\n newSet.add(folder.id)\n }\n return newSet\n })\n\n onFolderToggle?.(folder)\n },\n [onFolderToggle]\n )\n\n const renderFileNode = React.useCallback(\n (node: FileTreeNode, depth: number = 0) => {\n const Icon = showFileIcons\n ? getFileIcon(node.name, node.type === \"folder\")\n : null\n const FolderIcon =\n node.type === \"folder\" && node.isExpanded ? FolderOpen : Folder\n const isSelected =\n selectedFile === node.id || selectedFile === node.path\n\n return (\n <div key={node.id}>\n <div\n className={cn(\n \"flex items-center gap-2 px-2 py-1 hover:bg-muted/50 cursor-pointer transition-colors\",\n isSelected && \"bg-muted\",\n \"rounded-sm\"\n )}\n style={{ paddingLeft: `${depth * 16 + 8}px` }}\n onClick={() => {\n if (node.type === \"folder\") {\n toggleFolder(node)\n } else {\n onFileSelect?.(node)\n }\n }}\n >\n {/* Folder chevron */}\n {node.type === \"folder\" && (\n <div className=\"w-4 h-4 flex items-center justify-center\">\n {node.isExpanded ? (\n <ChevronDown className=\"h-3 w-3\" />\n ) : (\n <ChevronRight className=\"h-3 w-3\" />\n )}\n </div>\n )}\n\n {/* File/Folder icon */}\n {showFileIcons && (\n <div className=\"w-4 h-4 flex items-center justify-center flex-shrink-0\">\n {node.type === \"folder\" ? (\n <FolderIcon className=\"h-4 w-4 text-blue-500\" />\n ) : Icon ? (\n <Icon className=\"h-4 w-4 text-muted-foreground\" />\n ) : null}\n </div>\n )}\n\n {/* File/Folder name */}\n <span className=\"flex-1 truncate\">{node.name}</span>\n\n {/* File size */}\n {showFileSize && node.type === \"file\" && node.size && (\n <span className=\"text-xs text-muted-foreground flex-shrink-0\">\n {formatFileSize(node.size)}\n </span>\n )}\n\n {/* Modified date */}\n {showModifiedDate && node.modified && (\n <span className=\"text-xs text-muted-foreground flex-shrink-0\">\n {formatModifiedDate(node.modified)}\n </span>\n )}\n </div>\n\n {/* Render children if folder is expanded */}\n {node.type === \"folder\" && node.isExpanded && node.children && (\n <div>\n {node.children.map((child) => renderFileNode(child, depth + 1))}\n </div>\n )}\n </div>\n )\n },\n [\n showFileIcons,\n showFileSize,\n showModifiedDate,\n selectedFile,\n toggleFolder,\n onFileSelect,\n ]\n )\n\n const clearSearch = () => {\n setSearchTerm(\"\")\n }\n\n return (\n <div\n ref={ref}\n className={cn(codeExplorerVariants({ size }), className)}\n style={{ height }}\n {...props}\n >\n {/* Header with search */}\n {showSearch && (\n <div className=\"flex items-center gap-2 border-b px-4 py-2\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n <Input\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n placeholder={searchPlaceholder}\n className=\"pl-8 pr-8 h-8\"\n />\n {searchTerm && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={clearSearch}\n className=\"absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0\"\n >\n <X className=\"h-3 w-3\" />\n </Button>\n )}\n </div>\n </div>\n )}\n\n {/* File tree */}\n <ScrollArea className=\"flex-1\">\n <div className=\"p-2\">\n {processedFiles.length === 0 ? (\n <div className=\"flex items-center justify-center h-24 text-muted-foreground\">\n {searchTerm ? `No files match \"${searchTerm}\"` : emptyMessage}\n </div>\n ) : (\n <div className=\"space-y-0.5\">\n {processedFiles.map((node) => renderFileNode(node, 0))}\n </div>\n )}\n </div>\n </ScrollArea>\n\n {/* Footer with file count */}\n <div className=\"border-t px-4 py-2 text-xs text-muted-foreground\">\n {(() => {\n const countFiles = (\n nodes: FileTreeNode[]\n ): { files: number; folders: number } => {\n let files = 0\n let folders = 0\n\n for (const node of nodes) {\n if (node.type === \"file\") {\n files++\n } else {\n folders++\n if (node.children) {\n const childCounts = countFiles(node.children)\n files += childCounts.files\n folders += childCounts.folders\n }\n }\n }\n\n return { files, folders }\n }\n\n const { files: fileCount, folders: folderCount } =\n countFiles(processedFiles)\n const parts = []\n\n if (fileCount > 0)\n parts.push(`${fileCount} file${fileCount === 1 ? \"\" : \"s\"}`)\n if (folderCount > 0)\n parts.push(`${folderCount} folder${folderCount === 1 ? \"\" : \"s\"}`)\n\n return parts.join(\", \") || \"Empty\"\n })()}\n {searchTerm && <span> (filtered)</span>}\n </div>\n </div>\n )\n }\n)\n\nCodeExplorer.displayName = \"CodeExplorer\"\n\nexport { CodeExplorer, codeExplorerVariants }\n"
}
],
"type": "components:code"
}