66 * LICENSE file in the root directory of this source tree.
77 */
88
9- import React , { useState , useCallback , useMemo } from 'react' ;
9+ import React , { useState , useCallback , useMemo , useEffect , useRef } from 'react' ;
1010import type {
1111 DesignerComponent ,
1212 DesignerCanvasConfig ,
1313 DesignerPaletteCategory ,
1414 DesignerPaletteItem ,
1515} from '@object-ui/types' ;
16- import { GripVertical , Undo2 , Redo2 , Eye , Layers , Plus , Trash2 } from 'lucide-react' ;
16+ import { GripVertical , Undo2 , Redo2 , Eye , Layers , Plus , Minus , Maximize2 , Trash2 } from 'lucide-react' ;
1717import { clsx } from 'clsx' ;
1818import { twMerge } from 'tailwind-merge' ;
1919
@@ -42,6 +42,10 @@ export interface PageDesignerProps {
4242 className ?: string ;
4343}
4444
45+ const ZOOM_MIN = 0.25 ;
46+ const ZOOM_MAX = 3 ;
47+ const ZOOM_STEP = 0.1 ;
48+
4549/**
4650 * Drag-and-drop page designer component.
4751 * Allows visual composition of UI components on a canvas.
@@ -58,7 +62,8 @@ export function PageDesigner({
5862} : PageDesignerProps ) {
5963 const [ components , setComponents ] = useState < DesignerComponent [ ] > ( initialComponents ) ;
6064 const [ selectedId , setSelectedId ] = useState < string | null > ( null ) ;
61- const [ zoom , _setZoom ] = useState ( canvas . zoom ?? 1 ) ;
65+ const [ zoom , setZoom ] = useState ( canvas . zoom ?? 1 ) ;
66+ const containerRef = useRef < HTMLDivElement > ( null ) ;
6267
6368 const selectedComponent = useMemo (
6469 ( ) => components . find ( ( c ) => c . id === selectedId ) ,
@@ -94,11 +99,38 @@ export function PageDesigner({
9499 [ components , selectedId , readOnly , onChange ] ,
95100 ) ;
96101
102+ const handleUpdateLabel = useCallback (
103+ ( id : string , label : string ) => {
104+ if ( readOnly ) return ;
105+ const updated = components . map ( ( c ) => ( c . id === id ? { ...c , label } : c ) ) ;
106+ setComponents ( updated ) ;
107+ onChange ?.( updated ) ;
108+ } ,
109+ [ components , readOnly , onChange ] ,
110+ ) ;
111+
112+ useEffect ( ( ) => {
113+ const el = containerRef . current ;
114+ if ( ! el ) return ;
115+ const handleKeyDown = ( e : KeyboardEvent ) => {
116+ if ( ( e . key === 'Delete' || e . key === 'Backspace' ) && selectedId ) {
117+ if ( [ 'INPUT' , 'TEXTAREA' , 'SELECT' ] . includes ( ( e . target as HTMLElement ) . tagName ) ) return ;
118+ e . preventDefault ( ) ;
119+ handleDeleteComponent ( selectedId ) ;
120+ }
121+ if ( e . key === 'Escape' ) {
122+ setSelectedId ( null ) ;
123+ }
124+ } ;
125+ el . addEventListener ( 'keydown' , handleKeyDown ) ;
126+ return ( ) => el . removeEventListener ( 'keydown' , handleKeyDown ) ;
127+ } , [ selectedId , handleDeleteComponent ] ) ;
128+
97129 return (
98- < div className = { cn ( 'flex h-full w-full border rounded-lg overflow-hidden bg-background' , className ) } >
130+ < div ref = { containerRef } tabIndex = { 0 } className = { cn ( 'flex h-full w-full border rounded-lg overflow-hidden bg-background' , className ) } >
99131 { /* Left Panel - Component Palette */ }
100132 { ! readOnly && (
101- < div className = "w-60 border-r bg-muted/30 flex flex-col" >
133+ < div className = "w-60 border-r bg-muted/30 flex flex-col" role = "region" aria-label = "Component palette" >
102134 < div className = "p-3 border-b font-medium text-sm" > Components</ div >
103135 < div className = "flex-1 overflow-y-auto p-2" >
104136 { palette . map ( ( category ) => (
@@ -125,29 +157,52 @@ export function PageDesigner({
125157 { /* Center - Canvas */ }
126158 < div className = "flex-1 flex flex-col" >
127159 { /* Toolbar */ }
128- < div className = "flex items-center gap-2 p-2 border-b bg-muted/20" >
160+ < div className = "flex items-center gap-2 p-2 border-b bg-muted/20" role = "toolbar" aria-label = "Designer toolbar" >
129161 { undoRedo && ! readOnly && (
130162 < >
131- < button className = "p-1.5 rounded hover:bg-accent" title = "Undo" >
163+ < button className = "p-1.5 rounded hover:bg-accent" title = "Undo" aria-label = "Undo" >
132164 < Undo2 className = "h-4 w-4" />
133165 </ button >
134- < button className = "p-1.5 rounded hover:bg-accent" title = "Redo" >
166+ < button className = "p-1.5 rounded hover:bg-accent" title = "Redo" aria-label = "Redo" >
135167 < Redo2 className = "h-4 w-4" />
136168 </ button >
137169 < div className = "w-px h-5 bg-border mx-1" />
138170 </ >
139171 ) }
140- < button className = "p-1.5 rounded hover:bg-accent" title = "Preview" >
172+ < button className = "p-1.5 rounded hover:bg-accent" title = "Preview" aria-label = "Preview" >
141173 < Eye className = "h-4 w-4" />
142174 </ button >
143175 < div className = "flex-1" />
144- < span className = "text-xs text-muted-foreground" >
145- { Math . round ( zoom * 100 ) } %
146- </ span >
176+ < div className = "flex items-center gap-1" >
177+ < button
178+ className = "p-1 rounded hover:bg-accent text-xs"
179+ aria-label = "Zoom out"
180+ onClick = { ( ) => setZoom ( ( z ) => Math . max ( ZOOM_MIN , + ( z - ZOOM_STEP ) . toFixed ( 2 ) ) ) }
181+ >
182+ < Minus className = "h-3 w-3" />
183+ </ button >
184+ < span className = "text-xs text-muted-foreground w-10 text-center" >
185+ { Math . round ( zoom * 100 ) } %
186+ </ span >
187+ < button
188+ className = "p-1 rounded hover:bg-accent text-xs"
189+ aria-label = "Zoom in"
190+ onClick = { ( ) => setZoom ( ( z ) => Math . min ( ZOOM_MAX , + ( z + ZOOM_STEP ) . toFixed ( 2 ) ) ) }
191+ >
192+ < Plus className = "h-3 w-3" />
193+ </ button >
194+ < button
195+ className = "p-1 rounded hover:bg-accent text-xs"
196+ aria-label = "Reset zoom"
197+ onClick = { ( ) => setZoom ( 1 ) }
198+ >
199+ < Maximize2 className = "h-3 w-3" />
200+ </ button >
201+ </ div >
147202 </ div >
148203
149204 { /* Canvas Area */ }
150- < div className = "flex-1 overflow-auto bg-muted/10 p-4" >
205+ < div className = "flex-1 overflow-auto bg-muted/10 p-4" role = "region" aria-label = "Design canvas" >
151206 < div
152207 className = "relative bg-background border rounded shadow-sm mx-auto"
153208 style = { {
@@ -188,6 +243,7 @@ export function PageDesigner({
188243 handleDeleteComponent ( comp . id ) ;
189244 } }
190245 className = "ml-auto p-0.5 rounded hover:bg-destructive/10"
246+ aria-label = { `Delete ${ comp . label ?? comp . type } ` }
191247 >
192248 < Trash2 className = "h-3 w-3 text-destructive" />
193249 </ button >
@@ -204,15 +260,15 @@ export function PageDesigner({
204260
205261 { /* Right Panel - Component Tree / Properties */ }
206262 { showComponentTree && (
207- < div className = "w-60 border-l bg-muted/30 flex flex-col" >
263+ < div className = "w-60 border-l bg-muted/30 flex flex-col" role = "region" aria-label = "Component tree" >
208264 < div className = "p-3 border-b font-medium text-sm flex items-center gap-2" >
209265 < Layers className = "h-4 w-4" />
210266 Component Tree
211267 </ div >
212268 < div className = "flex-1 overflow-y-auto p-2" >
213269 { components . length === 0 ? (
214270 < div className = "text-xs text-muted-foreground text-center py-4" >
215- No components added yet
271+ No components added yet. Click a component in the palette to add it to the canvas.
216272 </ div >
217273 ) : (
218274 components . map ( ( comp ) => (
@@ -235,6 +291,15 @@ export function PageDesigner({
235291 < div className = "text-xs text-muted-foreground space-y-1" >
236292 < div > Type: { selectedComponent . type } </ div >
237293 < div > ID: { selectedComponent . id } </ div >
294+ < label className = "flex items-center gap-1" >
295+ Label:
296+ < input
297+ type = "text"
298+ value = { selectedComponent . label ?? '' }
299+ onChange = { ( e ) => handleUpdateLabel ( selectedComponent . id , e . target . value ) }
300+ className = "flex-1 px-1 py-0.5 border rounded text-xs bg-background"
301+ />
302+ </ label >
238303 < div >
239304 Position: { selectedComponent . position . x } , { selectedComponent . position . y }
240305 </ div >
0 commit comments