Skip to content

Commit d06883f

Browse files
Copilothuangyiirene
andcommitted
Add touch drag support and responsive design for tablets
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
1 parent de13c11 commit d06883f

4 files changed

Lines changed: 322 additions & 42 deletions

File tree

packages/designer/src/components/ComponentPalette.tsx

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from 'lucide-react';
3333
import { cn } from '@object-ui/components';
3434
import { ScrollArea } from '@object-ui/components';
35+
import { enableTouchDrag, isTouchDevice } from '../utils/touchDragPolyfill';
3536

3637
interface ComponentPaletteProps {
3738
className?: string;
@@ -98,6 +99,77 @@ const CATEGORIES = {
9899
'Navigation': ['tabs', 'breadcrumb', 'pagination', 'menubar']
99100
};
100101

102+
// Component item with touch support
103+
interface ComponentItemProps {
104+
type: string;
105+
config: any;
106+
Icon: any;
107+
isResizable: boolean;
108+
onDragStart: (e: React.DragEvent, type: string) => void;
109+
onDragEnd: () => void;
110+
}
111+
112+
const ComponentItem: React.FC<ComponentItemProps> = React.memo(({
113+
type,
114+
config,
115+
Icon,
116+
isResizable,
117+
onDragStart,
118+
onDragEnd
119+
}) => {
120+
const itemRef = React.useRef<HTMLDivElement>(null);
121+
const { setDraggingType } = useDesigner();
122+
123+
// Setup touch drag support
124+
React.useEffect(() => {
125+
if (!itemRef.current || !isTouchDevice()) return;
126+
127+
const cleanup = enableTouchDrag(itemRef.current, {
128+
dragData: { componentType: type },
129+
onDragStart: () => {
130+
setDraggingType(type);
131+
},
132+
onDragEnd: () => {
133+
setDraggingType(null);
134+
}
135+
});
136+
137+
return cleanup;
138+
}, [type, setDraggingType]);
139+
140+
return (
141+
<div
142+
ref={itemRef}
143+
draggable
144+
onDragStart={(e) => onDragStart(e, type)}
145+
onDragEnd={onDragEnd}
146+
className={cn(
147+
"group flex flex-col items-center justify-center gap-1.5 md:gap-2 p-2.5 md:p-3 rounded-xl border-2 border-transparent hover:border-indigo-200 hover:bg-gradient-to-br hover:from-indigo-50 hover:to-purple-50 hover:shadow-lg cursor-grab active:cursor-grabbing transition-all duration-200 bg-white relative overflow-hidden",
148+
"h-20 md:h-24 hover:scale-105 active:scale-95 touch-none"
149+
)}
150+
aria-label={`${config.label || type}${isResizable ? ' (resizable)' : ''}`}
151+
>
152+
{/* Resizable badge indicator */}
153+
{isResizable && (
154+
<div
155+
className="absolute top-1 right-1 w-1.5 h-1.5 md:w-2 md:h-2 bg-gradient-to-br from-emerald-400 to-green-500 rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-sm ring-2 ring-white"
156+
title="Resizable"
157+
aria-hidden="true"
158+
></div>
159+
)}
160+
161+
<div className="w-8 h-8 md:w-9 md:h-9 rounded-lg bg-gradient-to-br from-gray-50 to-gray-100 group-hover:from-indigo-100 group-hover:to-purple-100 flex items-center justify-center text-gray-600 group-hover:text-indigo-700 transition-all border border-gray-200 group-hover:border-indigo-300 group-hover:shadow-md">
162+
<Icon size={16} strokeWidth={2} className="md:w-[18px] md:h-[18px]" />
163+
</div>
164+
<span className="text-[10px] md:text-xs font-semibold text-gray-700 group-hover:text-indigo-800 text-center w-full truncate px-1 transition-colors leading-tight">
165+
{config.label || type}
166+
</span>
167+
</div>
168+
);
169+
});
170+
171+
ComponentItem.displayName = 'ComponentItem';
172+
101173
export const ComponentPalette: React.FC<ComponentPaletteProps> = React.memo(({ className }) => {
102174
const { setDraggingType } = useDesigner();
103175
const allConfigs = ComponentRegistry.getAllConfigs();
@@ -129,33 +201,15 @@ export const ComponentPalette: React.FC<ComponentPaletteProps> = React.memo(({ c
129201
const isResizable = config.resizable || false;
130202

131203
return (
132-
<div
204+
<ComponentItem
133205
key={type}
134-
draggable
135-
onDragStart={(e) => handleDragStart(e, type)}
206+
type={type}
207+
config={config}
208+
Icon={Icon}
209+
isResizable={isResizable}
210+
onDragStart={handleDragStart}
136211
onDragEnd={handleDragEnd}
137-
className={cn(
138-
"group flex flex-col items-center justify-center gap-2 p-3 rounded-xl border-2 border-transparent hover:border-indigo-200 hover:bg-gradient-to-br hover:from-indigo-50 hover:to-purple-50 hover:shadow-lg cursor-grab active:cursor-grabbing transition-all duration-200 bg-white relative overflow-hidden",
139-
"h-24 hover:scale-105 active:scale-95"
140-
)}
141-
aria-label={`${config.label || type}${isResizable ? ' (resizable)' : ''}`}
142-
>
143-
{/* Resizable badge indicator */}
144-
{isResizable && (
145-
<div
146-
className="absolute top-1 right-1 w-2 h-2 bg-gradient-to-br from-emerald-400 to-green-500 rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-sm ring-2 ring-white"
147-
title="Resizable"
148-
aria-hidden="true"
149-
></div>
150-
)}
151-
152-
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-gray-50 to-gray-100 group-hover:from-indigo-100 group-hover:to-purple-100 flex items-center justify-center text-gray-600 group-hover:text-indigo-700 transition-all border border-gray-200 group-hover:border-indigo-300 group-hover:shadow-md">
153-
<Icon size={18} strokeWidth={2} />
154-
</div>
155-
<span className="text-xs font-semibold text-gray-700 group-hover:text-indigo-800 text-center w-full truncate px-1 transition-colors">
156-
{config.label || type}
157-
</span>
158-
</div>
212+
/>
159213
);
160214
}, [handleDragStart, handleDragEnd]);
161215

@@ -177,15 +231,15 @@ export const ComponentPalette: React.FC<ComponentPaletteProps> = React.memo(({ c
177231
}, []);
178232

179233
return (
180-
<div className={cn("flex flex-col h-full bg-gradient-to-b from-gray-50 to-white w-72 overflow-hidden", className)}>
181-
<div className="px-4 py-4 border-b border-gray-200/80 bg-white/80 backdrop-blur-sm shrink-0 space-y-3">
234+
<div className={cn("flex flex-col h-full bg-gradient-to-b from-gray-50 to-white w-full overflow-hidden", className)}>
235+
<div className="px-3 md:px-4 py-3 md:py-4 border-b border-gray-200/80 bg-white/80 backdrop-blur-sm shrink-0 space-y-3">
182236
<div className="relative">
183237
<input
184238
type="text"
185-
placeholder="Search components..."
239+
placeholder="Search..."
186240
value={searchQuery}
187241
onChange={(e) => setSearchQuery(e.target.value)}
188-
className="w-full h-10 px-4 text-sm border-2 border-gray-200 rounded-xl bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-400 transition-all placeholder:text-gray-400 font-medium shadow-sm hover:shadow-md"
242+
className="w-full h-9 md:h-10 px-3 md:px-4 text-sm border-2 border-gray-200 rounded-xl bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-400 transition-all placeholder:text-gray-400 font-medium shadow-sm hover:shadow-md"
189243
/>
190244
{searchQuery && (
191245
<button
@@ -200,18 +254,18 @@ export const ComponentPalette: React.FC<ComponentPaletteProps> = React.memo(({ c
200254
</div>
201255

202256
<ScrollArea className="flex-1 w-full">
203-
<div className="p-4 space-y-6 pb-20">
257+
<div className="p-3 md:p-4 space-y-4 md:space-y-6 pb-20">
204258
{Object.entries(CATEGORIES).map(([category, types]) => {
205259
const availableTypes = filterBySearch(getComponentscategory(types));
206260
if (availableTypes.length === 0) return null;
207261

208262
return (
209263
<div key={category}>
210-
<h3 className="text-xs font-bold text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-purple-600 uppercase tracking-widest mb-3 px-1 flex items-center gap-2">
211-
<span className="w-1 h-4 bg-gradient-to-b from-indigo-500 to-purple-500 rounded-full"></span>
212-
{category}
264+
<h3 className="text-xs font-bold text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-purple-600 uppercase tracking-widest mb-2 md:mb-3 px-1 flex items-center gap-2">
265+
<span className="w-1 h-3 md:h-4 bg-gradient-to-b from-indigo-500 to-purple-500 rounded-full"></span>
266+
<span className="truncate">{category}</span>
213267
</h3>
214-
<div className="grid grid-cols-2 gap-2.5">
268+
<div className="grid grid-cols-2 gap-2 md:gap-2.5">
215269
{availableTypes.map(renderComponentItem)}
216270
</div>
217271
</div>

packages/designer/src/components/Designer.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,19 @@ export const DesignerContent: React.FC = () => {
7474

7575
<div className="flex-1 flex overflow-hidden">
7676
{/* Left Sidebar - Combined Component Palette and Tree */}
77-
<div className="w-72 flex-shrink-0 z-10 shadow-[1px_0_5px_rgba(0,0,0,0.03)] h-full">
77+
{/* Responsive: w-72 on desktop, w-64 on tablet (md:), hidden on mobile with toggle option */}
78+
<div className="w-64 md:w-72 flex-shrink-0 z-10 shadow-[1px_0_5px_rgba(0,0,0,0.03)] h-full">
7879
<LeftSidebar className="h-full border-r-0" />
7980
</div>
8081

8182
{/* Main Canvas Area */}
82-
<div className="flex-1 relative bg-gray-100 z-0">
83+
<div className="flex-1 relative bg-gray-100 z-0 min-w-0">
8384
<Canvas className="h-full w-full" />
8485
</div>
8586

8687
{/* Right Sidebar - Property Panel */}
87-
<div className="w-80 flex-shrink-0 z-10 shadow-[-1px_0_5px_rgba(0,0,0,0.03)] h-full">
88+
{/* Responsive: w-80 on desktop, w-72 on tablet (md:), can be toggled on small tablets */}
89+
<div className="w-72 md:w-80 flex-shrink-0 z-10 shadow-[-1px_0_5px_rgba(0,0,0,0.03)] h-full">
8890
<PropertyPanel className="h-full border-l-0 shadow-none border-l custom-scrollbar" />
8991
</div>
9092
</div>

packages/designer/src/components/LeftSidebar.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,32 @@ export const LeftSidebar: React.FC<LeftSidebarProps> = React.memo(({ className }
1414
const [activeTab, setActiveTab] = useState<TabType>('palette');
1515

1616
return (
17-
<div className={cn("flex flex-col h-full bg-white w-72", className)}>
17+
<div className={cn("flex flex-col h-full bg-white w-full", className)}>
1818
{/* Tab Headers */}
1919
<div className="flex border-b bg-gray-50/50 shrink-0">
2020
<button
2121
onClick={() => setActiveTab('palette')}
2222
className={cn(
23-
"flex-1 flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium transition-all relative",
23+
"flex-1 flex items-center justify-center gap-2 px-3 md:px-4 py-3 text-sm font-medium transition-all relative",
2424
activeTab === 'palette'
2525
? "text-blue-600 bg-white border-b-2 border-blue-600"
2626
: "text-gray-600 hover:text-gray-900 hover:bg-gray-100/50"
2727
)}
2828
>
2929
<Box size={16} />
30-
<span>Components</span>
30+
<span className="hidden sm:inline">Components</span>
3131
</button>
3232
<button
3333
onClick={() => setActiveTab('tree')}
3434
className={cn(
35-
"flex-1 flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium transition-all relative",
35+
"flex-1 flex items-center justify-center gap-2 px-3 md:px-4 py-3 text-sm font-medium transition-all relative",
3636
activeTab === 'tree'
3737
? "text-blue-600 bg-white border-b-2 border-blue-600"
3838
: "text-gray-600 hover:text-gray-900 hover:bg-gray-100/50"
3939
)}
4040
>
4141
<Layers size={16} />
42-
<span>Tree</span>
42+
<span className="hidden sm:inline">Tree</span>
4343
</button>
4444
</div>
4545

0 commit comments

Comments
 (0)