mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 14:14:57 +00:00
Generated by Spark: Load feature component trees from json, make monolithic components atomic.
This commit is contained in:
@@ -1,24 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef } from 'react'
|
||||
import { ComponentTree, ComponentNode } from '@/types/project'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Plus, Tree, Trash, Pencil, Copy, FolderOpen } from '@phosphor-icons/react'
|
||||
import { TreeFormDialog } from '@/components/molecules'
|
||||
import { TreeListPanel } from '@/components/organisms'
|
||||
import { ComponentTreeBuilder } from '@/components/ComponentTreeBuilder'
|
||||
import { TreeIcon } from '@/components/atoms'
|
||||
import { toast } from 'sonner'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
interface ComponentTreeManagerProps {
|
||||
trees: ComponentTree[]
|
||||
@@ -32,6 +18,7 @@ export function ComponentTreeManager({ trees, onTreesChange }: ComponentTreeMana
|
||||
const [newTreeName, setNewTreeName] = useState('')
|
||||
const [newTreeDescription, setNewTreeDescription] = useState('')
|
||||
const [editingTree, setEditingTree] = useState<ComponentTree | null>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const selectedTree = trees.find(t => t.id === selectedTreeId)
|
||||
|
||||
@@ -123,92 +110,88 @@ export function ComponentTreeManager({ trees, onTreesChange }: ComponentTreeMana
|
||||
setEditDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleExportJson = () => {
|
||||
if (!selectedTree) {
|
||||
toast.error('No tree selected to export')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.stringify(selectedTree, null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${selectedTree.name.toLowerCase().replace(/\s+/g, '-')}-tree.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
toast.success('Component tree exported as JSON')
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error)
|
||||
toast.error('Failed to export component tree')
|
||||
}
|
||||
}
|
||||
|
||||
const handleImportJson = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const text = await file.text()
|
||||
const importedTree = JSON.parse(text) as ComponentTree
|
||||
|
||||
if (!importedTree.id || !importedTree.name || !Array.isArray(importedTree.rootNodes)) {
|
||||
toast.error('Invalid component tree JSON format')
|
||||
return
|
||||
}
|
||||
|
||||
const newTree: ComponentTree = {
|
||||
...importedTree,
|
||||
id: `tree-${Date.now()}`,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
onTreesChange((current) => [...current, newTree])
|
||||
setSelectedTreeId(newTree.id)
|
||||
toast.success(`Component tree "${newTree.name}" imported successfully`)
|
||||
} catch (error) {
|
||||
console.error('Import failed:', error)
|
||||
toast.error('Failed to import component tree. Please check the JSON format.')
|
||||
} finally {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex">
|
||||
<div className="w-80 border-r border-border bg-card p-4 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Tree size={20} weight="duotone" />
|
||||
Component Trees
|
||||
</h2>
|
||||
<Button size="sm" onClick={() => setCreateDialogOpen(true)}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="space-y-2">
|
||||
{trees.map((tree) => (
|
||||
<Card
|
||||
key={tree.id}
|
||||
className={`cursor-pointer transition-all ${
|
||||
selectedTreeId === tree.id
|
||||
? 'ring-2 ring-primary bg-accent'
|
||||
: 'hover:bg-accent/50'
|
||||
}`}
|
||||
onClick={() => setSelectedTreeId(tree.id)}
|
||||
>
|
||||
<CardHeader className="p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-sm truncate">{tree.name}</CardTitle>
|
||||
{tree.description && (
|
||||
<CardDescription className="text-xs mt-1 line-clamp-2">
|
||||
{tree.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{tree.rootNodes.length} components
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => openEditDialog(tree)}
|
||||
title="Edit tree"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDuplicateTree(tree)}
|
||||
title="Duplicate tree"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDeleteTree(tree.id)}
|
||||
disabled={trees.length === 1}
|
||||
title="Delete tree"
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{trees.length === 0 && (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<FolderOpen size={48} className="mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No component trees yet</p>
|
||||
<Button size="sm" className="mt-2" onClick={() => setCreateDialogOpen(true)}>
|
||||
Create First Tree
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TreeListPanel
|
||||
trees={trees}
|
||||
selectedTreeId={selectedTreeId}
|
||||
onTreeSelect={setSelectedTreeId}
|
||||
onTreeEdit={openEditDialog}
|
||||
onTreeDuplicate={handleDuplicateTree}
|
||||
onTreeDelete={handleDeleteTree}
|
||||
onCreateNew={() => setCreateDialogOpen(true)}
|
||||
onImportJson={handleImportJson}
|
||||
onExportJson={handleExportJson}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{selectedTree ? (
|
||||
@@ -219,84 +202,38 @@ export function ComponentTreeManager({ trees, onTreesChange }: ComponentTreeMana
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<Tree size={64} className="mx-auto mb-4 opacity-50" weight="duotone" />
|
||||
<TreeIcon size={64} className="mx-auto mb-4 opacity-50" />
|
||||
<p>Select a component tree to edit</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Component Tree</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new component tree to organize your UI components
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="tree-name">Tree Name</Label>
|
||||
<Input
|
||||
id="tree-name"
|
||||
value={newTreeName}
|
||||
onChange={(e) => setNewTreeName(e.target.value)}
|
||||
placeholder="e.g., Main App, Dashboard, Admin Panel"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="tree-description">Description</Label>
|
||||
<Textarea
|
||||
id="tree-description"
|
||||
value={newTreeDescription}
|
||||
onChange={(e) => setNewTreeDescription(e.target.value)}
|
||||
placeholder="Describe the purpose of this component tree"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateTree}>Create Tree</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<TreeFormDialog
|
||||
open={createDialogOpen}
|
||||
onOpenChange={setCreateDialogOpen}
|
||||
title="Create Component Tree"
|
||||
description="Create a new component tree to organize your UI components"
|
||||
name={newTreeName}
|
||||
treeDescription={newTreeDescription}
|
||||
onNameChange={setNewTreeName}
|
||||
onDescriptionChange={setNewTreeDescription}
|
||||
onSubmit={handleCreateTree}
|
||||
submitLabel="Create Tree"
|
||||
/>
|
||||
|
||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Component Tree</DialogTitle>
|
||||
<DialogDescription>Update the component tree details</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="edit-tree-name">Tree Name</Label>
|
||||
<Input
|
||||
id="edit-tree-name"
|
||||
value={newTreeName}
|
||||
onChange={(e) => setNewTreeName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="edit-tree-description">Description</Label>
|
||||
<Textarea
|
||||
id="edit-tree-description"
|
||||
value={newTreeDescription}
|
||||
onChange={(e) => setNewTreeDescription(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleEditTree}>Save Changes</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<TreeFormDialog
|
||||
open={editDialogOpen}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
title="Edit Component Tree"
|
||||
description="Update the component tree details"
|
||||
name={newTreeName}
|
||||
treeDescription={newTreeDescription}
|
||||
onNameChange={setNewTreeName}
|
||||
onDescriptionChange={setNewTreeDescription}
|
||||
onSubmit={handleEditTree}
|
||||
submitLabel="Save Changes"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
22
src/components/atoms/ActionIcon.tsx
Normal file
22
src/components/atoms/ActionIcon.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Plus, Pencil, Trash, Copy, Download, Upload } from '@phosphor-icons/react'
|
||||
|
||||
interface ActionIconProps {
|
||||
action: 'add' | 'edit' | 'delete' | 'copy' | 'download' | 'upload'
|
||||
size?: number
|
||||
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ActionIcon({ action, size = 16, weight = 'regular', className = '' }: ActionIconProps) {
|
||||
const iconMap = {
|
||||
add: Plus,
|
||||
edit: Pencil,
|
||||
delete: Trash,
|
||||
copy: Copy,
|
||||
download: Download,
|
||||
upload: Upload,
|
||||
}
|
||||
|
||||
const IconComponent = iconMap[action]
|
||||
return <IconComponent size={size} weight={weight} className={className} />
|
||||
}
|
||||
19
src/components/atoms/FileIcon.tsx
Normal file
19
src/components/atoms/FileIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { FileCode, FileJs, FilePlus } from '@phosphor-icons/react'
|
||||
|
||||
interface FileIconProps {
|
||||
type?: 'code' | 'json' | 'plus'
|
||||
size?: number
|
||||
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function FileIcon({ type = 'code', size = 20, weight = 'regular', className = '' }: FileIconProps) {
|
||||
const iconMap = {
|
||||
code: FileCode,
|
||||
json: FileJs,
|
||||
plus: FilePlus,
|
||||
}
|
||||
|
||||
const IconComponent = iconMap[type]
|
||||
return <IconComponent size={size} weight={weight} className={className} />
|
||||
}
|
||||
11
src/components/atoms/TreeIcon.tsx
Normal file
11
src/components/atoms/TreeIcon.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Tree } from '@phosphor-icons/react'
|
||||
|
||||
interface TreeIconProps {
|
||||
size?: number
|
||||
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TreeIcon({ size = 20, weight = 'duotone', className = '' }: TreeIconProps) {
|
||||
return <Tree size={size} weight={weight} className={className} />
|
||||
}
|
||||
@@ -5,3 +5,6 @@ export { ErrorBadge } from './ErrorBadge'
|
||||
export { IconWrapper } from './IconWrapper'
|
||||
export { LoadingSpinner } from './LoadingSpinner'
|
||||
export { EmptyStateIcon } from './EmptyStateIcon'
|
||||
export { TreeIcon } from './TreeIcon'
|
||||
export { FileIcon } from './FileIcon'
|
||||
export { ActionIcon } from './ActionIcon'
|
||||
|
||||
69
src/components/molecules/TreeCard.tsx
Normal file
69
src/components/molecules/TreeCard.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ActionIcon } from '@/components/atoms'
|
||||
import { ComponentTree } from '@/types/project'
|
||||
|
||||
interface TreeCardProps {
|
||||
tree: ComponentTree
|
||||
isSelected: boolean
|
||||
onSelect: () => void
|
||||
onEdit: () => void
|
||||
onDuplicate: () => void
|
||||
onDelete: () => void
|
||||
disableDelete?: boolean
|
||||
}
|
||||
|
||||
export function TreeCard({
|
||||
tree,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onEdit,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
disableDelete = false,
|
||||
}: TreeCardProps) {
|
||||
return (
|
||||
<Card
|
||||
className={`cursor-pointer transition-all ${
|
||||
isSelected ? 'ring-2 ring-primary bg-accent' : 'hover:bg-accent/50'
|
||||
}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<CardHeader className="p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-sm truncate">{tree.name}</CardTitle>
|
||||
{tree.description && (
|
||||
<CardDescription className="text-xs mt-1 line-clamp-2">
|
||||
{tree.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{tree.rootNodes.length} components
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Button size="sm" variant="ghost" onClick={onEdit} title="Edit tree">
|
||||
<ActionIcon action="edit" size={14} />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={onDuplicate} title="Duplicate tree">
|
||||
<ActionIcon action="copy" size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={onDelete}
|
||||
disabled={disableDelete}
|
||||
title="Delete tree"
|
||||
>
|
||||
<ActionIcon action="delete" size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
76
src/components/molecules/TreeFormDialog.tsx
Normal file
76
src/components/molecules/TreeFormDialog.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
interface TreeFormDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
title: string
|
||||
description: string
|
||||
name: string
|
||||
treeDescription: string
|
||||
onNameChange: (name: string) => void
|
||||
onDescriptionChange: (description: string) => void
|
||||
onSubmit: () => void
|
||||
submitLabel?: string
|
||||
}
|
||||
|
||||
export function TreeFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
name,
|
||||
treeDescription,
|
||||
onNameChange,
|
||||
onDescriptionChange,
|
||||
onSubmit,
|
||||
submitLabel = 'Save',
|
||||
}: TreeFormDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="tree-name">Tree Name</Label>
|
||||
<Input
|
||||
id="tree-name"
|
||||
value={name}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
placeholder="e.g., Main App, Dashboard, Admin Panel"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="tree-description">Description</Label>
|
||||
<Textarea
|
||||
id="tree-description"
|
||||
value={treeDescription}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
placeholder="Describe the purpose of this component tree"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSubmit}>{submitLabel}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
52
src/components/molecules/TreeListHeader.tsx
Normal file
52
src/components/molecules/TreeListHeader.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { TreeIcon, ActionIcon } from '@/components/atoms'
|
||||
|
||||
interface TreeListHeaderProps {
|
||||
onCreateNew: () => void
|
||||
onImportJson: () => void
|
||||
onExportJson: () => void
|
||||
hasSelectedTree?: boolean
|
||||
}
|
||||
|
||||
export function TreeListHeader({
|
||||
onCreateNew,
|
||||
onImportJson,
|
||||
onExportJson,
|
||||
hasSelectedTree = false,
|
||||
}: TreeListHeaderProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<TreeIcon size={20} />
|
||||
Component Trees
|
||||
</h2>
|
||||
<Button size="sm" onClick={onCreateNew}>
|
||||
<ActionIcon action="add" size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onImportJson}
|
||||
className="flex-1 text-xs"
|
||||
>
|
||||
<ActionIcon action="upload" size={14} className="mr-1.5" />
|
||||
Import JSON
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onExportJson}
|
||||
disabled={!hasSelectedTree}
|
||||
className="flex-1 text-xs"
|
||||
>
|
||||
<ActionIcon action="download" size={14} className="mr-1.5" />
|
||||
Export JSON
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,3 +8,6 @@ export { EmptyState } from './EmptyState'
|
||||
export { LoadingState } from './LoadingState'
|
||||
export { StatCard } from './StatCard'
|
||||
export { LabelWithBadge } from './LabelWithBadge'
|
||||
export { TreeCard } from './TreeCard'
|
||||
export { TreeFormDialog } from './TreeFormDialog'
|
||||
export { TreeListHeader } from './TreeListHeader'
|
||||
|
||||
74
src/components/organisms/TreeListPanel.tsx
Normal file
74
src/components/organisms/TreeListPanel.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { TreeCard, TreeListHeader } from '@/components/molecules'
|
||||
import { EmptyState } from '@/components/molecules'
|
||||
import { ComponentTree } from '@/types/project'
|
||||
import { FolderOpen } from '@phosphor-icons/react'
|
||||
|
||||
interface TreeListPanelProps {
|
||||
trees: ComponentTree[]
|
||||
selectedTreeId: string | null
|
||||
onTreeSelect: (treeId: string) => void
|
||||
onTreeEdit: (tree: ComponentTree) => void
|
||||
onTreeDuplicate: (tree: ComponentTree) => void
|
||||
onTreeDelete: (treeId: string) => void
|
||||
onCreateNew: () => void
|
||||
onImportJson: () => void
|
||||
onExportJson: () => void
|
||||
}
|
||||
|
||||
export function TreeListPanel({
|
||||
trees,
|
||||
selectedTreeId,
|
||||
onTreeSelect,
|
||||
onTreeEdit,
|
||||
onTreeDuplicate,
|
||||
onTreeDelete,
|
||||
onCreateNew,
|
||||
onImportJson,
|
||||
onExportJson,
|
||||
}: TreeListPanelProps) {
|
||||
return (
|
||||
<div className="w-80 border-r border-border bg-card p-4 flex flex-col gap-4">
|
||||
<TreeListHeader
|
||||
onCreateNew={onCreateNew}
|
||||
onImportJson={onImportJson}
|
||||
onExportJson={onExportJson}
|
||||
hasSelectedTree={!!selectedTreeId}
|
||||
/>
|
||||
|
||||
{trees.length === 0 ? (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<EmptyState
|
||||
icon={<FolderOpen size={48} weight="duotone" />}
|
||||
title="No component trees yet"
|
||||
description="Create your first tree to get started"
|
||||
action={
|
||||
<Button size="sm" onClick={onCreateNew}>
|
||||
Create First Tree
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="space-y-2">
|
||||
{trees.map((tree) => (
|
||||
<TreeCard
|
||||
key={tree.id}
|
||||
tree={tree}
|
||||
isSelected={selectedTreeId === tree.id}
|
||||
onSelect={() => onTreeSelect(tree.id)}
|
||||
onEdit={() => onTreeEdit(tree)}
|
||||
onDuplicate={() => onTreeDuplicate(tree)}
|
||||
onDelete={() => onTreeDelete(tree.id)}
|
||||
disableDelete={trees.length === 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export { NavigationMenu } from './NavigationMenu'
|
||||
export { PageHeader } from './PageHeader'
|
||||
export { ToolbarActions } from './ToolbarActions'
|
||||
export { AppHeader } from './AppHeader'
|
||||
export { TreeListPanel } from './TreeListPanel'
|
||||
|
||||
Reference in New Issue
Block a user