UNPKG

@ichigo_san/graphing

Version:

A lightweight UML-style diagram editor built with React Flow and Tailwind CSS

1,328 lines (1,294 loc) 69.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _htmlToImage = require("html-to-image"); var _lucideReact = require("lucide-react"); var _reactflow = _interopRequireWildcard(require("reactflow")); require("reactflow/dist/style.css"); var _DiamondNode = _interopRequireDefault(require("../nodes/DiamondNode")); var _CircleNode = _interopRequireDefault(require("../nodes/CircleNode")); var _HexagonNode = _interopRequireDefault(require("../nodes/HexagonNode")); var _TriangleNode = _interopRequireDefault(require("../nodes/TriangleNode")); var _ContainerNode = _interopRequireDefault(require("../nodes/ContainerNode")); var _ComponentNode = _interopRequireDefault(require("../nodes/ComponentNode")); var _edges = require("../edges"); var _PromptModal = _interopRequireDefault(require("../modals/PromptModal")); var _ConfirmModal = _interopRequireDefault(require("../modals/ConfirmModal")); var _ContainerSelectorModal = _interopRequireDefault(require("../modals/ContainerSelectorModal")); var _ShapeSelectorModal = _interopRequireDefault(require("../modals/ShapeSelectorModal")); var _JsonPasteModal = _interopRequireDefault(require("../modals/JsonPasteModal")); var _JsonValidatorModal = _interopRequireDefault(require("../modals/JsonValidatorModal")); var _TailwindPropertyEditor = _interopRequireDefault(require("./TailwindPropertyEditor")); var _EnhancedMenuBar = _interopRequireDefault(require("./EnhancedMenuBar")); var _ajv = _interopRequireDefault(require("ajv")); var _autoLayout = require("../utils/autoLayout"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } // Import node components // Import modal components // Import editor components const diagramSchema = { type: 'object', properties: { containers: { type: 'array', items: { type: 'object', required: ['id', 'label', 'position', 'size'], properties: { id: { type: 'string' }, label: { type: 'string' }, position: { type: 'object', required: ['x', 'y'], properties: { x: { type: 'number' }, y: { type: 'number' } } }, size: { type: 'object', required: ['width', 'height'], properties: { width: { type: 'number' }, height: { type: 'number' } } } } } }, nodes: { type: 'array', items: { type: 'object', required: ['id', 'label', 'position'], properties: { id: { type: 'string' }, label: { type: 'string' }, type: { type: 'string' }, position: { type: 'object', required: ['x', 'y'], properties: { x: { type: 'number' }, y: { type: 'number' } } } } } }, connections: { type: 'array', items: { type: 'object', required: ['id', 'source', 'target'], properties: { id: { type: 'string' }, source: { type: 'string' }, target: { type: 'string' } } } } }, required: ['containers', 'nodes', 'connections'] }; const ajv = new _ajv.default(); const validateDiagram = ajv.compile(diagramSchema); const ArchitectureDiagramEditorContent = _ref => { let { initialDiagram, onToggleTheme, showThemeToggle, onToggleFullscreen, isFullscreen, onToggleMini, showMiniToggle } = _ref; // State for nodes and edges const [nodes, setNodes] = (0, _react.useState)([]); const [edges, setEdges] = (0, _react.useState)([]); const [selectedElements, setSelectedElements] = (0, _react.useState)({ nodes: [], edges: [] }); const [history, setHistory] = (0, _react.useState)({ past: [], present: { nodes: [], edges: [] }, future: [] }); const [isInitialized, setIsInitialized] = (0, _react.useState)(false); const [clipboardData, setClipboardData] = (0, _react.useState)(null); const [propertyPanelOpen, setPropertyPanelOpen] = (0, _react.useState)(true); const [propertyPanelMinimized, setPropertyPanelMinimized] = (0, _react.useState)(false); const [statsPanelOpen, setStatsPanelOpen] = (0, _react.useState)(true); const [panMode, setPanMode] = (0, _react.useState)(false); const getDiagramBounds = (0, _react.useCallback)(() => { if (nodes.length === 0) return { x: 0, y: 0, width: 0, height: 0 }; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; nodes.forEach(n => { var _n$__rf, _n$style, _n$__rf2, _n$style2; const width = ((_n$__rf = n.__rf) === null || _n$__rf === void 0 ? void 0 : _n$__rf.width) || ((_n$style = n.style) === null || _n$style === void 0 ? void 0 : _n$style.width) || 150; const height = ((_n$__rf2 = n.__rf) === null || _n$__rf2 === void 0 ? void 0 : _n$__rf2.height) || ((_n$style2 = n.style) === null || _n$style2 === void 0 ? void 0 : _n$style2.height) || 80; minX = Math.min(minX, n.position.x); minY = Math.min(minY, n.position.y); maxX = Math.max(maxX, n.position.x + width); maxY = Math.max(maxY, n.position.y + height); }); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }, [nodes]); const updateNodeInternals = (0, _reactflow.useUpdateNodeInternals)(); // State for modals const [promptModal, setPromptModal] = (0, _react.useState)({ isOpen: false, title: '', message: '', defaultValue: '', onConfirm: null }); const [confirmModal, setConfirmModal] = (0, _react.useState)({ isOpen: false, title: '', message: '', onConfirm: null }); const [containerSelectorModal, setContainerSelectorModal] = (0, _react.useState)({ isOpen: false, title: '', message: '', containers: [], onSelect: null }); const [shapeSelectorModal, setShapeSelectorModal] = (0, _react.useState)({ isOpen: false }); const [jsonPasteModal, setJsonPasteModal] = (0, _react.useState)({ isOpen: false, onConfirm: null }); const [jsonValidatorModal, setJsonValidatorModal] = (0, _react.useState)({ isOpen: false }); // Refs const reactFlowWrapper = (0, _react.useRef)(null); const saveTimeoutRef = (0, _react.useRef)(null); const togglePropertyPanel = (0, _react.useCallback)(() => { setPropertyPanelOpen(prev => { const next = !prev; if (next) setPropertyPanelMinimized(false); return next; }); }, []); const togglePropertyPanelMinimized = (0, _react.useCallback)(() => { setPropertyPanelMinimized(prev => !prev); }, []); const toggleStatsPanel = (0, _react.useCallback)(() => { setStatsPanelOpen(prev => !prev); }, []); const togglePanMode = (0, _react.useCallback)(() => { setPanMode(prev => !prev); }, []); const handleContextMenu = (0, _react.useCallback)(event => { if (panMode) { event.preventDefault(); } }, [panMode]); // Custom node types const nodeTypes = (0, _react.useMemo)(() => ({ diamond: _DiamondNode.default, circle: _CircleNode.default, hexagon: _HexagonNode.default, triangle: _TriangleNode.default, container: _ContainerNode.default, component: _ComponentNode.default }), []); const edgeTypes = (0, _react.useMemo)(() => ({ adjustable: _edges.AdjustableEdge }), []); // Stable node label change handler const handleNodeLabelChange = (0, _react.useCallback)((nodeId, label) => { setNodes(nds => nds.map(node => { if (node.id === nodeId) { return _objectSpread(_objectSpread({}, node), {}, { data: _objectSpread(_objectSpread({}, node.data), {}, { label }) }); } return node; })); // Debounced history save if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } saveTimeoutRef.current = setTimeout(() => { setHistory(prev => ({ past: [...prev.past, prev.present], present: { nodes, edges }, future: [] })); }, 500); }, [nodes, edges]); // Convert JSON configuration to React Flow format const jsonToReactFlow = (0, _react.useCallback)(config => { var _config$containers, _config$nodes, _config$connections; const flowNodes = []; const flowEdges = []; // Add container nodes first (_config$containers = config.containers) === null || _config$containers === void 0 || _config$containers.forEach(container => { var _container$size, _container$size2; flowNodes.push({ id: container.id, type: 'container', position: container.position, data: { label: container.label, color: container.color || '#f5f5f5', bgColor: container.bgColor || '#f5f5f5', borderColor: container.borderColor || '#ddd', icon: container.icon, description: container.description, onLabelChange: handleNodeLabelChange }, style: { width: ((_container$size = container.size) === null || _container$size === void 0 ? void 0 : _container$size.width) || 400, height: ((_container$size2 = container.size) === null || _container$size2 === void 0 ? void 0 : _container$size2.height) || 300, zIndex: container.zIndex || 1 }, draggable: true, selectable: true, zIndex: container.zIndex || 1 }); }); // Add component nodes (_config$nodes = config.nodes) === null || _config$nodes === void 0 || _config$nodes.forEach(node => { var _node$size, _node$size2; flowNodes.push({ id: node.id, type: 'component', position: node.position, parentNode: node.parentContainer, data: { label: node.label, color: node.color || '#E3F2FD', borderColor: node.borderColor || '#90CAF9', icon: node.icon, description: node.description, onLabelChange: handleNodeLabelChange }, style: { width: ((_node$size = node.size) === null || _node$size === void 0 ? void 0 : _node$size.width) || 150, height: ((_node$size2 = node.size) === null || _node$size2 === void 0 ? void 0 : _node$size2.height) || 80, zIndex: node.zIndex || 10 }, draggable: true, selectable: true, zIndex: node.zIndex || 10 }); }); // Add connections/edges (_config$connections = config.connections) === null || _config$connections === void 0 || _config$connections.forEach(connection => { flowEdges.push({ id: connection.id, source: connection.source, target: connection.target, label: connection.label, type: connection.type || 'floating', animated: connection.animated || false, style: { strokeWidth: 2, zIndex: connection.zIndex || 5 }, zIndex: connection.zIndex || 5, markerStart: connection.markerStart, markerEnd: connection.markerEnd || { type: 'arrow' }, data: { label: connection.label, description: connection.description || '', control: connection.control, intersection: connection.intersection || 'none' } }); }); return { nodes: flowNodes, edges: flowEdges }; }, [handleNodeLabelChange]); // Initialize diagram from the provided configuration (0, _react.useEffect)(() => { if (!isInitialized) { const config = initialDiagram || { containers: [], nodes: [], connections: [] }; const { nodes: initialNodes, edges: initialEdges } = jsonToReactFlow(config); const laidOut = (0, _autoLayout.autoLayoutNodes)(initialNodes); setNodes(laidOut); setEdges(initialEdges); setHistory({ past: [], present: { nodes: laidOut, edges: initialEdges }, future: [] }); laidOut.forEach(n => updateNodeInternals(n.id)); setIsInitialized(true); } }, [isInitialized, jsonToReactFlow, initialDiagram, updateNodeInternals]); // Optimized save to history function const saveToHistory = (0, _react.useCallback)(() => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } saveTimeoutRef.current = setTimeout(() => { setHistory(prev => ({ past: [...prev.past, prev.present], present: { nodes, edges }, future: [] })); }, 300); }, [nodes, edges]); // Helper functions for modals const showPromptModal = (0, _react.useCallback)((title, message, defaultValue, onConfirm) => { setPromptModal({ isOpen: true, title, message, defaultValue, onConfirm: value => { onConfirm(value); setPromptModal(prev => _objectSpread(_objectSpread({}, prev), {}, { isOpen: false })); } }); }, []); const showConfirmModal = (0, _react.useCallback)((title, message, onConfirm) => { setConfirmModal({ isOpen: true, title, message, onConfirm: () => { onConfirm(); setConfirmModal(prev => _objectSpread(_objectSpread({}, prev), {}, { isOpen: false })); } }); }, []); const showContainerSelectorModal = (0, _react.useCallback)((title, message, containers, onSelect) => { setContainerSelectorModal({ isOpen: true, title, message, containers, onSelect: containerId => { onSelect(containerId); setContainerSelectorModal(prev => _objectSpread(_objectSpread({}, prev), {}, { isOpen: false })); } }); }, []); const showShapeSelectorModal = (0, _react.useCallback)(() => { setShapeSelectorModal({ isOpen: true }); }, []); const showJsonPasteModal = (0, _react.useCallback)(onConfirm => { setJsonPasteModal({ isOpen: true, onConfirm }); }, []); const showJsonValidatorModal = (0, _react.useCallback)(() => { setJsonValidatorModal({ isOpen: true }); }, []); // Optimized change handlers const handleNodesChange = (0, _react.useCallback)(changes => { setNodes(nds => (0, _reactflow.applyNodeChanges)(changes, nds)); }, []); const onEdgesChange = (0, _react.useCallback)(changes => { setEdges(eds => (0, _reactflow.applyEdgeChanges)(changes, eds)); }, []); const onNodeDragStop = (0, _react.useCallback)(() => { saveToHistory(); }, [saveToHistory]); const onEdgeUpdateStart = (0, _react.useCallback)(() => {}, []); const onEdgeUpdateEnd = (0, _react.useCallback)(() => { saveToHistory(); }, [saveToHistory]); const onEdgeUpdate = (0, _react.useCallback)((oldEdge, newConnection) => { setEdges(eds => (0, _reactflow.reconnectEdge)(oldEdge, newConnection, eds)); }, []); const onConnectStart = (0, _react.useCallback)(() => {}, []); const onConnectEnd = (0, _react.useCallback)(() => {}, []); const defaultEdgeOptions = (0, _react.useMemo)(() => ({ type: 'adjustable', animated: true, style: { strokeWidth: 2, stroke: '#2563eb', strokeDasharray: '5 5' }, data: { intersection: 'none' } }), []); // Handle new connections const onConnect = (0, _react.useCallback)(params => { const newEdge = _objectSpread(_objectSpread({}, params), {}, { id: "edge-".concat(Date.now()), type: 'adjustable', animated: true, style: { strokeWidth: 2, stroke: '#2563eb', strokeDasharray: '5 5', zIndex: 5 }, zIndex: 5, markerEnd: { type: 'arrow' }, data: { label: '', description: '', intersection: 'none', // Default to no intersection control: null // Will be auto-calculated } }); setEdges(eds => (0, _reactflow.addEdge)(newEdge, eds)); saveToHistory(); }, [saveToHistory]); // Handle selection changes - now includes edges const onSelectionChange = (0, _react.useCallback)(_ref2 => { let { nodes: selectedNodes, edges: selectedEdges } = _ref2; setSelectedElements({ nodes: selectedNodes || [], edges: selectedEdges || [] }); }, []); // Handle edge click - now uses property panel instead of modal const onEdgeClick = (0, _react.useCallback)((event, edge) => { event.stopPropagation(); setSelectedElements({ nodes: [], edges: [edge] }); }, []); // Handle property changes for both nodes and edges const handleElementPropertyChange = (0, _react.useCallback)((elementType, property, value) => { if (elementType === 'node' && selectedElements.nodes.length === 1) { const nodeId = selectedElements.nodes[0].id; setNodes(nds => nds.map(node => { if (node.id === nodeId) { if (property === 'label') { return _objectSpread(_objectSpread({}, node), {}, { data: _objectSpread(_objectSpread({}, node.data), {}, { label: value }) }); } else if (property === 'zIndex') { return _objectSpread(_objectSpread({}, node), {}, { style: _objectSpread(_objectSpread({}, node.style), {}, { zIndex: parseInt(value) }), zIndex: parseInt(value) }); } else { return _objectSpread(_objectSpread({}, node), {}, { data: _objectSpread(_objectSpread({}, node.data), {}, { [property]: value }) }); } } return node; })); } else if (elementType === 'edge' && selectedElements.edges.length === 1) { const edgeId = selectedElements.edges[0].id; setEdges(eds => eds.map(edge => { if (edge.id === edgeId) { if (property === 'label') { return _objectSpread(_objectSpread({}, edge), {}, { label: value, data: _objectSpread(_objectSpread({}, edge.data), {}, { label: value }) }); } else if (property === 'zIndex') { return _objectSpread(_objectSpread({}, edge), {}, { style: _objectSpread(_objectSpread({}, edge.style), {}, { zIndex: parseInt(value) }), zIndex: parseInt(value) }); } else if (property === 'type' || property === 'animated') { return _objectSpread(_objectSpread({}, edge), {}, { [property]: value }); } else if (property === 'markerStart' || property === 'markerEnd') { return _objectSpread(_objectSpread({}, edge), {}, { [property]: value }); } else if (property.startsWith('style.')) { const styleProp = property.replace('style.', ''); return _objectSpread(_objectSpread({}, edge), {}, { style: _objectSpread(_objectSpread({}, edge.style), {}, { [styleProp]: value }) }); } else { return _objectSpread(_objectSpread({}, edge), {}, { data: _objectSpread(_objectSpread({}, edge.data), {}, { [property]: value }) }); } } return edge; })); } saveToHistory(); }, [selectedElements, saveToHistory]); const applyAutoLayout = (0, _react.useCallback)(currentNodes => { const laidOut = (0, _autoLayout.autoLayoutNodes)(currentNodes); setNodes(laidOut); laidOut.forEach(n => updateNodeInternals(n.id)); }, [updateNodeInternals]); // Add new container node const addContainerNode = (0, _react.useCallback)(() => { showPromptModal('Add Container', 'Enter container name:', 'New Container', name => { const newNode = { id: "container-".concat(Date.now()), type: 'container', position: { x: Math.random() * 300 + 100, y: Math.random() * 200 + 100 }, data: { label: name, icon: '📦', color: '#f5f5f5', borderColor: '#ddd', description: '', onLabelChange: handleNodeLabelChange }, style: { width: 400, height: 300, zIndex: 1 }, draggable: true, selectable: true, zIndex: 1 }; setNodes(nds => [...nds, newNode]); saveToHistory(); }); }, [nodes, handleNodeLabelChange, saveToHistory, showPromptModal]); // Add new component node const addComponentNode = (0, _react.useCallback)(() => { const containerNodes = nodes.filter(node => node.type === 'container'); if (containerNodes.length > 0) { showContainerSelectorModal('Select Container', 'Choose a container for this component:', containerNodes, containerId => { showPromptModal('Add Component', 'Enter component name:', 'New Component', name => { const container = nodes.find(node => node.id === containerId); let position = { x: 50, y: 50 }; if (container) { const componentsInContainer = nodes.filter(node => node.parentNode === containerId).length; position = { x: 50 + componentsInContainer % 2 * 170, y: 50 + Math.floor(componentsInContainer / 2) * 100 }; } const newNode = { id: "component-".concat(Date.now()), type: 'component', position, parentNode: containerId, data: { label: name, icon: '🔹', color: '#E3F2FD', borderColor: '#90CAF9', description: '', onLabelChange: handleNodeLabelChange }, style: { width: 150, height: 80, zIndex: 10 }, draggable: true, selectable: true, zIndex: 10 }; setNodes(nds => [...nds, newNode]); saveToHistory(); }); }); } else { showPromptModal('Add Component', 'Enter component name:', 'New Component', name => { const newNode = { id: "component-".concat(Date.now()), type: 'component', position: { x: Math.random() * 300 + 100, y: Math.random() * 200 + 100 }, data: { label: name, icon: '🔹', color: '#E3F2FD', borderColor: '#90CAF9', description: '', onLabelChange: handleNodeLabelChange }, style: { width: 150, height: 80, zIndex: 10 }, draggable: true, selectable: true, zIndex: 10 }; setNodes(nds => [...nds, newNode]); saveToHistory(); }); } }, [nodes, handleNodeLabelChange, saveToHistory, showContainerSelectorModal, showPromptModal]); // Add new shape node const addShapeNode = (0, _react.useCallback)(shapeType => { showPromptModal('Add Shape', 'Enter shape name:', 'New Shape', name => { const newNode = { id: "".concat(shapeType, "-").concat(Date.now()), type: shapeType, position: { x: Math.random() * 300 + 100, y: Math.random() * 200 + 100 }, data: { label: name, icon: shapeType === 'diamond' ? '♦️' : shapeType === 'circle' ? '⭕' : shapeType === 'triangle' ? '🔺' : '⬢', color: shapeType === 'diamond' ? '#81D4FA' : shapeType === 'circle' ? '#C5E1A5' : shapeType === 'triangle' ? '#FFD54F' : '#FFCC80', borderColor: '#ddd', description: '', onLabelChange: handleNodeLabelChange }, style: { width: 100, height: 100, zIndex: 15 }, draggable: true, selectable: true, zIndex: 15 }; setNodes(nds => [...nds, newNode]); saveToHistory(); }); }, [nodes, handleNodeLabelChange, saveToHistory, showPromptModal]); // Copy selected elements const copySelected = (0, _react.useCallback)(() => { if (selectedElements.nodes.length > 0 || selectedElements.edges.length > 0) { setClipboardData({ nodes: selectedElements.nodes, edges: selectedElements.edges }); } }, [selectedElements]); // Paste elements const pasteElements = (0, _react.useCallback)(() => { if (clipboardData) { const newNodes = clipboardData.nodes.map(node => _objectSpread(_objectSpread({}, node), {}, { id: "".concat(node.type, "-").concat(Date.now(), "-").concat(Math.random().toString(36).substr(2, 9)), position: { x: node.position.x + 50, y: node.position.y + 50 }, selected: false })); const newEdges = clipboardData.edges.map(edge => _objectSpread(_objectSpread({}, edge), {}, { id: "edge-".concat(Date.now(), "-").concat(Math.random().toString(36).substr(2, 9)), selected: false })); setNodes(nds => [...nds, ...newNodes]); setEdges(eds => [...eds, ...newEdges]); saveToHistory(); } }, [nodes, clipboardData, saveToHistory]); // Delete selected elements const deleteSelected = (0, _react.useCallback)(() => { if (selectedElements.nodes.length === 0 && selectedElements.edges.length === 0) return; const selectedNodeIds = selectedElements.nodes.map(node => node.id); const edgesToKeep = edges.filter(edge => !selectedNodeIds.includes(edge.source) && !selectedNodeIds.includes(edge.target) && !selectedElements.edges.map(e => e.id).includes(edge.id)); const nodesToKeep = nodes.filter(node => { return !selectedNodeIds.includes(node.id) && (!node.parentNode || !selectedNodeIds.includes(node.parentNode)); }); setNodes(nodesToKeep); setEdges(edgesToKeep); setSelectedElements({ nodes: [], edges: [] }); saveToHistory(); }, [selectedElements, nodes, edges, saveToHistory]); // Undo/Redo functions const undo = (0, _react.useCallback)(() => { setHistory(prev => { if (prev.past.length === 0) return prev; const newPresent = prev.past[prev.past.length - 1]; const newPast = prev.past.slice(0, prev.past.length - 1); setNodes(newPresent.nodes); setEdges(newPresent.edges); return { past: newPast, present: newPresent, future: [prev.present, ...prev.future] }; }); }, []); const redo = (0, _react.useCallback)(() => { setHistory(prev => { if (prev.future.length === 0) return prev; const newPresent = prev.future[0]; const newFuture = prev.future.slice(1); setNodes(newPresent.nodes); setEdges(newPresent.edges); return { past: [...prev.past, prev.present], present: newPresent, future: newFuture }; }); }, []); (0, _react.useEffect)(() => { const handleKeyDown = event => { if (event.target.closest('input, textarea, [contenteditable="true"]')) { return; // allow default behavior inside inputs and editable areas } if (event.ctrlKey || event.metaKey) { switch (event.key) { case 'c': event.preventDefault(); copySelected(); break; case 'v': event.preventDefault(); pasteElements(); break; case 'z': event.preventDefault(); if (event.shiftKey) { redo(); } else { undo(); } break; default: break; } } else if (event.key === 'Delete' || event.key === 'Backspace') { event.preventDefault(); deleteSelected(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [copySelected, pasteElements, deleteSelected, undo, redo]); // Export and import functions (same as before but with z-index) const exportJSON = (0, _react.useCallback)(() => { const diagramData = { metadata: { name: 'Architecture Diagram', description: 'Exported architecture diagram', version: '1.0', exportDate: new Date().toISOString() }, containers: nodes.filter(node => node.type === 'container').map(container => { var _container$style, _container$style2; return { id: container.id, label: container.data.label, position: container.position, size: { width: ((_container$style = container.style) === null || _container$style === void 0 ? void 0 : _container$style.width) || 400, height: ((_container$style2 = container.style) === null || _container$style2 === void 0 ? void 0 : _container$style2.height) || 300 }, color: container.data.color, bgColor: container.data.bgColor, borderColor: container.data.borderColor, icon: container.data.icon, description: container.data.description, zIndex: container.zIndex || 1 }; }), nodes: nodes.filter(node => node.type !== 'container').map(node => { var _node$style, _node$style2; return { id: node.id, label: node.data.label, type: node.type, position: node.position, parentContainer: node.parentNode, size: { width: ((_node$style = node.style) === null || _node$style === void 0 ? void 0 : _node$style.width) || 150, height: ((_node$style2 = node.style) === null || _node$style2 === void 0 ? void 0 : _node$style2.height) || 80 }, color: node.data.color, borderColor: node.data.borderColor, icon: node.data.icon, description: node.data.description, zIndex: node.zIndex || 10 }; }), connections: edges.map(edge => { var _edge$data, _edge$data2, _edge$data3, _edge$data4, _edge$style, _edge$style2; return { id: edge.id, source: edge.source, target: edge.target, label: (_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.label, type: edge.type, animated: edge.animated, description: (_edge$data2 = edge.data) === null || _edge$data2 === void 0 ? void 0 : _edge$data2.description, control: (_edge$data3 = edge.data) === null || _edge$data3 === void 0 ? void 0 : _edge$data3.control, markerStart: edge.markerStart, markerEnd: edge.markerEnd, intersection: (_edge$data4 = edge.data) === null || _edge$data4 === void 0 ? void 0 : _edge$data4.intersection, style: { strokeWidth: ((_edge$style = edge.style) === null || _edge$style === void 0 ? void 0 : _edge$style.strokeWidth) || 2, strokeDasharray: (_edge$style2 = edge.style) === null || _edge$style2 === void 0 ? void 0 : _edge$style2.strokeDasharray }, zIndex: edge.zIndex || 5 }; }) }; const dataStr = JSON.stringify(diagramData, null, 2); const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); const exportFileDefaultName = 'architecture-diagram.json'; const linkElement = document.createElement('a'); linkElement.setAttribute('href', dataUri); linkElement.setAttribute('download', exportFileDefaultName); linkElement.click(); }, [nodes, edges]); // Import from draw.io XML const importFromDrawioXML = (0, _react.useCallback)(() => { const inputElement = document.createElement('input'); inputElement.type = 'file'; inputElement.accept = '.drawio,.xml'; inputElement.onchange = event => { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = e => { try { const xmlContent = e.target.result; const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlContent, 'text/xml'); const cells = xmlDoc.querySelectorAll('mxCell'); const tempNodes = []; const tempEdges = []; cells.forEach(cell => { const id = cell.getAttribute('id'); if (id === '0' || id === '1') return; // skip root const value = cell.getAttribute('value') || ''; const style = cell.getAttribute('style') || ''; const vertex = cell.getAttribute('vertex'); const edge = cell.getAttribute('edge'); const geometry = cell.querySelector('mxGeometry'); if (!geometry) return; const x = parseFloat(geometry.getAttribute('x')) || 0; const y = parseFloat(geometry.getAttribute('y')) || 0; const width = parseFloat(geometry.getAttribute('width')) || 120; const height = parseFloat(geometry.getAttribute('height')) || 80; if (vertex) { tempNodes.push({ oldId: id, value, style, x, y, width, height, parent: cell.getAttribute('parent') }); } else if (edge) { tempEdges.push({ oldId: id, value, style, source: cell.getAttribute('source'), target: cell.getAttribute('target') }); } }); const cellIdMap = new Map(); const importedNodes = []; tempNodes.forEach(n => { const newId = "imported-".concat(Date.now(), "-").concat(Math.random().toString(36).substr(2, 9)); cellIdMap.set(n.oldId, newId); let nodeType = 'component'; if (n.style.includes('swimlane')) nodeType = 'container';else if (n.style.includes('rhombus')) nodeType = 'diamond';else if (n.style.includes('ellipse')) nodeType = 'circle';else if (n.style.includes('hexagon')) nodeType = 'hexagon';else if (n.style.includes('triangle')) nodeType = 'triangle'; const fillColorMatch = n.style.match(/fillColor=([^;]+)/); const strokeColorMatch = n.style.match(/strokeColor=([^;]+)/); const fillColor = fillColorMatch ? fillColorMatch[1] : '#ffffff'; const strokeColor = strokeColorMatch ? strokeColorMatch[1] : '#000000'; const [firstLine, ...restLines] = n.value.split('\n'); importedNodes.push({ id: newId, type: nodeType, position: { x: n.x, y: n.y }, data: { label: firstLine, color: fillColor, borderColor: strokeColor, icon: nodeType === 'container' ? '📦' : '🔹', description: restLines.join('\n'), onLabelChange: handleNodeLabelChange }, style: { width: n.width, height: n.height, zIndex: nodeType === 'container' ? 1 : 10 }, draggable: true, selectable: true, zIndex: nodeType === 'container' ? 1 : 10, __parentOldId: n.parent }); }); importedNodes.forEach(node => { if (node.__parentOldId && cellIdMap.has(node.__parentOldId)) { node.parentNode = cellIdMap.get(node.__parentOldId); } delete node.__parentOldId; }); const importedEdges = tempEdges.map(e => { const strokeWidthMatch = e.style.match(/strokeWidth=([^;]+)/); const strokeColorMatch = e.style.match(/strokeColor=([^;]+)/); const strokeWidth = strokeWidthMatch ? parseInt(strokeWidthMatch[1]) : 2; const strokeColor = strokeColorMatch ? strokeColorMatch[1] : '#000000'; const isDashed = e.style.includes('dashed=1'); return { id: "imported-edge-".concat(Date.now(), "-").concat(Math.random().toString(36).substr(2, 9)), source: cellIdMap.get(e.source) || e.source, target: cellIdMap.get(e.target) || e.target, label: e.value, type: 'smoothstep', animated: false, style: { strokeWidth, stroke: strokeColor, strokeDasharray: isDashed ? '5,5' : undefined, zIndex: 5 }, zIndex: 5, data: { label: e.value, description: '' } }; }); const nodeMap = Object.fromEntries(importedNodes.map(n => [n.id, n])); const filteredEdges = []; importedEdges.forEach(edge => { const s = nodeMap[edge.source]; const t = nodeMap[edge.target]; if (s && t && s.type === 'container' && t.type === 'container') { t.parentNode = s.id; t.position = { x: t.position.x - s.position.x, y: t.position.y - s.position.y }; } else { filteredEdges.push(edge); } }); setNodes(importedNodes); setEdges(filteredEdges); saveToHistory(); } catch (error) { console.error('Error importing draw.io XML:', error); alert('Error importing draw.io file. Please check the file format.'); } }; reader.readAsText(file); } }; inputElement.click(); }, [handleNodeLabelChange, saveToHistory]); // Convert to draw.io XML format const exportToDrawioXML = (0, _react.useCallback)(() => { // Map node types to draw.io shapes const getDrawioShape = nodeType => { switch (nodeType) { case 'container': return 'swimlane;'; case 'component': return 'rounded=1;whiteSpace=wrap;html=1;'; case 'diamond': return 'rhombus;whiteSpace=wrap;html=1;'; case 'circle': return 'ellipse;whiteSpace=wrap;html=1;'; case 'hexagon': return 'hexagon;whiteSpace=wrap;html=1;'; case 'triangle': return 'triangle;whiteSpace=wrap;html=1;'; default: return 'rounded=1;whiteSpace=wrap;html=1;'; } }; let xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<mxfile host=\"app.diagrams.net\" modified=\"".concat(new Date().toISOString(), "\" agent=\"Architecture Diagram Editor\" version=\"1.0\">\n <diagram name=\"Architecture Diagram\" id=\"diagram1\">\n <mxGraphModel dx=\"1422\" dy=\"794\" grid=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" page=\"1\" pageScale=\"1\" pageWidth=\"827\" pageHeight=\"1169\" math=\"0\" shadow=\"0\">\n <root>\n <mxCell id=\"0\"/>\n <mxCell id=\"1\" parent=\"0\"/>"); // Add nodes nodes.forEach(node => { var _node$style3, _node$style4; const shape = getDrawioShape(node.type); const valueText = [node.data.label || '', node.data.description || ''].filter(Boolean).join('\n'); xml += "\n <mxCell id=\"".concat(node.id, "\" value=\"").concat(valueText, "\" style=\"").concat(shape, "fillColor=").concat(node.data.color || '#ffffff', ";strokeColor=").concat(node.data.borderColor || '#000000', ";strokeWidth=2;\" vertex=\"1\" parent=\"").concat(node.parentNode || '1', "\">\n <mxGeometry x=\"").concat(node.position.x, "\" y=\"").concat(node.position.y, "\" width=\"").concat(((_node$style3 = node.style) === null || _node$style3 === void 0 ? void 0 : _node$style3.width) || 120, "\" height=\"").concat(((_node$style4 = node.style) === null || _node$style4 === void 0 ? void 0 : _node$style4.height) || 80, "\" as=\"geometry\"/>\n </mxCell>"); }); const allEdges = edges; // Add edges allEdges.forEach(edge => { var _edge$style3, _edge$style4, _edge$style5, _edge$data5; const strokeWidth = ((_edge$style3 = edge.style) === null || _edge$style3 === void 0 ? void 0 : _edge$style3.strokeWidth) || 2; const strokeColor = ((_edge$style4 = edge.style) === null || _edge$style4 === void 0 ? void 0 : _edge$style4.stroke) || '#000000'; const strokeDash = (_edge$style5 = edge.style) !== null && _edge$style5 !== void 0 && _edge$style5.strokeDasharray ? 'dashed=1;' : ''; xml += "\n <mxCell id=\"".concat(edge.id, "\" value=\"").concat(((_edge$data5 = edge.data) === null || _edge$data5 === void 0 ? void 0 : _edge$data5.label) || '', "\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=").concat(strokeWidth, ";strokeColor=").concat(strokeColor, ";").concat(strokeDash, "\" edge=\"1\" parent=\"1\" source=\"").concat(edge.source, "\" target=\"").concat(edge.target, "\">\n <mxGeometry relative=\"1\" as=\"geometry\"/>\n </mxCell>"); }); xml += "\n </root>\n </mxGraphModel>\n </diagram>\n</mxfile>"; const dataUri = 'data:application/xml;charset=utf-8,' + encodeURIComponent(xml); const linkElement = document.createElement('a'); linkElement.setAttribute('href', dataUri); linkElement.setAttribute('download', 'architecture-diagram.drawio'); linkElement.click(); }, [nodes, edges]); const importDiagram = (0, _react.useCallback)(() => { const inputElement = document.createElement('input'); inputElement.type = 'file'; inputElement.accept = '.json'; inputElement.onchange = event => { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = e => { try { const importedData = JSON.parse(e.target.result); const { nodes: importedNodes, edges: importedEdges } = jsonToReactFlow(importedData); applyAutoLayout(importedNodes); setEdges(importedEdges); saveToHistory(); } catch (error) { console.error('Error importing diagram:', error); alert('Error importing diagram. Please check the file format.'); } }; reader.readAsText(file); } }; inputElement.click(); }, [jsonToReactFlow, applyAutoLayout, saveToHistory]); const importDiagramObject = (0, _react.useCallback)(data => { if (!validateDiagram(data)) { alert('Invalid diagram JSON'); return; } const { nodes: importedNodes, edges: importedEdges } = jsonToReactFlow(data); applyAutoLayout(importedNodes); setEdges(importedEdges); saveToHistory(); }, [jsonToReactFlow, applyAutoLayout, saveToHistory]); const handleJsonPasteImport = (0, _react.useCallback)(data => { importDiagramObject(data); setJsonPasteModal({ isOpen: false, onConfirm: null }); }, [importDiagramObject]); const validateJson = (0, _react.useCallback)(data => { const valid = validateDiagram(data); const errors = valid ? [] : (validateDiagram.errors || []).map(e => "".concat(e.instancePath, " ").concat(e.message)); return { valid, errors }; }, []); // Clean up timeout on unmount (0, _react.useEffect)(() => { return () => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } }; }, []); const newDiagram = (0, _react.useCallback)(() => { showConfirmModal('New Diagram', 'Are you sure you want to create a new diagram? All unsaved changes will be lost.', () => { applyAutoLayout([]); setEdges([]); setSelectedElements({ nodes: [], edges: [] }); saveToHistory(); }); }, [applyAutoLayout, saveToHistory, showConfirmModal]); const saveDiagram = (0, _react.useCallback)(() => { exportJSON(); }, [exportJSON]); const saveAsDiagram = (0, _react.useCallback)(() => { exportJSON(); }, [exportJSON]); const openDiagram = (0, _react.useCallback)(() => { importDiagram(); }, [importDiagram]); // Image export helpers const exportImage = (0, _react.useCallback)(async type => { if (!reactFlowWrapper.current) return; const renderer = reactFlowWrapper.current.querySelector('.react-flow__renderer') || reactFlowWrapper.current; const isDark = !!reactFlowWrapper.current.closest('.dark'); const bounds = getDiagramBounds(); const margin = 20; const width = bounds.width + margin * 2; const height = bounds.height + margin * 2; const common = { cacheBust: true, backgroundColor: isDark ? '#111111' : '#ffffff', width, height, style: { width: "".concat(width, "px"), height: "".concat(height, "px"), transform: "translate(".concat(-bounds.x + margin, "px, ").concat(-bounds.y + margin, "px)") } }; let dataUrl; if (type === 'png') dataUrl = await (0, _htmlToImage.toPng)(renderer, common); if (type === 'jpg') dataUrl = await (0, _htmlToImage.toJpeg)(renderer, _objectSpread(_objectSpread({}, common), {}, { quality: 0.95 })); if (type === 'svg') dataUrl = await (0, _htmlToImage.toSvg)(renderer, common); const link = document.createElement('a'); link.download = "diagram.".concat(type); link.href = dataUrl; link.click(); }, [getDiagramBounds]); const exportAsPNG = (0, _react.useCallback)(() => exportImage("png"), [exportImage]); const exportAsJPG = (0, _react.useCallback)(() => exportImage('jpg'), [exportImage]); const exportAsSVG = (0, _react.useCallback)(() => exportImage('svg'), [exportImage]); const autoLayout = (0, _react.useCallback)(() => { applyAutoLayout(nodes); saveToHistory(); }, [nodes, saveToHistory, applyAutoLayout]); // Enhanced selection operations const selectAllElements = (0, _react.useCallback)(() => { setSelectedElements({ nodes: nodes, edges: edges }); }, [nodes, edges]); const deselectAllElements = (0, _react.useCallback)(() => { setSelectedElements({ nodes: [], edges: [] }); }, []); const cutSelected = (0, _react.useCallback)(() => { if (selectedElements.nodes.length > 0 || selectedElements.edges.length > 0) { copySelected(); deleteSelected(); } }, [selectedElements, copySelected, deleteSelected]); const linkSelectedNodes = (0, _react.useCallback)(() => { if (selectedElements.nodes.length < 2) { return; } const containers = selectedElements.nodes.filter(n => n.type === 'container'); let parent = containers[0] || selectedElements.nodes[0]; const children = selectedElements.nodes.filter(n => n.id !== parent.id); setN