diff --git a/packages/designer/README.md b/packages/designer/README.md index 8f52eecf..c95e334f 100644 --- a/packages/designer/README.md +++ b/packages/designer/README.md @@ -1,6 +1,14 @@ # @object-ui/designer -A professional drag-and-drop visual editor to generate Object UI schemas with advanced features including **component resizing**. +A professional drag-and-drop visual editor to generate Object UI schemas with advanced features including **component resizing** and **specialized designers**. + +## 🎯 Specialized Designers + +Object UI Designer now includes three specialized designer modes optimized for different use cases: + +- **General Designer** - The original all-purpose designer with all component types +- **Form Designer** (专用对象表单设计器) - Purpose-built for creating forms with validation and field controls +- **Page Layout Designer** (页面布局设计器) - Optimized for building page structures with sections, headers, and grids ## Features @@ -68,7 +76,9 @@ pnpm add @object-ui/designer @object-ui/react @object-ui/components ## Usage -### Basic Example +### General Designer (All Components) + +The default designer with access to all component types: ```tsx import { Designer } from '@object-ui/designer'; @@ -91,6 +101,68 @@ function App() { } ``` +### Form Designer (专用对象表单设计器) + +A specialized designer optimized for creating forms with form-specific components: + +```tsx +import { FormDesigner } from '@object-ui/designer'; +import { useState } from 'react'; +import type { SchemaNode } from '@object-ui/core'; + +function App() { + const [formSchema, setFormSchema] = useState({ + type: 'form', + className: 'p-8 max-w-2xl mx-auto', + body: [] + }); + + return ( + + ); +} +``` + +**Features:** +- Form-specific component palette (text inputs, email, password, number, checkboxes, selects, etc.) +- Form validation preview +- Quick access to common form field types +- Form submission configuration + +### Page Layout Designer (页面布局设计器) + +A specialized designer optimized for building page layouts and structures: + +```tsx +import { PageLayoutDesigner } from '@object-ui/designer'; +import { useState } from 'react'; +import type { SchemaNode } from '@object-ui/core'; + +function App() { + const [pageSchema, setPageSchema] = useState({ + type: 'div', + className: 'min-h-screen bg-white', + body: [] + }); + + return ( + + ); +} +``` + +**Features:** +- Layout-focused component palette (header, footer, main, aside, sections, grids, columns) +- Responsive viewport preview (desktop/tablet/mobile) +- Grid overlay toggle for precise alignment +- Layout-specific property controls + ### With Initial Schema ```tsx diff --git a/packages/designer/src/components/FormComponentPalette.tsx b/packages/designer/src/components/FormComponentPalette.tsx new file mode 100644 index 00000000..4d456bdb --- /dev/null +++ b/packages/designer/src/components/FormComponentPalette.tsx @@ -0,0 +1,258 @@ +import React, { useState } from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { useDesigner } from '../context/DesignerContext'; +import { + Type, + CheckSquare, + ToggleLeft, + List, + FileText, + Calendar, + Mail, + Phone, + Lock, + Hash, + DollarSign, + Link2, + MousePointer2, + Search, + Tag +} from 'lucide-react'; +import { cn } from '@object-ui/components'; +import { ScrollArea } from '@object-ui/components'; +import { enableTouchDrag, isTouchDevice } from '../utils/touchDragPolyfill'; + +interface FormComponentPaletteProps { + className?: string; +} + +// Map form component types to Lucide icons +const getIconForType = (type: string) => { + switch (type) { + case 'input': return Type; + case 'textarea': return FileText; + case 'checkbox': return CheckSquare; + case 'switch': return ToggleLeft; + case 'select': return List; + case 'button': return MousePointer2; + case 'label': return Tag; + case 'date-picker': return Calendar; + case 'email-input': return Mail; + case 'phone-input': return Phone; + case 'password-input': return Lock; + case 'number-input': return Hash; + case 'url-input': return Link2; + case 'search-input': return Search; + case 'currency-input': return DollarSign; + default: return Type; + } +}; + +// Form-specific component categories +const FORM_CATEGORIES = { + 'Text Fields': [ + { type: 'input', label: 'Text Input', description: 'Single line text input' }, + { type: 'textarea', label: 'Text Area', description: 'Multi-line text input' }, + { type: 'email-input', label: 'Email', description: 'Email address input' }, + { type: 'password-input', label: 'Password', description: 'Password input field' }, + { type: 'url-input', label: 'URL', description: 'URL input field' }, + { type: 'search-input', label: 'Search', description: 'Search input field' }, + ], + 'Number Fields': [ + { type: 'number-input', label: 'Number', description: 'Numeric input' }, + { type: 'currency-input', label: 'Currency', description: 'Money/currency input' }, + ], + 'Selection': [ + { type: 'checkbox', label: 'Checkbox', description: 'Single checkbox' }, + { type: 'switch', label: 'Switch', description: 'Toggle switch' }, + { type: 'select', label: 'Select', description: 'Dropdown selection' }, + ], + 'Other': [ + { type: 'date-picker', label: 'Date Picker', description: 'Date selection' }, + { type: 'phone-input', label: 'Phone', description: 'Phone number input' }, + { type: 'label', label: 'Label', description: 'Form field label' }, + { type: 'button', label: 'Button', description: 'Action button' }, + ] +}; + +// Component item with touch support +interface ComponentItemProps { + type: string; + label: string; + description: string; + Icon: any; + onDragStart: (e: React.DragEvent, type: string) => void; + onDragEnd: () => void; +} + +const ComponentItem: React.FC = React.memo(({ + type, + label, + description, + Icon, + onDragStart, + onDragEnd +}) => { + const itemRef = React.useRef(null); + const { setDraggingType } = useDesigner(); + + // Setup touch drag support + React.useEffect(() => { + if (!itemRef.current || !isTouchDevice()) return; + + const cleanup = enableTouchDrag(itemRef.current, { + dragData: { componentType: type }, + onDragStart: () => { + setDraggingType(type); + }, + onDragEnd: () => { + setDraggingType(null); + } + }); + + return cleanup; + }, [type, setDraggingType]); + + return ( +
onDragStart(e, type)} + onDragEnd={onDragEnd} + className={cn( + "group flex items-center gap-3 p-3 rounded-lg border-2 border-transparent hover:border-indigo-200 hover:bg-gradient-to-br hover:from-indigo-50 hover:to-purple-50 hover:shadow-md cursor-grab active:cursor-grabbing transition-all duration-200 bg-white", + "hover:scale-102 active:scale-95 touch-none" + )} + aria-label={`${label} - ${description}`} + > +
+ +
+
+
{label}
+
{description}
+
+
+ ); +}); + +ComponentItem.displayName = 'ComponentItem'; + +/** + * FormComponentPalette Component + * A specialized component palette for form fields and controls + */ +export const FormComponentPalette: React.FC = ({ className }) => { + const { setDraggingType } = useDesigner(); + const [searchQuery, setSearchQuery] = useState(''); + const [expandedCategories, setExpandedCategories] = useState>( + new Set(Object.keys(FORM_CATEGORIES)) + ); + + const handleDragStart = (e: React.DragEvent, type: string) => { + e.dataTransfer.effectAllowed = 'copy'; + e.dataTransfer.setData('application/json', JSON.stringify({ componentType: type })); + setDraggingType(type); + }; + + const handleDragEnd = () => { + setDraggingType(null); + }; + + const toggleCategory = (category: string) => { + const newExpanded = new Set(expandedCategories); + if (newExpanded.has(category)) { + newExpanded.delete(category); + } else { + newExpanded.add(category); + } + setExpandedCategories(newExpanded); + }; + + // Filter components based on search + const filteredCategories = Object.entries(FORM_CATEGORIES).reduce((acc, [category, items]) => { + const filtered = items.filter(item => + item.label.toLowerCase().includes(searchQuery.toLowerCase()) || + item.type.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); + if (filtered.length > 0) { + acc[category] = filtered; + } + return acc; + }, {} as Record); + + return ( +
+ {/* Header */} +
+

Form Components

+

Drag components to canvas

+
+ + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + /> +
+
+ + {/* Component List */} + +
+ {Object.keys(filteredCategories).length === 0 ? ( +
+ No components found +
+ ) : ( + Object.entries(filteredCategories).map(([category, items]) => ( +
+ + {expandedCategories.has(category) && ( +
+ {items.map((item) => { + const Icon = getIconForType(item.type); + return ( + + ); + })} +
+ )} +
+ )) + )} +
+
+ + {/* Footer Tip */} +
+

+ 💡 Tip: Configure field validation in the property panel +

+
+
+ ); +}; diff --git a/packages/designer/src/components/FormDesigner.tsx b/packages/designer/src/components/FormDesigner.tsx new file mode 100644 index 00000000..951502b5 --- /dev/null +++ b/packages/designer/src/components/FormDesigner.tsx @@ -0,0 +1,147 @@ +import React, { useEffect } from 'react'; +import { DesignerProvider } from '../context/DesignerContext'; +import { Canvas } from './Canvas'; +import { PropertyPanel } from './PropertyPanel'; +import { useDesigner } from '../context/DesignerContext'; +import type { SchemaNode } from '@object-ui/core'; +import { FormComponentPalette } from './FormComponentPalette'; +import { FormToolbar } from './FormToolbar'; + +interface FormDesignerProps { + initialSchema?: SchemaNode; + onSchemaChange?: (schema: SchemaNode) => void; +} + +/** + * FormDesigner Content Component + * Handles keyboard shortcuts and layout for form-specific designer + */ +export const FormDesignerContent: React.FC = () => { + const { + undo, + redo, + copyNode, + cutNode, + duplicateNode, + pasteNode, + removeNode, + selectedNodeId, + canUndo, + canRedo + } = useDesigner(); + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const target = e.target as HTMLElement; + const isEditing = + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.tagName === 'SELECT' || + target.isContentEditable; + + // Undo: Ctrl+Z / Cmd+Z + if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey && canUndo) { + e.preventDefault(); + undo(); + } + // Redo: Ctrl+Y / Cmd+Shift+Z + else if (((e.ctrlKey || e.metaKey) && e.key === 'y') || ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z')) { + if (canRedo) { + e.preventDefault(); + redo(); + } + } + // Copy: Ctrl+C / Cmd+C (only when not editing) + else if ((e.ctrlKey || e.metaKey) && e.key === 'c' && !isEditing && selectedNodeId) { + e.preventDefault(); + copyNode(selectedNodeId); + } + // Cut: Ctrl+X / Cmd+X (only when not editing) + else if ((e.ctrlKey || e.metaKey) && e.key === 'x' && !isEditing && selectedNodeId) { + e.preventDefault(); + cutNode(selectedNodeId); + } + // Paste: Ctrl+V / Cmd+V (only when not editing) + else if ((e.ctrlKey || e.metaKey) && e.key === 'v' && !isEditing) { + e.preventDefault(); + pasteNode(selectedNodeId); + } + // Duplicate: Ctrl+D / Cmd+D (only when not editing) + else if ((e.ctrlKey || e.metaKey) && e.key === 'd' && !isEditing && selectedNodeId) { + e.preventDefault(); + duplicateNode(selectedNodeId); + } + // Delete: Delete / Backspace (only when not editing) + else if ((e.key === 'Delete' || e.key === 'Backspace') && !isEditing && selectedNodeId) { + e.preventDefault(); + removeNode(selectedNodeId); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [undo, redo, copyNode, cutNode, duplicateNode, pasteNode, removeNode, selectedNodeId, canUndo, canRedo]); + + return ( +
+ + +
+ {/* Left Sidebar - Form Component Palette */} +
+ +
+ + {/* Main Canvas Area */} +
+ +
+ + {/* Right Sidebar - Property Panel */} +
+ +
+
+
+ ); +}; + +/** + * FormDesigner Component + * A specialized designer focused on creating and editing forms + * + * @example + * ```tsx + * import { FormDesigner } from '@object-ui/designer'; + * + * function App() { + * const [schema, setSchema] = useState({ + * type: 'form', + * body: [] + * }); + * + * return ( + * + * ); + * } + * ``` + */ +export const FormDesigner: React.FC = ({ initialSchema, onSchemaChange }) => { + // Default form schema if none provided + const defaultSchema: SchemaNode = initialSchema || { + type: 'form', + id: 'form-root', + className: 'p-8 max-w-2xl mx-auto', + body: [] + }; + + return ( + + + + ); +}; diff --git a/packages/designer/src/components/FormToolbar.tsx b/packages/designer/src/components/FormToolbar.tsx new file mode 100644 index 00000000..63fe7334 --- /dev/null +++ b/packages/designer/src/components/FormToolbar.tsx @@ -0,0 +1,174 @@ +import React, { useState } from 'react'; +import { useDesigner } from '../context/DesignerContext'; +import { + Undo, + Redo, + Download, + Upload, + Play, + Save, + FileJson, + Copy, + Check, + Eye, + EyeOff, + Settings +} from 'lucide-react'; +import { cn } from '@object-ui/components'; + +interface FormToolbarProps { + className?: string; +} + +/** + * FormToolbar Component + * Toolbar specifically designed for form designer with form-specific actions + */ +export const FormToolbar: React.FC = ({ className }) => { + const { undo, redo, canUndo, canRedo, schema } = useDesigner(); + const [copied, setCopied] = useState(false); + const [showValidation, setShowValidation] = useState(true); + + const handleExportJSON = () => { + const json = JSON.stringify(schema, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'form-schema.json'; + a.click(); + URL.revokeObjectURL(url); + }; + + const handleImportJSON = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const json = JSON.parse(e.target?.result as string); + // TODO: Validate and load schema + console.log('Imported schema:', json); + } catch (err) { + console.error('Failed to parse JSON:', err); + } + }; + reader.readAsText(file); + } + }; + input.click(); + }; + + const handleCopyJSON = () => { + const json = JSON.stringify(schema, null, 2); + navigator.clipboard.writeText(json); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const handlePreviewForm = () => { + // TODO: Open preview modal + console.log('Preview form'); + }; + + return ( +
+ {/* Left Section - Title */} +
+
+ + Form Designer +
+
+ + {/* Center Section - Actions */} +
+ {/* Undo/Redo */} +
+ + +
+ + {/* Validation Toggle */} + + + {/* Preview */} + +
+ + {/* Right Section - Export/Import */} +
+ + + + + +
+
+ ); +}; diff --git a/packages/designer/src/components/PageLayoutComponentPalette.tsx b/packages/designer/src/components/PageLayoutComponentPalette.tsx new file mode 100644 index 00000000..908f237f --- /dev/null +++ b/packages/designer/src/components/PageLayoutComponentPalette.tsx @@ -0,0 +1,259 @@ +import React, { useState } from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { useDesigner } from '../context/DesignerContext'; +import { + Layout, + Columns3, + Grid as GridIcon, + Box, + Rows3, + PanelTop, + PanelBottom, + PanelLeft, + PanelRight, + Square, + CreditCard, + AlignJustify, + Minus, + Image, + Type, + Search +} from 'lucide-react'; +import { cn } from '@object-ui/components'; +import { ScrollArea } from '@object-ui/components'; +import { enableTouchDrag, isTouchDevice } from '../utils/touchDragPolyfill'; + +interface PageLayoutComponentPaletteProps { + className?: string; +} + +// Map layout component types to Lucide icons +const getIconForType = (type: string) => { + switch (type) { + case 'div': return Box; + case 'container': return Layout; + case 'section': return Square; + case 'header': return PanelTop; + case 'footer': return PanelBottom; + case 'aside': return PanelLeft; + case 'main': return PanelRight; + case 'grid': return GridIcon; + case 'stack': return AlignJustify; + case 'columns': return Columns3; + case 'rows': return Rows3; + case 'card': return CreditCard; + case 'separator': return Minus; + case 'image': return Image; + case 'text': return Type; + default: return Layout; + } +}; + +// Page layout specific component categories +const LAYOUT_CATEGORIES = { + 'Structural': [ + { type: 'header', label: 'Header', description: 'Page header section' }, + { type: 'main', label: 'Main', description: 'Main content area' }, + { type: 'footer', label: 'Footer', description: 'Page footer section' }, + { type: 'aside', label: 'Sidebar', description: 'Sidebar section' }, + { type: 'section', label: 'Section', description: 'Content section' }, + ], + 'Containers': [ + { type: 'div', label: 'Div', description: 'Generic container' }, + { type: 'container', label: 'Container', description: 'Max-width container' }, + { type: 'card', label: 'Card', description: 'Card container' }, + ], + 'Layout': [ + { type: 'grid', label: 'Grid', description: 'Grid layout' }, + { type: 'columns', label: 'Columns', description: 'Multi-column layout' }, + { type: 'rows', label: 'Rows', description: 'Row-based layout' }, + { type: 'stack', label: 'Stack', description: 'Vertical stack' }, + ], + 'Content': [ + { type: 'text', label: 'Text', description: 'Text content' }, + { type: 'image', label: 'Image', description: 'Image element' }, + { type: 'separator', label: 'Separator', description: 'Horizontal line' }, + ] +}; + +// Component item with touch support +interface ComponentItemProps { + type: string; + label: string; + description: string; + Icon: any; + onDragStart: (e: React.DragEvent, type: string) => void; + onDragEnd: () => void; +} + +const ComponentItem: React.FC = React.memo(({ + type, + label, + description, + Icon, + onDragStart, + onDragEnd +}) => { + const itemRef = React.useRef(null); + const { setDraggingType } = useDesigner(); + + // Setup touch drag support + React.useEffect(() => { + if (!itemRef.current || !isTouchDevice()) return; + + const cleanup = enableTouchDrag(itemRef.current, { + dragData: { componentType: type }, + onDragStart: () => { + setDraggingType(type); + }, + onDragEnd: () => { + setDraggingType(null); + } + }); + + return cleanup; + }, [type, setDraggingType]); + + return ( +
onDragStart(e, type)} + onDragEnd={onDragEnd} + className={cn( + "group flex items-center gap-3 p-3 rounded-lg border-2 border-transparent hover:border-blue-200 hover:bg-gradient-to-br hover:from-blue-50 hover:to-cyan-50 hover:shadow-md cursor-grab active:cursor-grabbing transition-all duration-200 bg-white", + "hover:scale-102 active:scale-95 touch-none" + )} + aria-label={`${label} - ${description}`} + > +
+ +
+
+
{label}
+
{description}
+
+
+ ); +}); + +ComponentItem.displayName = 'ComponentItem'; + +/** + * PageLayoutComponentPalette Component + * A specialized component palette for page layout elements + */ +export const PageLayoutComponentPalette: React.FC = ({ className }) => { + const { setDraggingType } = useDesigner(); + const [searchQuery, setSearchQuery] = useState(''); + const [expandedCategories, setExpandedCategories] = useState>( + new Set(Object.keys(LAYOUT_CATEGORIES)) + ); + + const handleDragStart = (e: React.DragEvent, type: string) => { + e.dataTransfer.effectAllowed = 'copy'; + e.dataTransfer.setData('application/json', JSON.stringify({ componentType: type })); + setDraggingType(type); + }; + + const handleDragEnd = () => { + setDraggingType(null); + }; + + const toggleCategory = (category: string) => { + const newExpanded = new Set(expandedCategories); + if (newExpanded.has(category)) { + newExpanded.delete(category); + } else { + newExpanded.add(category); + } + setExpandedCategories(newExpanded); + }; + + // Filter components based on search + const filteredCategories = Object.entries(LAYOUT_CATEGORIES).reduce((acc, [category, items]) => { + const filtered = items.filter(item => + item.label.toLowerCase().includes(searchQuery.toLowerCase()) || + item.type.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description.toLowerCase().includes(searchQuery.toLowerCase()) + ); + if (filtered.length > 0) { + acc[category] = filtered; + } + return acc; + }, {} as Record); + + return ( +
+ {/* Header */} +
+

Layout Components

+

Drag components to build pages

+
+ + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ + {/* Component List */} + +
+ {Object.keys(filteredCategories).length === 0 ? ( +
+ No components found +
+ ) : ( + Object.entries(filteredCategories).map(([category, items]) => ( +
+ + {expandedCategories.has(category) && ( +
+ {items.map((item) => { + const Icon = getIconForType(item.type); + return ( + + ); + })} +
+ )} +
+ )) + )} +
+
+ + {/* Footer Tip */} +
+

+ 💡 Tip: Use responsive grid layouts for mobile-friendly pages +

+
+
+ ); +}; diff --git a/packages/designer/src/components/PageLayoutDesigner.tsx b/packages/designer/src/components/PageLayoutDesigner.tsx new file mode 100644 index 00000000..267e854c --- /dev/null +++ b/packages/designer/src/components/PageLayoutDesigner.tsx @@ -0,0 +1,148 @@ +import React, { useEffect } from 'react'; +import { DesignerProvider } from '../context/DesignerContext'; +import { Canvas } from './Canvas'; +import { PropertyPanel } from './PropertyPanel'; +import { useDesigner } from '../context/DesignerContext'; +import type { SchemaNode } from '@object-ui/core'; +import { PageLayoutComponentPalette } from './PageLayoutComponentPalette'; +import { PageLayoutToolbar } from './PageLayoutToolbar'; + +interface PageLayoutDesignerProps { + initialSchema?: SchemaNode; + onSchemaChange?: (schema: SchemaNode) => void; +} + +/** + * PageLayoutDesigner Content Component + * Handles keyboard shortcuts and layout for page layout designer + */ +export const PageLayoutDesignerContent: React.FC = () => { + const { + undo, + redo, + copyNode, + cutNode, + duplicateNode, + pasteNode, + removeNode, + selectedNodeId, + canUndo, + canRedo + } = useDesigner(); + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const target = e.target as HTMLElement; + const isEditing = + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.tagName === 'SELECT' || + target.isContentEditable; + + // Undo: Ctrl+Z / Cmd+Z + if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey && canUndo) { + e.preventDefault(); + undo(); + } + // Redo: Ctrl+Y / Cmd+Shift+Z + else if (((e.ctrlKey || e.metaKey) && e.key === 'y') || ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z')) { + if (canRedo) { + e.preventDefault(); + redo(); + } + } + // Copy: Ctrl+C / Cmd+C (only when not editing) + else if ((e.ctrlKey || e.metaKey) && e.key === 'c' && !isEditing && selectedNodeId) { + e.preventDefault(); + copyNode(selectedNodeId); + } + // Cut: Ctrl+X / Cmd+X (only when not editing) + else if ((e.ctrlKey || e.metaKey) && e.key === 'x' && !isEditing && selectedNodeId) { + e.preventDefault(); + cutNode(selectedNodeId); + } + // Paste: Ctrl+V / Cmd+V (only when not editing) + else if ((e.ctrlKey || e.metaKey) && e.key === 'v' && !isEditing) { + e.preventDefault(); + pasteNode(selectedNodeId); + } + // Duplicate: Ctrl+D / Cmd+D (only when not editing) + else if ((e.ctrlKey || e.metaKey) && e.key === 'd' && !isEditing && selectedNodeId) { + e.preventDefault(); + duplicateNode(selectedNodeId); + } + // Delete: Delete / Backspace (only when not editing) + else if ((e.key === 'Delete' || e.key === 'Backspace') && !isEditing && selectedNodeId) { + e.preventDefault(); + removeNode(selectedNodeId); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [undo, redo, copyNode, cutNode, duplicateNode, pasteNode, removeNode, selectedNodeId, canUndo, canRedo]); + + return ( +
+ + +
+ {/* Left Sidebar - Page Layout Component Palette */} +
+ +
+ + {/* Main Canvas Area */} +
+ +
+ + {/* Right Sidebar - Property Panel */} +
+ +
+
+
+ ); +}; + +/** + * PageLayoutDesigner Component + * A specialized designer focused on creating page layouts with sections, headers, footers, and grids + * + * @example + * ```tsx + * import { PageLayoutDesigner } from '@object-ui/designer'; + * + * function App() { + * const [schema, setSchema] = useState({ + * type: 'div', + * className: 'min-h-screen', + * body: [] + * }); + * + * return ( + * + * ); + * } + * ``` + */ +export const PageLayoutDesigner: React.FC = ({ initialSchema, onSchemaChange }) => { + // Default page layout schema if none provided + const defaultSchema: SchemaNode = initialSchema || { + type: 'div', + id: 'page-root', + className: 'min-h-screen bg-white', + body: [] + }; + + return ( + + + + ); +}; diff --git a/packages/designer/src/components/PageLayoutToolbar.tsx b/packages/designer/src/components/PageLayoutToolbar.tsx new file mode 100644 index 00000000..a884a982 --- /dev/null +++ b/packages/designer/src/components/PageLayoutToolbar.tsx @@ -0,0 +1,202 @@ +import React, { useState } from 'react'; +import { useDesigner } from '../context/DesignerContext'; +import { + Undo, + Redo, + Download, + Upload, + Monitor, + Tablet, + Smartphone, + Save, + Layout, + Copy, + Check, + Ruler, + Palette +} from 'lucide-react'; +import { cn } from '@object-ui/components'; + +interface PageLayoutToolbarProps { + className?: string; +} + +type ViewportSize = 'desktop' | 'tablet' | 'mobile'; + +/** + * PageLayoutToolbar Component + * Toolbar specifically designed for page layout designer with responsive preview controls + */ +export const PageLayoutToolbar: React.FC = ({ className }) => { + const { undo, redo, canUndo, canRedo, schema, viewportMode, setViewportMode } = useDesigner(); + const [copied, setCopied] = useState(false); + const [showGrid, setShowGrid] = useState(false); + + const handleExportJSON = () => { + const json = JSON.stringify(schema, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'page-layout-schema.json'; + a.click(); + URL.revokeObjectURL(url); + }; + + const handleImportJSON = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const json = JSON.parse(e.target?.result as string); + // TODO: Validate and load schema + console.log('Imported schema:', json); + } catch (err) { + console.error('Failed to parse JSON:', err); + } + }; + reader.readAsText(file); + } + }; + input.click(); + }; + + const handleCopyJSON = () => { + const json = JSON.stringify(schema, null, 2); + navigator.clipboard.writeText(json); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ {/* Left Section - Title */} +
+
+ + Page Layout Designer +
+
+ + {/* Center Section - Viewport Controls */} +
+ {/* Undo/Redo */} +
+ + +
+ + {/* Viewport Size Toggle */} +
+ + + +
+ + {/* Grid Toggle */} + +
+ + {/* Right Section - Export/Import */} +
+ + + + + +
+
+ ); +}; diff --git a/packages/designer/src/index.ts b/packages/designer/src/index.ts index 03221a6b..7c067a39 100644 --- a/packages/designer/src/index.ts +++ b/packages/designer/src/index.ts @@ -1,6 +1,10 @@ -// Main Designer Component +// Main Designer Component (General Purpose) export { Designer, DesignerContent } from './components/Designer'; +// Specialized Designers +export { FormDesigner, FormDesignerContent } from './components/FormDesigner'; +export { PageLayoutDesigner, PageLayoutDesignerContent } from './components/PageLayoutDesigner'; + // Context and Hooks export { DesignerProvider, useDesigner } from './context/DesignerContext'; export type { DesignerContextValue, ViewportMode, ResizingState } from './context/DesignerContext'; @@ -8,8 +12,12 @@ export type { DesignerContextValue, ViewportMode, ResizingState } from './contex // Individual Components (for custom layouts) export { Canvas } from './components/Canvas'; export { ComponentPalette } from './components/ComponentPalette'; +export { FormComponentPalette } from './components/FormComponentPalette'; +export { PageLayoutComponentPalette } from './components/PageLayoutComponentPalette'; export { PropertyPanel } from './components/PropertyPanel'; export { Toolbar } from './components/Toolbar'; +export { FormToolbar } from './components/FormToolbar'; +export { PageLayoutToolbar } from './components/PageLayoutToolbar'; export { ComponentTree } from './components/ComponentTree'; export { ContextMenu } from './components/ContextMenu'; export { LeftSidebar } from './components/LeftSidebar';