diff --git a/docs/composite-components-quick-reference.md b/docs/composite-components-quick-reference.md new file mode 100644 index 0000000..665ccc9 --- /dev/null +++ b/docs/composite-components-quick-reference.md @@ -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 + {}} + onExport={() => {}} + onCopy={() => {}} + onPreview={() => {}} + onClear={() => {}} +/> +``` + +#### SchemaEditorSidebar +**Purpose**: Left panel with component palette +**Location**: `src/components/organisms/SchemaEditorSidebar.tsx` +```tsx + {}} +/> +``` + +#### SchemaEditorCanvas +**Purpose**: Central canvas for rendering components +**Location**: `src/components/organisms/SchemaEditorCanvas.tsx` +```tsx + 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 + 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 + {}} + onImportSchema={() => {}} +/> +``` + +#### SchemaEditorStatusBar +**Purpose**: Bottom status bar showing metrics +**Location**: `src/components/organisms/SchemaEditorStatusBar.tsx` +```tsx + +``` + +#### SchemaCodeViewer +**Purpose**: View generated JSON schema +**Location**: `src/components/organisms/SchemaCodeViewer.tsx` +```tsx + +``` + +### Complete Layout + +#### SchemaEditorLayout +**Purpose**: Orchestrates all panels into complete editor +**Location**: `src/components/organisms/SchemaEditorLayout.tsx` +```tsx + 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 ( +
+ + + i.id === selectedId)} + onUpdate={onUpdate} + /> +
+ ) +} +``` + +### 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 ( +
+
+

Panel Title

+
+
+ {children} +
+
+ ) +} +``` + +### Action Bar Pattern +```tsx +function MyActionBar({ onAction1, onAction2 }: ActionBarProps) { + return ( +
+ + +
+ ) +} +``` + +### Split View Pattern +```tsx +function MySplitView({ left, right }: SplitViewProps) { + return ( +
+
{left}
+
{right}
+
+ ) +} +``` + +## 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) diff --git a/docs/schema-editor-composite-components.md b/docs/schema-editor-composite-components.md new file mode 100644 index 0000000..a714149 --- /dev/null +++ b/docs/schema-editor-composite-components.md @@ -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 + + +// Use just the canvas + +``` + +### 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 +
+ +
+ + +
+
+ +// Or minimal layout with just canvas + +``` + +## 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) => 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 ( + + ) +} +``` + +## Extension Points + +### Adding New Panels +Create a new organism component and add it to the layout: +```tsx +// New component +export function SchemaEditorMetricsPanel({ ... }) { + return
Metrics content
+} + +// Add to layout + + {/* existing panels */} + + +``` + +### 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) diff --git a/src/components/SchemaEditorPage.tsx b/src/components/SchemaEditorPage.tsx index 9d16e73..617beb4 100644 --- a/src/components/SchemaEditorPage.tsx +++ b/src/components/SchemaEditorPage.tsx @@ -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 ( -
-
-
-
-

- Schema Editor -

-

- Build JSON UI schemas with drag-and-drop -

-
- -
- - - - - - -
-
-
- -
-
- -
- -
- setHoveredId(null)} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} - onDrop={handleCanvasDrop} - /> -
- -
-
- setHoveredId(null)} - onDragStart={handleComponentTreeDragStart} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} - onDrop={handleCanvasDrop} - /> -
- - - -
- { - if (selectedId) { - updateComponent(selectedId, updates) - } - }} - onDelete={() => { - if (selectedId) { - deleteComponent(selectedId) - } - }} - /> -
-
-
-
+ 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} + /> ) } diff --git a/src/components/organisms/EmptyCanvasState.tsx b/src/components/organisms/EmptyCanvasState.tsx new file mode 100644 index 0000000..dddeb70 --- /dev/null +++ b/src/components/organisms/EmptyCanvasState.tsx @@ -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 ( +
+
+
+ +
+
+ +

Empty Canvas

+

+ Start building your UI by dragging components from the left panel, or import an existing schema. +

+ +
+ {onImportSchema && ( + + )} + {onAddFirstComponent && ( + + )} +
+
+ ) +} diff --git a/src/components/organisms/SchemaCodeViewer.tsx b/src/components/organisms/SchemaCodeViewer.tsx new file mode 100644 index 0000000..5bbff67 --- /dev/null +++ b/src/components/organisms/SchemaCodeViewer.tsx @@ -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 ( +
+
+
+
+ +

Schema Output

+
+
+
+ + + + JSON + Preview + + + + +
+              {jsonString}
+            
+
+
+ + +
+

+ Live preview coming soon +

+
+
+
+
+ ) +} diff --git a/src/components/organisms/SchemaEditorCanvas.tsx b/src/components/organisms/SchemaEditorCanvas.tsx new file mode 100644 index 0000000..4f5f855 --- /dev/null +++ b/src/components/organisms/SchemaEditorCanvas.tsx @@ -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 ( +
+ +
+ ) +} diff --git a/src/components/organisms/SchemaEditorLayout.tsx b/src/components/organisms/SchemaEditorLayout.tsx new file mode 100644 index 0000000..78a9d9f --- /dev/null +++ b/src/components/organisms/SchemaEditorLayout.tsx @@ -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) => 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 ( +
+ + +
+ + + + + +
+
+ ) +} diff --git a/src/components/organisms/SchemaEditorPropertiesPanel.tsx b/src/components/organisms/SchemaEditorPropertiesPanel.tsx new file mode 100644 index 0000000..4420c52 --- /dev/null +++ b/src/components/organisms/SchemaEditorPropertiesPanel.tsx @@ -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) => void + onDelete: () => void +} + +export function SchemaEditorPropertiesPanel({ + components, + selectedId, + hoveredId, + draggedOverId, + dropPosition, + selectedComponent, + onSelect, + onHover, + onHoverEnd, + onDragStart, + onDragOver, + onDragLeave, + onDrop, + onUpdate, + onDelete, +}: SchemaEditorPropertiesPanelProps) { + return ( +
+
+ +
+ + + +
+ +
+
+ ) +} diff --git a/src/components/organisms/SchemaEditorSidebar.tsx b/src/components/organisms/SchemaEditorSidebar.tsx new file mode 100644 index 0000000..1ffb087 --- /dev/null +++ b/src/components/organisms/SchemaEditorSidebar.tsx @@ -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 ( +
+ +
+ ) +} diff --git a/src/components/organisms/SchemaEditorStatusBar.tsx b/src/components/organisms/SchemaEditorStatusBar.tsx new file mode 100644 index 0000000..b718dad --- /dev/null +++ b/src/components/organisms/SchemaEditorStatusBar.tsx @@ -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 ( +
+
+ + {componentCount} component{componentCount !== 1 ? 's' : ''} + + + {selectedComponentType && ( +
+ Selected: + + {selectedComponentType} + +
+ )} +
+ +
+ {hasUnsavedChanges && ( + + Unsaved changes + + )} +
+
+ ) +} diff --git a/src/components/organisms/SchemaEditorToolbar.tsx b/src/components/organisms/SchemaEditorToolbar.tsx new file mode 100644 index 0000000..df22c32 --- /dev/null +++ b/src/components/organisms/SchemaEditorToolbar.tsx @@ -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 ( +
+
+
+

+ Schema Editor +

+

+ Build JSON UI schemas with drag-and-drop +

+
+ +
+ + + + + + +
+
+
+ ) +} diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts index 9e673cf..17abb7a 100644 --- a/src/components/organisms/index.ts +++ b/src/components/organisms/index.ts @@ -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'