From 600c3959af9865799da0c9f37ddd122e012862b3 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sat, 4 Apr 2026 07:53:52 +0000 Subject: [PATCH] feat: add NetworkCanvas wrapper and DiagramHeader components Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/network/DiagramHeader.tsx | 162 ++++++++++++++++++ .../src/components/network/NetworkCanvas.tsx | 87 ++++++++++ 2 files changed, 249 insertions(+) create mode 100644 frontend/src/components/network/DiagramHeader.tsx create mode 100644 frontend/src/components/network/NetworkCanvas.tsx diff --git a/frontend/src/components/network/DiagramHeader.tsx b/frontend/src/components/network/DiagramHeader.tsx new file mode 100644 index 00000000..4cd3b0cf --- /dev/null +++ b/frontend/src/components/network/DiagramHeader.tsx @@ -0,0 +1,162 @@ +import { useState, useCallback, useRef, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { ChevronLeft, Save, Download, FileJson, Image, FileText } from 'lucide-react' + +interface DiagramHeaderProps { + name: string + clientName: string | null + isSaving: boolean + lastSavedAt: Date | null + diagramId: string | null + onNameChange: (name: string) => void + onSave: () => void + onExportPng: () => void + onExportPdf: () => void + onExportJson: () => void +} + +export function DiagramHeader({ + name, + clientName, + isSaving, + lastSavedAt, + diagramId, + onNameChange, + onSave, + onExportPng, + onExportPdf, + onExportJson, +}: DiagramHeaderProps) { + const navigate = useNavigate() + const [editing, setEditing] = useState(false) + const [editValue, setEditValue] = useState(name) + const [showExportMenu, setShowExportMenu] = useState(false) + const inputRef = useRef(null) + const exportMenuRef = useRef(null) + + useEffect(() => { + if (editing && inputRef.current) { + inputRef.current.focus() + inputRef.current.select() + } + }, [editing]) + + useEffect(() => { + setEditValue(name) + }, [name]) + + useEffect(() => { + if (!showExportMenu) return + const handleClick = (e: MouseEvent) => { + if (exportMenuRef.current && !exportMenuRef.current.contains(e.target as HTMLElement)) { + setShowExportMenu(false) + } + } + document.addEventListener('mousedown', handleClick) + return () => document.removeEventListener('mousedown', handleClick) + }, [showExportMenu]) + + const handleConfirmName = useCallback(() => { + setEditing(false) + if (editValue.trim() && editValue !== name) { + onNameChange(editValue.trim()) + } else { + setEditValue(name) + } + }, [editValue, name, onNameChange]) + + const formatLastSaved = () => { + if (!lastSavedAt) return null + const diff = Date.now() - lastSavedAt.getTime() + if (diff < 60_000) return 'Saved just now' + const mins = Math.floor(diff / 60_000) + return `Saved ${mins}m ago` + } + + return ( +
+ + +
+ + {editing ? ( + setEditValue(e.target.value)} + onBlur={handleConfirmName} + onKeyDown={e => { if (e.key === 'Enter') handleConfirmName(); if (e.key === 'Escape') { setEditing(false); setEditValue(name) } }} + className="rounded border border-accent bg-input px-2 py-1 text-sm font-heading font-semibold text-heading focus:outline-none" + /> + ) : ( + + )} + + {clientName && ( + + {clientName} + + )} + +
+ + {lastSavedAt && ( + {formatLastSaved()} + )} + + + +
+ + {showExportMenu && ( +
+ + + {diagramId && ( + + )} +
+ )} +
+
+ ) +} diff --git a/frontend/src/components/network/NetworkCanvas.tsx b/frontend/src/components/network/NetworkCanvas.tsx new file mode 100644 index 00000000..c52625db --- /dev/null +++ b/frontend/src/components/network/NetworkCanvas.tsx @@ -0,0 +1,87 @@ +import { useCallback } from 'react' +import { + ReactFlow, + Background, + Controls, + MiniMap, + BackgroundVariant, + type OnConnect, + type OnNodesChange, + type OnEdgesChange, + type Node, + type Edge, +} from '@xyflow/react' +import { nodeTypes } from './nodes/nodeTypes' +import { edgeTypes } from './edges/edgeTypes' + +interface NetworkCanvasProps { + nodes: Node[] + edges: Edge[] + onNodesChange: OnNodesChange + onEdgesChange: OnEdgesChange + onConnect: OnConnect + onNodeSelect: (node: Node | null) => void + onEdgeSelect: (edge: Edge | null) => void + onDrop: (event: React.DragEvent) => void + onDragOver: (event: React.DragEvent) => void +} + +export function NetworkCanvas({ + nodes, + edges, + onNodesChange, + onEdgesChange, + onConnect, + onNodeSelect, + onEdgeSelect, + onDrop, + onDragOver, +}: NetworkCanvasProps) { + const handleSelectionChange = useCallback(({ nodes: selectedNodes, edges: selectedEdges }: { nodes: Node[]; edges: Edge[] }) => { + if (selectedNodes.length === 1) { + onNodeSelect(selectedNodes[0]) + onEdgeSelect(null) + } else if (selectedEdges.length === 1) { + onEdgeSelect(selectedEdges[0]) + onNodeSelect(null) + } else { + onNodeSelect(null) + onEdgeSelect(null) + } + }, [onNodeSelect, onEdgeSelect]) + + const handlePaneClick = useCallback(() => { + onNodeSelect(null) + onEdgeSelect(null) + }, [onNodeSelect, onEdgeSelect]) + + return ( + + + + + + ) +}