mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
Generated by Spark: Load molecule and organism component trees from json
This commit is contained in:
240
COMPONENT_TREE_JSON_LOADING.md
Normal file
240
COMPONENT_TREE_JSON_LOADING.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Component Tree JSON Loading System
|
||||
|
||||
## Overview
|
||||
|
||||
This system loads molecule and organism component trees from JSON files into the application's KV store on startup. Component trees define the hierarchical structure of reusable UI components with their props, children, and metadata.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── config/
|
||||
│ └── component-trees/
|
||||
│ ├── index.ts # Exports and utility functions
|
||||
│ ├── molecules.json # Molecule component trees
|
||||
│ └── organisms.json # Organism component trees
|
||||
├── hooks/
|
||||
│ └── use-component-tree-loader.ts # Hook for loading and managing trees
|
||||
└── components/
|
||||
├── ComponentTreeViewer.tsx # UI for viewing component trees
|
||||
└── ComponentTreeDemoPage.tsx # Demo page
|
||||
```
|
||||
|
||||
## Component Tree Structure
|
||||
|
||||
Each component tree follows this schema:
|
||||
|
||||
```typescript
|
||||
interface ComponentTree {
|
||||
id: string // Unique identifier
|
||||
name: string // Display name
|
||||
description: string // Description of the component
|
||||
category: 'molecule' | 'organism' // Component category
|
||||
rootNodes: ComponentNode[] // Root component nodes
|
||||
createdAt: number // Timestamp
|
||||
updatedAt: number // Timestamp
|
||||
}
|
||||
|
||||
interface ComponentNode {
|
||||
id: string // Unique node ID
|
||||
type: string // Component type (Button, Card, etc.)
|
||||
name: string // Node name
|
||||
props: Record<string, any> // Component props
|
||||
children: ComponentNode[] // Child nodes
|
||||
}
|
||||
```
|
||||
|
||||
## JSON Files
|
||||
|
||||
### molecules.json
|
||||
|
||||
Contains component trees for molecule-level components:
|
||||
- SearchInput
|
||||
- DataCard
|
||||
- StatCard
|
||||
- ActionBar
|
||||
- EmptyState
|
||||
- FileTabs
|
||||
|
||||
### organisms.json
|
||||
|
||||
Contains component trees for organism-level components:
|
||||
- AppHeader
|
||||
- NavigationMenu
|
||||
- SchemaEditorCanvas
|
||||
- TreeListPanel
|
||||
- DataTable
|
||||
- SchemaEditorSidebar
|
||||
|
||||
## Usage
|
||||
|
||||
### Loading Component Trees
|
||||
|
||||
Component trees are automatically loaded on application startup via the `App.tsx` initialization:
|
||||
|
||||
```typescript
|
||||
const { loadComponentTrees } = useComponentTreeLoader()
|
||||
|
||||
useEffect(() => {
|
||||
loadSeedData()
|
||||
.then(() => loadComponentTrees())
|
||||
.then(() => console.log('Trees loaded'))
|
||||
}, [])
|
||||
```
|
||||
|
||||
### Using the Hook
|
||||
|
||||
```typescript
|
||||
import { useComponentTreeLoader } from '@/hooks/use-component-tree-loader'
|
||||
|
||||
function MyComponent() {
|
||||
const {
|
||||
isLoaded,
|
||||
isLoading,
|
||||
error,
|
||||
moleculeTrees,
|
||||
organismTrees,
|
||||
allTrees,
|
||||
getComponentTreeById,
|
||||
getComponentTreeByName,
|
||||
getComponentTreesByCategory,
|
||||
reloadFromJSON,
|
||||
} = useComponentTreeLoader()
|
||||
|
||||
// Access trees from memory
|
||||
const molecules = moleculeTrees
|
||||
|
||||
// Get tree from KV store
|
||||
const tree = await getComponentTreeById('mol-tree-1')
|
||||
|
||||
// Reload from JSON files
|
||||
await reloadFromJSON()
|
||||
}
|
||||
```
|
||||
|
||||
### Viewing Component Trees
|
||||
|
||||
Use the `ComponentTreeViewer` component to visualize and explore loaded trees:
|
||||
|
||||
```typescript
|
||||
import { ComponentTreeViewer } from '@/components/ComponentTreeViewer'
|
||||
|
||||
function MyPage() {
|
||||
return <ComponentTreeViewer />
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### useComponentTreeLoader()
|
||||
|
||||
Returns an object with:
|
||||
|
||||
- `isLoaded: boolean` - Whether trees have been loaded
|
||||
- `isLoading: boolean` - Whether loading is in progress
|
||||
- `error: Error | null` - Any loading error
|
||||
- `moleculeTrees: ComponentTree[]` - Array of molecule trees
|
||||
- `organismTrees: ComponentTree[]` - Array of organism trees
|
||||
- `allTrees: ComponentTree[]` - All trees combined
|
||||
- `loadComponentTrees()` - Load trees from JSON to KV store
|
||||
- `getComponentTrees()` - Get all trees from KV store
|
||||
- `getComponentTreesByCategory(category)` - Get trees by category
|
||||
- `getComponentTreeById(id)` - Get tree by ID
|
||||
- `getComponentTreeByName(name)` - Get tree by name
|
||||
- `reloadFromJSON()` - Force reload from JSON files
|
||||
|
||||
### Config Functions
|
||||
|
||||
From `@/config/component-trees`:
|
||||
|
||||
```typescript
|
||||
import componentTreesData from '@/config/component-trees'
|
||||
|
||||
// Access trees
|
||||
const molecules = componentTreesData.molecules
|
||||
const organisms = componentTreesData.organisms
|
||||
const all = componentTreesData.all
|
||||
|
||||
// Utility functions
|
||||
const tree = componentTreesData.getById('mol-tree-1')
|
||||
const tree = componentTreesData.getByName('SearchInput')
|
||||
const trees = componentTreesData.getByCategory('molecule')
|
||||
```
|
||||
|
||||
## Adding New Component Trees
|
||||
|
||||
1. Edit `molecules.json` or `organisms.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"molecules": [
|
||||
{
|
||||
"id": "mol-tree-new",
|
||||
"name": "NewComponent",
|
||||
"description": "Description of the component",
|
||||
"category": "molecule",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "root-1",
|
||||
"type": "div",
|
||||
"name": "Container",
|
||||
"props": {
|
||||
"className": "flex gap-2"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
2. Reload in the application:
|
||||
|
||||
```typescript
|
||||
const { reloadFromJSON } = useComponentTreeLoader()
|
||||
await reloadFromJSON()
|
||||
```
|
||||
|
||||
Or restart the application for automatic loading.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic Loading**: Trees are loaded on app startup
|
||||
- **Merge Strategy**: New trees are merged with existing trees, preserving user modifications
|
||||
- **Category Filtering**: Filter trees by molecule/organism category
|
||||
- **Type Safety**: Full TypeScript support
|
||||
- **Error Handling**: Graceful error handling with user feedback
|
||||
- **Hot Reload**: Reload from JSON without restarting the app
|
||||
- **Visual Explorer**: Built-in UI for viewing tree structures
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Separation of Concerns**: Component structures defined in JSON, separate from implementation
|
||||
2. **Reusability**: Trees can be referenced and reused across the application
|
||||
3. **Documentation**: JSON serves as living documentation of component hierarchy
|
||||
4. **Version Control**: Easy to track changes to component structures
|
||||
5. **Tooling**: Can be generated or validated by external tools
|
||||
|
||||
## Integration with Existing System
|
||||
|
||||
The component tree system integrates with:
|
||||
|
||||
- **Project State**: Trees stored in KV store alongside other project data
|
||||
- **Seed Data**: Loaded automatically with seed data on startup
|
||||
- **Component Trees Feature**: Used by existing component tree builder/manager
|
||||
- **Atomic Design**: Follows atomic design principles (molecules, organisms)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
|
||||
- JSON schema validation
|
||||
- Component tree code generation
|
||||
- Visual tree editor with drag-and-drop
|
||||
- Tree composition (combining multiple trees)
|
||||
- Tree versioning and history
|
||||
- Export trees to React code
|
||||
- Import trees from existing React components
|
||||
@@ -18,6 +18,7 @@ import { useFileOperations } from '@/hooks/use-file-operations'
|
||||
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
|
||||
import { useSeedData } from '@/hooks/data/use-seed-data'
|
||||
import { useRouterNavigation } from '@/hooks/use-router-navigation'
|
||||
import { useComponentTreeLoader } from '@/hooks/use-component-tree-loader'
|
||||
console.log('[APP_ROUTER] ✅ Custom hooks imported')
|
||||
|
||||
import { getPageShortcuts } from '@/config/page-loader'
|
||||
@@ -311,6 +312,7 @@ function App() {
|
||||
|
||||
console.log('[APP_ROUTER] 🌱 Initializing seed data hook')
|
||||
const { loadSeedData } = useSeedData()
|
||||
const { loadComponentTrees } = useComponentTreeLoader()
|
||||
const projectState = useProjectState()
|
||||
const { featureToggles, files, setFiles, ...restState } = projectState
|
||||
console.log('[APP_ROUTER] ✅ Hooks initialized')
|
||||
@@ -335,6 +337,11 @@ function App() {
|
||||
loadSeedData()
|
||||
.then(() => {
|
||||
console.log('[APP_ROUTER] ✅ Seed data loaded successfully')
|
||||
console.log('[APP_ROUTER] 📦 Loading component trees from JSON')
|
||||
return loadComponentTrees()
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[APP_ROUTER] ✅ Component trees loaded successfully')
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[APP_ROUTER] ❌ Seed data loading failed:', err)
|
||||
@@ -354,7 +361,7 @@ function App() {
|
||||
console.log('[APP_ROUTER] 🧹 Cleaning up initialization effect')
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [loadSeedData])
|
||||
}, [loadSeedData, loadComponentTrees])
|
||||
|
||||
const stateContext = {
|
||||
files,
|
||||
|
||||
9
src/components/ComponentTreeDemoPage.tsx
Normal file
9
src/components/ComponentTreeDemoPage.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ComponentTreeViewer } from '@/components/ComponentTreeViewer'
|
||||
|
||||
export default function ComponentTreeDemoPage() {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<ComponentTreeViewer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
323
src/components/ComponentTreeViewer.tsx
Normal file
323
src/components/ComponentTreeViewer.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import { useState } from 'react'
|
||||
import { useComponentTreeLoader } from '@/hooks/use-component-tree-loader'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Cube,
|
||||
TreeStructure,
|
||||
ArrowsClockwise,
|
||||
CheckCircle,
|
||||
Warning,
|
||||
Package,
|
||||
Stack
|
||||
} from '@phosphor-icons/react'
|
||||
|
||||
export function ComponentTreeViewer() {
|
||||
const {
|
||||
isLoaded,
|
||||
isLoading,
|
||||
error,
|
||||
moleculeTrees,
|
||||
organismTrees,
|
||||
allTrees,
|
||||
reloadFromJSON,
|
||||
} = useComponentTreeLoader()
|
||||
|
||||
const [selectedTreeId, setSelectedTreeId] = useState<string | null>(null)
|
||||
|
||||
const handleReload = async () => {
|
||||
try {
|
||||
await reloadFromJSON()
|
||||
toast.success('Component trees reloaded from JSON')
|
||||
} catch (err) {
|
||||
toast.error('Failed to reload component trees')
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTree = allTrees.find(tree => tree.id === selectedTreeId)
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<div className="flex items-center gap-3">
|
||||
<TreeStructure size={24} weight="duotone" className="text-primary" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Component Trees</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Molecules and organisms loaded from JSON
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isLoaded && (
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<CheckCircle size={14} weight="fill" className="text-accent" />
|
||||
{allTrees.length} trees loaded
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReload}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<ArrowsClockwise size={16} className={isLoading ? 'animate-spin' : ''} />
|
||||
Reload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mx-4 mt-4 p-4 bg-destructive/10 border border-destructive/20 rounded-lg flex items-start gap-3">
|
||||
<Warning size={20} weight="fill" className="text-destructive mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-destructive">Error loading component trees</p>
|
||||
<p className="text-sm text-destructive/80 mt-1">{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="molecules" className="flex-1 flex flex-col">
|
||||
<TabsList className="mx-4 mt-4 grid w-auto grid-cols-3">
|
||||
<TabsTrigger value="molecules" className="gap-2">
|
||||
<Package size={16} />
|
||||
Molecules
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{moleculeTrees.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="organisms" className="gap-2">
|
||||
<Stack size={16} />
|
||||
Organisms
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{organismTrees.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="all" className="gap-2">
|
||||
<Cube size={16} />
|
||||
All
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{allTrees.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="molecules" className="flex-1 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4 px-4">
|
||||
<ScrollArea className="h-[calc(100vh-280px)]">
|
||||
<div className="space-y-3 pr-4">
|
||||
{moleculeTrees.map(tree => (
|
||||
<Card
|
||||
key={tree.id}
|
||||
className={`cursor-pointer transition-colors hover:bg-accent/50 ${
|
||||
selectedTreeId === tree.id ? 'border-primary' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedTreeId(tree.id)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Package size={18} weight="duotone" className="text-primary" />
|
||||
{tree.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{tree.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-3">
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span>{tree.rootNodes.length} root nodes</span>
|
||||
<Separator orientation="vertical" className="h-3" />
|
||||
<span>
|
||||
{new Date(tree.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="border-l pl-4">
|
||||
{selectedTree ? (
|
||||
<TreeDetails tree={selectedTree} />
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<TreeStructure size={48} weight="duotone" className="mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">Select a tree to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="organisms" className="flex-1 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4 px-4">
|
||||
<ScrollArea className="h-[calc(100vh-280px)]">
|
||||
<div className="space-y-3 pr-4">
|
||||
{organismTrees.map(tree => (
|
||||
<Card
|
||||
key={tree.id}
|
||||
className={`cursor-pointer transition-colors hover:bg-accent/50 ${
|
||||
selectedTreeId === tree.id ? 'border-primary' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedTreeId(tree.id)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Stack size={18} weight="duotone" className="text-primary" />
|
||||
{tree.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{tree.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-3">
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span>{tree.rootNodes.length} root nodes</span>
|
||||
<Separator orientation="vertical" className="h-3" />
|
||||
<span>
|
||||
{new Date(tree.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="border-l pl-4">
|
||||
{selectedTree ? (
|
||||
<TreeDetails tree={selectedTree} />
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<TreeStructure size={48} weight="duotone" className="mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">Select a tree to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="all" className="flex-1 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4 px-4">
|
||||
<ScrollArea className="h-[calc(100vh-280px)]">
|
||||
<div className="space-y-3 pr-4">
|
||||
{allTrees.map(tree => {
|
||||
const category = (tree as any).category
|
||||
return (
|
||||
<Card
|
||||
key={tree.id}
|
||||
className={`cursor-pointer transition-colors hover:bg-accent/50 ${
|
||||
selectedTreeId === tree.id ? 'border-primary' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedTreeId(tree.id)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
{category === 'molecule' ? (
|
||||
<Package size={18} weight="duotone" className="text-primary" />
|
||||
) : (
|
||||
<Stack size={18} weight="duotone" className="text-primary" />
|
||||
)}
|
||||
{tree.name}
|
||||
<Badge variant="outline" className="ml-auto text-xs">
|
||||
{category}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{tree.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-3">
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span>{tree.rootNodes.length} root nodes</span>
|
||||
<Separator orientation="vertical" className="h-3" />
|
||||
<span>
|
||||
{new Date(tree.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="border-l pl-4">
|
||||
{selectedTree ? (
|
||||
<TreeDetails tree={selectedTree} />
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<TreeStructure size={48} weight="duotone" className="mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">Select a tree to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TreeDetails({ tree }: { tree: any }) {
|
||||
const renderNode = (node: any, depth = 0) => {
|
||||
return (
|
||||
<div key={node.id} className="space-y-2">
|
||||
<div
|
||||
className="p-2 rounded-md bg-muted/40 border text-xs"
|
||||
style={{ marginLeft: `${depth * 16}px` }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-medium">{node.name || node.type}</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{node.type}
|
||||
</Badge>
|
||||
</div>
|
||||
{node.props && Object.keys(node.props).length > 0 && (
|
||||
<div className="text-muted-foreground mt-1">
|
||||
Props: {Object.keys(node.props).length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{node.children && node.children.map((child: any) => renderNode(child, depth + 1))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-280px)]">
|
||||
<div className="pr-4">
|
||||
<div className="mb-4">
|
||||
<h3 className="font-semibold mb-2">{tree.name}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">{tree.description}</p>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Badge variant="outline">
|
||||
{tree.rootNodes.length} root nodes
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
ID: {tree.id}
|
||||
</Badge>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold mb-3">Component Tree Structure</h4>
|
||||
{tree.rootNodes.map((node: any) => renderNode(node))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
35
src/config/component-trees/index.ts
Normal file
35
src/config/component-trees/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import moleculesData from './molecules.json'
|
||||
import organismsData from './organisms.json'
|
||||
import { ComponentTree } from '@/types/project'
|
||||
|
||||
export const moleculeComponentTrees: ComponentTree[] = moleculesData.molecules
|
||||
export const organismComponentTrees: ComponentTree[] = organismsData.organisms
|
||||
|
||||
export const allComponentTrees: ComponentTree[] = [
|
||||
...moleculeComponentTrees,
|
||||
...organismComponentTrees,
|
||||
]
|
||||
|
||||
export function getComponentTreesByCategory(category: 'molecule' | 'organism'): ComponentTree[] {
|
||||
if (category === 'molecule') {
|
||||
return moleculeComponentTrees
|
||||
}
|
||||
return organismComponentTrees
|
||||
}
|
||||
|
||||
export function getComponentTreeById(id: string): ComponentTree | undefined {
|
||||
return allComponentTrees.find(tree => tree.id === id)
|
||||
}
|
||||
|
||||
export function getComponentTreeByName(name: string): ComponentTree | undefined {
|
||||
return allComponentTrees.find(tree => tree.name === name)
|
||||
}
|
||||
|
||||
export default {
|
||||
molecules: moleculeComponentTrees,
|
||||
organisms: organismComponentTrees,
|
||||
all: allComponentTrees,
|
||||
getByCategory: getComponentTreesByCategory,
|
||||
getById: getComponentTreeById,
|
||||
getByName: getComponentTreeByName,
|
||||
}
|
||||
363
src/config/component-trees/molecules.json
Normal file
363
src/config/component-trees/molecules.json
Normal file
@@ -0,0 +1,363 @@
|
||||
{
|
||||
"molecules": [
|
||||
{
|
||||
"id": "mol-tree-1",
|
||||
"name": "SearchInput",
|
||||
"description": "Search input with icon and clear button",
|
||||
"category": "molecule",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "search-root",
|
||||
"type": "div",
|
||||
"name": "SearchContainer",
|
||||
"props": {
|
||||
"className": "relative"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "search-icon",
|
||||
"type": "Icon",
|
||||
"name": "SearchIcon",
|
||||
"props": {
|
||||
"icon": "MagnifyingGlass",
|
||||
"className": "absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "search-input",
|
||||
"type": "Input",
|
||||
"name": "SearchField",
|
||||
"props": {
|
||||
"placeholder": "Search...",
|
||||
"className": "pl-10 pr-10"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "clear-button",
|
||||
"type": "Button",
|
||||
"name": "ClearButton",
|
||||
"props": {
|
||||
"variant": "ghost",
|
||||
"size": "sm",
|
||||
"className": "absolute right-1 top-1/2 -translate-y-1/2"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "mol-tree-2",
|
||||
"name": "DataCard",
|
||||
"description": "Card component for displaying data with header and content",
|
||||
"category": "molecule",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "card-root",
|
||||
"type": "Card",
|
||||
"name": "DataCardContainer",
|
||||
"props": {
|
||||
"className": "overflow-hidden"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "card-header",
|
||||
"type": "CardHeader",
|
||||
"name": "DataCardHeader",
|
||||
"props": {},
|
||||
"children": [
|
||||
{
|
||||
"id": "card-title",
|
||||
"type": "CardTitle",
|
||||
"name": "CardTitle",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "card-description",
|
||||
"type": "CardDescription",
|
||||
"name": "CardDescription",
|
||||
"props": {},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "card-content",
|
||||
"type": "CardContent",
|
||||
"name": "DataCardContent",
|
||||
"props": {},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "mol-tree-3",
|
||||
"name": "StatCard",
|
||||
"description": "Card for displaying statistics with label, value, and trend",
|
||||
"category": "molecule",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "stat-card-root",
|
||||
"type": "Card",
|
||||
"name": "StatCardContainer",
|
||||
"props": {
|
||||
"className": "p-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "stat-header",
|
||||
"type": "div",
|
||||
"name": "StatHeader",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between mb-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "stat-label",
|
||||
"type": "Text",
|
||||
"name": "StatLabel",
|
||||
"props": {
|
||||
"className": "text-sm font-medium text-muted-foreground"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "stat-icon",
|
||||
"type": "Icon",
|
||||
"name": "StatIcon",
|
||||
"props": {
|
||||
"className": "text-muted-foreground"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "stat-value",
|
||||
"type": "div",
|
||||
"name": "StatValue",
|
||||
"props": {
|
||||
"className": "text-3xl font-bold"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "stat-trend",
|
||||
"type": "div",
|
||||
"name": "StatTrend",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1 mt-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "trend-icon",
|
||||
"type": "Icon",
|
||||
"name": "TrendIcon",
|
||||
"props": {
|
||||
"className": "text-xs"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "trend-text",
|
||||
"type": "Text",
|
||||
"name": "TrendText",
|
||||
"props": {
|
||||
"className": "text-xs text-muted-foreground"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "mol-tree-4",
|
||||
"name": "ActionBar",
|
||||
"description": "Toolbar with action buttons",
|
||||
"category": "molecule",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "action-bar-root",
|
||||
"type": "div",
|
||||
"name": "ActionBarContainer",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 p-2 bg-muted/50 rounded-lg border"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "action-buttons",
|
||||
"type": "div",
|
||||
"name": "ActionButtons",
|
||||
"props": {
|
||||
"className": "flex gap-1"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "action-separator",
|
||||
"type": "Separator",
|
||||
"name": "ActionSeparator",
|
||||
"props": {
|
||||
"orientation": "vertical",
|
||||
"className": "h-6"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "action-secondary",
|
||||
"type": "div",
|
||||
"name": "SecondaryActions",
|
||||
"props": {
|
||||
"className": "flex gap-1 ml-auto"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "mol-tree-5",
|
||||
"name": "EmptyState",
|
||||
"description": "Empty state component with icon, heading, and description",
|
||||
"category": "molecule",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "empty-state-root",
|
||||
"type": "div",
|
||||
"name": "EmptyStateContainer",
|
||||
"props": {
|
||||
"className": "flex flex-col items-center justify-center py-12 px-4 text-center"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-icon",
|
||||
"type": "div",
|
||||
"name": "EmptyIcon",
|
||||
"props": {
|
||||
"className": "mb-4 text-muted-foreground"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "empty-heading",
|
||||
"type": "Heading",
|
||||
"name": "EmptyHeading",
|
||||
"props": {
|
||||
"level": 3,
|
||||
"className": "mb-2 font-semibold"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "empty-description",
|
||||
"type": "Text",
|
||||
"name": "EmptyDescription",
|
||||
"props": {
|
||||
"className": "text-muted-foreground mb-4 max-w-md"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "empty-action",
|
||||
"type": "Button",
|
||||
"name": "EmptyAction",
|
||||
"props": {
|
||||
"variant": "default"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "mol-tree-6",
|
||||
"name": "FileTabs",
|
||||
"description": "Tabbed interface for file navigation",
|
||||
"category": "molecule",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "file-tabs-root",
|
||||
"type": "div",
|
||||
"name": "FileTabsContainer",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1 bg-muted/30 px-2 overflow-x-auto"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "tab-list",
|
||||
"type": "div",
|
||||
"name": "TabList",
|
||||
"props": {
|
||||
"className": "flex gap-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "tab-item",
|
||||
"type": "div",
|
||||
"name": "TabItem",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 px-3 py-2 rounded-t-md hover:bg-background cursor-pointer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "tab-icon",
|
||||
"type": "FileIcon",
|
||||
"name": "TabIcon",
|
||||
"props": {},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "tab-label",
|
||||
"type": "Text",
|
||||
"name": "TabLabel",
|
||||
"props": {
|
||||
"className": "text-sm"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "tab-close",
|
||||
"type": "Button",
|
||||
"name": "TabClose",
|
||||
"props": {
|
||||
"variant": "ghost",
|
||||
"size": "sm",
|
||||
"className": "h-4 w-4 p-0"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
}
|
||||
]
|
||||
}
|
||||
625
src/config/component-trees/organisms.json
Normal file
625
src/config/component-trees/organisms.json
Normal file
@@ -0,0 +1,625 @@
|
||||
{
|
||||
"organisms": [
|
||||
{
|
||||
"id": "org-tree-1",
|
||||
"name": "AppHeader",
|
||||
"description": "Main application header with navigation and actions",
|
||||
"category": "organism",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "app-header-root",
|
||||
"type": "header",
|
||||
"name": "AppHeaderContainer",
|
||||
"props": {
|
||||
"className": "sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "header-wrapper",
|
||||
"type": "div",
|
||||
"name": "HeaderWrapper",
|
||||
"props": {
|
||||
"className": "flex h-14 items-center px-4 gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "app-logo",
|
||||
"type": "AppLogo",
|
||||
"name": "AppLogo",
|
||||
"props": {},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "navigation-tabs",
|
||||
"type": "Tabs",
|
||||
"name": "NavigationTabs",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "header-actions",
|
||||
"type": "div",
|
||||
"name": "HeaderActions",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "search-button",
|
||||
"type": "Button",
|
||||
"name": "SearchButton",
|
||||
"props": {
|
||||
"variant": "ghost",
|
||||
"size": "sm"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "notifications-button",
|
||||
"type": "Button",
|
||||
"name": "NotificationsButton",
|
||||
"props": {
|
||||
"variant": "ghost",
|
||||
"size": "sm"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "user-menu",
|
||||
"type": "DropdownMenu",
|
||||
"name": "UserMenu",
|
||||
"props": {},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "org-tree-2",
|
||||
"name": "NavigationMenu",
|
||||
"description": "Sidebar navigation menu with hierarchical structure",
|
||||
"category": "organism",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "nav-menu-root",
|
||||
"type": "nav",
|
||||
"name": "NavigationMenuContainer",
|
||||
"props": {
|
||||
"className": "flex flex-col h-full w-64 border-r bg-muted/40"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "nav-header",
|
||||
"type": "div",
|
||||
"name": "NavHeader",
|
||||
"props": {
|
||||
"className": "p-4 border-b"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "nav-title",
|
||||
"type": "Heading",
|
||||
"name": "NavTitle",
|
||||
"props": {
|
||||
"level": 3,
|
||||
"className": "font-semibold"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "nav-content",
|
||||
"type": "ScrollArea",
|
||||
"name": "NavContent",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "nav-groups",
|
||||
"type": "div",
|
||||
"name": "NavGroups",
|
||||
"props": {
|
||||
"className": "py-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "nav-group",
|
||||
"type": "div",
|
||||
"name": "NavGroup",
|
||||
"props": {
|
||||
"className": "px-3 py-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "group-header",
|
||||
"type": "div",
|
||||
"name": "GroupHeader",
|
||||
"props": {
|
||||
"className": "mb-2 px-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "nav-items",
|
||||
"type": "div",
|
||||
"name": "NavItems",
|
||||
"props": {
|
||||
"className": "space-y-1"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "nav-footer",
|
||||
"type": "div",
|
||||
"name": "NavFooter",
|
||||
"props": {
|
||||
"className": "p-4 border-t"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "org-tree-3",
|
||||
"name": "SchemaEditorCanvas",
|
||||
"description": "Main canvas for visual schema editing with drag and drop",
|
||||
"category": "organism",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "canvas-root",
|
||||
"type": "div",
|
||||
"name": "CanvasContainer",
|
||||
"props": {
|
||||
"className": "relative h-full w-full bg-background"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "canvas-grid",
|
||||
"type": "div",
|
||||
"name": "CanvasGrid",
|
||||
"props": {
|
||||
"className": "absolute inset-0 bg-grid-pattern opacity-5"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "canvas-content",
|
||||
"type": "div",
|
||||
"name": "CanvasContent",
|
||||
"props": {
|
||||
"className": "relative z-10 h-full p-8"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "canvas-elements",
|
||||
"type": "div",
|
||||
"name": "CanvasElements",
|
||||
"props": {
|
||||
"className": "space-y-4"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "canvas-toolbar",
|
||||
"type": "div",
|
||||
"name": "CanvasToolbar",
|
||||
"props": {
|
||||
"className": "absolute top-4 right-4 flex gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "zoom-controls",
|
||||
"type": "div",
|
||||
"name": "ZoomControls",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1 bg-background border rounded-lg p-1 shadow-lg"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "view-controls",
|
||||
"type": "div",
|
||||
"name": "ViewControls",
|
||||
"props": {
|
||||
"className": "flex items-center gap-1 bg-background border rounded-lg p-1 shadow-lg"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "org-tree-4",
|
||||
"name": "TreeListPanel",
|
||||
"description": "Panel for displaying and managing tree structures",
|
||||
"category": "organism",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "tree-panel-root",
|
||||
"type": "div",
|
||||
"name": "TreePanelContainer",
|
||||
"props": {
|
||||
"className": "flex flex-col h-full"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "panel-header",
|
||||
"type": "div",
|
||||
"name": "PanelHeader",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between p-4 border-b"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "panel-title",
|
||||
"type": "Heading",
|
||||
"name": "PanelTitle",
|
||||
"props": {
|
||||
"level": 3,
|
||||
"className": "font-semibold"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "panel-actions",
|
||||
"type": "div",
|
||||
"name": "PanelActions",
|
||||
"props": {
|
||||
"className": "flex gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "add-tree-button",
|
||||
"type": "Button",
|
||||
"name": "AddTreeButton",
|
||||
"props": {
|
||||
"size": "sm",
|
||||
"variant": "default"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "refresh-button",
|
||||
"type": "Button",
|
||||
"name": "RefreshButton",
|
||||
"props": {
|
||||
"size": "sm",
|
||||
"variant": "ghost"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "search-section",
|
||||
"type": "div",
|
||||
"name": "SearchSection",
|
||||
"props": {
|
||||
"className": "p-4 border-b"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "search-input",
|
||||
"type": "SearchInput",
|
||||
"name": "TreeSearch",
|
||||
"props": {
|
||||
"placeholder": "Search trees..."
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tree-list",
|
||||
"type": "ScrollArea",
|
||||
"name": "TreeList",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "tree-items",
|
||||
"type": "div",
|
||||
"name": "TreeItems",
|
||||
"props": {
|
||||
"className": "p-4 space-y-2"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "panel-footer",
|
||||
"type": "div",
|
||||
"name": "PanelFooter",
|
||||
"props": {
|
||||
"className": "p-4 border-t bg-muted/40"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "footer-stats",
|
||||
"type": "div",
|
||||
"name": "FooterStats",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between text-xs text-muted-foreground"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "org-tree-5",
|
||||
"name": "DataTable",
|
||||
"description": "Complex data table with sorting, filtering, and pagination",
|
||||
"category": "organism",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "data-table-root",
|
||||
"type": "div",
|
||||
"name": "DataTableContainer",
|
||||
"props": {
|
||||
"className": "flex flex-col gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "table-toolbar",
|
||||
"type": "div",
|
||||
"name": "TableToolbar",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between gap-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "table-search",
|
||||
"type": "SearchInput",
|
||||
"name": "TableSearch",
|
||||
"props": {
|
||||
"placeholder": "Filter results...",
|
||||
"className": "max-w-sm"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "table-actions",
|
||||
"type": "div",
|
||||
"name": "TableActions",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "filter-button",
|
||||
"type": "Button",
|
||||
"name": "FilterButton",
|
||||
"props": {
|
||||
"variant": "outline",
|
||||
"size": "sm"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "export-button",
|
||||
"type": "Button",
|
||||
"name": "ExportButton",
|
||||
"props": {
|
||||
"variant": "outline",
|
||||
"size": "sm"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "table-wrapper",
|
||||
"type": "div",
|
||||
"name": "TableWrapper",
|
||||
"props": {
|
||||
"className": "rounded-lg border bg-background"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "table",
|
||||
"type": "Table",
|
||||
"name": "DataTable",
|
||||
"props": {},
|
||||
"children": [
|
||||
{
|
||||
"id": "table-header",
|
||||
"type": "TableHeader",
|
||||
"name": "TableHeader",
|
||||
"props": {},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "table-body",
|
||||
"type": "TableBody",
|
||||
"name": "TableBody",
|
||||
"props": {},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "table-pagination",
|
||||
"type": "div",
|
||||
"name": "TablePagination",
|
||||
"props": {
|
||||
"className": "flex items-center justify-between"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "pagination-info",
|
||||
"type": "Text",
|
||||
"name": "PaginationInfo",
|
||||
"props": {
|
||||
"className": "text-sm text-muted-foreground"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "pagination-controls",
|
||||
"type": "div",
|
||||
"name": "PaginationControls",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
},
|
||||
{
|
||||
"id": "org-tree-6",
|
||||
"name": "SchemaEditorSidebar",
|
||||
"description": "Sidebar with component palette and properties panel",
|
||||
"category": "organism",
|
||||
"rootNodes": [
|
||||
{
|
||||
"id": "sidebar-root",
|
||||
"type": "aside",
|
||||
"name": "SidebarContainer",
|
||||
"props": {
|
||||
"className": "w-80 border-l bg-muted/20 flex flex-col h-full"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "sidebar-tabs",
|
||||
"type": "Tabs",
|
||||
"name": "SidebarTabs",
|
||||
"props": {
|
||||
"className": "flex-1 flex flex-col",
|
||||
"defaultValue": "components"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "tabs-list",
|
||||
"type": "TabsList",
|
||||
"name": "TabsList",
|
||||
"props": {
|
||||
"className": "w-full grid grid-cols-2 p-1 m-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "components-tab",
|
||||
"type": "TabsTrigger",
|
||||
"name": "ComponentsTab",
|
||||
"props": {
|
||||
"value": "components"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "properties-tab",
|
||||
"type": "TabsTrigger",
|
||||
"name": "PropertiesTab",
|
||||
"props": {
|
||||
"value": "properties"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "components-content",
|
||||
"type": "TabsContent",
|
||||
"name": "ComponentsContent",
|
||||
"props": {
|
||||
"value": "components",
|
||||
"className": "flex-1 mt-0"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "components-search",
|
||||
"type": "div",
|
||||
"name": "ComponentsSearch",
|
||||
"props": {
|
||||
"className": "p-4 border-b"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "components-list",
|
||||
"type": "ScrollArea",
|
||||
"name": "ComponentsList",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "properties-content",
|
||||
"type": "TabsContent",
|
||||
"name": "PropertiesContent",
|
||||
"props": {
|
||||
"value": "properties",
|
||||
"className": "flex-1 mt-0"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "properties-panel",
|
||||
"type": "ScrollArea",
|
||||
"name": "PropertiesPanel",
|
||||
"props": {
|
||||
"className": "flex-1"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1704067200000,
|
||||
"updatedAt": 1704067200000
|
||||
}
|
||||
]
|
||||
}
|
||||
123
src/hooks/use-component-tree-loader.ts
Normal file
123
src/hooks/use-component-tree-loader.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useCallback, useState, useEffect } from 'react'
|
||||
import { ComponentTree } from '@/types/project'
|
||||
import componentTreesData from '@/config/component-trees'
|
||||
|
||||
export function useComponentTreeLoader() {
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
const loadComponentTrees = useCallback(async () => {
|
||||
if (isLoading || isLoaded) {
|
||||
console.log('[COMPONENT_TREES] ⏭️ Skipping load (already loading or loaded)')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[COMPONENT_TREES] 🚀 Starting component trees load')
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
console.time('[COMPONENT_TREES] Load duration')
|
||||
|
||||
try {
|
||||
if (!window.spark?.kv) {
|
||||
console.warn('[COMPONENT_TREES] ⚠️ Spark KV not available')
|
||||
throw new Error('Spark KV not available')
|
||||
}
|
||||
|
||||
const existingTrees = await window.spark.kv.get<ComponentTree[]>('project-component-trees')
|
||||
|
||||
if (!existingTrees || existingTrees.length === 0) {
|
||||
console.log('[COMPONENT_TREES] 📦 No existing component trees, loading from JSON')
|
||||
await window.spark.kv.set('project-component-trees', componentTreesData.all)
|
||||
console.log('[COMPONENT_TREES] ✅ Loaded', componentTreesData.all.length, 'component trees')
|
||||
} else {
|
||||
console.log('[COMPONENT_TREES] ✅ Found', existingTrees.length, 'existing component trees')
|
||||
|
||||
const newTrees = componentTreesData.all.filter(
|
||||
newTree => !existingTrees.some(existingTree => existingTree.id === newTree.id)
|
||||
)
|
||||
|
||||
if (newTrees.length > 0) {
|
||||
console.log('[COMPONENT_TREES] 📦 Merging', newTrees.length, 'new component trees')
|
||||
const mergedTrees = [...existingTrees, ...newTrees]
|
||||
await window.spark.kv.set('project-component-trees', mergedTrees)
|
||||
console.log('[COMPONENT_TREES] ✅ Merged component trees, total:', mergedTrees.length)
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoaded(true)
|
||||
console.log('[COMPONENT_TREES] ✅ Component trees load complete')
|
||||
} catch (err) {
|
||||
console.error('[COMPONENT_TREES] ❌ Failed to load component trees:', err)
|
||||
setError(err instanceof Error ? err : new Error('Unknown error'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
console.timeEnd('[COMPONENT_TREES] Load duration')
|
||||
}
|
||||
}, [isLoading, isLoaded])
|
||||
|
||||
const getComponentTrees = useCallback(async (): Promise<ComponentTree[]> => {
|
||||
if (!window.spark?.kv) {
|
||||
console.warn('[COMPONENT_TREES] ⚠️ Spark KV not available')
|
||||
return []
|
||||
}
|
||||
|
||||
const trees = await window.spark.kv.get<ComponentTree[]>('project-component-trees')
|
||||
return trees || []
|
||||
}, [])
|
||||
|
||||
const getComponentTreesByCategory = useCallback(async (category: 'molecule' | 'organism'): Promise<ComponentTree[]> => {
|
||||
const trees = await getComponentTrees()
|
||||
return trees.filter(tree => (tree as any).category === category)
|
||||
}, [getComponentTrees])
|
||||
|
||||
const getComponentTreeById = useCallback(async (id: string): Promise<ComponentTree | undefined> => {
|
||||
const trees = await getComponentTrees()
|
||||
return trees.find(tree => tree.id === id)
|
||||
}, [getComponentTrees])
|
||||
|
||||
const getComponentTreeByName = useCallback(async (name: string): Promise<ComponentTree | undefined> => {
|
||||
const trees = await getComponentTrees()
|
||||
return trees.find(tree => tree.name === name)
|
||||
}, [getComponentTrees])
|
||||
|
||||
const reloadFromJSON = useCallback(async () => {
|
||||
console.log('[COMPONENT_TREES] 🔄 Reloading component trees from JSON')
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
if (!window.spark?.kv) {
|
||||
throw new Error('Spark KV not available')
|
||||
}
|
||||
|
||||
await window.spark.kv.set('project-component-trees', componentTreesData.all)
|
||||
console.log('[COMPONENT_TREES] ✅ Reloaded', componentTreesData.all.length, 'component trees')
|
||||
setIsLoaded(true)
|
||||
} catch (err) {
|
||||
console.error('[COMPONENT_TREES] ❌ Failed to reload component trees:', err)
|
||||
setError(err instanceof Error ? err : new Error('Unknown error'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadComponentTrees()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
isLoaded,
|
||||
isLoading,
|
||||
error,
|
||||
loadComponentTrees,
|
||||
getComponentTrees,
|
||||
getComponentTreesByCategory,
|
||||
getComponentTreeById,
|
||||
getComponentTreeByName,
|
||||
reloadFromJSON,
|
||||
moleculeTrees: componentTreesData.molecules,
|
||||
organismTrees: componentTreesData.organisms,
|
||||
allTrees: componentTreesData.all,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user