feat: add Group Rectangle — resizable decorative zones on canvas

- New GroupRectNode: rounded rect with NodeResizer (8 handles), no
  connection handles, always behind network nodes via negative zIndex
- GroupRectModal: label, font preset (Inter/Mono/Serif), 3×3 text
  position grid, text/border/background color pickers, z-order 1–9
- Sidebar: "Add Rectangle" button (below Add Node), group rects
  excluded from node count stats
- Save/load: size persisted in custom_colors.width/height, no backend
  schema changes required
- elevateNodesOnSelect=false on ReactFlow so selected rects never
  pop above network nodes
- Tests: 3 new store tests + 9 GroupRectModal tests (66 total, all pass)
This commit is contained in:
Pouzor
2026-03-10 16:59:40 +01:00
parent c0f1d1ff1a
commit 1e72366d03
10 changed files with 604 additions and 27 deletions
@@ -25,6 +25,7 @@ describe('canvasStore', () => {
edges: [],
hasUnsavedChanges: false,
selectedNodeId: null,
editingGroupRectId: null,
})
})
@@ -190,6 +191,40 @@ describe('canvasStore', () => {
expect(updatedChild?.extent).toBeUndefined()
})
it('setEditingGroupRectId sets and clears the editing id', () => {
useCanvasStore.getState().setEditingGroupRectId('rect-1')
expect(useCanvasStore.getState().editingGroupRectId).toBe('rect-1')
useCanvasStore.getState().setEditingGroupRectId(null)
expect(useCanvasStore.getState().editingGroupRectId).toBeNull()
})
it('setNodeZIndex updates the node zIndex and marks unsaved', () => {
useCanvasStore.getState().addNode(makeNode('n1'))
useCanvasStore.getState().markSaved()
useCanvasStore.getState().setNodeZIndex('n1', -5)
const node = useCanvasStore.getState().nodes.find((n) => n.id === 'n1')
expect(node?.zIndex).toBe(-5)
expect(useCanvasStore.getState().hasUnsavedChanges).toBe(true)
})
it('addNode with groupRect type preserves zIndex and dimensions', () => {
const rectNode: Node<NodeData> = {
id: 'rect-1',
type: 'groupRect',
position: { x: 100, y: 100 },
data: { label: 'Zone A', type: 'groupRect', status: 'unknown', services: [] },
width: 360,
height: 240,
zIndex: -9,
}
useCanvasStore.getState().addNode(rectNode)
const stored = useCanvasStore.getState().nodes.find((n) => n.id === 'rect-1')
expect(stored?.type).toBe('groupRect')
expect(stored?.zIndex).toBe(-9)
expect(stored?.width).toBe(360)
expect(stored?.height).toBe(240)
})
it('loadCanvas sorts parents before children', () => {
const parent = makeNode('p1')
const child: Node<NodeData> = { ...makeNode('c1', { parent_id: 'p1' }), parentId: 'p1', extent: 'parent' }