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
+8 -5
View File
@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect, useRef } from 'react'
import { Network, Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2 } from 'lucide-react'
import { Network, Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2, Square } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useCanvasStore } from '@/stores/canvasStore'
import { scanApi } from '@/api/client'
@@ -30,18 +30,20 @@ interface ScanRun {
interface SidebarProps {
onAddNode: () => void
onAddGroupRect: () => void
onScan: () => void
onSave: () => void
onNodeApproved: (nodeId: string) => void
}
export function Sidebar({ onAddNode, onScan, onSave, onNodeApproved }: SidebarProps) {
export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved }: SidebarProps) {
const [collapsed, setCollapsed] = useState(false)
const [activeView, setActiveView] = useState<SidebarView>('canvas')
const { nodes, hasUnsavedChanges } = useCanvasStore()
const onlineCount = nodes.filter((n) => n.data.status === 'online').length
const offlineCount = nodes.filter((n) => n.data.status === 'offline').length
const networkNodes = nodes.filter((n) => n.data.type !== 'groupRect')
const onlineCount = networkNodes.filter((n) => n.data.status === 'online').length
const offlineCount = networkNodes.filter((n) => n.data.status === 'offline').length
const handleScan = useCallback(async () => {
try {
@@ -110,7 +112,7 @@ export function Sidebar({ onAddNode, onScan, onSave, onNodeApproved }: SidebarPr
<div className="px-3 py-2 border-t border-border text-xs text-muted-foreground space-y-0.5">
<div className="flex justify-between">
<span>Total</span>
<span className="text-foreground font-mono">{nodes.length}</span>
<span className="text-foreground font-mono">{networkNodes.length}</span>
</div>
<div className="flex justify-between">
<span className="text-[#39d353]">Online</span>
@@ -126,6 +128,7 @@ export function Sidebar({ onAddNode, onScan, onSave, onNodeApproved }: SidebarPr
{/* Actions */}
<div className="flex flex-col gap-0.5 p-2 border-t border-border">
<SidebarItem icon={Plus} label="Add Node" collapsed={collapsed} onClick={onAddNode} />
<SidebarItem icon={Square} label="Add Rectangle" collapsed={collapsed} onClick={onAddGroupRect} />
{!STANDALONE && <SidebarItem icon={ScanLine} label="Scan Network" collapsed={collapsed} onClick={handleScan} />}
<SidebarItem
icon={Save}