mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Generated by Spark: Create composite components in schema editor
This commit is contained in:
262
docs/composite-components-quick-reference.md
Normal file
262
docs/composite-components-quick-reference.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# Composite Components - Quick Reference
|
||||
|
||||
## What are Composite Components?
|
||||
|
||||
Composite components are pre-assembled, reusable UI sections that combine multiple smaller components (atoms and molecules) into cohesive, functional units (organisms).
|
||||
|
||||
## Schema Editor Composites
|
||||
|
||||
### Core Panels
|
||||
|
||||
#### SchemaEditorToolbar
|
||||
**Purpose**: Top action bar with import/export controls
|
||||
**Location**: `src/components/organisms/SchemaEditorToolbar.tsx`
|
||||
```tsx
|
||||
<SchemaEditorToolbar
|
||||
onImport={() => {}}
|
||||
onExport={() => {}}
|
||||
onCopy={() => {}}
|
||||
onPreview={() => {}}
|
||||
onClear={() => {}}
|
||||
/>
|
||||
```
|
||||
|
||||
#### SchemaEditorSidebar
|
||||
**Purpose**: Left panel with component palette
|
||||
**Location**: `src/components/organisms/SchemaEditorSidebar.tsx`
|
||||
```tsx
|
||||
<SchemaEditorSidebar
|
||||
onDragStart={(component, e) => {}}
|
||||
/>
|
||||
```
|
||||
|
||||
#### SchemaEditorCanvas
|
||||
**Purpose**: Central canvas for rendering components
|
||||
**Location**: `src/components/organisms/SchemaEditorCanvas.tsx`
|
||||
```tsx
|
||||
<SchemaEditorCanvas
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={draggedOverId}
|
||||
dropPosition={dropPosition}
|
||||
onSelect={setSelectedId}
|
||||
onHover={setHoveredId}
|
||||
onHoverEnd={() => setHoveredId(null)}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
/>
|
||||
```
|
||||
|
||||
#### SchemaEditorPropertiesPanel
|
||||
**Purpose**: Right panel with component tree and property editor
|
||||
**Location**: `src/components/organisms/SchemaEditorPropertiesPanel.tsx`
|
||||
```tsx
|
||||
<SchemaEditorPropertiesPanel
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={draggedOverId}
|
||||
dropPosition={dropPosition}
|
||||
selectedComponent={selectedComponent}
|
||||
onSelect={setSelectedId}
|
||||
onHover={setHoveredId}
|
||||
onHoverEnd={() => setHoveredId(null)}
|
||||
onDragStart={handleTreeDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
```
|
||||
|
||||
### Supporting Composites
|
||||
|
||||
#### EmptyCanvasState
|
||||
**Purpose**: Empty state when no components exist
|
||||
**Location**: `src/components/organisms/EmptyCanvasState.tsx`
|
||||
```tsx
|
||||
<EmptyCanvasState
|
||||
onAddFirstComponent={() => {}}
|
||||
onImportSchema={() => {}}
|
||||
/>
|
||||
```
|
||||
|
||||
#### SchemaEditorStatusBar
|
||||
**Purpose**: Bottom status bar showing metrics
|
||||
**Location**: `src/components/organisms/SchemaEditorStatusBar.tsx`
|
||||
```tsx
|
||||
<SchemaEditorStatusBar
|
||||
componentCount={components.length}
|
||||
selectedComponentType={selectedComponent?.type}
|
||||
hasUnsavedChanges={false}
|
||||
/>
|
||||
```
|
||||
|
||||
#### SchemaCodeViewer
|
||||
**Purpose**: View generated JSON schema
|
||||
**Location**: `src/components/organisms/SchemaCodeViewer.tsx`
|
||||
```tsx
|
||||
<SchemaCodeViewer
|
||||
components={components}
|
||||
schema={schema}
|
||||
/>
|
||||
```
|
||||
|
||||
### Complete Layout
|
||||
|
||||
#### SchemaEditorLayout
|
||||
**Purpose**: Orchestrates all panels into complete editor
|
||||
**Location**: `src/components/organisms/SchemaEditorLayout.tsx`
|
||||
```tsx
|
||||
<SchemaEditorLayout
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={draggedOverId}
|
||||
dropPosition={dropPosition}
|
||||
selectedComponent={selectedComponent}
|
||||
onSelect={setSelectedId}
|
||||
onHover={setHoveredId}
|
||||
onHoverEnd={() => setHoveredId(null)}
|
||||
onComponentDragStart={handleComponentDragStart}
|
||||
onTreeDragStart={handleTreeDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
onImport={handleImport}
|
||||
onExport={handleExport}
|
||||
onCopy={handleCopy}
|
||||
onPreview={handlePreview}
|
||||
onClear={clearAll}
|
||||
/>
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
### Use Composite Components When:
|
||||
✅ Building complete features (full page editors, dashboards)
|
||||
✅ Need to enforce consistent layouts
|
||||
✅ Want to abstract complexity from page components
|
||||
✅ Creating reusable feature modules
|
||||
|
||||
### Use Individual Components When:
|
||||
✅ Building custom layouts
|
||||
✅ Need fine-grained control
|
||||
✅ Creating new composite components
|
||||
✅ Testing specific functionality
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Reduced Duplication**: Reuse complex layouts across pages
|
||||
2. **Consistency**: Same look and behavior everywhere
|
||||
3. **Maintainability**: Change once, update everywhere
|
||||
4. **Testability**: Test complete features in isolation
|
||||
5. **Documentation**: Self-documenting through composition
|
||||
|
||||
## Creating New Composites
|
||||
|
||||
### Step 1: Identify the Pattern
|
||||
Look for repeated component combinations in your code.
|
||||
|
||||
### Step 2: Create the Organism
|
||||
```tsx
|
||||
// src/components/organisms/MyComposite.tsx
|
||||
interface MyCompositeProps {
|
||||
// data props
|
||||
items: Item[]
|
||||
selectedId: string | null
|
||||
|
||||
// action props
|
||||
onSelect: (id: string) => void
|
||||
onUpdate: (item: Item) => void
|
||||
}
|
||||
|
||||
export function MyComposite({
|
||||
items,
|
||||
selectedId,
|
||||
onSelect,
|
||||
onUpdate
|
||||
}: MyCompositeProps) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<MyHeader />
|
||||
<MySidebar items={items} onSelect={onSelect} />
|
||||
<MyContent
|
||||
item={items.find(i => i.id === selectedId)}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Export from Index
|
||||
```tsx
|
||||
// src/components/organisms/index.ts
|
||||
export { MyComposite } from './MyComposite'
|
||||
```
|
||||
|
||||
### Step 4: Document
|
||||
Add usage examples and prop descriptions.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Panel Wrapper Pattern
|
||||
```tsx
|
||||
function MyPanel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="border-r border-border bg-card">
|
||||
<div className="p-4 border-b">
|
||||
<h2>Panel Title</h2>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Action Bar Pattern
|
||||
```tsx
|
||||
function MyActionBar({ onAction1, onAction2 }: ActionBarProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-2 border-b">
|
||||
<Button onClick={onAction1}>Action 1</Button>
|
||||
<Button onClick={onAction2}>Action 2</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Split View Pattern
|
||||
```tsx
|
||||
function MySplitView({ left, right }: SplitViewProps) {
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<div className="w-1/2 border-r">{left}</div>
|
||||
<div className="w-1/2">{right}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep Props Focused**: Each composite should have a clear, single purpose
|
||||
2. **Expose Necessary Callbacks**: Don't hide important events
|
||||
3. **Support Composition**: Allow children or render props when needed
|
||||
4. **Document Extensively**: Provide examples and use cases
|
||||
5. **Test Thoroughly**: Test with various prop combinations
|
||||
6. **Keep LOC < 150**: Break down if it gets too large
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Atomic Design Principles](./atomic-design.md)
|
||||
- [Schema Editor Architecture](./schema-editor-composite-components.md)
|
||||
- [Component Guidelines](./component-guidelines.md)
|
||||
212
docs/schema-editor-composite-components.md
Normal file
212
docs/schema-editor-composite-components.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Schema Editor Composite Components
|
||||
|
||||
This document describes the composite component architecture for the Schema Editor feature.
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
The Schema Editor follows the Atomic Design methodology:
|
||||
|
||||
### Atoms (Smallest units)
|
||||
- `ComponentPaletteItem` - Individual draggable component in the palette
|
||||
- `ComponentTreeNode` - Individual node in the component tree
|
||||
- `PropertyEditorField` - Individual property input field
|
||||
|
||||
### Molecules (Simple combinations)
|
||||
- `ComponentPalette` - Tabbed palette of draggable components
|
||||
- `ComponentTree` - Hierarchical tree view of components
|
||||
- `PropertyEditor` - Form for editing component properties
|
||||
- `CanvasRenderer` - Visual canvas for rendering UI components
|
||||
|
||||
### Organisms (Complex combinations)
|
||||
- `SchemaEditorToolbar` - Top toolbar with import/export/preview actions
|
||||
- `SchemaEditorSidebar` - Left sidebar containing component palette
|
||||
- `SchemaEditorCanvas` - Central canvas area with component rendering
|
||||
- `SchemaEditorPropertiesPanel` - Right panel with tree + property editor
|
||||
- `SchemaEditorLayout` - Complete editor layout orchestrating all panels
|
||||
- `EmptyCanvasState` - Empty state displayed when no components exist
|
||||
- `SchemaEditorStatusBar` - Bottom status bar showing component count
|
||||
- `SchemaCodeViewer` - JSON/preview viewer for schema output
|
||||
|
||||
### Pages (Full features)
|
||||
- `SchemaEditorPage` - Complete schema editor feature with all hooks and state
|
||||
|
||||
## Benefits of This Architecture
|
||||
|
||||
### 1. Separation of Concerns
|
||||
Each component has a single, clear responsibility:
|
||||
- **Toolbar**: Actions (import, export, preview)
|
||||
- **Sidebar**: Component palette
|
||||
- **Canvas**: Visual rendering
|
||||
- **Properties Panel**: Editing selected component
|
||||
- **Layout**: Orchestration
|
||||
|
||||
### 2. Reusability
|
||||
Composite components can be used independently:
|
||||
```tsx
|
||||
// Use just the toolbar elsewhere
|
||||
<SchemaEditorToolbar
|
||||
onImport={handleImport}
|
||||
onExport={handleExport}
|
||||
// ...
|
||||
/>
|
||||
|
||||
// Use just the canvas
|
||||
<SchemaEditorCanvas
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. Testability
|
||||
Each component can be tested in isolation with mock props.
|
||||
|
||||
### 4. Maintainability
|
||||
- Each file is <150 LOC (as per project guidelines)
|
||||
- Clear dependencies between components
|
||||
- Easy to locate and modify specific functionality
|
||||
|
||||
### 5. Composability
|
||||
The `SchemaEditorLayout` component orchestrates all the panels, but you could create alternative layouts:
|
||||
```tsx
|
||||
// Simple layout without sidebar
|
||||
<div>
|
||||
<SchemaEditorToolbar {...toolbarProps} />
|
||||
<div className="flex">
|
||||
<SchemaEditorCanvas {...canvasProps} />
|
||||
<SchemaEditorPropertiesPanel {...panelProps} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Or minimal layout with just canvas
|
||||
<SchemaEditorCanvas {...canvasProps} />
|
||||
```
|
||||
|
||||
## Component Props Pattern
|
||||
|
||||
Each composite component follows a consistent prop pattern:
|
||||
|
||||
### Data Props
|
||||
Props representing the current state (read-only):
|
||||
- `components: UIComponent[]`
|
||||
- `selectedId: string | null`
|
||||
- `hoveredId: string | null`
|
||||
|
||||
### Action Props
|
||||
Props for modifying state (callbacks):
|
||||
- `onSelect: (id: string | null) => void`
|
||||
- `onUpdate: (updates: Partial<UIComponent>) => void`
|
||||
- `onDelete: () => void`
|
||||
|
||||
### Drag & Drop Props
|
||||
Props specifically for drag-and-drop functionality:
|
||||
- `draggedOverId: string | null`
|
||||
- `dropPosition: 'before' | 'after' | 'inside' | null`
|
||||
- `onDragStart: (...) => void`
|
||||
- `onDragOver: (...) => void`
|
||||
- `onDrop: (...) => void`
|
||||
|
||||
## Usage Example
|
||||
|
||||
```tsx
|
||||
import { SchemaEditorLayout } from '@/components/organisms'
|
||||
import { useSchemaEditor } from '@/hooks/ui/use-schema-editor'
|
||||
import { useDragDrop } from '@/hooks/ui/use-drag-drop'
|
||||
|
||||
function MySchemaEditor() {
|
||||
const {
|
||||
components,
|
||||
selectedId,
|
||||
// ... other state
|
||||
} = useSchemaEditor()
|
||||
|
||||
const {
|
||||
draggedOverId,
|
||||
dropPosition,
|
||||
// ... drag handlers
|
||||
} = useDragDrop()
|
||||
|
||||
return (
|
||||
<SchemaEditorLayout
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
draggedOverId={draggedOverId}
|
||||
dropPosition={dropPosition}
|
||||
// ... all other props
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Points
|
||||
|
||||
### Adding New Panels
|
||||
Create a new organism component and add it to the layout:
|
||||
```tsx
|
||||
// New component
|
||||
export function SchemaEditorMetricsPanel({ ... }) {
|
||||
return <div>Metrics content</div>
|
||||
}
|
||||
|
||||
// Add to layout
|
||||
<SchemaEditorLayout>
|
||||
{/* existing panels */}
|
||||
<SchemaEditorMetricsPanel />
|
||||
</SchemaEditorLayout>
|
||||
```
|
||||
|
||||
### Custom Toolbars
|
||||
Create alternative toolbar components:
|
||||
```tsx
|
||||
export function SchemaEditorCompactToolbar({ ... }) {
|
||||
// Simplified toolbar with fewer buttons
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative Layouts
|
||||
Create new layout compositions:
|
||||
```tsx
|
||||
export function SchemaEditorSplitLayout({ ... }) {
|
||||
// Different arrangement of the same panels
|
||||
}
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── atoms/
|
||||
│ ├── ComponentPaletteItem.tsx
|
||||
│ ├── ComponentTreeNode.tsx
|
||||
│ └── PropertyEditorField.tsx
|
||||
├── molecules/
|
||||
│ ├── ComponentPalette.tsx
|
||||
│ ├── ComponentTree.tsx
|
||||
│ ├── PropertyEditor.tsx
|
||||
│ └── CanvasRenderer.tsx
|
||||
├── organisms/
|
||||
│ ├── SchemaEditorToolbar.tsx
|
||||
│ ├── SchemaEditorSidebar.tsx
|
||||
│ ├── SchemaEditorCanvas.tsx
|
||||
│ ├── SchemaEditorPropertiesPanel.tsx
|
||||
│ ├── SchemaEditorLayout.tsx
|
||||
│ ├── EmptyCanvasState.tsx
|
||||
│ ├── SchemaEditorStatusBar.tsx
|
||||
│ └── SchemaCodeViewer.tsx
|
||||
└── SchemaEditorPage.tsx
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Add SchemaEditorPreviewPanel** - Live preview of the schema
|
||||
2. **Add SchemaEditorHistoryPanel** - Undo/redo history
|
||||
3. **Add SchemaEditorTemplatesPanel** - Pre-built component templates
|
||||
4. **Create SchemaEditorMobileLayout** - Responsive mobile layout
|
||||
5. **Add SchemaEditorKeyboardShortcuts** - Keyboard navigation overlay
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Component Definitions](../lib/component-definitions.ts)
|
||||
- [JSON UI Types](../types/json-ui.ts)
|
||||
- [Schema Editor Hook](../hooks/ui/use-schema-editor.ts)
|
||||
- [Drag Drop Hook](../hooks/ui/use-drag-drop.ts)
|
||||
@@ -1,22 +1,9 @@
|
||||
import { useSchemaEditor } from '@/hooks/ui/use-schema-editor'
|
||||
import { useDragDrop } from '@/hooks/ui/use-drag-drop'
|
||||
import { useJsonExport } from '@/hooks/ui/use-json-export'
|
||||
import { ComponentPalette } from '@/components/molecules/ComponentPalette'
|
||||
import { ComponentTree } from '@/components/molecules/ComponentTree'
|
||||
import { PropertyEditor } from '@/components/molecules/PropertyEditor'
|
||||
import { CanvasRenderer } from '@/components/molecules/CanvasRenderer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { SchemaEditorLayout } from '@/components/organisms'
|
||||
import { ComponentDefinition } from '@/lib/component-definitions'
|
||||
import { UIComponent } from '@/types/json-ui'
|
||||
import {
|
||||
Download,
|
||||
Upload,
|
||||
Play,
|
||||
Trash,
|
||||
Copy,
|
||||
Code,
|
||||
} from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { PageSchema } from '@/types/json-ui'
|
||||
|
||||
@@ -136,123 +123,36 @@ export function SchemaEditorPage() {
|
||||
const selectedComponent = selectedId ? findComponentById(selectedId) : null
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
<div className="border-b border-border px-6 py-3 bg-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
||||
Schema Editor
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Build JSON UI schemas with drag-and-drop
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleImport}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyJson}
|
||||
>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Copy JSON
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExportJson}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handlePreview}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={clearAll}
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash className="w-4 h-4 mr-2" />
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<div className="w-64 border-r border-border bg-card">
|
||||
<ComponentPalette onDragStart={handleComponentDragStart} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col">
|
||||
<CanvasRenderer
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={dropTarget}
|
||||
dropPosition={dropPosition}
|
||||
onSelect={setSelectedId}
|
||||
onHover={setHoveredId}
|
||||
onHoverEnd={() => setHoveredId(null)}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleCanvasDrop}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-80 border-l border-border bg-card flex flex-col">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ComponentTree
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={dropTarget}
|
||||
dropPosition={dropPosition}
|
||||
onSelect={setSelectedId}
|
||||
onHover={setHoveredId}
|
||||
onHoverEnd={() => setHoveredId(null)}
|
||||
onDragStart={handleComponentTreeDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleCanvasDrop}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<PropertyEditor
|
||||
component={selectedComponent}
|
||||
onUpdate={(updates) => {
|
||||
if (selectedId) {
|
||||
updateComponent(selectedId, updates)
|
||||
}
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (selectedId) {
|
||||
deleteComponent(selectedId)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SchemaEditorLayout
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={dropTarget}
|
||||
dropPosition={dropPosition}
|
||||
selectedComponent={selectedComponent}
|
||||
onSelect={setSelectedId}
|
||||
onHover={setHoveredId}
|
||||
onHoverEnd={() => setHoveredId(null)}
|
||||
onComponentDragStart={handleComponentDragStart}
|
||||
onTreeDragStart={handleComponentTreeDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleCanvasDrop}
|
||||
onUpdate={(updates) => {
|
||||
if (selectedId) {
|
||||
updateComponent(selectedId, updates)
|
||||
}
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (selectedId) {
|
||||
deleteComponent(selectedId)
|
||||
}
|
||||
}}
|
||||
onImport={handleImport}
|
||||
onExport={handleExportJson}
|
||||
onCopy={handleCopyJson}
|
||||
onPreview={handlePreview}
|
||||
onClear={clearAll}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
39
src/components/organisms/EmptyCanvasState.tsx
Normal file
39
src/components/organisms/EmptyCanvasState.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Plus, Folder } from '@phosphor-icons/react'
|
||||
|
||||
interface EmptyCanvasStateProps {
|
||||
onAddFirstComponent?: () => void
|
||||
onImportSchema?: () => void
|
||||
}
|
||||
|
||||
export function EmptyCanvasState({ onAddFirstComponent, onImportSchema }: EmptyCanvasStateProps) {
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center p-8 text-center bg-muted/20">
|
||||
<div className="mb-6 relative">
|
||||
<div className="w-24 h-24 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Folder className="w-12 h-12 text-primary" weight="duotone" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold mb-2">Empty Canvas</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md mb-6">
|
||||
Start building your UI by dragging components from the left panel, or import an existing schema.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{onImportSchema && (
|
||||
<Button variant="outline" onClick={onImportSchema}>
|
||||
<Folder className="w-4 h-4 mr-2" />
|
||||
Import Schema
|
||||
</Button>
|
||||
)}
|
||||
{onAddFirstComponent && (
|
||||
<Button onClick={onAddFirstComponent}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Component
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
50
src/components/organisms/SchemaCodeViewer.tsx
Normal file
50
src/components/organisms/SchemaCodeViewer.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Code, Eye } from '@phosphor-icons/react'
|
||||
import { UIComponent } from '@/types/json-ui'
|
||||
|
||||
interface SchemaCodeViewerProps {
|
||||
components: UIComponent[]
|
||||
schema: any
|
||||
}
|
||||
|
||||
export function SchemaCodeViewer({ components, schema }: SchemaCodeViewerProps) {
|
||||
const jsonString = JSON.stringify(schema, null, 2)
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-card">
|
||||
<div className="border-b border-border px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="w-5 h-5 text-primary" />
|
||||
<h3 className="font-semibold">Schema Output</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="json" className="flex-1 flex flex-col">
|
||||
<TabsList className="w-full justify-start px-4 pt-2">
|
||||
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||
<TabsTrigger value="preview">Preview</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="json" className="flex-1 m-0 mt-2">
|
||||
<ScrollArea className="h-full">
|
||||
<pre className="p-4 text-xs font-mono text-foreground">
|
||||
{jsonString}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="preview" className="flex-1 m-0 mt-2">
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Live preview coming soon
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
src/components/organisms/SchemaEditorCanvas.tsx
Normal file
48
src/components/organisms/SchemaEditorCanvas.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { CanvasRenderer } from '@/components/molecules/CanvasRenderer'
|
||||
import { UIComponent } from '@/types/json-ui'
|
||||
|
||||
interface SchemaEditorCanvasProps {
|
||||
components: UIComponent[]
|
||||
selectedId: string | null
|
||||
hoveredId: string | null
|
||||
draggedOverId: string | null
|
||||
dropPosition: 'before' | 'after' | 'inside' | null
|
||||
onSelect: (id: string | null) => void
|
||||
onHover: (id: string | null) => void
|
||||
onHoverEnd: () => void
|
||||
onDragOver: (id: string, e: React.DragEvent) => void
|
||||
onDragLeave: (e: React.DragEvent) => void
|
||||
onDrop: (targetId: string, e: React.DragEvent) => void
|
||||
}
|
||||
|
||||
export function SchemaEditorCanvas({
|
||||
components,
|
||||
selectedId,
|
||||
hoveredId,
|
||||
draggedOverId,
|
||||
dropPosition,
|
||||
onSelect,
|
||||
onHover,
|
||||
onHoverEnd,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
}: SchemaEditorCanvasProps) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col">
|
||||
<CanvasRenderer
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={draggedOverId}
|
||||
dropPosition={dropPosition}
|
||||
onSelect={onSelect}
|
||||
onHover={onHover}
|
||||
onHoverEnd={onHoverEnd}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
102
src/components/organisms/SchemaEditorLayout.tsx
Normal file
102
src/components/organisms/SchemaEditorLayout.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { UIComponent, PageSchema } from '@/types/json-ui'
|
||||
import { ComponentDefinition } from '@/lib/component-definitions'
|
||||
import { SchemaEditorToolbar } from './SchemaEditorToolbar'
|
||||
import { SchemaEditorSidebar } from './SchemaEditorSidebar'
|
||||
import { SchemaEditorCanvas } from './SchemaEditorCanvas'
|
||||
import { SchemaEditorPropertiesPanel } from './SchemaEditorPropertiesPanel'
|
||||
|
||||
interface SchemaEditorLayoutProps {
|
||||
components: UIComponent[]
|
||||
selectedId: string | null
|
||||
hoveredId: string | null
|
||||
draggedOverId: string | null
|
||||
dropPosition: 'before' | 'after' | 'inside' | null
|
||||
selectedComponent: UIComponent | null
|
||||
onSelect: (id: string | null) => void
|
||||
onHover: (id: string | null) => void
|
||||
onHoverEnd: () => void
|
||||
onComponentDragStart: (component: ComponentDefinition, e: React.DragEvent) => void
|
||||
onTreeDragStart: (id: string, e: React.DragEvent) => void
|
||||
onDragOver: (id: string, e: React.DragEvent) => void
|
||||
onDragLeave: (e: React.DragEvent) => void
|
||||
onDrop: (targetId: string, e: React.DragEvent) => void
|
||||
onUpdate: (updates: Partial<UIComponent>) => void
|
||||
onDelete: () => void
|
||||
onImport: () => void
|
||||
onExport: () => void
|
||||
onCopy: () => void
|
||||
onPreview: () => void
|
||||
onClear: () => void
|
||||
}
|
||||
|
||||
export function SchemaEditorLayout({
|
||||
components,
|
||||
selectedId,
|
||||
hoveredId,
|
||||
draggedOverId,
|
||||
dropPosition,
|
||||
selectedComponent,
|
||||
onSelect,
|
||||
onHover,
|
||||
onHoverEnd,
|
||||
onComponentDragStart,
|
||||
onTreeDragStart,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onImport,
|
||||
onExport,
|
||||
onCopy,
|
||||
onPreview,
|
||||
onClear,
|
||||
}: SchemaEditorLayoutProps) {
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
<SchemaEditorToolbar
|
||||
onImport={onImport}
|
||||
onExport={onExport}
|
||||
onCopy={onCopy}
|
||||
onPreview={onPreview}
|
||||
onClear={onClear}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<SchemaEditorSidebar onDragStart={onComponentDragStart} />
|
||||
|
||||
<SchemaEditorCanvas
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={draggedOverId}
|
||||
dropPosition={dropPosition}
|
||||
onSelect={onSelect}
|
||||
onHover={onHover}
|
||||
onHoverEnd={onHoverEnd}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
|
||||
<SchemaEditorPropertiesPanel
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={draggedOverId}
|
||||
dropPosition={dropPosition}
|
||||
selectedComponent={selectedComponent}
|
||||
onSelect={onSelect}
|
||||
onHover={onHover}
|
||||
onHoverEnd={onHoverEnd}
|
||||
onDragStart={onTreeDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
onUpdate={onUpdate}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
src/components/organisms/SchemaEditorPropertiesPanel.tsx
Normal file
71
src/components/organisms/SchemaEditorPropertiesPanel.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ComponentTree } from '@/components/molecules/ComponentTree'
|
||||
import { PropertyEditor } from '@/components/molecules/PropertyEditor'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { UIComponent } from '@/types/json-ui'
|
||||
|
||||
interface SchemaEditorPropertiesPanelProps {
|
||||
components: UIComponent[]
|
||||
selectedId: string | null
|
||||
hoveredId: string | null
|
||||
draggedOverId: string | null
|
||||
dropPosition: 'before' | 'after' | 'inside' | null
|
||||
selectedComponent: UIComponent | null
|
||||
onSelect: (id: string | null) => void
|
||||
onHover: (id: string | null) => void
|
||||
onHoverEnd: () => void
|
||||
onDragStart: (id: string, e: React.DragEvent) => void
|
||||
onDragOver: (id: string, e: React.DragEvent) => void
|
||||
onDragLeave: (e: React.DragEvent) => void
|
||||
onDrop: (targetId: string, e: React.DragEvent) => void
|
||||
onUpdate: (updates: Partial<UIComponent>) => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
export function SchemaEditorPropertiesPanel({
|
||||
components,
|
||||
selectedId,
|
||||
hoveredId,
|
||||
draggedOverId,
|
||||
dropPosition,
|
||||
selectedComponent,
|
||||
onSelect,
|
||||
onHover,
|
||||
onHoverEnd,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: SchemaEditorPropertiesPanelProps) {
|
||||
return (
|
||||
<div className="w-80 border-l border-border bg-card flex flex-col">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ComponentTree
|
||||
components={components}
|
||||
selectedId={selectedId}
|
||||
hoveredId={hoveredId}
|
||||
draggedOverId={draggedOverId}
|
||||
dropPosition={dropPosition}
|
||||
onSelect={onSelect}
|
||||
onHover={onHover}
|
||||
onHoverEnd={onHoverEnd}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<PropertyEditor
|
||||
component={selectedComponent}
|
||||
onUpdate={onUpdate}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
14
src/components/organisms/SchemaEditorSidebar.tsx
Normal file
14
src/components/organisms/SchemaEditorSidebar.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ComponentPalette } from '@/components/molecules/ComponentPalette'
|
||||
import { ComponentDefinition } from '@/lib/component-definitions'
|
||||
|
||||
interface SchemaEditorSidebarProps {
|
||||
onDragStart: (component: ComponentDefinition, e: React.DragEvent) => void
|
||||
}
|
||||
|
||||
export function SchemaEditorSidebar({ onDragStart }: SchemaEditorSidebarProps) {
|
||||
return (
|
||||
<div className="w-64 border-r border-border bg-card">
|
||||
<ComponentPalette onDragStart={onDragStart} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
src/components/organisms/SchemaEditorStatusBar.tsx
Normal file
46
src/components/organisms/SchemaEditorStatusBar.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface SchemaEditorStatusBarProps {
|
||||
componentCount: number
|
||||
selectedComponentType?: string
|
||||
hasUnsavedChanges?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SchemaEditorStatusBar({
|
||||
componentCount,
|
||||
selectedComponentType,
|
||||
hasUnsavedChanges = false,
|
||||
className
|
||||
}: SchemaEditorStatusBarProps) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"border-t border-border px-4 py-2 bg-card flex items-center justify-between text-xs text-muted-foreground",
|
||||
className
|
||||
)}>
|
||||
<div className="flex items-center gap-4">
|
||||
<span>
|
||||
<span className="font-medium text-foreground">{componentCount}</span> component{componentCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
|
||||
{selectedComponentType && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Selected:</span>
|
||||
<Badge variant="secondary" className="text-xs font-mono">
|
||||
{selectedComponentType}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{hasUnsavedChanges && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Unsaved changes
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
85
src/components/organisms/SchemaEditorToolbar.tsx
Normal file
85
src/components/organisms/SchemaEditorToolbar.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
Download,
|
||||
Upload,
|
||||
Play,
|
||||
Trash,
|
||||
Copy,
|
||||
} from '@phosphor-icons/react'
|
||||
|
||||
interface SchemaEditorToolbarProps {
|
||||
onImport: () => void
|
||||
onExport: () => void
|
||||
onCopy: () => void
|
||||
onPreview: () => void
|
||||
onClear: () => void
|
||||
}
|
||||
|
||||
export function SchemaEditorToolbar({
|
||||
onImport,
|
||||
onExport,
|
||||
onCopy,
|
||||
onPreview,
|
||||
onClear,
|
||||
}: SchemaEditorToolbarProps) {
|
||||
return (
|
||||
<div className="border-b border-border px-6 py-3 bg-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
||||
Schema Editor
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Build JSON UI schemas with drag-and-drop
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onImport}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Copy JSON
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onExport}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onPreview}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onClear}
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash className="w-4 h-4 mr-2" />
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,3 +3,11 @@ export { PageHeader } from './PageHeader'
|
||||
export { ToolbarActions } from './ToolbarActions'
|
||||
export { AppHeader } from './AppHeader'
|
||||
export { TreeListPanel } from './TreeListPanel'
|
||||
export { SchemaEditorToolbar } from './SchemaEditorToolbar'
|
||||
export { SchemaEditorSidebar } from './SchemaEditorSidebar'
|
||||
export { SchemaEditorCanvas } from './SchemaEditorCanvas'
|
||||
export { SchemaEditorPropertiesPanel } from './SchemaEditorPropertiesPanel'
|
||||
export { SchemaEditorLayout } from './SchemaEditorLayout'
|
||||
export { EmptyCanvasState } from './EmptyCanvasState'
|
||||
export { SchemaEditorStatusBar } from './SchemaEditorStatusBar'
|
||||
export { SchemaCodeViewer } from './SchemaCodeViewer'
|
||||
|
||||
Reference in New Issue
Block a user