Skip to content

Commit 92b14d6

Browse files
Copilothotlong
andcommitted
Changes before error encountered
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent ef87dae commit 92b14d6

8 files changed

Lines changed: 1271 additions & 3 deletions

File tree

packages/designer/README.md

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# @object-ui/designer
22

3-
A professional drag-and-drop visual editor to generate Object UI schemas with advanced features including **component resizing**.
3+
A professional drag-and-drop visual editor to generate Object UI schemas with advanced features including **component resizing** and **specialized designers**.
4+
5+
## 🎯 Specialized Designers
6+
7+
Object UI Designer now includes three specialized designer modes optimized for different use cases:
8+
9+
- **General Designer** - The original all-purpose designer with all component types
10+
- **Form Designer** (专用对象表单设计器) - Purpose-built for creating forms with validation and field controls
11+
- **Page Layout Designer** (页面布局设计器) - Optimized for building page structures with sections, headers, and grids
412

513
## Features
614

@@ -68,7 +76,9 @@ pnpm add @object-ui/designer @object-ui/react @object-ui/components
6876

6977
## Usage
7078

71-
### Basic Example
79+
### General Designer (All Components)
80+
81+
The default designer with access to all component types:
7282

7383
```tsx
7484
import { Designer } from '@object-ui/designer';
@@ -91,6 +101,68 @@ function App() {
91101
}
92102
```
93103

104+
### Form Designer (专用对象表单设计器)
105+
106+
A specialized designer optimized for creating forms with form-specific components:
107+
108+
```tsx
109+
import { FormDesigner } from '@object-ui/designer';
110+
import { useState } from 'react';
111+
import type { SchemaNode } from '@object-ui/core';
112+
113+
function App() {
114+
const [formSchema, setFormSchema] = useState<SchemaNode>({
115+
type: 'form',
116+
className: 'p-8 max-w-2xl mx-auto',
117+
body: []
118+
});
119+
120+
return (
121+
<FormDesigner
122+
initialSchema={formSchema}
123+
onSchemaChange={setFormSchema}
124+
/>
125+
);
126+
}
127+
```
128+
129+
**Features:**
130+
- Form-specific component palette (text inputs, email, password, number, checkboxes, selects, etc.)
131+
- Form validation preview
132+
- Quick access to common form field types
133+
- Form submission configuration
134+
135+
### Page Layout Designer (页面布局设计器)
136+
137+
A specialized designer optimized for building page layouts and structures:
138+
139+
```tsx
140+
import { PageLayoutDesigner } from '@object-ui/designer';
141+
import { useState } from 'react';
142+
import type { SchemaNode } from '@object-ui/core';
143+
144+
function App() {
145+
const [pageSchema, setPageSchema] = useState<SchemaNode>({
146+
type: 'div',
147+
className: 'min-h-screen bg-white',
148+
body: []
149+
});
150+
151+
return (
152+
<PageLayoutDesigner
153+
initialSchema={pageSchema}
154+
onSchemaChange={setPageSchema}
155+
/>
156+
);
157+
}
158+
```
159+
160+
**Features:**
161+
- Layout-focused component palette (header, footer, main, aside, sections, grids, columns)
162+
- Responsive viewport preview (desktop/tablet/mobile)
163+
- Grid overlay toggle for precise alignment
164+
- Layout-specific property controls
165+
94166
### With Initial Schema
95167

96168
```tsx
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import React, { useState } from 'react';
2+
import { ComponentRegistry } from '@object-ui/core';
3+
import { useDesigner } from '../context/DesignerContext';
4+
import {
5+
Type,
6+
CheckSquare,
7+
ToggleLeft,
8+
List,
9+
FileText,
10+
Calendar,
11+
Mail,
12+
Phone,
13+
Lock,
14+
Hash,
15+
DollarSign,
16+
Link2,
17+
MousePointer2,
18+
Search,
19+
Tag
20+
} from 'lucide-react';
21+
import { cn } from '@object-ui/components';
22+
import { ScrollArea } from '@object-ui/components';
23+
import { enableTouchDrag, isTouchDevice } from '../utils/touchDragPolyfill';
24+
25+
interface FormComponentPaletteProps {
26+
className?: string;
27+
}
28+
29+
// Map form component types to Lucide icons
30+
const getIconForType = (type: string) => {
31+
switch (type) {
32+
case 'input': return Type;
33+
case 'textarea': return FileText;
34+
case 'checkbox': return CheckSquare;
35+
case 'switch': return ToggleLeft;
36+
case 'select': return List;
37+
case 'button': return MousePointer2;
38+
case 'label': return Tag;
39+
case 'date-picker': return Calendar;
40+
case 'email-input': return Mail;
41+
case 'phone-input': return Phone;
42+
case 'password-input': return Lock;
43+
case 'number-input': return Hash;
44+
case 'url-input': return Link2;
45+
case 'search-input': return Search;
46+
case 'currency-input': return DollarSign;
47+
default: return Type;
48+
}
49+
};
50+
51+
// Form-specific component categories
52+
const FORM_CATEGORIES = {
53+
'Text Fields': [
54+
{ type: 'input', label: 'Text Input', description: 'Single line text input' },
55+
{ type: 'textarea', label: 'Text Area', description: 'Multi-line text input' },
56+
{ type: 'email-input', label: 'Email', description: 'Email address input' },
57+
{ type: 'password-input', label: 'Password', description: 'Password input field' },
58+
{ type: 'url-input', label: 'URL', description: 'URL input field' },
59+
{ type: 'search-input', label: 'Search', description: 'Search input field' },
60+
],
61+
'Number Fields': [
62+
{ type: 'number-input', label: 'Number', description: 'Numeric input' },
63+
{ type: 'currency-input', label: 'Currency', description: 'Money/currency input' },
64+
],
65+
'Selection': [
66+
{ type: 'checkbox', label: 'Checkbox', description: 'Single checkbox' },
67+
{ type: 'switch', label: 'Switch', description: 'Toggle switch' },
68+
{ type: 'select', label: 'Select', description: 'Dropdown selection' },
69+
],
70+
'Other': [
71+
{ type: 'date-picker', label: 'Date Picker', description: 'Date selection' },
72+
{ type: 'phone-input', label: 'Phone', description: 'Phone number input' },
73+
{ type: 'label', label: 'Label', description: 'Form field label' },
74+
{ type: 'button', label: 'Button', description: 'Action button' },
75+
]
76+
};
77+
78+
// Component item with touch support
79+
interface ComponentItemProps {
80+
type: string;
81+
label: string;
82+
description: string;
83+
Icon: any;
84+
onDragStart: (e: React.DragEvent, type: string) => void;
85+
onDragEnd: () => void;
86+
}
87+
88+
const ComponentItem: React.FC<ComponentItemProps> = React.memo(({
89+
type,
90+
label,
91+
description,
92+
Icon,
93+
onDragStart,
94+
onDragEnd
95+
}) => {
96+
const itemRef = React.useRef<HTMLDivElement>(null);
97+
const { setDraggingType } = useDesigner();
98+
99+
// Setup touch drag support
100+
React.useEffect(() => {
101+
if (!itemRef.current || !isTouchDevice()) return;
102+
103+
const cleanup = enableTouchDrag(itemRef.current, {
104+
dragData: { componentType: type },
105+
onDragStart: () => {
106+
setDraggingType(type);
107+
},
108+
onDragEnd: () => {
109+
setDraggingType(null);
110+
}
111+
});
112+
113+
return cleanup;
114+
}, [type, setDraggingType]);
115+
116+
return (
117+
<div
118+
ref={itemRef}
119+
draggable
120+
onDragStart={(e) => onDragStart(e, type)}
121+
onDragEnd={onDragEnd}
122+
className={cn(
123+
"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",
124+
"hover:scale-102 active:scale-95 touch-none"
125+
)}
126+
aria-label={`${label} - ${description}`}
127+
>
128+
<div className="flex-shrink-0 w-8 h-8 rounded-md bg-indigo-100 flex items-center justify-center group-hover:bg-indigo-200 transition-colors">
129+
<Icon className="w-4 h-4 text-indigo-600" />
130+
</div>
131+
<div className="flex-1 min-w-0">
132+
<div className="text-sm font-medium text-gray-900 truncate">{label}</div>
133+
<div className="text-xs text-gray-500 truncate">{description}</div>
134+
</div>
135+
</div>
136+
);
137+
});
138+
139+
ComponentItem.displayName = 'ComponentItem';
140+
141+
/**
142+
* FormComponentPalette Component
143+
* A specialized component palette for form fields and controls
144+
*/
145+
export const FormComponentPalette: React.FC<FormComponentPaletteProps> = ({ className }) => {
146+
const { setDraggingType } = useDesigner();
147+
const [searchQuery, setSearchQuery] = useState('');
148+
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
149+
new Set(Object.keys(FORM_CATEGORIES))
150+
);
151+
152+
const handleDragStart = (e: React.DragEvent, type: string) => {
153+
e.dataTransfer.effectAllowed = 'copy';
154+
e.dataTransfer.setData('application/json', JSON.stringify({ componentType: type }));
155+
setDraggingType(type);
156+
};
157+
158+
const handleDragEnd = () => {
159+
setDraggingType(null);
160+
};
161+
162+
const toggleCategory = (category: string) => {
163+
const newExpanded = new Set(expandedCategories);
164+
if (newExpanded.has(category)) {
165+
newExpanded.delete(category);
166+
} else {
167+
newExpanded.add(category);
168+
}
169+
setExpandedCategories(newExpanded);
170+
};
171+
172+
// Filter components based on search
173+
const filteredCategories = Object.entries(FORM_CATEGORIES).reduce((acc, [category, items]) => {
174+
const filtered = items.filter(item =>
175+
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
176+
item.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
177+
item.description.toLowerCase().includes(searchQuery.toLowerCase())
178+
);
179+
if (filtered.length > 0) {
180+
acc[category] = filtered;
181+
}
182+
return acc;
183+
}, {} as Record<string, typeof FORM_CATEGORIES[keyof typeof FORM_CATEGORIES]>);
184+
185+
return (
186+
<div className={cn("flex flex-col h-full bg-white border-r border-gray-200", className)}>
187+
{/* Header */}
188+
<div className="p-4 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-purple-50">
189+
<h2 className="text-lg font-bold text-gray-900 mb-1">Form Components</h2>
190+
<p className="text-xs text-gray-600">Drag components to canvas</p>
191+
</div>
192+
193+
{/* Search */}
194+
<div className="p-3 border-b border-gray-200">
195+
<div className="relative">
196+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
197+
<input
198+
type="text"
199+
placeholder="Search components..."
200+
value={searchQuery}
201+
onChange={(e) => setSearchQuery(e.target.value)}
202+
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"
203+
/>
204+
</div>
205+
</div>
206+
207+
{/* Component List */}
208+
<ScrollArea className="flex-1">
209+
<div className="p-3 space-y-4">
210+
{Object.keys(filteredCategories).length === 0 ? (
211+
<div className="text-center py-8 text-gray-500 text-sm">
212+
No components found
213+
</div>
214+
) : (
215+
Object.entries(filteredCategories).map(([category, items]) => (
216+
<div key={category} className="space-y-2">
217+
<button
218+
onClick={() => toggleCategory(category)}
219+
className="w-full flex items-center justify-between text-xs font-semibold text-gray-600 uppercase tracking-wider hover:text-gray-900 transition-colors"
220+
>
221+
<span>{category}</span>
222+
<span className="text-gray-400">
223+
{expandedCategories.has(category) ? '▼' : '▶'}
224+
</span>
225+
</button>
226+
{expandedCategories.has(category) && (
227+
<div className="space-y-2">
228+
{items.map((item) => {
229+
const Icon = getIconForType(item.type);
230+
return (
231+
<ComponentItem
232+
key={item.type}
233+
type={item.type}
234+
label={item.label}
235+
description={item.description}
236+
Icon={Icon}
237+
onDragStart={handleDragStart}
238+
onDragEnd={handleDragEnd}
239+
/>
240+
);
241+
})}
242+
</div>
243+
)}
244+
</div>
245+
))
246+
)}
247+
</div>
248+
</ScrollArea>
249+
250+
{/* Footer Tip */}
251+
<div className="p-3 border-t border-gray-200 bg-gray-50">
252+
<p className="text-xs text-gray-600 text-center">
253+
💡 Tip: Configure field validation in the property panel
254+
</p>
255+
</div>
256+
</div>
257+
);
258+
};

0 commit comments

Comments
 (0)