diff --git a/frontend/src/components/LayerPanel.tsx b/frontend/src/components/LayerPanel.tsx index 5415610..16c6c51 100644 --- a/frontend/src/components/LayerPanel.tsx +++ b/frontend/src/components/LayerPanel.tsx @@ -18,6 +18,7 @@ interface LayerPanelProps { onToggleLock: (id: string) => void; onReorder: (fromIndex: number, toIndex: number) => void; onDelete: (id: string) => void; + onRename: (id: string, name: string) => void; onGroup: () => void; onUngroup: () => void; hasSelection: boolean; @@ -26,10 +27,13 @@ interface LayerPanelProps { export default function LayerPanel({ layers, selectedIds, onSelect, onToggleVisible, onToggleLock, - onReorder, onDelete, onGroup, onUngroup, hasSelection, hasGroupSelection, + onReorder, onDelete, onRename, onGroup, onUngroup, hasSelection, hasGroupSelection, }: LayerPanelProps) { const [collapsed, setCollapsed] = useState(false); const [dragIdx, setDragIdx] = useState(null); + const [editingId, setEditingId] = useState(null); + const [editValue, setEditValue] = useState(''); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); const onDragStart = useCallback((idx: number) => setDragIdx(idx), []); const onDragOver = useCallback((e: React.DragEvent) => e.preventDefault(), []); @@ -40,15 +44,36 @@ export default function LayerPanel({ setDragIdx(null); }, [dragIdx, onReorder]); + const toggleGroupExpand = useCallback((id: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }, []); + + const startRename = useCallback((id: string, currentName: string) => { + setEditingId(id); + setEditValue(currentName); + }, []); + + const commitRename = useCallback(() => { + if (editingId && editValue.trim()) { + onRename(editingId, editValue.trim()); + } + setEditingId(null); + setEditValue(''); + }, [editingId, editValue, onRename]); + if (collapsed) { return (
+ ) : ( + + )} + + {/* Visibility toggle */} + + + {/* Lock toggle */} + + + {/* Type icon */} + + {getTypeIcon(layer.type, layer.isGroup)} + + + {/* Name (editable) */} + {isEditing ? ( + setEditValue(e.target.value)} + onBlur={commitRename} + onKeyDown={(e) => { + if (e.key === 'Enter') commitRename(); + if (e.key === 'Escape') { setEditingId(null); setEditValue(''); } + }} + onClick={(e) => e.stopPropagation()} + style={{ + flex: 1, fontSize: '11px', color: '#ddd', background: '#0d0d0d', + border: '1px solid #333', borderRadius: '3px', padding: '1px 4px', + outline: 'none', minWidth: 0, + }} + /> + ) : ( + { e.stopPropagation(); startRename(layer.id, layer.name); }} + style={{ + flex: 1, fontSize: '11px', color: selected ? '#ccc' : '#888', + overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', + cursor: 'text', + }} + title="Double-click to rename" + > + {layer.name} + + )} + + {/* Delete */} + +
+ + {/* Group children */} + {layer.isGroup && isExpanded && layer.children && ( + layer.children.map((child, ci) => renderLayer(child, realIdx, depth + 1)) + )} + + ); + } + return (
{/* Header */}
- + Layers
- - - - - - - - - - - - + G+ + G- setCollapsed(true)}> - + @@ -96,79 +237,10 @@ export default function LayerPanel({
{[...layers].reverse().map((layer, i) => { const realIdx = layers.length - 1 - i; - const selected = selectedIds.includes(layer.id); - return ( -
onDragStart(realIdx)} - onDragOver={onDragOver} - onDrop={() => onDrop(realIdx)} - onClick={() => onSelect(layer.id)} - style={{ - display: 'flex', alignItems: 'center', gap: '4px', - padding: '3px 6px', cursor: 'pointer', - background: selected ? '#2a3a50' : dragIdx === realIdx ? '#2a2a2a' : 'transparent', - borderBottom: '1px solid #222', - opacity: layer.visible ? 1 : 0.4, - }} - onMouseEnter={(e) => { if (!selected) (e.currentTarget as HTMLDivElement).style.background = '#252525'; }} - onMouseLeave={(e) => { if (!selected) (e.currentTarget as HTMLDivElement).style.background = 'transparent'; }} - > - {/* Visibility toggle */} - - - {/* Lock toggle */} - - - {/* Type icon */} - - {layer.isGroup ? '📁' : layer.type === 'image' ? '🖼' : layer.type === 'i-text' ? 'T' : layer.type === 'path' ? '✏' : '◇'} - - - {/* Name */} - - {layer.name} - - - {/* Delete */} - -
- ); + return renderLayer(layer, realIdx); })} {layers.length === 0 && ( -
+
No objects
)} @@ -183,10 +255,10 @@ function SmallBtn({ onClick, title, disabled, children }: { return ( diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index a192d10..db0c951 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -296,30 +296,49 @@ export default function Editor({ isPublicView }: EditorProps) { }, []); // Refresh layer list from canvas + // Auto-name counters + const nameCounters = useRef>({}); + + function autoName(obj: any, i: number): string { + if (obj.name) return obj.name; + const type = obj.type || 'object'; + if (type === 'image') { + nameCounters.current.image = (nameCounters.current.image || 0) + 1; + return `Image ${nameCounters.current.image}`; + } + if (type === 'i-text') return (obj.text || 'Text').slice(0, 20); + if (type === 'path') { + nameCounters.current.path = (nameCounters.current.path || 0) + 1; + return `Drawing ${nameCounters.current.path}`; + } + if (type === 'group') return `Group (${obj._objects?.length || 0})`; + return `${type} ${i + 1}`; + } + + function buildLayerItem(obj: any, i: number): any { + const id = obj.id || `layer-${i}`; + const isGroup = obj.type === 'group'; + let children: any[] | undefined; + if (isGroup && obj._objects) { + children = obj._objects.map((child: any, ci: number) => buildLayerItem(child, ci)); + } + return { + id, + name: autoName(obj, i), + type: obj.type || 'object', + visible: obj.visible !== false, + locked: !obj.selectable, + isGroup, + children, + }; + } + const refreshLayers = useCallback(() => { const canvas = canvasRef.current?.getCanvas(); if (!canvas) return; + nameCounters.current = {}; const objects = canvas.getObjects(); - const layers = objects.map((obj: any, i: number) => { - const id = obj.id || `layer-${i}`; - const isGroup = obj.type === 'group'; - let name = obj.name || ''; - if (!name) { - if (obj.type === 'image') name = 'Image'; - else if (obj.type === 'i-text') name = (obj.text || 'Text').slice(0, 20); - else if (obj.type === 'path') name = 'Drawing'; - else if (isGroup) name = `Group (${obj._objects?.length || 0})`; - else name = obj.type || 'Object'; - } - return { - id, - name: `${name}`, - type: obj.type || 'object', - visible: obj.visible !== false, - locked: !obj.selectable, - isGroup, - }; - }); + const layers = objects.map((obj: any, i: number) => buildLayerItem(obj, i)); setLayerList(layers); const activeIds = canvas.getActiveObjects().map((o: any) => o.id).filter(Boolean); setSelectedLayerIds(activeIds); @@ -938,6 +957,15 @@ export default function Editor({ isPublicView }: EditorProps) { refreshLayers(); } }} + onRename={(id, name) => { + const canvas = canvasRef.current?.getCanvas(); + if (!canvas) return; + const obj = canvas.getObjects().find((o: any) => o.id === id); + if (obj) { + (obj as any).name = name; + refreshLayers(); + } + }} onGroup={handleGroup} onUngroup={handleUngroup} hasSelection={canvasRef.current?.getCanvas()?.getActiveObjects()?.length! > 1}