From 28584c700dbcdc486fbd4cf2647467c208935c91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:12:13 +0000 Subject: [PATCH 01/10] Initial plan From b30abcb499e56b52bd564e94b975543cda8f0141 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:20:24 +0000 Subject: [PATCH 02/10] Add core functionality improvements: undo/redo, copy/paste, JSON import/export, component search, keyboard shortcuts Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../src/components/ComponentPalette.tsx | 44 ++- packages/designer/src/components/Designer.tsx | 89 ++++-- .../designer/src/components/PropertyPanel.tsx | 45 ++- packages/designer/src/components/Toolbar.tsx | 281 ++++++++++++++++-- .../designer/src/context/DesignerContext.tsx | 134 +++++++-- 5 files changed, 508 insertions(+), 85 deletions(-) diff --git a/packages/designer/src/components/ComponentPalette.tsx b/packages/designer/src/components/ComponentPalette.tsx index 2f2f9c80c..e151767a6 100644 --- a/packages/designer/src/components/ComponentPalette.tsx +++ b/packages/designer/src/components/ComponentPalette.tsx @@ -57,6 +57,7 @@ const CATEGORIES = { export const ComponentPalette: React.FC = ({ className }) => { const { setDraggingType } = useDesigner(); const allConfigs = ComponentRegistry.getAllConfigs(); + const [searchQuery, setSearchQuery] = React.useState(''); const handleDragStart = (e: React.DragEvent, type: string) => { e.dataTransfer.setData('componentType', type); @@ -103,6 +104,18 @@ export const ComponentPalette: React.FC = ({ className }) ); }; + // Filter components by search query + const filterBySearch = (types: string[]) => { + if (!searchQuery.trim()) return types; + + const query = searchQuery.toLowerCase(); + return types.filter(type => { + const config = ComponentRegistry.getConfig(type); + return type.toLowerCase().includes(query) || + config?.label?.toLowerCase().includes(query); + }); + }; + // Filter available components based on category const getComponentscategory = (categoryComponents: string[]) => { return categoryComponents.filter(type => ComponentRegistry.getConfig(type)); @@ -110,15 +123,36 @@ export const ComponentPalette: React.FC = ({ className }) return (
-
-

Components

-

Drag to add to canvas

+
+
+

Components

+

Drag to add to canvas

+
+
+ setSearchQuery(e.target.value)} + className="w-full h-8 px-3 text-xs border rounded-md bg-gray-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + {searchQuery && ( + + )} +
{Object.entries(CATEGORIES).map(([category, types]) => { - const availableTypes = getComponentscategory(types); + const availableTypes = filterBySearch(getComponentscategory(types)); if (availableTypes.length === 0) return null; return ( @@ -134,7 +168,7 @@ export const ComponentPalette: React.FC = ({ className }) {/* Fallback for uncategorized */} {(() => { const categorized = new Set(Object.values(CATEGORIES).flat()); - const uncategorized = Object.keys(allConfigs).filter(t => !categorized.has(t)); + const uncategorized = filterBySearch(Object.keys(allConfigs).filter(t => !categorized.has(t))); if (uncategorized.length === 0) return null; diff --git a/packages/designer/src/components/Designer.tsx b/packages/designer/src/components/Designer.tsx index 384d5ec11..0c9fc48e4 100644 --- a/packages/designer/src/components/Designer.tsx +++ b/packages/designer/src/components/Designer.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { DesignerProvider } from '../context/DesignerContext'; import { ComponentPalette } from './ComponentPalette'; import { Canvas } from './Canvas'; import { PropertyPanel } from './PropertyPanel'; import { Toolbar } from './Toolbar'; +import { useDesigner } from '../context/DesignerContext'; import type { SchemaNode } from '@object-ui/core'; interface DesignerProps { @@ -11,29 +12,77 @@ interface DesignerProps { onSchemaChange?: (schema: SchemaNode) => void; } -export const Designer: React.FC = ({ initialSchema, onSchemaChange }) => { +const DesignerContent: React.FC = () => { + const { undo, redo, copyNode, pasteNode, removeNode, selectedNodeId, canUndo, canRedo } = useDesigner(); + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Check if we're in an input/textarea + const target = e.target as HTMLElement; + const isEditing = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA'; + + // 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); + } + // Paste: Ctrl+V / Cmd+V (only when not editing) + else if ((e.ctrlKey || e.metaKey) && e.key === 'v' && !isEditing) { + e.preventDefault(); + pasteNode(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, pasteNode, removeNode, selectedNodeId, canUndo, canRedo]); + return ( - -
- +
+ + +
+ {/* Left Sidebar */} +
+ +
-
- {/* Left Sidebar */} -
- -
- - {/* Main Canvas Area */} -
- -
- - {/* Right Sidebar */} -
- -
+ {/* Main Canvas Area */} +
+ +
+ + {/* Right Sidebar */} +
+
+
+ ); +}; + +export const Designer: React.FC = ({ initialSchema, onSchemaChange }) => { + return ( + + ); }; diff --git a/packages/designer/src/components/PropertyPanel.tsx b/packages/designer/src/components/PropertyPanel.tsx index 4ee707a80..9f62bf237 100644 --- a/packages/designer/src/components/PropertyPanel.tsx +++ b/packages/designer/src/components/PropertyPanel.tsx @@ -14,7 +14,7 @@ import { } from "@object-ui/components" import { Textarea } from '@object-ui/components'; import { ScrollArea } from '@object-ui/components'; -import { Settings2, Trash2, Layers, Type } from 'lucide-react'; +import { Settings2, Trash2, Layers, Copy, ClipboardPaste } from 'lucide-react'; import { cn } from '@object-ui/components'; import type { ComponentInput } from '@object-ui/core'; @@ -23,7 +23,7 @@ interface PropertyPanelProps { } export const PropertyPanel: React.FC = ({ className }) => { - const { schema, selectedNodeId, updateNode, removeNode } = useDesigner(); + const { schema, selectedNodeId, updateNode, removeNode, copyNode, pasteNode, canPaste } = useDesigner(); // Recursive finder const findNode = (node: any, id: string): any => { @@ -155,22 +155,43 @@ export const PropertyPanel: React.FC = ({ className }) => { return (
-
+
{config?.label || selectedNode.type} {selectedNode.type}

ID: {selectedNode.id}

- +
+ + + +
diff --git a/packages/designer/src/components/Toolbar.tsx b/packages/designer/src/components/Toolbar.tsx index e7a75a4c7..3f5fa6a75 100644 --- a/packages/designer/src/components/Toolbar.tsx +++ b/packages/designer/src/components/Toolbar.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useRef } from 'react'; import { Button } from '@object-ui/components'; import { Monitor, @@ -7,48 +7,287 @@ import { Undo, Redo, Play, - Share2, + Share2, + Download, + Upload, + Copy, + FileJson, } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@object-ui/components"; +import { Textarea } from '@object-ui/components'; +import { useDesigner } from '../context/DesignerContext'; +import { cn } from '@object-ui/components'; + +type ViewportMode = 'desktop' | 'tablet' | 'mobile'; export const Toolbar: React.FC = () => { + const { schema, setSchema, undo, redo, canUndo, canRedo } = useDesigner(); + const [viewportMode, setViewportMode] = useState('desktop'); + const [showExportDialog, setShowExportDialog] = useState(false); + const [showImportDialog, setShowImportDialog] = useState(false); + const [importJson, setImportJson] = useState(''); + const [importError, setImportError] = useState(''); + const fileInputRef = useRef(null); + + const handleExport = () => { + 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 = 'schema.json'; + a.click(); + URL.revokeObjectURL(url); + }; + + const handleCopyJson = async () => { + const json = JSON.stringify(schema, null, 2); + await navigator.clipboard.writeText(json); + // Could add a toast notification here + }; + + const handleImport = () => { + try { + setImportError(''); + const parsed = JSON.parse(importJson); + setSchema(parsed); + setShowImportDialog(false); + setImportJson(''); + } catch (error) { + setImportError(error instanceof Error ? error.message : 'Invalid JSON'); + } + }; + + const handleFileImport = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const json = event.target?.result as string; + const parsed = JSON.parse(json); + setSchema(parsed); + } catch (error) { + console.error('Failed to import file:', error); + } + }; + reader.readAsText(file); + }; + return (
O
-

Object UI

- Beta +

Object UI Designer

+ Beta
-
- - - + {/* Center Controls */} +
+ {/* Viewport Toggle */} +
+ + + +
+ {/* Right Actions */}
-
- -
+ + {/* Import/Export */} +
+ + + + + + + Import Schema + + Paste your JSON schema below or upload a file. + + +
+
+ + +
+
+
+
+
or paste JSON
+
+
+
+