{"version":3,"sources":["../../src/hooks/useNodeSelection.ts","../../src/core/selection-store.ts","../../src/utils/debug.ts","../../src/hooks/useNodeDrag.ts","../../src/core/graph-store.ts","../../src/core/graph-position.ts","../../src/utils/mutation-queue.ts","../../src/core/perf.ts","../../src/core/graph-mutations.ts","../../src/core/graph-derived.ts","../../src/core/viewport-store.ts","../../src/utils/layout.ts","../../src/core/group-store.ts","../../src/core/history-store.ts","../../src/core/history-actions.ts","../../src/core/graph-mutations-edges.ts","../../src/core/reduced-motion-store.ts","../../src/core/graph-mutations-advanced.ts","../../src/core/sync-store.ts","../../src/utils/gesture-configs.ts","../../src/core/input-store.ts","../../src/core/input-classifier.ts","../../src/utils/hit-test.ts","../../src/hooks/useDragStateMachine.ts","../../src/hooks/useNodeResize.ts","../../src/hooks/useCanvasHistory.ts","../../src/core/toast-store.ts","../../src/hooks/useCanvasSelection.ts","../../src/hooks/useCanvasViewport.ts","../../src/hooks/useCanvasDrag.ts","../../src/hooks/useLayout.ts","../../src/hooks/useForceLayout.ts","../../src/hooks/useCanvasSettings.ts","../../src/core/settings-store.ts","../../src/core/event-types.ts","../../src/core/action-types.ts","../../src/core/settings-state-types.ts","../../src/core/settings-presets.ts","../../src/hooks/useActionExecutor.ts","../../src/core/actions-node.ts","../../src/core/actions-viewport.ts","../../src/core/built-in-actions.ts","../../src/core/action-registry.ts","../../src/core/locked-node-store.ts","../../src/core/action-executor.ts","../../src/hooks/useGestureResolver.ts","../../src/core/gesture-rules-defaults.ts","../../src/core/gesture-rules.ts","../../src/core/gesture-rule-store.ts","../../src/hooks/useCommandLine.ts","../../src/commands/store.ts","../../src/commands/registry.ts","../../src/commands/store-atoms.ts","../../src/hooks/useVirtualization.ts","../../src/core/virtualization-store.ts","../../src/core/spatial-index.ts","../../src/hooks/useTapGesture.ts","../../src/hooks/useArrowKeyNavigation.ts","../../src/hooks/useCanvasGraph.ts","../../src/hooks/useZoomTransition.ts","../../src/hooks/useSplitGesture.ts","../../src/hooks/useAnimatedLayout.ts","../../src/hooks/useTreeLayout.ts","../../src/hooks/useGridLayout.ts","../../src/hooks/usePlugin.ts","../../src/core/plugin-types.ts","../../src/core/node-type-registry.tsx","../../src/gestures/types.ts","../../src/gestures/dispatcher.ts","../../src/utils/edge-path-registry.ts","../../src/core/plugin-registry.ts"],"sourcesContent":["import { c as _c } from \"react/compiler-runtime\";\n/**\n * Hook for managing node selection state\n */\n\nimport { useAtom, useSetAtom } from 'jotai';\nimport { selectedNodeIdsAtom, selectSingleNodeAtom, toggleNodeInSelectionAtom } from '../core/selection-store';\n\n/**\n * Hook to manage selection for a specific node\n *\n * @param nodeId - The ID of the node\n * @returns Selection state and actions for the node\n */\nexport function useNodeSelection(nodeId) {\n const $ = _c(13);\n const [selectedIds] = useAtom(selectedNodeIdsAtom);\n const selectSingle = useSetAtom(selectSingleNodeAtom);\n const toggleNode = useSetAtom(toggleNodeInSelectionAtom);\n let t0;\n if ($[0] !== nodeId || $[1] !== selectedIds) {\n t0 = selectedIds.has(nodeId);\n $[0] = nodeId;\n $[1] = selectedIds;\n $[2] = t0;\n } else {\n t0 = $[2];\n }\n let t1;\n if ($[3] !== nodeId || $[4] !== selectSingle) {\n t1 = () => selectSingle(nodeId);\n $[3] = nodeId;\n $[4] = selectSingle;\n $[5] = t1;\n } else {\n t1 = $[5];\n }\n let t2;\n if ($[6] !== nodeId || $[7] !== toggleNode) {\n t2 = () => toggleNode(nodeId);\n $[6] = nodeId;\n $[7] = toggleNode;\n $[8] = t2;\n } else {\n t2 = $[8];\n }\n let t3;\n if ($[9] !== t0 || $[10] !== t1 || $[11] !== t2) {\n t3 = {\n isSelected: t0,\n selectNode: t1,\n toggleNode: t2\n };\n $[9] = t0;\n $[10] = t1;\n $[11] = t2;\n $[12] = t3;\n } else {\n t3 = $[12];\n }\n return t3;\n}","/**\n * Selection state management\n *\n * Manages node and edge selection state.\n */\n\nimport { atom } from 'jotai';\nimport { createDebug } from '../utils/debug';\nconst debug = createDebug('selection');\n\n// --- Core Selection Atoms ---\n\n/**\n * Set of currently selected node IDs\n */\nexport const selectedNodeIdsAtom = atom(new Set());\n\n/**\n * Currently selected edge ID (only one edge can be selected at a time)\n */\nexport const selectedEdgeIdAtom = atom(null);\n\n// --- Selection Actions ---\n\n/**\n * Handle node pointer down with shift-click support for multi-select\n */\n\nexport const handleNodePointerDownSelectionAtom = atom(null, (get, set, {\n nodeId,\n isShiftPressed\n}) => {\n const currentSelection = get(selectedNodeIdsAtom);\n debug('handleNodePointerDownSelection: nodeId=%s, shift=%s, current=%o', nodeId, isShiftPressed, Array.from(currentSelection));\n\n // Clear edge selection when selecting a node\n set(selectedEdgeIdAtom, null);\n if (isShiftPressed) {\n const newSelection = new Set(currentSelection);\n if (newSelection.has(nodeId)) {\n newSelection.delete(nodeId);\n } else {\n newSelection.add(nodeId);\n }\n debug('Shift-click, setting selection to: %o', Array.from(newSelection));\n set(selectedNodeIdsAtom, newSelection);\n } else {\n // Only change selection if node is NOT already selected\n // This allows multi-node drag to work\n if (!currentSelection.has(nodeId)) {\n debug('Node not in selection, selecting: %s', nodeId);\n set(selectedNodeIdsAtom, new Set([nodeId]));\n } else {\n debug('Node already selected, preserving multi-select');\n }\n }\n});\n\n/**\n * Select a single node, clearing any previous selection\n */\nexport const selectSingleNodeAtom = atom(null, (get, set, nodeId) => {\n debug('selectSingleNode: %s', nodeId);\n set(selectedEdgeIdAtom, null);\n if (nodeId === null || nodeId === undefined) {\n debug('Clearing selection');\n set(selectedNodeIdsAtom, new Set());\n } else {\n const currentSelection = get(selectedNodeIdsAtom);\n if (currentSelection.has(nodeId) && currentSelection.size === 1) {\n return; // Already the only selection\n }\n set(selectedNodeIdsAtom, new Set([nodeId]));\n }\n});\n\n/**\n * Toggle a node in/out of selection\n */\nexport const toggleNodeInSelectionAtom = atom(null, (get, set, nodeId) => {\n const currentSelection = get(selectedNodeIdsAtom);\n const newSelection = new Set(currentSelection);\n if (newSelection.has(nodeId)) {\n newSelection.delete(nodeId);\n } else {\n newSelection.add(nodeId);\n }\n set(selectedNodeIdsAtom, newSelection);\n});\n\n/**\n * Clear all node selection\n */\nexport const clearSelectionAtom = atom(null, (_get, set) => {\n debug('clearSelection');\n set(selectedNodeIdsAtom, new Set());\n});\n\n/**\n * Add multiple nodes to selection\n */\nexport const addNodesToSelectionAtom = atom(null, (get, set, nodeIds) => {\n const currentSelection = get(selectedNodeIdsAtom);\n const newSelection = new Set(currentSelection);\n for (const nodeId of nodeIds) {\n newSelection.add(nodeId);\n }\n set(selectedNodeIdsAtom, newSelection);\n});\n\n/**\n * Remove multiple nodes from selection\n */\nexport const removeNodesFromSelectionAtom = atom(null, (get, set, nodeIds) => {\n const currentSelection = get(selectedNodeIdsAtom);\n const newSelection = new Set(currentSelection);\n for (const nodeId of nodeIds) {\n newSelection.delete(nodeId);\n }\n set(selectedNodeIdsAtom, newSelection);\n});\n\n/**\n * Select an edge (clears node selection)\n */\nexport const selectEdgeAtom = atom(null, (get, set, edgeId) => {\n set(selectedEdgeIdAtom, edgeId);\n if (edgeId !== null) {\n set(selectedNodeIdsAtom, new Set());\n }\n});\n\n/**\n * Clear edge selection\n */\nexport const clearEdgeSelectionAtom = atom(null, (_get, set) => {\n set(selectedEdgeIdAtom, null);\n});\n\n// --- Keyboard Focus ---\n\n/**\n * The node that has keyboard focus (distinct from selection).\n * Used for arrow key navigation between nodes.\n * null means no node has keyboard focus.\n */\nexport const focusedNodeIdAtom = atom(null);\n\n/**\n * Set keyboard focus to a node, or clear focus with null\n */\nexport const setFocusedNodeAtom = atom(null, (_get, set, nodeId) => {\n set(focusedNodeIdAtom, nodeId);\n});\n\n/**\n * Whether a node currently has keyboard focus\n */\nexport const hasFocusedNodeAtom = atom(get => get(focusedNodeIdAtom) !== null);\n\n// --- Derived Atoms ---\n\n/**\n * Count of selected nodes\n */\nexport const selectedNodesCountAtom = atom(get => get(selectedNodeIdsAtom).size);\n\n/**\n * Whether any node is selected\n */\nexport const hasSelectionAtom = atom(get => get(selectedNodeIdsAtom).size > 0);","/**\n * Debug utility for @blinksgg/canvas\n *\n * Uses the `debug` package with `canvas:*` namespaces.\n * Enable in browser: `localStorage.debug = 'canvas:*'`\n * Enable in Node: `DEBUG=canvas:* node ...`\n *\n * Log levels:\n * debug('message') — verbose trace (blue)\n * debug.warn('message') — warnings (yellow, always stderr)\n * debug.error('message') — errors (red, always stderr)\n */\n\nimport debugFactory from 'debug';\nconst NAMESPACE = 'canvas';\n/**\n * Create a debug logger for a specific module.\n *\n * @example\n * ```ts\n * const debug = createDebug('graph');\n * debug('loaded %d nodes', count); // canvas:graph\n * debug.warn('node %s not found', id); // canvas:graph:warn\n * debug.error('sync failed: %O', err); // canvas:graph:error\n * ```\n */\nexport function createDebug(module) {\n const base = debugFactory(`${NAMESPACE}:${module}`);\n const warn = debugFactory(`${NAMESPACE}:${module}:warn`);\n const error = debugFactory(`${NAMESPACE}:${module}:error`);\n\n // Warnings and errors always log (even without DEBUG=canvas:*)\n warn.enabled = true;\n error.enabled = true;\n\n // Color hints: warn = yellow, error = red\n warn.log = console.warn.bind(console);\n error.log = console.error.bind(console);\n\n // Build the debugger with warn/error sub-loggers\n const debugFn = Object.assign(base, {\n warn,\n error\n });\n return debugFn;\n}\n\n// Pre-configured debug loggers\nexport const debug = {\n graph: {\n node: createDebug('graph:node'),\n edge: createDebug('graph:edge'),\n sync: createDebug('graph:sync')\n },\n ui: {\n selection: createDebug('ui:selection'),\n drag: createDebug('ui:drag'),\n resize: createDebug('ui:resize')\n },\n sync: {\n status: createDebug('sync:status'),\n mutations: createDebug('sync:mutations'),\n queue: createDebug('sync:queue')\n },\n viewport: createDebug('viewport')\n};","import { c as _c } from \"react/compiler-runtime\";\n/**\n * Hook for managing node drag behavior\n *\n * Pure drag mechanics — always moves nodes. Gesture resolution\n * (which button/source/modifier maps to which action) is handled\n * by the v2 gesture pipeline in useNodeGestures / useCanvasGestures.\n *\n * Type definitions are in ./drag-types.ts\n */\n\nimport { useAtom, useAtomValue, useSetAtom, atom } from 'jotai';\nimport { useGesture } from '@use-gesture/react';\nimport { useRef } from 'react';\nimport { graphAtom, preDragNodeAttributesAtom, edgeCreationAtom, currentGraphIdAtom } from '../core/graph-store';\nimport { nodePositionUpdateCounterAtom } from '../core/graph-position';\nimport { startNodeDragAtom, endNodeDragAtom, dropTargetNodeIdAtom } from '../core/graph-mutations';\nimport { panAtom, zoomAtom } from '../core/viewport-store';\nimport { selectedNodeIdsAtom } from '../core/selection-store';\nimport { startMutationAtom, completeMutationAtom } from '../core/sync-store';\nimport { pushHistoryAtom } from '../core/history-store';\nimport { nestNodesOnDropAtom } from '../core/group-store';\nimport { getNodeGestureConfig } from '../utils/gesture-configs';\nimport { primaryInputSourceAtom } from '../core/input-store';\nimport { getPendingState } from '../utils/mutation-queue';\nimport { createDebug } from '../utils/debug';\nimport { hitTestNode } from '../utils/hit-test';\nimport { buildDragPositions, computeDragUpdates, isDragPrevented } from './useDragStateMachine';\n\n// Re-export extracted modules\nexport { buildDragPositions, computeDragUpdates, isDragPrevented } from './useDragStateMachine';\nexport { snapToGrid, clampToBounds, applyDragConstraints } from './useDragConstraints';\n\n// Re-export types for backward compat\n\nconst debug = createDebug('drag');\n\n/**\n * Hook for node drag behavior with multi-select support.\n * Always treats drag as move-node — no gesture resolution.\n */\nexport function useNodeDrag(id, t0) {\n const $ = _c(49);\n let t1;\n if ($[0] !== t0) {\n t1 = t0 === undefined ? {} : t0;\n $[0] = t0;\n $[1] = t1;\n } else {\n t1 = $[1];\n }\n const options = t1;\n const {\n onPersist,\n onPersistError,\n heldKeys\n } = options;\n const graph = useAtomValue(graphAtom);\n const [pan, setPan] = useAtom(panAtom);\n const startMutation = useSetAtom(startMutationAtom);\n const completeMutation = useSetAtom(completeMutationAtom);\n const setStartDrag = useSetAtom(startNodeDragAtom);\n const setEndDrag = useSetAtom(endNodeDragAtom);\n const getPreDragAttributes = useAtomValue(preDragNodeAttributesAtom);\n const currentZoom = useAtomValue(zoomAtom);\n const getSelectedNodeIds = useAtomValue(selectedNodeIdsAtom);\n const currentGraphId = useAtomValue(currentGraphIdAtom);\n const edgeCreation = useAtomValue(edgeCreationAtom);\n const setGraph = useSetAtom(graphAtom);\n useSetAtom(nodePositionUpdateCounterAtom);\n const pushHistory = useSetAtom(pushHistoryAtom);\n const setDropTarget = useSetAtom(dropTargetNodeIdAtom);\n const nestNodesOnDrop = useSetAtom(nestNodesOnDropAtom);\n const inputSource = useAtomValue(primaryInputSourceAtom);\n let t2;\n if ($[2] === Symbol.for(\"react.memo_cache_sentinel\")) {\n t2 = atom(null, _temp2);\n $[2] = t2;\n } else {\n t2 = $[2];\n }\n const batchUpdatePositions = useSetAtom(t2);\n let t3;\n if ($[3] !== batchUpdatePositions) {\n t3 = updates_0 => {\n batchUpdatePositions(updates_0);\n };\n $[3] = batchUpdatePositions;\n $[4] = t3;\n } else {\n t3 = $[4];\n }\n const updateNodePositions = t3;\n const gestureInstanceRef = useRef(0);\n let t4;\n if ($[5] === Symbol.for(\"react.memo_cache_sentinel\")) {\n t4 = {\n x: 0,\n y: 0\n };\n $[5] = t4;\n } else {\n t4 = $[5];\n }\n const panStartRef = useRef(t4);\n const isSpaceHeld = Boolean(heldKeys?.byCode.Space || heldKeys?.byKey[\" \"] || heldKeys?.byKey.Spacebar);\n let t5;\n if ($[6] !== isSpaceHeld || $[7] !== pan) {\n t5 = state => {\n if (isDragPrevented(state.event.target)) {\n return;\n }\n gestureInstanceRef.current = gestureInstanceRef.current + 1;\n if (isSpaceHeld) {\n panStartRef.current = pan;\n }\n };\n $[6] = isSpaceHeld;\n $[7] = pan;\n $[8] = t5;\n } else {\n t5 = $[8];\n }\n let t6;\n if ($[9] !== currentZoom || $[10] !== edgeCreation || $[11] !== getSelectedNodeIds || $[12] !== graph || $[13] !== id || $[14] !== isSpaceHeld || $[15] !== pushHistory || $[16] !== setDropTarget || $[17] !== setPan || $[18] !== setStartDrag || $[19] !== startMutation || $[20] !== updateNodePositions) {\n t6 = state_0 => {\n if (isDragPrevented(state_0.event.target)) {\n return;\n }\n if (edgeCreation.isCreating) {\n return;\n }\n state_0.event.stopPropagation();\n if (isSpaceHeld) {\n if (!state_0.tap && state_0.active) {\n const [mx, my] = state_0.movement;\n setPan({\n x: panStartRef.current.x + mx,\n y: panStartRef.current.y + my\n });\n }\n return state_0.memo;\n }\n const {\n movement: t7,\n first,\n active,\n down,\n pinching,\n cancel,\n tap\n } = state_0;\n const [mx_0, my_0] = t7;\n let currentMemo = state_0.memo;\n if (tap || !active) {\n return currentMemo;\n }\n const currentGestureInstance = gestureInstanceRef.current;\n if (first) {\n const selectionSize = getSelectedNodeIds.size;\n const label = selectionSize > 1 ? `Move ${selectionSize} nodes` : \"Move node\";\n pushHistory(label);\n setStartDrag({\n nodeId: id\n });\n const initialPositions = buildDragPositions(graph, getSelectedNodeIds);\n initialPositions.forEach(() => startMutation());\n currentMemo = {\n initialPositions,\n gestureInstance: currentGestureInstance\n };\n }\n if (!currentMemo || currentMemo.gestureInstance !== currentGestureInstance || !currentMemo.initialPositions) {\n if (cancel && !pinching && !down && !tap && !active) {\n cancel();\n }\n return currentMemo;\n }\n const updates_1 = computeDragUpdates(currentMemo.initialPositions, mx_0, my_0, currentZoom, graph);\n if (updates_1.length > 0) {\n updateNodePositions(updates_1);\n }\n if (state_0.event && \"clientX\" in state_0.event) {\n const {\n nodeId: hoveredId\n } = hitTestNode(state_0.event.clientX, state_0.event.clientY);\n const validTarget = hoveredId && !currentMemo.initialPositions.has(hoveredId) ? hoveredId : null;\n setDropTarget(validTarget);\n }\n return currentMemo;\n };\n $[9] = currentZoom;\n $[10] = edgeCreation;\n $[11] = getSelectedNodeIds;\n $[12] = graph;\n $[13] = id;\n $[14] = isSpaceHeld;\n $[15] = pushHistory;\n $[16] = setDropTarget;\n $[17] = setPan;\n $[18] = setStartDrag;\n $[19] = startMutation;\n $[20] = updateNodePositions;\n $[21] = t6;\n } else {\n t6 = $[21];\n }\n let t7;\n if ($[22] !== completeMutation || $[23] !== currentGraphId || $[24] !== currentZoom || $[25] !== edgeCreation || $[26] !== getPreDragAttributes || $[27] !== getSelectedNodeIds || $[28] !== graph || $[29] !== id || $[30] !== isSpaceHeld || $[31] !== nestNodesOnDrop || $[32] !== onPersist || $[33] !== onPersistError || $[34] !== setDropTarget || $[35] !== setEndDrag || $[36] !== setGraph || $[37] !== startMutation || $[38] !== updateNodePositions) {\n t7 = state_1 => {\n if (isDragPrevented(state_1.event.target)) {\n return;\n }\n if (edgeCreation.isCreating) {\n setEndDrag({\n nodeId: id\n });\n return;\n }\n if (isSpaceHeld) {\n return;\n }\n state_1.event.stopPropagation();\n const memo = state_1.memo;\n setDropTarget(null);\n if (state_1.event && \"clientX\" in state_1.event && memo?.initialPositions) {\n const {\n nodeId: hoveredId_0\n } = hitTestNode(state_1.event.clientX, state_1.event.clientY);\n if (hoveredId_0 && !memo.initialPositions.has(hoveredId_0)) {\n const draggedNodeIds = Array.from(memo.initialPositions.keys()).filter(nid => getSelectedNodeIds.has(nid));\n if (draggedNodeIds.length > 0) {\n nestNodesOnDrop({\n nodeIds: draggedNodeIds,\n targetId: hoveredId_0\n });\n }\n }\n }\n if (!currentGraphId) {\n debug(\"Cannot update node position: currentGraphId is not set\");\n setEndDrag({\n nodeId: id\n });\n return;\n }\n const nodesToUpdate = memo?.initialPositions ? Array.from(memo.initialPositions.keys()) : [id];\n nodesToUpdate.forEach(draggedNodeId => {\n if (!graph.hasNode(draggedNodeId)) {\n completeMutation(false);\n return;\n }\n const finalAttrs = graph.getNodeAttributes(draggedNodeId);\n const initialPos = memo?.initialPositions.get(draggedNodeId);\n if (!initialPos) {\n completeMutation(false);\n return;\n }\n const [mx_1, my_1] = state_1.movement;\n const deltaX = mx_1 / currentZoom;\n const deltaY = my_1 / currentZoom;\n const finalPosition = {\n x: initialPos.x + deltaX,\n y: initialPos.y + deltaY\n };\n updateNodePositions([{\n id: draggedNodeId,\n pos: finalPosition\n }]);\n if (!onPersist) {\n completeMutation(true);\n setEndDrag({\n nodeId: draggedNodeId\n });\n return;\n }\n const existingDbUiProps = typeof finalAttrs.dbData.ui_properties === \"object\" && finalAttrs.dbData.ui_properties !== null && !Array.isArray(finalAttrs.dbData.ui_properties) ? finalAttrs.dbData.ui_properties : {};\n const newUiProperties = {\n ...existingDbUiProps,\n x: finalPosition.x,\n y: finalPosition.y,\n zIndex: finalAttrs.zIndex\n };\n const pendingState = getPendingState(draggedNodeId);\n if (pendingState.inFlight) {\n pendingState.queuedPosition = finalPosition;\n pendingState.queuedUiProperties = newUiProperties;\n pendingState.graphId = currentGraphId;\n return;\n }\n pendingState.inFlight = true;\n pendingState.graphId = currentGraphId;\n const processQueuedUpdate = async nodeId => {\n const state_2 = getPendingState(nodeId);\n if (state_2 && state_2.queuedPosition && state_2.queuedUiProperties && state_2.graphId) {\n const queuedProps = state_2.queuedUiProperties;\n const queuedGraphId = state_2.graphId;\n state_2.queuedPosition = null;\n state_2.queuedUiProperties = null;\n startMutation();\n ;\n try {\n await onPersist(nodeId, queuedGraphId, queuedProps);\n completeMutation(true);\n } catch (t8) {\n const error = t8;\n completeMutation(false);\n onPersistError?.(nodeId, error);\n }\n state_2.inFlight = false;\n processQueuedUpdate(nodeId);\n } else {\n if (state_2) {\n state_2.inFlight = false;\n }\n }\n };\n onPersist(draggedNodeId, currentGraphId, newUiProperties).then(() => {\n completeMutation(true);\n processQueuedUpdate(draggedNodeId);\n }).catch(error_0 => {\n completeMutation(false);\n const state_3 = getPendingState(draggedNodeId);\n if (state_3) {\n state_3.inFlight = false;\n }\n const preDragAttrsForNode = getPreDragAttributes;\n if (preDragAttrsForNode && preDragAttrsForNode.dbData.id === draggedNodeId && graph.hasNode(draggedNodeId)) {\n graph.replaceNodeAttributes(draggedNodeId, preDragAttrsForNode);\n setGraph(graph);\n }\n onPersistError?.(draggedNodeId, error_0);\n processQueuedUpdate(draggedNodeId);\n }).finally(() => {\n setEndDrag({\n nodeId: draggedNodeId\n });\n });\n });\n };\n $[22] = completeMutation;\n $[23] = currentGraphId;\n $[24] = currentZoom;\n $[25] = edgeCreation;\n $[26] = getPreDragAttributes;\n $[27] = getSelectedNodeIds;\n $[28] = graph;\n $[29] = id;\n $[30] = isSpaceHeld;\n $[31] = nestNodesOnDrop;\n $[32] = onPersist;\n $[33] = onPersistError;\n $[34] = setDropTarget;\n $[35] = setEndDrag;\n $[36] = setGraph;\n $[37] = startMutation;\n $[38] = updateNodePositions;\n $[39] = t7;\n } else {\n t7 = $[39];\n }\n let t8;\n if ($[40] !== t5 || $[41] !== t6 || $[42] !== t7) {\n t8 = {\n onPointerDown: t5,\n onDrag: t6,\n onDragEnd: t7\n };\n $[40] = t5;\n $[41] = t6;\n $[42] = t7;\n $[43] = t8;\n } else {\n t8 = $[43];\n }\n let t9;\n if ($[44] !== inputSource) {\n t9 = getNodeGestureConfig(inputSource);\n $[44] = inputSource;\n $[45] = t9;\n } else {\n t9 = $[45];\n }\n const bind = useGesture(t8, t9);\n let t10;\n if ($[46] !== bind || $[47] !== updateNodePositions) {\n t10 = {\n bind,\n updateNodePositions\n };\n $[46] = bind;\n $[47] = updateNodePositions;\n $[48] = t10;\n } else {\n t10 = $[48];\n }\n return t10;\n}\nfunction _temp2(get, set, updates) {\n const graph_0 = get(graphAtom);\n updates.forEach(u => {\n if (graph_0.hasNode(u.id)) {\n graph_0.setNodeAttribute(u.id, \"x\", u.pos.x);\n graph_0.setNodeAttribute(u.id, \"y\", u.pos.y);\n }\n });\n set(nodePositionUpdateCounterAtom, _temp);\n}\nfunction _temp(c) {\n return c + 1;\n}","/**\n * Graph state management — core atoms\n *\n * Foundation atoms for the Graphology + Jotai graph system.\n * Position, derived, and mutation atoms live in their own modules.\n *\n * @see ./graph-position.ts — position atoms\n * @see ./graph-derived.ts — read-only UI derivation atoms\n * @see ./graph-mutations.ts — write atoms (CRUD, split/merge, DB sync)\n */\n\nimport { atom } from 'jotai';\nimport Graph from 'graphology';\n// --- Graph Configuration ---\n\nexport const graphOptions = {\n type: 'directed',\n multi: true,\n allowSelfLoops: true\n};\n\n// --- Core Graph Atoms ---\n\n/**\n * Currently active graph ID\n */\nexport const currentGraphIdAtom = atom(null);\n\n/**\n * Main graph instance\n */\nexport const graphAtom = atom(new Graph(graphOptions));\n\n/**\n * Version counter to trigger re-renders\n */\nexport const graphUpdateVersionAtom = atom(0);\n\n// --- Edge Creation State ---\n\nexport const edgeCreationAtom = atom({\n isCreating: false,\n sourceNodeId: null,\n sourceNodePosition: null,\n targetPosition: null,\n hoveredTargetNodeId: null,\n sourceHandle: null,\n targetHandle: null,\n sourcePort: null,\n targetPort: null,\n snappedTargetPosition: null\n});\n\n// --- Drag State ---\n\nexport const draggingNodeIdAtom = atom(null);\nexport const preDragNodeAttributesAtom = atom(null);","/**\n * Graph position management\n *\n * Per-node position atoms derived from Graphology.\n * Structural equality caching prevents unnecessary re-renders.\n */\n\nimport { atom } from 'jotai';\nimport { atomFamily } from 'jotai-family';\nimport Graph from 'graphology';\nimport { graphAtom, graphUpdateVersionAtom, graphOptions } from './graph-store';\nimport { createDebug } from '../utils/debug';\nimport { clearAllPendingMutations } from '../utils/mutation-queue';\nimport { canvasMark } from './perf';\nconst debug = createDebug('graph:position');\n\n// --- Position Cache ---\n\n/**\n * Cache for the previous position values, scoped per Graph instance via WeakMap.\n * Enables structural equality — only notify dependents when x/y actually changed.\n */\nconst _positionCacheByGraph = new WeakMap();\nfunction getPositionCache(graph) {\n let cache = _positionCacheByGraph.get(graph);\n if (!cache) {\n cache = new Map();\n _positionCacheByGraph.set(graph, cache);\n }\n return cache;\n}\n\n// --- Position Atoms ---\n\n/**\n * Counter to trigger edge re-renders when positions change\n */\nexport const nodePositionUpdateCounterAtom = atom(0);\n\n/**\n * Per-node position atoms - derived from Graphology (single source of truth)\n * These are read-only atoms that derive position from the graph.\n */\nexport const nodePositionAtomFamily = atomFamily(nodeId => atom(get => {\n get(nodePositionUpdateCounterAtom); // Invalidation trigger for fine-grained updates\n const graph = get(graphAtom);\n if (!graph.hasNode(nodeId)) {\n return {\n x: 0,\n y: 0\n };\n }\n const x = graph.getNodeAttribute(nodeId, 'x');\n const y = graph.getNodeAttribute(nodeId, 'y');\n\n // Structural equality: return the same reference if position unchanged\n const cache = getPositionCache(graph);\n const prev = cache.get(nodeId);\n if (prev && prev.x === x && prev.y === y) {\n return prev;\n }\n const pos = {\n x,\n y\n };\n cache.set(nodeId, pos);\n return pos;\n}));\n\n/**\n * Update a node's position\n */\nexport const updateNodePositionAtom = atom(null, (get, set, {\n nodeId,\n position\n}) => {\n const end = canvasMark('drag-frame');\n const graph = get(graphAtom);\n if (graph.hasNode(nodeId)) {\n debug('Updating node %s position to %o', nodeId, position);\n graph.setNodeAttribute(nodeId, 'x', position.x);\n graph.setNodeAttribute(nodeId, 'y', position.y);\n // Position atom is derived - no need to set it, just trigger re-read\n set(nodePositionUpdateCounterAtom, c => c + 1);\n }\n end();\n});\n\n/**\n * Cleanup position atom for a deleted node\n * Note: Position atoms are now derived, so cleanup is optional but helps memory\n */\nexport const cleanupNodePositionAtom = atom(null, (get, _set, nodeId) => {\n nodePositionAtomFamily.remove(nodeId);\n const graph = get(graphAtom);\n getPositionCache(graph).delete(nodeId);\n debug('Removed position atom for node: %s', nodeId);\n});\n\n/**\n * Cleanup all position atoms (used when switching graphs)\n * Note: Position atoms are now derived, so cleanup is optional but helps memory\n */\nexport const cleanupAllNodePositionsAtom = atom(null, (get, _set) => {\n const graph = get(graphAtom);\n const nodeIds = graph.nodes();\n nodeIds.forEach(nodeId => {\n nodePositionAtomFamily.remove(nodeId);\n });\n _positionCacheByGraph.delete(graph);\n debug('Removed %d position atoms', nodeIds.length);\n});\n\n/**\n * Clear graph when switching to a new graph\n */\nexport const clearGraphOnSwitchAtom = atom(null, (get, set) => {\n debug('Clearing graph for switch');\n set(cleanupAllNodePositionsAtom);\n clearAllPendingMutations();\n const emptyGraph = new Graph(graphOptions);\n set(graphAtom, emptyGraph);\n set(graphUpdateVersionAtom, v => v + 1);\n});","/**\n * Mutation queue for tracking pending node updates\n *\n * Prevents race conditions during rapid drag operations.\n */\n\n/**\n * Global map to track pending mutations per node\n */\nexport const pendingNodeMutations = new Map();\n\n/**\n * Get or create pending mutation state for a node\n */\nexport function getPendingState(nodeId) {\n let state = pendingNodeMutations.get(nodeId);\n if (!state) {\n state = {\n inFlight: false,\n queuedPosition: null,\n queuedUiProperties: null,\n graphId: null\n };\n pendingNodeMutations.set(nodeId, state);\n }\n return state;\n}\n\n/**\n * Clear pending state for a node\n */\nexport function clearPendingState(nodeId) {\n pendingNodeMutations.delete(nodeId);\n}\n\n/**\n * Clear all pending mutation state (used on graph switch)\n */\nexport function clearAllPendingMutations() {\n pendingNodeMutations.clear();\n}\n\n/**\n * Check if any node has pending mutations\n */\nexport function hasPendingMutations() {\n for (const state of pendingNodeMutations.values()) {\n if (state.inFlight || state.queuedPosition !== null || state.queuedUiProperties !== null) {\n return true;\n }\n }\n return false;\n}","/**\n * Performance Instrumentation\n *\n * Opt-in performance.mark / performance.measure wrappers for DevTools profiling.\n * All marks are prefixed with \"canvas:\" for easy filtering.\n *\n * Usage:\n * const end = canvasMark('drag-frame');\n * // ... work ...\n * end(); // calls performance.measure automatically\n */\n\nimport { atom } from 'jotai';\n\n/**\n * Whether performance instrumentation is enabled.\n * Off by default — set to true to see marks in DevTools Performance tab.\n */\nexport const perfEnabledAtom = atom(false);\n\n// Module-level flag mirrored from atom for zero-overhead checks in hot paths.\n// Updated by `setPerfEnabled`.\nlet _enabled = false;\n\n/**\n * Imperatively set perf instrumentation on/off.\n * Useful from devtools console: `window.__canvasPerf?.(true)`.\n */\nexport function setPerfEnabled(enabled) {\n _enabled = enabled;\n}\n\n// Expose on window for easy devtools access\nif (typeof window !== 'undefined') {\n window.__canvasPerf = setPerfEnabled;\n}\n\n/**\n * Start a performance mark. Returns a function that, when called,\n * creates a measure from the mark to the current time.\n *\n * When instrumentation is disabled, returns a no-op (zero overhead).\n */\nexport function canvasMark(name) {\n if (!_enabled) return _noop;\n const markName = `canvas:${name}`;\n try {\n performance.mark(markName);\n } catch {\n return _noop;\n }\n return () => {\n try {\n performance.measure(`canvas:${name}`, markName);\n } catch {\n // mark may have been cleared\n }\n };\n}\nfunction _noop() {}\n\n/**\n * Measure a synchronous function call.\n */\nexport function canvasWrap(name, fn) {\n const end = canvasMark(name);\n try {\n return fn();\n } finally {\n end();\n }\n}","/**\n * Graph mutations\n *\n * Write atoms for graph CRUD operations, drag lifecycle,\n * edge animations, split/merge, and DB sync.\n */\n\nimport { atom } from 'jotai';\nimport Graph from 'graphology';\nimport { graphAtom, graphUpdateVersionAtom, graphOptions, currentGraphIdAtom, draggingNodeIdAtom, preDragNodeAttributesAtom } from './graph-store';\nimport { nodePositionAtomFamily, nodePositionUpdateCounterAtom, cleanupNodePositionAtom } from './graph-position';\nimport { highestZIndexAtom } from './graph-derived';\nimport { autoResizeGroupAtom } from './group-store';\nimport { createDebug } from '../utils/debug';\nconst debug = createDebug('graph:mutations');\n\n// --- Node Action Atoms ---\n\n/**\n * Start dragging a node\n */\nexport const startNodeDragAtom = atom(null, (get, set, {\n nodeId\n}) => {\n const graph = get(graphAtom);\n if (!graph.hasNode(nodeId)) return;\n const currentAttributes = graph.getNodeAttributes(nodeId);\n set(preDragNodeAttributesAtom, JSON.parse(JSON.stringify(currentAttributes)));\n const currentHighestZIndex = get(highestZIndexAtom);\n const newZIndex = currentHighestZIndex + 1;\n graph.setNodeAttribute(nodeId, 'zIndex', newZIndex);\n set(draggingNodeIdAtom, nodeId);\n});\n\n/**\n * End dragging a node\n */\nexport const endNodeDragAtom = atom(null, (get, set, _payload) => {\n const currentDraggingId = get(draggingNodeIdAtom);\n if (currentDraggingId) {\n debug('Node %s drag ended', currentDraggingId);\n\n // Auto-resize parent group if the dragged node belongs to one\n const graph = get(graphAtom);\n if (graph.hasNode(currentDraggingId)) {\n const parentId = graph.getNodeAttribute(currentDraggingId, 'parentId');\n if (parentId) {\n set(autoResizeGroupAtom, parentId);\n }\n }\n }\n set(draggingNodeIdAtom, null);\n set(preDragNodeAttributesAtom, null);\n});\n\n/**\n * Optimistically delete a node from local graph\n */\nexport const optimisticDeleteNodeAtom = atom(null, (get, set, {\n nodeId\n}) => {\n const graph = get(graphAtom);\n if (graph.hasNode(nodeId)) {\n graph.dropNode(nodeId);\n set(cleanupNodePositionAtom, nodeId);\n set(graphAtom, graph.copy());\n debug('Optimistically deleted node %s', nodeId);\n }\n});\n\n/**\n * Optimistically delete an edge from local graph\n */\nexport const optimisticDeleteEdgeAtom = atom(null, (get, set, {\n edgeKey\n}) => {\n const graph = get(graphAtom);\n if (graph.hasEdge(edgeKey)) {\n graph.dropEdge(edgeKey);\n set(graphAtom, graph.copy());\n debug('Optimistically deleted edge %s', edgeKey);\n }\n});\n\n// --- Local Graph Mutations ---\n\n/**\n * Add a node directly to local graph\n */\nexport const addNodeToLocalGraphAtom = atom(null, (get, set, newNode) => {\n const graph = get(graphAtom);\n if (graph.hasNode(newNode.id)) {\n debug('Node %s already exists, skipping', newNode.id);\n return;\n }\n const uiProps = newNode.ui_properties || {};\n const attributes = {\n x: typeof uiProps.x === 'number' ? uiProps.x : Math.random() * 800,\n y: typeof uiProps.y === 'number' ? uiProps.y : Math.random() * 600,\n size: typeof uiProps.size === 'number' ? uiProps.size : 15,\n width: typeof uiProps.width === 'number' ? uiProps.width : 500,\n height: typeof uiProps.height === 'number' ? uiProps.height : 500,\n color: typeof uiProps.color === 'string' ? uiProps.color : '#ccc',\n label: newNode.label || newNode.node_type || newNode.id,\n zIndex: typeof uiProps.zIndex === 'number' ? uiProps.zIndex : 0,\n dbData: newNode\n };\n debug('Adding node %s to local graph at (%d, %d)', newNode.id, attributes.x, attributes.y);\n graph.addNode(newNode.id, attributes);\n // Position is derived from graph - just update graph and trigger re-render\n set(graphAtom, graph.copy());\n set(graphUpdateVersionAtom, v => v + 1);\n set(nodePositionUpdateCounterAtom, c => c + 1);\n});\n\n// Re-export all edge and advanced atoms for backward compat\nexport { swapEdgeAtomicAtom, departingEdgesAtom, EDGE_ANIMATION_DURATION, removeEdgeWithAnimationAtom, editingEdgeLabelAtom, updateEdgeLabelAtom } from './graph-mutations-edges';\nexport { dropTargetNodeIdAtom, splitNodeAtom, mergeNodesAtom } from './graph-mutations-advanced';\n\n// --- DB Sync Atom ---\n\n/**\n * Load/sync graph data from database\n * Merges DB data with local state, preserving positions during drag\n */\nexport const loadGraphFromDbAtom = atom(null, (get, set, fetchedNodes, fetchedEdges) => {\n debug('========== START SYNC ==========');\n debug('Fetched nodes: %d, edges: %d', fetchedNodes.length, fetchedEdges.length);\n const currentGraphId = get(currentGraphIdAtom);\n\n // Validate data belongs to current graph\n if (fetchedNodes.length > 0 && fetchedNodes[0].graph_id !== currentGraphId) {\n debug('Skipping sync - data belongs to different graph');\n return;\n }\n const existingGraph = get(graphAtom);\n const isDragging = get(draggingNodeIdAtom) !== null;\n if (isDragging) {\n debug('Skipping sync - drag in progress');\n return;\n }\n\n // Detect graph switch\n const existingNodeIds = new Set(existingGraph.nodes());\n const fetchedNodeIds = new Set(fetchedNodes.map(n => n.id));\n const hasAnyCommonNodes = Array.from(existingNodeIds).some(id => fetchedNodeIds.has(id));\n let graph;\n if (hasAnyCommonNodes && existingNodeIds.size > 0) {\n debug('Merging DB data into existing graph');\n graph = existingGraph.copy();\n } else {\n debug('Creating fresh graph (graph switch detected)');\n graph = new Graph(graphOptions);\n }\n const fetchedEdgeIds = new Set(fetchedEdges.map(e => e.id));\n\n // Remove deleted nodes\n if (hasAnyCommonNodes && existingNodeIds.size > 0) {\n graph.forEachNode(nodeId => {\n if (!fetchedNodeIds.has(nodeId)) {\n debug('Removing deleted node: %s', nodeId);\n graph.dropNode(nodeId);\n nodePositionAtomFamily.remove(nodeId);\n }\n });\n }\n\n // Merge/add nodes\n fetchedNodes.forEach(node => {\n const uiProps = node.ui_properties || {};\n const newX = typeof uiProps.x === 'number' ? uiProps.x : Math.random() * 800;\n const newY = typeof uiProps.y === 'number' ? uiProps.y : Math.random() * 600;\n if (graph.hasNode(node.id)) {\n const currentAttrs = graph.getNodeAttributes(node.id);\n const attributes = {\n x: newX,\n y: newY,\n size: typeof uiProps.size === 'number' ? uiProps.size : currentAttrs.size,\n width: typeof uiProps.width === 'number' ? uiProps.width : currentAttrs.width ?? 500,\n height: typeof uiProps.height === 'number' ? uiProps.height : currentAttrs.height ?? 500,\n color: typeof uiProps.color === 'string' ? uiProps.color : currentAttrs.color,\n label: node.label || node.node_type || node.id,\n zIndex: typeof uiProps.zIndex === 'number' ? uiProps.zIndex : currentAttrs.zIndex,\n dbData: node\n };\n graph.replaceNodeAttributes(node.id, attributes);\n } else {\n const attributes = {\n x: newX,\n y: newY,\n size: typeof uiProps.size === 'number' ? uiProps.size : 15,\n width: typeof uiProps.width === 'number' ? uiProps.width : 500,\n height: typeof uiProps.height === 'number' ? uiProps.height : 500,\n color: typeof uiProps.color === 'string' ? uiProps.color : '#ccc',\n label: node.label || node.node_type || node.id,\n zIndex: typeof uiProps.zIndex === 'number' ? uiProps.zIndex : 0,\n dbData: node\n };\n graph.addNode(node.id, attributes);\n }\n });\n\n // Remove deleted edges\n graph.forEachEdge(edgeId => {\n if (!fetchedEdgeIds.has(edgeId)) {\n debug('Removing deleted edge: %s', edgeId);\n graph.dropEdge(edgeId);\n }\n });\n\n // Merge/add edges\n fetchedEdges.forEach(edge => {\n if (graph.hasNode(edge.source_node_id) && graph.hasNode(edge.target_node_id)) {\n const uiProps = edge.ui_properties || {};\n const attributes = {\n type: typeof uiProps.style === 'string' ? uiProps.style : 'solid',\n color: typeof uiProps.color === 'string' ? uiProps.color : '#999',\n label: edge.edge_type ?? undefined,\n weight: typeof uiProps.weight === 'number' ? uiProps.weight : 1,\n dbData: edge\n };\n if (graph.hasEdge(edge.id)) {\n graph.replaceEdgeAttributes(edge.id, attributes);\n } else {\n try {\n graph.addEdgeWithKey(edge.id, edge.source_node_id, edge.target_node_id, attributes);\n } catch (e) {\n debug('Failed to add edge %s: %o', edge.id, e);\n }\n }\n }\n });\n set(graphAtom, graph);\n set(graphUpdateVersionAtom, v => v + 1);\n debug('========== SYNC COMPLETE ==========');\n debug('Final graph: %d nodes, %d edges', graph.order, graph.size);\n});","/**\n * Graph derived atoms\n *\n * Read-only UI atoms derived from the Graphology graph.\n * Provides node/edge state for rendering with structural equality caching.\n */\n\nimport { atom } from 'jotai';\nimport { atomFamily } from 'jotai-family';\nimport { graphAtom, graphUpdateVersionAtom, currentGraphIdAtom, edgeCreationAtom, draggingNodeIdAtom } from './graph-store';\nimport { nodePositionAtomFamily } from './graph-position';\nimport { panAtom, zoomAtom, viewportRectAtom } from './viewport-store';\nimport { collapsedGroupsAtom, collapsedEdgeRemapAtom } from './group-store';\n\n// --- Derived Node Atoms ---\n\n/**\n * Highest z-index among nodes\n */\nexport const highestZIndexAtom = atom(get => {\n get(graphUpdateVersionAtom);\n const graph = get(graphAtom);\n let maxZ = 0;\n graph.forEachNode((_node, attributes) => {\n if (attributes.zIndex > maxZ) {\n maxZ = attributes.zIndex;\n }\n });\n return maxZ;\n});\n\n/**\n * All UI nodes with current positions\n *\n * Uses structural equality: if every entry is reference-equal to the\n * previous result, the same array is returned to avoid downstream re-renders.\n */\nconst _prevUiNodesByGraph = new WeakMap();\nexport const uiNodesAtom = atom(get => {\n get(graphUpdateVersionAtom);\n const graph = get(graphAtom);\n const currentDraggingId = get(draggingNodeIdAtom);\n const collapsed = get(collapsedGroupsAtom);\n const nodes = [];\n graph.forEachNode((nodeId, attributes) => {\n // Skip nodes whose parent (or ancestor) is collapsed\n if (collapsed.size > 0) {\n let current = nodeId;\n let hidden = false;\n while (true) {\n if (!graph.hasNode(current)) break;\n const pid = graph.getNodeAttributes(current).parentId;\n if (!pid) break;\n if (collapsed.has(pid)) {\n hidden = true;\n break;\n }\n current = pid;\n }\n if (hidden) return;\n }\n const position = get(nodePositionAtomFamily(nodeId));\n nodes.push({\n ...attributes,\n id: nodeId,\n position,\n isDragging: nodeId === currentDraggingId\n });\n });\n\n // Structural equality: return previous array if length and entries match\n const prev = _prevUiNodesByGraph.get(graph) ?? [];\n if (nodes.length === prev.length && nodes.every((n, i) => n.id === prev[i].id && n.position === prev[i].position && n.isDragging === prev[i].isDragging)) {\n return prev;\n }\n _prevUiNodesByGraph.set(graph, nodes);\n return nodes;\n});\n\n/**\n * All node keys\n */\nexport const nodeKeysAtom = atom(get => {\n get(graphUpdateVersionAtom);\n const graph = get(graphAtom);\n return graph.nodes();\n});\n\n/**\n * Per-node UI state\n */\nexport const nodeFamilyAtom = atomFamily(nodeId => atom(get => {\n get(graphUpdateVersionAtom);\n const graph = get(graphAtom);\n if (!graph.hasNode(nodeId)) {\n return null;\n }\n const attributes = graph.getNodeAttributes(nodeId);\n const position = get(nodePositionAtomFamily(nodeId));\n const currentDraggingId = get(draggingNodeIdAtom);\n return {\n ...attributes,\n id: nodeId,\n position,\n isDragging: nodeId === currentDraggingId\n };\n}), (a, b) => a === b);\n\n// --- Derived Edge Atoms ---\n\n/**\n * All edge keys\n */\nexport const edgeKeysAtom = atom(get => {\n get(graphUpdateVersionAtom);\n const graph = get(graphAtom);\n return graph.edges();\n});\n\n/**\n * Edge keys including temp edge during creation\n */\nexport const edgeKeysWithTempEdgeAtom = atom(get => {\n const keys = get(edgeKeysAtom);\n const edgeCreation = get(edgeCreationAtom);\n if (edgeCreation.isCreating) {\n return [...keys, 'temp-creating-edge'];\n }\n return keys;\n});\n\n/**\n * Per-edge UI state\n *\n * Structural equality cache: stores previous result per edge key.\n * Returns previous object when all fields match — prevents downstream re-renders.\n */\nconst _edgeCacheByGraph = new WeakMap();\nfunction getEdgeCache(graph) {\n let cache = _edgeCacheByGraph.get(graph);\n if (!cache) {\n cache = new Map();\n _edgeCacheByGraph.set(graph, cache);\n }\n return cache;\n}\nexport const edgeFamilyAtom = atomFamily(key => atom(get => {\n // Position reactivity flows through nodePositionAtomFamily(sourceId/targetId)\n // with structural equality caching — only connected edges re-render on drag.\n // graphUpdateVersionAtom handles non-position attribute changes (label, color)\n // and is NOT bumped during drag, so it won't cause O(E) re-renders.\n get(graphUpdateVersionAtom);\n if (key === 'temp-creating-edge') {\n const edgeCreationState = get(edgeCreationAtom);\n const graph = get(graphAtom);\n if (edgeCreationState.isCreating && edgeCreationState.sourceNodeId && edgeCreationState.targetPosition) {\n const sourceNodeAttrs = graph.getNodeAttributes(edgeCreationState.sourceNodeId);\n const sourceNodePosition = get(nodePositionAtomFamily(edgeCreationState.sourceNodeId));\n const pan = get(panAtom);\n const zoom = get(zoomAtom);\n const viewportRect = get(viewportRectAtom);\n if (sourceNodeAttrs && viewportRect) {\n const mouseX = edgeCreationState.targetPosition.x - viewportRect.left;\n const mouseY = edgeCreationState.targetPosition.y - viewportRect.top;\n const worldTargetX = (mouseX - pan.x) / zoom;\n const worldTargetY = (mouseY - pan.y) / zoom;\n const tempEdge = {\n key: 'temp-creating-edge',\n sourceId: edgeCreationState.sourceNodeId,\n targetId: 'temp-cursor',\n sourcePosition: sourceNodePosition,\n targetPosition: {\n x: worldTargetX,\n y: worldTargetY\n },\n sourceNodeSize: sourceNodeAttrs.size,\n sourceNodeWidth: sourceNodeAttrs.width,\n sourceNodeHeight: sourceNodeAttrs.height,\n targetNodeSize: 0,\n targetNodeWidth: 0,\n targetNodeHeight: 0,\n type: 'dashed',\n color: '#FF9800',\n weight: 2,\n label: undefined,\n dbData: {\n id: 'temp-creating-edge',\n graph_id: get(currentGraphIdAtom) || '',\n source_node_id: edgeCreationState.sourceNodeId,\n target_node_id: 'temp-cursor',\n edge_type: 'temp',\n filter_condition: null,\n ui_properties: null,\n data: null,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString()\n }\n };\n return tempEdge;\n }\n }\n return null;\n }\n const graph = get(graphAtom);\n if (!graph.hasEdge(key)) {\n getEdgeCache(graph).delete(key);\n return null;\n }\n const sourceId = graph.source(key);\n const targetId = graph.target(key);\n const attributes = graph.getEdgeAttributes(key);\n\n // Re-route edges through collapsed groups: if a source/target is inside\n // a collapsed group, use the group node's position and dimensions instead\n const remap = get(collapsedEdgeRemapAtom);\n const effectiveSourceId = remap.get(sourceId) ?? sourceId;\n const effectiveTargetId = remap.get(targetId) ?? targetId;\n if (!graph.hasNode(effectiveSourceId) || !graph.hasNode(effectiveTargetId)) {\n getEdgeCache(graph).delete(key);\n return null;\n }\n const sourceAttributes = graph.getNodeAttributes(effectiveSourceId);\n const targetAttributes = graph.getNodeAttributes(effectiveTargetId);\n const sourcePosition = get(nodePositionAtomFamily(effectiveSourceId));\n const targetPosition = get(nodePositionAtomFamily(effectiveTargetId));\n if (sourceAttributes && targetAttributes) {\n const next = {\n ...attributes,\n key,\n sourceId: effectiveSourceId,\n targetId: effectiveTargetId,\n sourcePosition,\n targetPosition,\n sourceNodeSize: sourceAttributes.size,\n targetNodeSize: targetAttributes.size,\n sourceNodeWidth: sourceAttributes.width ?? sourceAttributes.size,\n sourceNodeHeight: sourceAttributes.height ?? sourceAttributes.size,\n targetNodeWidth: targetAttributes.width ?? targetAttributes.size,\n targetNodeHeight: targetAttributes.height ?? targetAttributes.size\n };\n\n // Structural equality: return cached object if all fields match\n const edgeCache = getEdgeCache(graph);\n const prev = edgeCache.get(key);\n if (prev && prev.sourcePosition === next.sourcePosition && prev.targetPosition === next.targetPosition && prev.sourceId === next.sourceId && prev.targetId === next.targetId && prev.type === next.type && prev.color === next.color && prev.weight === next.weight && prev.label === next.label && prev.sourceNodeSize === next.sourceNodeSize && prev.targetNodeSize === next.targetNodeSize && prev.sourceNodeWidth === next.sourceNodeWidth && prev.sourceNodeHeight === next.sourceNodeHeight && prev.targetNodeWidth === next.targetNodeWidth && prev.targetNodeHeight === next.targetNodeHeight) {\n return prev;\n }\n edgeCache.set(key, next);\n return next;\n }\n getEdgeCache(graph).delete(key);\n return null;\n}), (a, b) => a === b);","/**\n * Viewport state management\n *\n * Manages pan, zoom, and viewport rect for the canvas.\n */\n\nimport { atom } from 'jotai';\n// --- Core Viewport Atoms ---\n\n/**\n * Current zoom level (1 = 100%)\n */\nexport const zoomAtom = atom(1);\n\n/**\n * Current pan offset in pixels\n */\nexport const panAtom = atom({\n x: 0,\n y: 0\n});\n\n/**\n * Viewport DOM rect (set by the Viewport component)\n */\nexport const viewportRectAtom = atom(null);\n\n// --- Coordinate Conversion ---\n\n/**\n * Convert screen coordinates to world coordinates\n */\nexport const screenToWorldAtom = atom(get => {\n return (screenX, screenY) => {\n const pan = get(panAtom);\n const zoom = get(zoomAtom);\n const rect = get(viewportRectAtom);\n if (!rect) {\n return {\n x: screenX,\n y: screenY\n };\n }\n const relativeX = screenX - rect.left;\n const relativeY = screenY - rect.top;\n return {\n x: (relativeX - pan.x) / zoom,\n y: (relativeY - pan.y) / zoom\n };\n };\n});\n\n/**\n * Convert world coordinates to screen coordinates\n */\nexport const worldToScreenAtom = atom(get => {\n return (worldX, worldY) => {\n const pan = get(panAtom);\n const zoom = get(zoomAtom);\n const rect = get(viewportRectAtom);\n if (!rect) {\n return {\n x: worldX,\n y: worldY\n };\n }\n return {\n x: worldX * zoom + pan.x + rect.left,\n y: worldY * zoom + pan.y + rect.top\n };\n };\n});\n\n// --- Viewport Actions ---\n\n/**\n * Set zoom level with optional center point\n */\nexport const setZoomAtom = atom(null, (get, set, {\n zoom,\n centerX,\n centerY\n}) => {\n const currentZoom = get(zoomAtom);\n const pan = get(panAtom);\n const rect = get(viewportRectAtom);\n\n // Clamp zoom\n const newZoom = Math.max(0.1, Math.min(5, zoom));\n if (centerX !== undefined && centerY !== undefined && rect) {\n // Zoom towards the center point\n const relativeX = centerX - rect.left;\n const relativeY = centerY - rect.top;\n const worldX = (relativeX - pan.x) / currentZoom;\n const worldY = (relativeY - pan.y) / currentZoom;\n const newPanX = relativeX - worldX * newZoom;\n const newPanY = relativeY - worldY * newZoom;\n set(panAtom, {\n x: newPanX,\n y: newPanY\n });\n }\n set(zoomAtom, newZoom);\n});\n\n/**\n * Reset viewport to default state\n */\nexport const resetViewportAtom = atom(null, (_get, set) => {\n set(zoomAtom, 1);\n set(panAtom, {\n x: 0,\n y: 0\n });\n});\n\n// =============================================================================\n// Headless Layout Atoms\n// Fit-to-bounds and center-on-node without React hooks\n// =============================================================================\n\nimport { graphAtom } from './graph-store';\nimport { nodePositionUpdateCounterAtom } from './graph-position';\nimport { uiNodesAtom } from './graph-derived';\nimport { selectedNodeIdsAtom } from './selection-store';\nimport { calculateBounds, FitToBoundsMode } from '../utils/layout';\n\n/**\n * Write atom: fit viewport to show all nodes or selection.\n * Pure Jotai — no React required.\n */\nexport const fitToBoundsAtom = atom(null, (get, set, {\n mode,\n padding = 20\n}) => {\n const normalizedMode = typeof mode === 'string' ? mode === 'graph' ? FitToBoundsMode.Graph : FitToBoundsMode.Selection : mode;\n const viewportSize = get(viewportRectAtom);\n if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return;\n\n // Force position counter read so bounds are fresh\n get(nodePositionUpdateCounterAtom);\n let bounds;\n if (normalizedMode === FitToBoundsMode.Graph) {\n const graph = get(graphAtom);\n const nodes = graph.nodes().map(node => {\n const attrs = graph.getNodeAttributes(node);\n return {\n x: attrs.x,\n y: attrs.y,\n width: attrs.width || 500,\n height: attrs.height || 500\n };\n });\n bounds = calculateBounds(nodes);\n } else {\n const selectedIds = get(selectedNodeIdsAtom);\n const allNodes = get(uiNodesAtom);\n const selectedNodes = allNodes.filter(n => selectedIds.has(n.id)).map(n => ({\n x: n.position.x,\n y: n.position.y,\n width: n.width ?? 500,\n height: n.height ?? 500\n }));\n bounds = calculateBounds(selectedNodes);\n }\n if (bounds.width <= 0 || bounds.height <= 0) return;\n const maxHPad = Math.max(0, viewportSize.width / 2 - 1);\n const maxVPad = Math.max(0, viewportSize.height / 2 - 1);\n const safePadding = Math.max(0, Math.min(padding, maxHPad, maxVPad));\n const effW = Math.max(1, viewportSize.width - 2 * safePadding);\n const effH = Math.max(1, viewportSize.height - 2 * safePadding);\n const scale = Math.min(effW / bounds.width, effH / bounds.height);\n if (scale <= 0 || !isFinite(scale)) return;\n set(zoomAtom, scale);\n const scaledW = bounds.width * scale;\n const scaledH = bounds.height * scale;\n const startX = safePadding + (effW - scaledW) / 2;\n const startY = safePadding + (effH - scaledH) / 2;\n set(panAtom, {\n x: startX - bounds.x * scale,\n y: startY - bounds.y * scale\n });\n});\n\n/**\n * Write atom: center viewport on a specific node.\n */\nexport const centerOnNodeAtom = atom(null, (get, set, nodeId) => {\n const nodes = get(uiNodesAtom);\n const node = nodes.find(n => n.id === nodeId);\n if (!node) return;\n const {\n x,\n y,\n width = 200,\n height = 100\n } = node;\n const zoom = get(zoomAtom);\n const centerX = x + width / 2;\n const centerY = y + height / 2;\n const rect = get(viewportRectAtom);\n const halfWidth = rect ? rect.width / 2 : 400;\n const halfHeight = rect ? rect.height / 2 : 300;\n set(panAtom, {\n x: halfWidth - centerX * zoom,\n y: halfHeight - centerY * zoom\n });\n});\n\n// =============================================================================\n// Zoom Transition State\n// When zooming in on a node past a threshold, trigger a fade transition\n// =============================================================================\n\n/** The zoom level at which we start the transition (zoom in) */\nexport const ZOOM_TRANSITION_THRESHOLD = 3.5;\n\n/** The zoom level at which we exit transition mode (zoom out) */\nexport const ZOOM_EXIT_THRESHOLD = 2.0;\n\n/** The node ID that's currently being zoomed into (center of viewport) */\nexport const zoomFocusNodeIdAtom = atom(null);\n\n/** Transition progress: 0 = normal view, 1 = fully transitioned to locked view */\nexport const zoomTransitionProgressAtom = atom(0);\n\n/** Computed: whether we're in a zoom transition */\nexport const isZoomTransitioningAtom = atom(get => {\n const progress = get(zoomTransitionProgressAtom);\n return progress > 0 && progress < 1;\n});\n\n// --- Zoom Animation Target ---\n\n/** Current zoom animation target, or null when idle */\nexport const zoomAnimationTargetAtom = atom(null);\n\n/**\n * Write atom: start an animated zoom-to-node transition.\n * Sets the animation target; the actual animation loop runs in useZoomTransition hook.\n */\nexport const animateZoomToNodeAtom = atom(null, (get, set, {\n nodeId,\n targetZoom,\n duration = 300\n}) => {\n const nodes = get(uiNodesAtom);\n const node = nodes.find(n => n.id === nodeId);\n if (!node) return;\n const {\n x,\n y,\n width = 200,\n height = 100\n } = node;\n const centerX = x + width / 2;\n const centerY = y + height / 2;\n const rect = get(viewportRectAtom);\n const halfWidth = rect ? rect.width / 2 : 400;\n const halfHeight = rect ? rect.height / 2 : 300;\n const finalZoom = targetZoom ?? get(zoomAtom);\n const targetPan = {\n x: halfWidth - centerX * finalZoom,\n y: halfHeight - centerY * finalZoom\n };\n set(zoomFocusNodeIdAtom, nodeId);\n set(zoomAnimationTargetAtom, {\n targetZoom: finalZoom,\n targetPan,\n startZoom: get(zoomAtom),\n startPan: {\n ...get(panAtom)\n },\n duration,\n startTime: performance.now()\n });\n});\n\n/**\n * Write atom: start an animated fit-to-bounds transition.\n * Computes the target viewport, then animates to it.\n */\nexport const animateFitToBoundsAtom = atom(null, (get, set, {\n mode,\n padding = 20,\n duration = 300\n}) => {\n const viewportSize = get(viewportRectAtom);\n if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return;\n get(nodePositionUpdateCounterAtom);\n let bounds;\n if (mode === 'graph') {\n const graph = get(graphAtom);\n const nodes = graph.nodes().map(node => {\n const attrs = graph.getNodeAttributes(node);\n return {\n x: attrs.x,\n y: attrs.y,\n width: attrs.width || 500,\n height: attrs.height || 500\n };\n });\n bounds = calculateBounds(nodes);\n } else {\n const selectedIds = get(selectedNodeIdsAtom);\n const allNodes = get(uiNodesAtom);\n const selectedNodes = allNodes.filter(n => selectedIds.has(n.id)).map(n => ({\n x: n.position.x,\n y: n.position.y,\n width: n.width ?? 500,\n height: n.height ?? 500\n }));\n bounds = calculateBounds(selectedNodes);\n }\n if (bounds.width <= 0 || bounds.height <= 0) return;\n const safePadding = Math.max(0, Math.min(padding, viewportSize.width / 2 - 1, viewportSize.height / 2 - 1));\n const effW = Math.max(1, viewportSize.width - 2 * safePadding);\n const effH = Math.max(1, viewportSize.height - 2 * safePadding);\n const scale = Math.min(effW / bounds.width, effH / bounds.height);\n if (scale <= 0 || !isFinite(scale)) return;\n const scaledW = bounds.width * scale;\n const scaledH = bounds.height * scale;\n const startX = safePadding + (effW - scaledW) / 2;\n const startY = safePadding + (effH - scaledH) / 2;\n const targetPan = {\n x: startX - bounds.x * scale,\n y: startY - bounds.y * scale\n };\n set(zoomAnimationTargetAtom, {\n targetZoom: scale,\n targetPan,\n startZoom: get(zoomAtom),\n startPan: {\n ...get(panAtom)\n },\n duration,\n startTime: performance.now()\n });\n});","/**\n * Layout utilities for canvas operations.\n *\n * Provides geometry calculations, bounds computation, and node overlap detection.\n */\n\n// ============================================================\n// Types\n// ============================================================\n\n/**\n * Rectangle with position and dimensions\n */\n\n/**\n * Node with position and dimensions (alias for geometry functions)\n */\n\n/**\n * Extended bounds with computed edges\n */\n\n/**\n * Mode for fitToBounds operation\n */\nexport let FitToBoundsMode = /*#__PURE__*/function (FitToBoundsMode) {\n /** Fit all nodes in the graph */\n FitToBoundsMode[\"Graph\"] = \"graph\";\n /** Fit only selected nodes */\n FitToBoundsMode[\"Selection\"] = \"selection\";\n return FitToBoundsMode;\n}({});\n\n// ============================================================\n// Bounds Calculation\n// ============================================================\n\n/**\n * Calculate the bounding rectangle that contains all nodes.\n *\n * @param nodes - Array of rectangles (nodes with x, y, width, height)\n * @returns Bounding rectangle containing all nodes\n *\n * @example\n * const bounds = calculateBounds([\n * { x: 0, y: 0, width: 100, height: 100 },\n * { x: 200, y: 150, width: 100, height: 100 },\n * ]);\n * // bounds = { x: 0, y: 0, width: 300, height: 250 }\n */\nexport const calculateBounds = nodes => {\n if (nodes.length === 0) {\n return {\n x: 0,\n y: 0,\n width: 0,\n height: 0\n };\n }\n const minX = Math.min(...nodes.map(node => node.x));\n const minY = Math.min(...nodes.map(node => node.y));\n const maxX = Math.max(...nodes.map(node => node.x + node.width));\n const maxY = Math.max(...nodes.map(node => node.y + node.height));\n return {\n x: minX,\n y: minY,\n width: maxX - minX,\n height: maxY - minY\n };\n};\n\n// ============================================================\n// Node Geometry\n// ============================================================\n\n/**\n * Get the center point of a node.\n */\nexport function getNodeCenter(node) {\n return {\n x: node.x + node.width / 2,\n y: node.y + node.height / 2\n };\n}\n\n/**\n * Set a node's position based on center coordinates.\n * Returns a new node object with updated position.\n */\nexport function setNodeCenter(node, centerX, centerY) {\n return {\n ...node,\n x: centerX - node.width / 2,\n y: centerY - node.height / 2\n };\n}\n\n/**\n * Check if two nodes overlap.\n */\nexport function checkNodesOverlap(node1, node2) {\n const center1 = getNodeCenter(node1);\n const center2 = getNodeCenter(node2);\n const dx = Math.abs(center1.x - center2.x);\n const dy = Math.abs(center1.y - center2.y);\n const minDistanceX = (node1.width + node2.width) / 2;\n const minDistanceY = (node1.height + node2.height) / 2;\n return dx < minDistanceX && dy < minDistanceY;\n}\n\n/**\n * Get the bounding box of a node.\n */\nexport function getNodeBounds(node) {\n return {\n x: node.x,\n y: node.y,\n width: node.width,\n height: node.height,\n left: node.x,\n right: node.x + node.width,\n top: node.y,\n bottom: node.y + node.height\n };\n}\n\n/**\n * Check if two nodes are within a certain distance of each other.\n */\nexport function areNodesClose(node1, node2, threshold = 200) {\n const center1 = getNodeCenter(node1);\n const center2 = getNodeCenter(node2);\n const distance = Math.sqrt(Math.pow(center1.x - center2.x, 2) + Math.pow(center1.y - center2.y, 2));\n return distance <= threshold;\n}","/**\n * Group Store\n *\n * Manages node grouping/nesting: parent-child relationships,\n * collapse/expand state, and group membership operations.\n */\n\nimport { atom } from 'jotai';\nimport { graphAtom, graphUpdateVersionAtom } from './graph-store';\nimport { nodePositionUpdateCounterAtom } from './graph-position';\nimport { pushHistoryAtom } from './history-store';\n// =============================================================================\n// Collapse State\n// =============================================================================\n\n/**\n * Set of group node IDs that are currently collapsed.\n * Collapsed groups hide their children from uiNodesAtom.\n */\nexport const collapsedGroupsAtom = atom(new Set());\n\n/**\n * Toggle a group's collapsed state\n */\nexport const toggleGroupCollapseAtom = atom(null, (get, set, groupId) => {\n const current = get(collapsedGroupsAtom);\n const next = new Set(current);\n if (next.has(groupId)) {\n next.delete(groupId);\n } else {\n next.add(groupId);\n }\n set(collapsedGroupsAtom, next);\n});\n\n/**\n * Collapse a group\n */\nexport const collapseGroupAtom = atom(null, (get, set, groupId) => {\n const current = get(collapsedGroupsAtom);\n if (!current.has(groupId)) {\n const next = new Set(current);\n next.add(groupId);\n set(collapsedGroupsAtom, next);\n }\n});\n\n/**\n * Expand a group\n */\nexport const expandGroupAtom = atom(null, (get, set, groupId) => {\n const current = get(collapsedGroupsAtom);\n if (current.has(groupId)) {\n const next = new Set(current);\n next.delete(groupId);\n set(collapsedGroupsAtom, next);\n }\n});\n\n// =============================================================================\n// Parent/Child Queries\n// =============================================================================\n\n/**\n * Get children of a group node (reads from graph attributes)\n */\nexport const nodeChildrenAtom = atom(get => {\n get(graphUpdateVersionAtom);\n const graph = get(graphAtom);\n return parentId => {\n const children = [];\n graph.forEachNode((nodeId, attrs) => {\n if (attrs.parentId === parentId) {\n children.push(nodeId);\n }\n });\n return children;\n };\n});\n\n/**\n * Get the parent ID of a node\n */\nexport const nodeParentAtom = atom(get => {\n get(graphUpdateVersionAtom);\n const graph = get(graphAtom);\n return nodeId => {\n if (!graph.hasNode(nodeId)) return undefined;\n return graph.getNodeAttribute(nodeId, 'parentId');\n };\n});\n\n/**\n * Check if a node is a group (has any children)\n */\nexport const isGroupNodeAtom = atom(get => {\n const getChildren = get(nodeChildrenAtom);\n return nodeId => getChildren(nodeId).length > 0;\n});\n\n/**\n * Get count of children for a group node\n */\nexport const groupChildCountAtom = atom(get => {\n const getChildren = get(nodeChildrenAtom);\n return groupId => getChildren(groupId).length;\n});\n\n// =============================================================================\n// Group Membership Operations\n// =============================================================================\n\n/**\n * Set a node's parent (move into a group)\n */\nexport const setNodeParentAtom = atom(null, (get, set, {\n nodeId,\n parentId\n}) => {\n const graph = get(graphAtom);\n if (!graph.hasNode(nodeId)) return;\n\n // Prevent circular grouping\n if (parentId) {\n if (parentId === nodeId) return;\n // Check if parentId is a descendant of nodeId\n let current = parentId;\n while (current) {\n if (current === nodeId) return; // Circular!\n if (!graph.hasNode(current)) break;\n current = graph.getNodeAttribute(current, 'parentId');\n }\n }\n graph.setNodeAttribute(nodeId, 'parentId', parentId);\n set(graphUpdateVersionAtom, v => v + 1);\n});\n\n/**\n * Move multiple nodes into a group\n */\nexport const moveNodesToGroupAtom = atom(null, (get, set, {\n nodeIds,\n groupId\n}) => {\n for (const nodeId of nodeIds) {\n set(setNodeParentAtom, {\n nodeId,\n parentId: groupId\n });\n }\n});\n\n/**\n * Remove a node from its group (set parentId to undefined)\n */\nexport const removeFromGroupAtom = atom(null, (get, set, nodeId) => {\n set(setNodeParentAtom, {\n nodeId,\n parentId: undefined\n });\n});\n\n/**\n * Create a group from selected nodes:\n * 1. Create a new \"group\" type node at the bounding box center\n * 2. Set all selected nodes as children\n * Returns the new group node ID (set via callback since node creation is async)\n */\nexport const groupSelectedNodesAtom = atom(null, (get, set, {\n nodeIds,\n groupNodeId\n}) => {\n set(pushHistoryAtom, `Group ${nodeIds.length} nodes`);\n const graph = get(graphAtom);\n\n // Compute bounding box of selected nodes\n let minX = Infinity,\n minY = Infinity,\n maxX = -Infinity,\n maxY = -Infinity;\n for (const nodeId of nodeIds) {\n if (!graph.hasNode(nodeId)) continue;\n const attrs = graph.getNodeAttributes(nodeId);\n minX = Math.min(minX, attrs.x);\n minY = Math.min(minY, attrs.y);\n maxX = Math.max(maxX, attrs.x + (attrs.width || 200));\n maxY = Math.max(maxY, attrs.y + (attrs.height || 100));\n }\n\n // Position group node at bounding box origin with padding\n const padding = 20;\n if (graph.hasNode(groupNodeId)) {\n graph.setNodeAttribute(groupNodeId, 'x', minX - padding);\n graph.setNodeAttribute(groupNodeId, 'y', minY - padding - 30); // extra for header\n graph.setNodeAttribute(groupNodeId, 'width', maxX - minX + 2 * padding);\n graph.setNodeAttribute(groupNodeId, 'height', maxY - minY + 2 * padding + 30);\n }\n\n // Set parent for all nodes\n for (const nodeId of nodeIds) {\n if (nodeId !== groupNodeId && graph.hasNode(nodeId)) {\n graph.setNodeAttribute(nodeId, 'parentId', groupNodeId);\n }\n }\n set(graphUpdateVersionAtom, v => v + 1);\n set(nodePositionUpdateCounterAtom, c => c + 1);\n});\n\n/**\n * Ungroup: remove parent from all children of a group node\n */\nexport const ungroupNodesAtom = atom(null, (get, set, groupId) => {\n set(pushHistoryAtom, 'Ungroup nodes');\n const graph = get(graphAtom);\n graph.forEachNode((nodeId, attrs) => {\n if (attrs.parentId === groupId) {\n graph.setNodeAttribute(nodeId, 'parentId', undefined);\n }\n });\n set(graphUpdateVersionAtom, v => v + 1);\n});\n\n// =============================================================================\n// Drag-to-Nest\n// =============================================================================\n\n/**\n * Nest dragged nodes into a target group node.\n * Sets parentId for each dragged node, then auto-resizes the target.\n */\nexport const nestNodesOnDropAtom = atom(null, (get, set, {\n nodeIds,\n targetId\n}) => {\n set(pushHistoryAtom, 'Nest nodes');\n for (const nodeId of nodeIds) {\n if (nodeId === targetId) continue;\n set(setNodeParentAtom, {\n nodeId,\n parentId: targetId\n });\n }\n set(autoResizeGroupAtom, targetId);\n});\n\n// =============================================================================\n// Descendants Helper\n// =============================================================================\n\n/**\n * Recursively collects all descendant node IDs of a group node.\n * Used by useNodeDrag to move all children when dragging a group.\n */\nexport function getNodeDescendants(graph, groupId) {\n const descendants = [];\n const stack = [groupId];\n while (stack.length > 0) {\n const current = stack.pop();\n graph.forEachNode((nodeId, attrs) => {\n if (attrs.parentId === current) {\n descendants.push(nodeId);\n stack.push(nodeId);\n }\n });\n }\n return descendants;\n}\n\n// =============================================================================\n// Edge Re-routing for Collapsed Groups\n// =============================================================================\n\n/**\n * Derived: maps each collapsed node to its outermost collapsed ancestor.\n * Used by visibleEdgeKeysAtom and edgeFamilyAtom to re-route edges\n * from hidden children to their visible group node.\n *\n * Only nodes that are inside a collapsed group appear in this map.\n * The value is always a visible group node (the outermost collapsed ancestor).\n */\nexport const collapsedEdgeRemapAtom = atom(get => {\n const collapsed = get(collapsedGroupsAtom);\n if (collapsed.size === 0) return new Map();\n get(graphUpdateVersionAtom);\n const graph = get(graphAtom);\n const remap = new Map();\n for (const nodeId of graph.nodes()) {\n let current = nodeId;\n let outermost = null;\n while (true) {\n if (!graph.hasNode(current)) break;\n const parent = graph.getNodeAttribute(current, 'parentId');\n if (!parent) break;\n if (collapsed.has(parent)) outermost = parent;\n current = parent;\n }\n if (outermost) remap.set(nodeId, outermost);\n }\n return remap;\n});\n\n// =============================================================================\n// Group Auto-resize\n// =============================================================================\n\n/**\n * Write atom: recomputes a group node's position and dimensions\n * to contain all its direct children with padding.\n */\nexport const autoResizeGroupAtom = atom(null, (get, set, groupId) => {\n const graph = get(graphAtom);\n if (!graph.hasNode(groupId)) return;\n const children = [];\n graph.forEachNode((nodeId, attrs) => {\n if (attrs.parentId === groupId) {\n children.push(nodeId);\n }\n });\n if (children.length === 0) return;\n const padding = 20;\n const headerHeight = 30;\n let minX = Infinity,\n minY = Infinity,\n maxX = -Infinity,\n maxY = -Infinity;\n for (const childId of children) {\n const attrs = graph.getNodeAttributes(childId);\n minX = Math.min(minX, attrs.x);\n minY = Math.min(minY, attrs.y);\n maxX = Math.max(maxX, attrs.x + (attrs.width || 200));\n maxY = Math.max(maxY, attrs.y + (attrs.height || 100));\n }\n graph.setNodeAttribute(groupId, 'x', minX - padding);\n graph.setNodeAttribute(groupId, 'y', minY - padding - headerHeight);\n graph.setNodeAttribute(groupId, 'width', maxX - minX + 2 * padding);\n graph.setNodeAttribute(groupId, 'height', maxY - minY + 2 * padding + headerHeight);\n set(nodePositionUpdateCounterAtom, c => c + 1);\n});\n\n// =============================================================================\n// Visibility Helper\n// =============================================================================\n\n/**\n * Check if a node should be hidden because it's inside a collapsed group.\n * Walks up the parent chain — if ANY ancestor is collapsed, the node is hidden.\n */\nexport function isNodeCollapsed(nodeId, getParentId, collapsed) {\n let current = nodeId;\n while (true) {\n const parentId = getParentId(current);\n if (!parentId) return false;\n if (collapsed.has(parentId)) return true;\n current = parentId;\n }\n}","/**\n * Canvas History Store — v2\n *\n * Delta-based undo/redo — replaces full-graph JSON.stringify snapshots.\n *\n * Performance improvement:\n * - Before: O(N) per operation (deep-clone all nodes + edges)\n * - After: O(1) for moves, O(K) for batch operations\n *\n * Each history entry stores forward + reverse deltas, not full snapshots.\n * Falls back to full snapshot for complex operations (graph import).\n *\n * Pure delta functions are in ./history-actions.ts\n */\n\nimport { atom } from 'jotai';\nimport { graphAtom, graphUpdateVersionAtom } from './graph-store';\nimport { nodePositionUpdateCounterAtom } from './graph-position';\nimport { createDebug } from '../utils/debug';\n\n// Re-export types for backward compat\n\n// Re-export pure functions for backward compat\nexport { applyDelta, invertDelta, createSnapshot } from './history-actions';\nimport { applyDelta, invertDelta, createSnapshot } from './history-actions';\nconst debug = createDebug('history');\n\n// =============================================================================\n// Configuration\n// =============================================================================\n\n/** Maximum number of history entries to keep */\nconst MAX_HISTORY_SIZE = 50;\n\n// =============================================================================\n// Atoms\n// =============================================================================\n\nexport const historyStateAtom = atom({\n past: [],\n future: [],\n isApplying: false\n});\nexport const canUndoAtom = atom(get => {\n const history = get(historyStateAtom);\n return history.past.length > 0 && !history.isApplying;\n});\nexport const canRedoAtom = atom(get => {\n const history = get(historyStateAtom);\n return history.future.length > 0 && !history.isApplying;\n});\nexport const undoCountAtom = atom(get => get(historyStateAtom).past.length);\nexport const redoCountAtom = atom(get => get(historyStateAtom).future.length);\n\n// =============================================================================\n// Push Delta (replaces pushHistoryAtom's full-snapshot approach)\n// =============================================================================\n\n/**\n * Push a delta to history. Call AFTER making the change.\n * The delta describes what changed; the reverse is auto-computed.\n */\nexport const pushDeltaAtom = atom(null, (get, set, delta) => {\n const history = get(historyStateAtom);\n if (history.isApplying) return;\n const {\n label,\n ...cleanDelta\n } = delta;\n const entry = {\n forward: cleanDelta,\n reverse: invertDelta(cleanDelta),\n timestamp: Date.now(),\n label\n };\n const newPast = [...history.past, entry];\n if (newPast.length > MAX_HISTORY_SIZE) newPast.shift();\n set(historyStateAtom, {\n past: newPast,\n future: [],\n // Clear redo stack\n isApplying: false\n });\n debug('Pushed delta: %s (past: %d)', label || delta.type, newPast.length);\n});\n\n/**\n * Legacy: Push a full-graph snapshot to history.\n * Use for complex operations where computing deltas is impractical.\n * Call BEFORE making changes.\n */\nexport const pushHistoryAtom = atom(null, (get, set, label) => {\n const history = get(historyStateAtom);\n if (history.isApplying) return;\n const graph = get(graphAtom);\n const snapshot = createSnapshot(graph, label);\n\n // Store as a full-snapshot delta\n const forward = {\n type: 'full-snapshot',\n nodes: snapshot.nodes,\n edges: snapshot.edges\n };\n const entry = {\n forward,\n reverse: forward,\n // For full snapshots, reverse IS the current state\n timestamp: Date.now(),\n label\n };\n const newPast = [...history.past, entry];\n if (newPast.length > MAX_HISTORY_SIZE) newPast.shift();\n set(historyStateAtom, {\n past: newPast,\n future: [],\n isApplying: false\n });\n debug('Pushed snapshot: %s (past: %d)', label || 'unnamed', newPast.length);\n});\n\n// =============================================================================\n// Undo / Redo\n// =============================================================================\n\nexport const undoAtom = atom(null, (get, set) => {\n const history = get(historyStateAtom);\n if (history.past.length === 0 || history.isApplying) return false;\n set(historyStateAtom, {\n ...history,\n isApplying: true\n });\n try {\n const graph = get(graphAtom);\n const newPast = [...history.past];\n const entry = newPast.pop();\n\n // For full-snapshot entries, save current state before restoring\n let forwardForRedo = entry.forward;\n if (entry.reverse.type === 'full-snapshot') {\n const currentSnapshot = createSnapshot(graph, 'current');\n forwardForRedo = {\n type: 'full-snapshot',\n nodes: currentSnapshot.nodes,\n edges: currentSnapshot.edges\n };\n }\n const structuralChange = applyDelta(graph, entry.reverse);\n if (structuralChange) {\n set(graphAtom, graph);\n set(graphUpdateVersionAtom, v => v + 1);\n }\n set(nodePositionUpdateCounterAtom, c => c + 1);\n const redoEntry = {\n forward: forwardForRedo,\n reverse: entry.reverse,\n timestamp: entry.timestamp,\n label: entry.label\n };\n set(historyStateAtom, {\n past: newPast,\n future: [redoEntry, ...history.future],\n isApplying: false\n });\n debug('Undo: %s (past: %d, future: %d)', entry.label, newPast.length, history.future.length + 1);\n return true;\n } catch (error) {\n debug.error('Undo failed: %O', error);\n set(historyStateAtom, {\n ...history,\n isApplying: false\n });\n return false;\n }\n});\nexport const redoAtom = atom(null, (get, set) => {\n const history = get(historyStateAtom);\n if (history.future.length === 0 || history.isApplying) return false;\n set(historyStateAtom, {\n ...history,\n isApplying: true\n });\n try {\n const graph = get(graphAtom);\n const newFuture = [...history.future];\n const entry = newFuture.shift();\n\n // For full-snapshot entries, save current state before restoring\n let reverseForUndo = entry.reverse;\n if (entry.forward.type === 'full-snapshot') {\n const currentSnapshot = createSnapshot(graph, 'current');\n reverseForUndo = {\n type: 'full-snapshot',\n nodes: currentSnapshot.nodes,\n edges: currentSnapshot.edges\n };\n }\n const structuralChange = applyDelta(graph, entry.forward);\n if (structuralChange) {\n set(graphAtom, graph);\n set(graphUpdateVersionAtom, v => v + 1);\n }\n set(nodePositionUpdateCounterAtom, c => c + 1);\n const undoEntry = {\n forward: entry.forward,\n reverse: reverseForUndo,\n timestamp: entry.timestamp,\n label: entry.label\n };\n set(historyStateAtom, {\n past: [...history.past, undoEntry],\n future: newFuture,\n isApplying: false\n });\n debug('Redo: %s (past: %d, future: %d)', entry.label, history.past.length + 1, newFuture.length);\n return true;\n } catch (error) {\n debug.error('Redo failed: %O', error);\n set(historyStateAtom, {\n ...history,\n isApplying: false\n });\n return false;\n }\n});\nexport const clearHistoryAtom = atom(null, (_get, set) => {\n set(historyStateAtom, {\n past: [],\n future: [],\n isApplying: false\n });\n debug('History cleared');\n});\nexport const historyLabelsAtom = atom(get => {\n const history = get(historyStateAtom);\n return {\n past: history.past.map(e => e.label || 'Unnamed'),\n future: history.future.map(e => e.label || 'Unnamed')\n };\n});","/**\n * History Actions — Pure delta functions\n *\n * applyDelta, invertDelta, createSnapshot extracted from history-store.ts.\n * These are pure functions operating on Graphology graphs and delta objects.\n */\n\n// =============================================================================\n// Delta Application\n// =============================================================================\n\n/**\n * Apply a delta to the graph in-place.\n * Returns true if graph structure changed (needs version bump).\n */\nexport function applyDelta(graph, delta) {\n switch (delta.type) {\n case 'move-node':\n {\n if (!graph.hasNode(delta.nodeId)) return false;\n graph.setNodeAttribute(delta.nodeId, 'x', delta.to.x);\n graph.setNodeAttribute(delta.nodeId, 'y', delta.to.y);\n return false; // Position change, not structural\n }\n case 'resize-node':\n {\n if (!graph.hasNode(delta.nodeId)) return false;\n graph.setNodeAttribute(delta.nodeId, 'width', delta.to.width);\n graph.setNodeAttribute(delta.nodeId, 'height', delta.to.height);\n return false;\n }\n case 'add-node':\n {\n if (graph.hasNode(delta.nodeId)) return false;\n graph.addNode(delta.nodeId, delta.attributes);\n return true;\n }\n case 'remove-node':\n {\n if (!graph.hasNode(delta.nodeId)) return false;\n graph.dropNode(delta.nodeId); // Also removes connected edges\n return true;\n }\n case 'add-edge':\n {\n if (graph.hasEdge(delta.edgeId)) return false;\n if (!graph.hasNode(delta.source) || !graph.hasNode(delta.target)) return false;\n graph.addEdgeWithKey(delta.edgeId, delta.source, delta.target, delta.attributes);\n return true;\n }\n case 'remove-edge':\n {\n if (!graph.hasEdge(delta.edgeId)) return false;\n graph.dropEdge(delta.edgeId);\n return true;\n }\n case 'update-node-attr':\n {\n if (!graph.hasNode(delta.nodeId)) return false;\n graph.setNodeAttribute(delta.nodeId, delta.key, delta.to);\n return false;\n }\n case 'batch':\n {\n let structuralChange = false;\n for (const d of delta.deltas) {\n if (applyDelta(graph, d)) structuralChange = true;\n }\n return structuralChange;\n }\n case 'full-snapshot':\n {\n // Full graph restore — clear and rebuild\n graph.clear();\n for (const node of delta.nodes) {\n graph.addNode(node.id, node.attributes);\n }\n for (const edge of delta.edges) {\n if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) {\n graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge.attributes);\n }\n }\n return true;\n }\n }\n}\n\n/**\n * Invert a delta (for creating reverse operations).\n */\nexport function invertDelta(delta) {\n switch (delta.type) {\n case 'move-node':\n return {\n ...delta,\n from: delta.to,\n to: delta.from\n };\n case 'resize-node':\n return {\n ...delta,\n from: delta.to,\n to: delta.from\n };\n case 'add-node':\n return {\n type: 'remove-node',\n nodeId: delta.nodeId,\n attributes: delta.attributes,\n connectedEdges: []\n };\n case 'remove-node':\n {\n // Restore node AND its connected edges\n const batch = [{\n type: 'add-node',\n nodeId: delta.nodeId,\n attributes: delta.attributes\n }, ...delta.connectedEdges.map(e => ({\n type: 'add-edge',\n edgeId: e.id,\n source: e.source,\n target: e.target,\n attributes: e.attributes\n }))];\n return batch.length === 1 ? batch[0] : {\n type: 'batch',\n deltas: batch\n };\n }\n case 'add-edge':\n return {\n type: 'remove-edge',\n edgeId: delta.edgeId,\n source: delta.source,\n target: delta.target,\n attributes: delta.attributes\n };\n case 'remove-edge':\n return {\n type: 'add-edge',\n edgeId: delta.edgeId,\n source: delta.source,\n target: delta.target,\n attributes: delta.attributes\n };\n case 'update-node-attr':\n return {\n ...delta,\n from: delta.to,\n to: delta.from\n };\n case 'batch':\n return {\n type: 'batch',\n deltas: delta.deltas.map(invertDelta).reverse()\n };\n case 'full-snapshot':\n // Can't invert a full snapshot — caller handles this\n return delta;\n }\n}\n\n// =============================================================================\n// Helper: Create full snapshot (for complex operations)\n// =============================================================================\n\nexport function createSnapshot(graph, label) {\n const nodes = [];\n const edges = [];\n graph.forEachNode((nodeId, attributes) => {\n nodes.push({\n id: nodeId,\n attributes: {\n ...attributes\n }\n });\n });\n graph.forEachEdge((edgeId, attributes, source, target) => {\n edges.push({\n id: edgeId,\n source,\n target,\n attributes: {\n ...attributes\n }\n });\n });\n return {\n timestamp: Date.now(),\n label,\n nodes,\n edges\n };\n}","/**\n * Graph Mutations — Edge Operations\n *\n * Edge CRUD, atomic swap, animation, and label editing atoms.\n * Split from graph-mutations.ts for modularity.\n */\n\nimport { atom } from 'jotai';\nimport { graphAtom, graphUpdateVersionAtom } from './graph-store';\nimport { nodePositionUpdateCounterAtom } from './graph-position';\nimport { edgeFamilyAtom } from './graph-derived';\nimport { createDebug } from '../utils/debug';\nimport { prefersReducedMotionAtom } from './reduced-motion-store';\nconst debug = createDebug('graph:mutations:edges');\n\n// --- Edge CRUD ---\n\n/**\n * Add an edge directly to local graph\n */\nexport const addEdgeToLocalGraphAtom = atom(null, (get, set, newEdge) => {\n const graph = get(graphAtom);\n if (graph.hasNode(newEdge.source_node_id) && graph.hasNode(newEdge.target_node_id)) {\n const uiProps = newEdge.ui_properties || {};\n const attributes = {\n type: typeof uiProps.style === 'string' ? uiProps.style : 'solid',\n color: typeof uiProps.color === 'string' ? uiProps.color : '#999',\n label: newEdge.edge_type ?? undefined,\n weight: typeof uiProps.weight === 'number' ? uiProps.weight : 1,\n dbData: newEdge\n };\n if (!graph.hasEdge(newEdge.id)) {\n try {\n debug('Adding edge %s to local graph', newEdge.id);\n graph.addEdgeWithKey(newEdge.id, newEdge.source_node_id, newEdge.target_node_id, attributes);\n set(graphAtom, graph.copy());\n set(graphUpdateVersionAtom, v => v + 1);\n } catch (e) {\n debug('Failed to add edge %s: %o', newEdge.id, e);\n }\n }\n }\n});\n\n/**\n * Remove an edge from local graph\n */\nexport const removeEdgeFromLocalGraphAtom = atom(null, (get, set, edgeId) => {\n const graph = get(graphAtom);\n if (graph.hasEdge(edgeId)) {\n graph.dropEdge(edgeId);\n set(graphAtom, graph.copy());\n set(graphUpdateVersionAtom, v => v + 1);\n }\n});\n\n/**\n * Atomic swap of temp edge with real edge (prevents render flash)\n */\nexport const swapEdgeAtomicAtom = atom(null, (get, set, {\n tempEdgeId,\n newEdge\n}) => {\n const graph = get(graphAtom);\n if (graph.hasEdge(tempEdgeId)) {\n graph.dropEdge(tempEdgeId);\n }\n if (graph.hasNode(newEdge.source_node_id) && graph.hasNode(newEdge.target_node_id)) {\n const uiProps = newEdge.ui_properties || {};\n const attributes = {\n type: typeof uiProps.style === 'string' ? uiProps.style : 'solid',\n color: typeof uiProps.color === 'string' ? uiProps.color : '#999',\n label: newEdge.edge_type ?? undefined,\n weight: typeof uiProps.weight === 'number' ? uiProps.weight : 1,\n dbData: newEdge\n };\n if (!graph.hasEdge(newEdge.id)) {\n try {\n debug('Atomically swapping temp edge %s with real edge %s', tempEdgeId, newEdge.id);\n graph.addEdgeWithKey(newEdge.id, newEdge.source_node_id, newEdge.target_node_id, attributes);\n } catch (e) {\n debug('Failed to add edge %s: %o', newEdge.id, e);\n }\n }\n }\n set(graphAtom, graph.copy());\n set(graphUpdateVersionAtom, v => v + 1);\n});\n\n// --- Edge Animation State ---\n\n/**\n * Departing edges: snapshots of edges that have been removed from the graph\n * but are still being rendered with an exit animation.\n */\nexport const departingEdgesAtom = atom(new Map());\n\n/**\n * Edge animation duration in milliseconds\n */\nexport const EDGE_ANIMATION_DURATION = 300;\n\n/**\n * Remove an edge with a fade-out animation.\n * Snapshots the edge state, removes it from the graph, then cleans up\n * the departing entry after the animation duration.\n */\nexport const removeEdgeWithAnimationAtom = atom(null, (get, set, edgeKey) => {\n const edgeState = get(edgeFamilyAtom(edgeKey));\n if (edgeState) {\n const departing = new Map(get(departingEdgesAtom));\n departing.set(edgeKey, edgeState);\n set(departingEdgesAtom, departing);\n set(removeEdgeFromLocalGraphAtom, edgeKey);\n const duration = get(prefersReducedMotionAtom) ? 0 : EDGE_ANIMATION_DURATION;\n setTimeout(() => {\n const current = new Map(get(departingEdgesAtom));\n current.delete(edgeKey);\n set(departingEdgesAtom, current);\n }, duration);\n }\n});\n\n// --- Edge Label Editing ---\n\n/**\n * Edge key currently being edited (label inline editing).\n * null when no edge label is being edited.\n */\nexport const editingEdgeLabelAtom = atom(null);\n\n/**\n * Update an edge's label attribute\n */\nexport const updateEdgeLabelAtom = atom(null, (get, set, {\n edgeKey,\n label\n}) => {\n const graph = get(graphAtom);\n if (graph.hasEdge(edgeKey)) {\n graph.setEdgeAttribute(edgeKey, 'label', label || undefined);\n set(graphUpdateVersionAtom, v => v + 1);\n set(nodePositionUpdateCounterAtom, c => c + 1);\n }\n});","/**\n * Reduced Motion Store\n *\n * Tracks the user's `prefers-reduced-motion` OS setting.\n * When enabled, animations (inertia, layout transitions, edge fades)\n * should be skipped or set to instant.\n */\n\nimport { atom } from 'jotai';\n\n/**\n * Whether the user prefers reduced motion.\n * Initialized from `matchMedia` and updated on changes.\n *\n * Components/hooks should read this atom to skip animations:\n * - Pan/zoom inertia → stop immediately\n * - Layout transitions → apply positions instantly\n * - Edge fade animations → duration = 0\n */\nexport const prefersReducedMotionAtom = atom(typeof window !== 'undefined' && typeof window.matchMedia === 'function' ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false);\n\n/**\n * Effect atom: subscribes to matchMedia changes.\n * Mount this once in CanvasProvider via useAtom.\n */\nexport const watchReducedMotionAtom = atom(null, (_get, set) => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;\n const mql = window.matchMedia('(prefers-reduced-motion: reduce)');\n const handler = e => {\n set(prefersReducedMotionAtom, e.matches);\n };\n\n // Sync initial state\n set(prefersReducedMotionAtom, mql.matches);\n mql.addEventListener('change', handler);\n return () => mql.removeEventListener('change', handler);\n});","/**\n * Graph Mutations — Advanced Operations\n *\n * Split/merge node atoms and drop-target state.\n * Split from graph-mutations.ts for modularity.\n */\n\nimport { atom } from 'jotai';\nimport { graphAtom, graphUpdateVersionAtom, currentGraphIdAtom } from './graph-store';\nimport { nodePositionUpdateCounterAtom } from './graph-position';\nimport { pushHistoryAtom } from './history-store';\nimport { createDebug } from '../utils/debug';\nimport { addEdgeToLocalGraphAtom } from './graph-mutations-edges';\nimport { addNodeToLocalGraphAtom, optimisticDeleteNodeAtom } from './graph-mutations';\nconst debug = createDebug('graph:mutations:advanced');\n\n// --- Drop Target ---\n\n/**\n * ID of the node currently being hovered over as a drop target during drag.\n * Used by drag-to-nest: when a dragged node is released over a target,\n * the dragged node becomes a child of the target.\n */\nexport const dropTargetNodeIdAtom = atom(null);\n\n// --- Split Node ---\n\n/**\n * Split a node into two copies: original stays at position1, clone at position2.\n * All incident edges are duplicated so both copies have the same connections.\n */\nexport const splitNodeAtom = atom(null, (get, set, {\n nodeId,\n position1,\n position2\n}) => {\n const graph = get(graphAtom);\n if (!graph.hasNode(nodeId)) return;\n const attrs = graph.getNodeAttributes(nodeId);\n const graphId = get(currentGraphIdAtom) || attrs.dbData.graph_id;\n\n // Snapshot for undo (must be before any mutations)\n set(pushHistoryAtom, 'Split node');\n\n // Move original to position1 (before addNodeToLocalGraphAtom which calls graph.copy())\n graph.setNodeAttribute(nodeId, 'x', position1.x);\n graph.setNodeAttribute(nodeId, 'y', position1.y);\n\n // Collect edges before addNodeToLocalGraphAtom replaces graph ref\n const edges = [];\n graph.forEachEdge(nodeId, (_key, eAttrs, source, target) => {\n edges.push({\n source,\n target,\n attrs: eAttrs\n });\n });\n\n // Create clone\n const cloneId = `split-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n const cloneDbNode = {\n ...attrs.dbData,\n id: cloneId,\n graph_id: graphId,\n ui_properties: {\n ...(attrs.dbData.ui_properties || {}),\n x: position2.x,\n y: position2.y\n },\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString()\n };\n set(addNodeToLocalGraphAtom, cloneDbNode);\n\n // Duplicate all edges to clone\n for (const edge of edges) {\n const newSource = edge.source === nodeId ? cloneId : edge.source;\n const newTarget = edge.target === nodeId ? cloneId : edge.target;\n set(addEdgeToLocalGraphAtom, {\n ...edge.attrs.dbData,\n id: `split-e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n source_node_id: newSource,\n target_node_id: newTarget\n });\n }\n set(graphUpdateVersionAtom, v => v + 1);\n set(nodePositionUpdateCounterAtom, c => c + 1);\n debug('Split node %s → clone %s', nodeId, cloneId);\n});\n\n// --- Merge Nodes ---\n\n/**\n * Merge multiple nodes into one. The first node in the array survives.\n * Edges from doomed nodes are re-routed to the survivor.\n * Edges between merged nodes are discarded (would be self-loops).\n */\nexport const mergeNodesAtom = atom(null, (get, set, {\n nodeIds\n}) => {\n if (nodeIds.length < 2) return;\n const graph = get(graphAtom);\n const [survivorId, ...doomed] = nodeIds;\n if (!graph.hasNode(survivorId)) return;\n\n // Snapshot for undo (must be before any mutations)\n set(pushHistoryAtom, `Merge ${nodeIds.length} nodes`);\n const doomedSet = new Set(doomed);\n for (const doomedId of doomed) {\n if (!graph.hasNode(doomedId)) continue;\n\n // Collect edges before deletion (dropNode removes them)\n const edges = [];\n graph.forEachEdge(doomedId, (_key, eAttrs, source, target) => {\n edges.push({\n source,\n target,\n attrs: eAttrs\n });\n });\n for (const edge of edges) {\n const newSource = doomedSet.has(edge.source) ? survivorId : edge.source;\n const newTarget = doomedSet.has(edge.target) ? survivorId : edge.target;\n // Skip self-loops (edges between merged nodes)\n if (newSource === newTarget) continue;\n set(addEdgeToLocalGraphAtom, {\n ...edge.attrs.dbData,\n id: `merge-e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n source_node_id: newSource,\n target_node_id: newTarget\n });\n }\n\n // Delete doomed node (auto-removes its original edges)\n set(optimisticDeleteNodeAtom, {\n nodeId: doomedId\n });\n }\n set(graphUpdateVersionAtom, v => v + 1);\n debug('Merged nodes %o → survivor %s', nodeIds, survivorId);\n});","/**\n * Sync state management\n *\n * Tracks sync status, pending mutations, and mutation queue for retry.\n */\n\nimport { atom } from 'jotai';\nimport { createDebug } from '../utils/debug';\nconst debug = createDebug('sync');\n\n// --- Core State Atoms ---\n\n/**\n * Current sync status\n */\nexport const syncStatusAtom = atom('synced');\n\n/**\n * Number of pending (in-flight) mutations\n */\nexport const pendingMutationsCountAtom = atom(0);\n\n/**\n * Network online status\n */\nexport const isOnlineAtom = atom(typeof navigator !== 'undefined' ? navigator.onLine : true);\n\n/**\n * Last sync error message\n */\nexport const lastSyncErrorAtom = atom(null);\n\n/**\n * Last successful sync timestamp\n */\nexport const lastSyncTimeAtom = atom(Date.now());\n\n/**\n * Queued mutations for retry\n */\nexport const mutationQueueAtom = atom([]);\n\n// --- Derived State ---\n\n/**\n * Combined sync state for easy consumption\n */\nexport const syncStateAtom = atom(get => ({\n status: get(syncStatusAtom),\n pendingMutations: get(pendingMutationsCountAtom),\n lastError: get(lastSyncErrorAtom),\n lastSyncTime: get(lastSyncTimeAtom),\n isOnline: get(isOnlineAtom),\n queuedMutations: get(mutationQueueAtom).length\n}));\n\n// --- Mutation Tracking Actions ---\n\n/**\n * Start tracking a mutation (increment counter)\n */\nexport const startMutationAtom = atom(null, (get, set) => {\n const currentCount = get(pendingMutationsCountAtom);\n const newCount = currentCount + 1;\n set(pendingMutationsCountAtom, newCount);\n debug('Mutation started. Pending count: %d -> %d', currentCount, newCount);\n if (newCount > 0 && get(syncStatusAtom) !== 'syncing') {\n set(syncStatusAtom, 'syncing');\n debug('Status -> syncing');\n }\n});\n\n/**\n * Complete a mutation (decrement counter)\n */\nexport const completeMutationAtom = atom(null, (get, set, success = true) => {\n const currentCount = get(pendingMutationsCountAtom);\n const newCount = Math.max(0, currentCount - 1);\n set(pendingMutationsCountAtom, newCount);\n debug('Mutation completed (success: %s). Pending count: %d -> %d', success, currentCount, newCount);\n if (success) {\n set(lastSyncTimeAtom, Date.now());\n if (newCount === 0) {\n set(lastSyncErrorAtom, null);\n }\n }\n\n // Update status if no more pending mutations\n if (newCount === 0) {\n const isOnline = get(isOnlineAtom);\n const hasError = get(lastSyncErrorAtom) !== null;\n if (hasError) {\n set(syncStatusAtom, 'error');\n debug('Status -> error');\n } else if (!isOnline) {\n set(syncStatusAtom, 'offline');\n debug('Status -> offline');\n } else {\n set(syncStatusAtom, 'synced');\n debug('Status -> synced');\n }\n }\n});\n\n/**\n * Track a mutation error\n */\nexport const trackMutationErrorAtom = atom(null, (_get, set, error) => {\n set(lastSyncErrorAtom, error);\n debug('Mutation failed: %s', error);\n});\n\n// --- Network Status ---\n\n/**\n * Set online/offline status\n */\nexport const setOnlineStatusAtom = atom(null, (get, set, isOnline) => {\n set(isOnlineAtom, isOnline);\n const pendingCount = get(pendingMutationsCountAtom);\n const hasError = get(lastSyncErrorAtom) !== null;\n const queueLength = get(mutationQueueAtom).length;\n if (pendingCount === 0) {\n if (hasError || queueLength > 0) {\n set(syncStatusAtom, 'error');\n } else {\n set(syncStatusAtom, isOnline ? 'synced' : 'offline');\n }\n }\n});\n\n// --- Mutation Queue ---\n\n/**\n * Add a mutation to the retry queue\n */\nexport const queueMutationAtom = atom(null, (get, set, mutation) => {\n const queue = get(mutationQueueAtom);\n const newMutation = {\n ...mutation,\n id: crypto.randomUUID(),\n timestamp: Date.now(),\n retryCount: 0,\n maxRetries: mutation.maxRetries ?? 3\n };\n const newQueue = [...queue, newMutation];\n set(mutationQueueAtom, newQueue);\n debug('Queued mutation: %s. Queue size: %d', mutation.type, newQueue.length);\n if (get(pendingMutationsCountAtom) === 0) {\n set(syncStatusAtom, 'error');\n }\n return newMutation.id;\n});\n\n/**\n * Remove a mutation from the queue\n */\nexport const dequeueMutationAtom = atom(null, (get, set, mutationId) => {\n const queue = get(mutationQueueAtom);\n const newQueue = queue.filter(m => m.id !== mutationId);\n set(mutationQueueAtom, newQueue);\n debug('Dequeued mutation: %s. Queue size: %d', mutationId, newQueue.length);\n if (newQueue.length === 0 && get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) {\n set(syncStatusAtom, get(isOnlineAtom) ? 'synced' : 'offline');\n }\n});\n\n/**\n * Increment retry count for a mutation\n */\nexport const incrementRetryCountAtom = atom(null, (get, set, mutationId) => {\n const queue = get(mutationQueueAtom);\n const newQueue = queue.map(m => m.id === mutationId ? {\n ...m,\n retryCount: m.retryCount + 1\n } : m);\n set(mutationQueueAtom, newQueue);\n});\n\n/**\n * Get the next mutation to retry\n */\nexport const getNextQueuedMutationAtom = atom(get => {\n const queue = get(mutationQueueAtom);\n return queue.find(m => m.retryCount < m.maxRetries) ?? null;\n});\n\n/**\n * Clear all queued mutations\n */\nexport const clearMutationQueueAtom = atom(null, (get, set) => {\n set(mutationQueueAtom, []);\n debug('Cleared mutation queue');\n if (get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) {\n set(syncStatusAtom, get(isOnlineAtom) ? 'synced' : 'offline');\n }\n});","/**\n * Gesture configuration for @use-gesture/react\n *\n * Input-source-aware configurations.\n * Components should select the appropriate config based on `primaryInputSourceAtom`.\n */\n\n// =============================================================================\n// Source-Specific Configs\n// =============================================================================\n\n/** Gesture config for finger input (larger thresholds for imprecise touch) */\nconst fingerGestureConfig = {\n eventOptions: {\n passive: false,\n capture: false\n },\n drag: {\n pointer: {\n touch: true,\n keys: false,\n capture: false,\n buttons: -1\n },\n filterTaps: true,\n tapsThreshold: 10,\n // Was 3 — too strict for fingers\n threshold: 10 // Was 3 — needs larger dead zone\n }\n};\n\n/** Gesture config for pencil/stylus input (precise, tight thresholds) */\nconst pencilGestureConfig = {\n eventOptions: {\n passive: false,\n capture: false\n },\n drag: {\n pointer: {\n touch: true,\n keys: false,\n capture: false,\n buttons: -1\n },\n filterTaps: true,\n tapsThreshold: 3,\n threshold: 2 // Very precise — small dead zone\n }\n};\n\n/** Gesture config for mouse input */\nconst mouseGestureConfig = {\n eventOptions: {\n passive: false,\n capture: false\n },\n drag: {\n pointer: {\n touch: true,\n keys: false,\n capture: false,\n buttons: -1\n },\n filterTaps: true,\n tapsThreshold: 5,\n // Was 3\n threshold: 3\n }\n};\n\n// =============================================================================\n// Config Selectors\n// =============================================================================\n\n/**\n * Get the appropriate node gesture config for the given input source.\n */\nexport function getNodeGestureConfig(source) {\n switch (source) {\n case 'finger':\n return fingerGestureConfig;\n case 'pencil':\n return pencilGestureConfig;\n case 'mouse':\n return mouseGestureConfig;\n }\n}\n\n/**\n * Get the appropriate viewport gesture config for the given input source.\n */\nexport function getViewportGestureConfig(source) {\n const base = getNodeGestureConfig(source);\n return {\n ...base,\n eventOptions: {\n passive: false\n },\n pinch: {\n pointer: {\n touch: true\n }\n },\n wheel: {\n eventOptions: {\n passive: false\n }\n }\n };\n}","/**\n * Input Store\n *\n * Jotai atoms tracking active pointers, input sources, and device capabilities.\n * Components read these atoms to adapt behavior per input source.\n *\n * Key derived atoms:\n * - `primaryInputSourceAtom` — the last-used input source ('finger' | 'pencil' | 'mouse')\n * - `isStylusActiveAtom` — true when a pen pointer is currently down\n * - `isMultiTouchAtom` — true when 2+ fingers are touching\n */\n\nimport { atom } from 'jotai';\nimport { detectInputCapabilities } from './input-classifier';\n\n// =============================================================================\n// Core Atoms\n// =============================================================================\n\n/**\n * Map of all currently active (down) pointers.\n * Updated on pointer down/up/cancel events.\n */\nexport const activePointersAtom = atom(new Map());\n\n/**\n * The primary/last-used input source.\n * Updated whenever a pointer goes down — the most recent source wins.\n * Defaults to 'mouse' on desktop, 'finger' on mobile.\n */\nexport const primaryInputSourceAtom = atom('mouse');\n\n/**\n * Device input capabilities.\n * Initialized once on mount, updated when stylus is first detected.\n */\nexport const inputCapabilitiesAtom = atom(detectInputCapabilities());\n\n// =============================================================================\n// Derived Atoms\n// =============================================================================\n\n/**\n * Whether a stylus/pen is currently touching the screen.\n * When true, finger touches should be routed to pan/zoom only (palm rejection).\n */\nexport const isStylusActiveAtom = atom(get => {\n const pointers = get(activePointersAtom);\n for (const [, pointer] of pointers) {\n if (pointer.source === 'pencil') return true;\n }\n return false;\n});\n\n/**\n * Whether multiple fingers are currently touching.\n * When true, the gesture is likely a pinch or two-finger pan.\n */\nexport const isMultiTouchAtom = atom(get => {\n const pointers = get(activePointersAtom);\n let fingerCount = 0;\n for (const [, pointer] of pointers) {\n if (pointer.source === 'finger') fingerCount++;\n }\n return fingerCount > 1;\n});\n\n/**\n * Count of active finger pointers.\n */\nexport const fingerCountAtom = atom(get => {\n const pointers = get(activePointersAtom);\n let count = 0;\n for (const [, pointer] of pointers) {\n if (pointer.source === 'finger') count++;\n }\n return count;\n});\n\n/**\n * Whether the device primarily uses touch (tablet/phone).\n * Used for showing/hiding touch-specific UI (e.g., ViewportControls).\n */\nexport const isTouchDeviceAtom = atom(get => {\n const caps = get(inputCapabilitiesAtom);\n return caps.hasTouch;\n});\n\n// =============================================================================\n// Action Atoms\n// =============================================================================\n\n/**\n * Register a pointer down event.\n * Updates active pointers and primary input source.\n */\nexport const pointerDownAtom = atom(null, (get, set, pointer) => {\n const pointers = new Map(get(activePointersAtom));\n pointers.set(pointer.pointerId, pointer);\n set(activePointersAtom, pointers);\n set(primaryInputSourceAtom, pointer.source);\n\n // Auto-detect stylus capability on first pen event\n if (pointer.source === 'pencil') {\n const caps = get(inputCapabilitiesAtom);\n if (!caps.hasStylus) {\n set(inputCapabilitiesAtom, {\n ...caps,\n hasStylus: true\n });\n }\n }\n});\n\n/**\n * Unregister a pointer up/cancel event.\n */\nexport const pointerUpAtom = atom(null, (get, set, pointerId) => {\n const pointers = new Map(get(activePointersAtom));\n pointers.delete(pointerId);\n set(activePointersAtom, pointers);\n});\n\n/**\n * Clear all active pointers.\n * Call on blur or visibility change to prevent stuck pointers.\n */\nexport const clearPointersAtom = atom(null, (_get, set) => {\n set(activePointersAtom, new Map());\n});","/**\n * Input Classifier\n *\n * Classifies pointer events into input sources (finger, pencil, mouse)\n * and extracts stylus-specific data (pressure, tilt).\n *\n * This is the foundation of the touch-first input architecture:\n * - Pencil draws/selects\n * - Fingers navigate (pan/zoom)\n * - Mouse does both\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** Input device type */\n\n/** Classified pointer with source-specific metadata */\n\n/** Capabilities detected for the current device */\n\n// =============================================================================\n// Classification\n// =============================================================================\n\n/**\n * Classify a PointerEvent into an InputSource with metadata.\n *\n * Uses `PointerEvent.pointerType` as the primary signal:\n * - 'pen' → pencil (Apple Pencil, Surface Pen, Wacom, etc.)\n * - 'touch' → finger\n * - 'mouse' → mouse\n *\n * Falls back to 'mouse' for unknown pointer types.\n */\nexport function classifyPointer(e) {\n const source = pointerTypeToSource(e.pointerType);\n return {\n source,\n pointerId: e.pointerId,\n pressure: e.pressure,\n tiltX: e.tiltX,\n tiltY: e.tiltY,\n isPrimary: e.isPrimary,\n rawPointerType: e.pointerType\n };\n}\n\n/**\n * Map PointerEvent.pointerType string to InputSource.\n */\nfunction pointerTypeToSource(pointerType) {\n switch (pointerType) {\n case 'pen':\n return 'pencil';\n case 'touch':\n return 'finger';\n case 'mouse':\n return 'mouse';\n default:\n // Unknown pointer types (e.g. future devices) default to mouse behavior\n return 'mouse';\n }\n}\n\n// =============================================================================\n// Device Capability Detection\n// =============================================================================\n\n/**\n * Detect input capabilities of the current device.\n *\n * Uses media queries and navigator APIs for initial detection.\n * Note: `hasStylus` starts as false and is set to true on first pen event\n * (there's no reliable way to detect stylus support without an event).\n */\nexport function detectInputCapabilities() {\n if (typeof window === 'undefined') {\n // SSR: assume desktop defaults\n return {\n hasTouch: false,\n hasStylus: false,\n hasMouse: true,\n hasCoarsePointer: false\n };\n }\n const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;\n\n // matchMedia for pointer capabilities\n const supportsMatchMedia = typeof window.matchMedia === 'function';\n const hasCoarsePointer = supportsMatchMedia ? window.matchMedia('(pointer: coarse)').matches : false;\n const hasFinePointer = supportsMatchMedia ? window.matchMedia('(pointer: fine)').matches : true;\n\n // Heuristic: if we have both coarse and fine, likely a tablet with stylus support\n // But we won't confirm stylus until we see a pen event\n const hasMouse = hasFinePointer || !hasTouch;\n return {\n hasTouch,\n hasStylus: false,\n // Set to true on first pen event\n hasMouse,\n hasCoarsePointer\n };\n}\n\n// =============================================================================\n// Gesture Threshold Helpers\n// =============================================================================\n\n/** Threshold values per input source */\n\n/**\n * Get gesture thresholds appropriate for the given input source.\n *\n * Fingers need larger thresholds (imprecise, large contact area).\n * Stylus is the most precise.\n * Mouse is in between.\n */\nexport function getGestureThresholds(source) {\n switch (source) {\n case 'finger':\n return {\n dragThreshold: 10,\n tapThreshold: 10,\n longPressDuration: 600,\n longPressMoveLimit: 10\n };\n case 'pencil':\n return {\n dragThreshold: 2,\n tapThreshold: 3,\n longPressDuration: 500,\n longPressMoveLimit: 5\n };\n case 'mouse':\n return {\n dragThreshold: 3,\n tapThreshold: 5,\n longPressDuration: 0,\n // Mouse uses right-click instead\n longPressMoveLimit: 0\n };\n }\n}\n\n// =============================================================================\n// Hit Target Helpers\n// =============================================================================\n\n/** Minimum interactive target sizes per Apple HIG / Material Design */\nexport const HIT_TARGET_SIZES = {\n /** Minimum touch target (Apple HIG: 44pt) */\n finger: 44,\n /** Stylus target (precise, can use smaller targets) */\n pencil: 24,\n /** Mouse target (hover-discoverable, smallest) */\n mouse: 16\n};\n\n/**\n * Get the appropriate hit target size for the current input source.\n * Used to size invisible padding around small visual elements\n * (resize handles, ports, edge connection handles).\n */\nexport function getHitTargetSize(source) {\n return HIT_TARGET_SIZES[source];\n}","/**\n * Hit-testing utilities for canvas elements.\n *\n * Abstracts `document.elementFromPoint` / `document.elementsFromPoint`\n * behind testable functions. In tests or SSR, consumers can provide\n * a mock implementation via `setHitTestProvider`.\n */\n\n// --- Types ---\n\n// --- Default provider (real DOM) ---\n\nconst defaultProvider = {\n elementFromPoint: (x, y) => document.elementFromPoint(x, y),\n elementsFromPoint: (x, y) => document.elementsFromPoint(x, y)\n};\nlet _provider = defaultProvider;\n\n/**\n * Override the hit-test provider (for testing or SSR).\n * Pass `null` to restore the default DOM provider.\n */\nexport function setHitTestProvider(provider) {\n _provider = provider ?? defaultProvider;\n}\n\n// --- Hit-test functions ---\n\n/**\n * Find the canvas node at a screen position.\n * Looks for the nearest ancestor with `[data-node-id]`.\n */\nexport function hitTestNode(screenX, screenY) {\n const element = _provider.elementFromPoint(screenX, screenY);\n const nodeElement = element?.closest('[data-node-id]') ?? null;\n const nodeId = nodeElement?.getAttribute('data-node-id') ?? null;\n return {\n nodeId,\n element: nodeElement\n };\n}\n\n/**\n * Find a port at a screen position.\n * Scans through all elements at the position for `[data-drag-port-id]`.\n */\nexport function hitTestPort(screenX, screenY) {\n const elements = _provider.elementsFromPoint(screenX, screenY);\n for (const el of elements) {\n const portElement = el.closest('[data-drag-port-id]');\n if (portElement) {\n const portId = portElement.dataset.dragPortId;\n const portBar = portElement.closest('[data-port-bar]');\n const nodeId = portBar?.dataset.nodeId;\n if (portId && nodeId) {\n return {\n nodeId,\n portId\n };\n }\n }\n }\n return null;\n}","/**\n * useDragStateMachine\n *\n * Pure drag state helpers extracted from useNodeDrag.\n * Handles: memo initialization, multi-select expansion,\n * and gesture instance tracking.\n *\n * @since 1.6.0\n */\n\nimport { getNodeDescendants } from '../core/group-store';\n/**\n * Build the initial positions map for a drag operation.\n * Includes all selected nodes + their group descendants.\n */\nexport function buildDragPositions(graph, selectedNodeIds) {\n const positions = new Map();\n\n // Collect selected nodes\n for (const nodeId of selectedNodeIds) {\n if (graph.hasNode(nodeId)) {\n const attrs = graph.getNodeAttributes(nodeId);\n positions.set(nodeId, {\n x: attrs.x,\n y: attrs.y\n });\n }\n }\n\n // Expand to include descendants of any group nodes\n const currentKeys = Array.from(positions.keys());\n for (const nodeId of currentKeys) {\n const descendants = getNodeDescendants(graph, nodeId);\n for (const descId of descendants) {\n if (!positions.has(descId) && graph.hasNode(descId)) {\n const attrs = graph.getNodeAttributes(descId);\n positions.set(descId, {\n x: attrs.x,\n y: attrs.y\n });\n }\n }\n }\n return positions;\n}\n\n/**\n * Compute the position updates for all dragged nodes given\n * pixel movement and zoom level.\n */\nexport function computeDragUpdates(initialPositions, movementX, movementY, zoom, graph) {\n const deltaX = movementX / zoom;\n const deltaY = movementY / zoom;\n const updates = [];\n initialPositions.forEach((initialPos, nodeId) => {\n if (graph.hasNode(nodeId)) {\n updates.push({\n id: nodeId,\n pos: {\n x: initialPos.x + deltaX,\n y: initialPos.y + deltaY\n }\n });\n }\n });\n return updates;\n}\n\n/**\n * Check whether a pointer target should prevent drag.\n */\nexport function isDragPrevented(target) {\n return !!target.closest('[data-no-drag=\"true\"]') || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT';\n}","import { c as _c } from \"react/compiler-runtime\";\n/**\n * Hook for managing node resize behavior\n */\n\nimport { useAtomValue, useSetAtom } from 'jotai';\nimport { useRef, useState, useEffect } from 'react';\nimport { flushSync } from 'react-dom';\nimport { graphAtom, currentGraphIdAtom, graphUpdateVersionAtom } from '../core/graph-store';\nimport { nodePositionUpdateCounterAtom } from '../core/graph-position';\nimport { zoomAtom } from '../core/viewport-store';\nimport { startMutationAtom, completeMutationAtom } from '../core/sync-store';\nimport { createDebug } from '../utils/debug';\nconst debug = createDebug('resize');\n/**\n * Hook for node resize behavior\n */\nexport function useNodeResize(t0) {\n const $ = _c(38);\n const {\n id,\n nodeData,\n updateNodePositions,\n options: t1\n } = t0;\n let t2;\n if ($[0] !== t1) {\n t2 = t1 === undefined ? {} : t1;\n $[0] = t1;\n $[1] = t2;\n } else {\n t2 = $[1];\n }\n const options = t2;\n const {\n onPersist,\n onPersistError,\n minWidth: t3,\n minHeight: t4\n } = options;\n const minWidth = t3 === undefined ? 200 : t3;\n const minHeight = t4 === undefined ? 150 : t4;\n const [localWidth, setLocalWidth] = useState(nodeData.width || 500);\n const [localHeight, setLocalHeight] = useState(nodeData.height || 500);\n const [isResizing, setIsResizing] = useState(false);\n const resizeStartRef = useRef(null);\n const graph = useAtomValue(graphAtom);\n const currentZoom = useAtomValue(zoomAtom);\n const currentGraphId = useAtomValue(currentGraphIdAtom);\n const startMutation = useSetAtom(startMutationAtom);\n const completeMutation = useSetAtom(completeMutationAtom);\n const setGraphUpdateVersion = useSetAtom(graphUpdateVersionAtom);\n const setNodePositionUpdateCounter = useSetAtom(nodePositionUpdateCounterAtom);\n let t5;\n let t6;\n if ($[2] !== isResizing || $[3] !== nodeData.height || $[4] !== nodeData.width) {\n t5 = () => {\n if (!isResizing) {\n setLocalWidth(nodeData.width || 500);\n setLocalHeight(nodeData.height || 500);\n }\n };\n t6 = [nodeData.width, nodeData.height, isResizing];\n $[2] = isResizing;\n $[3] = nodeData.height;\n $[4] = nodeData.width;\n $[5] = t5;\n $[6] = t6;\n } else {\n t5 = $[5];\n t6 = $[6];\n }\n useEffect(t5, t6);\n let t7;\n if ($[7] !== graph || $[8] !== id || $[9] !== localHeight || $[10] !== localWidth) {\n t7 = direction => e => {\n e.stopPropagation();\n e.preventDefault();\n setIsResizing(true);\n const nodeAttrs = graph.hasNode(id) ? graph.getNodeAttributes(id) : {\n x: 0,\n y: 0\n };\n resizeStartRef.current = {\n width: localWidth,\n height: localHeight,\n startX: e.clientX,\n startY: e.clientY,\n startNodeX: nodeAttrs.x,\n startNodeY: nodeAttrs.y,\n direction\n };\n e.target.setPointerCapture(e.pointerId);\n };\n $[7] = graph;\n $[8] = id;\n $[9] = localHeight;\n $[10] = localWidth;\n $[11] = t7;\n } else {\n t7 = $[11];\n }\n const createResizeStart = t7;\n let t8;\n if ($[12] !== currentZoom || $[13] !== graph || $[14] !== id || $[15] !== minHeight || $[16] !== minWidth || $[17] !== setGraphUpdateVersion || $[18] !== setNodePositionUpdateCounter || $[19] !== updateNodePositions) {\n t8 = e_0 => {\n if (!resizeStartRef.current) {\n return;\n }\n e_0.stopPropagation();\n e_0.preventDefault();\n const deltaX = (e_0.clientX - resizeStartRef.current.startX) / currentZoom;\n const deltaY = (e_0.clientY - resizeStartRef.current.startY) / currentZoom;\n const {\n direction: direction_0,\n width: startWidth,\n height: startHeight,\n startNodeX,\n startNodeY\n } = resizeStartRef.current;\n let newWidth = startWidth;\n let newHeight = startHeight;\n let newX = startNodeX;\n let newY = startNodeY;\n if (direction_0.includes(\"e\")) {\n newWidth = Math.max(minWidth, startWidth + deltaX);\n }\n if (direction_0.includes(\"w\")) {\n newWidth = Math.max(minWidth, startWidth - deltaX);\n newX = startNodeX + (startWidth - newWidth);\n }\n if (direction_0.includes(\"s\")) {\n newHeight = Math.max(minHeight, startHeight + deltaY);\n }\n if (direction_0.includes(\"n\")) {\n newHeight = Math.max(minHeight, startHeight - deltaY);\n newY = startNodeY + (startHeight - newHeight);\n }\n if (graph.hasNode(id)) {\n graph.setNodeAttribute(id, \"width\", newWidth);\n graph.setNodeAttribute(id, \"height\", newHeight);\n graph.setNodeAttribute(id, \"x\", newX);\n graph.setNodeAttribute(id, \"y\", newY);\n }\n flushSync(() => {\n setLocalWidth(newWidth);\n setLocalHeight(newHeight);\n setGraphUpdateVersion(_temp);\n });\n if (direction_0.includes(\"n\") || direction_0.includes(\"w\")) {\n updateNodePositions([{\n id,\n pos: {\n x: newX,\n y: newY\n }\n }]);\n } else {\n setNodePositionUpdateCounter(_temp2);\n }\n };\n $[12] = currentZoom;\n $[13] = graph;\n $[14] = id;\n $[15] = minHeight;\n $[16] = minWidth;\n $[17] = setGraphUpdateVersion;\n $[18] = setNodePositionUpdateCounter;\n $[19] = updateNodePositions;\n $[20] = t8;\n } else {\n t8 = $[20];\n }\n const handleResizeMove = t8;\n let t9;\n if ($[21] !== completeMutation || $[22] !== currentGraphId || $[23] !== graph || $[24] !== id || $[25] !== localHeight || $[26] !== localWidth || $[27] !== onPersist || $[28] !== onPersistError || $[29] !== startMutation) {\n t9 = e_1 => {\n if (!resizeStartRef.current) {\n return;\n }\n e_1.stopPropagation();\n e_1.target.releasePointerCapture(e_1.pointerId);\n setIsResizing(false);\n if (!currentGraphId || !resizeStartRef.current) {\n resizeStartRef.current = null;\n return;\n }\n const finalAttrs = graph.hasNode(id) ? graph.getNodeAttributes(id) : null;\n if (!finalAttrs) {\n resizeStartRef.current = null;\n return;\n }\n const finalWidth = finalAttrs.width || localWidth;\n const finalHeight = finalAttrs.height || localHeight;\n const finalX = finalAttrs.x;\n const finalY = finalAttrs.y;\n setLocalWidth(finalWidth);\n setLocalHeight(finalHeight);\n if (!onPersist) {\n resizeStartRef.current = null;\n return;\n }\n const existingDbUiProps = typeof finalAttrs.dbData.ui_properties === \"object\" && finalAttrs.dbData.ui_properties !== null && !Array.isArray(finalAttrs.dbData.ui_properties) ? finalAttrs.dbData.ui_properties : {};\n const newUiProperties = {\n ...existingDbUiProps,\n width: finalWidth,\n height: finalHeight,\n x: finalX,\n y: finalY\n };\n startMutation();\n onPersist(id, currentGraphId, newUiProperties).then(() => {\n completeMutation(true);\n }).catch(error => {\n completeMutation(false);\n if (resizeStartRef.current) {\n setLocalWidth(resizeStartRef.current.width);\n setLocalHeight(resizeStartRef.current.height);\n }\n onPersistError?.(id, error);\n }).finally(() => {\n resizeStartRef.current = null;\n });\n };\n $[21] = completeMutation;\n $[22] = currentGraphId;\n $[23] = graph;\n $[24] = id;\n $[25] = localHeight;\n $[26] = localWidth;\n $[27] = onPersist;\n $[28] = onPersistError;\n $[29] = startMutation;\n $[30] = t9;\n } else {\n t9 = $[30];\n }\n const handleResizeEnd = t9;\n let t10;\n if ($[31] !== createResizeStart || $[32] !== handleResizeEnd || $[33] !== handleResizeMove || $[34] !== isResizing || $[35] !== localHeight || $[36] !== localWidth) {\n t10 = {\n localWidth,\n localHeight,\n isResizing,\n createResizeStart,\n handleResizeMove,\n handleResizeEnd\n };\n $[31] = createResizeStart;\n $[32] = handleResizeEnd;\n $[33] = handleResizeMove;\n $[34] = isResizing;\n $[35] = localHeight;\n $[36] = localWidth;\n $[37] = t10;\n } else {\n t10 = $[37];\n }\n return t10;\n}\nfunction _temp2(c) {\n return c + 1;\n}\nfunction _temp(v) {\n return v + 1;\n}","import { c as _c } from \"react/compiler-runtime\";\n/**\n * useCanvasHistory Hook\n *\n * Provides undo/redo functionality for canvas operations.\n */\n\nimport { useAtomValue, useSetAtom, useStore } from 'jotai';\nimport { canUndoAtom, canRedoAtom, undoAtom, redoAtom, pushHistoryAtom, clearHistoryAtom, undoCountAtom, redoCountAtom, historyLabelsAtom, historyStateAtom } from '../core/history-store';\nimport { showToastAtom } from '../core/toast-store';\n/**\n * Hook for undo/redo functionality.\n *\n * @example\n * ```tsx\n * function CanvasToolbar() {\n * const { undo, redo, canUndo, canRedo, recordSnapshot } = useCanvasHistory();\n *\n * // Before making changes, record a snapshot:\n * const handleAddNode = () => {\n * recordSnapshot('Add node');\n * addNode({ ... });\n * };\n *\n * return (\n * <>\n * \n * \n * >\n * );\n * }\n * ```\n */\nexport function useCanvasHistory(t0) {\n const $ = _c(22);\n const options = t0 === undefined ? {} : t0;\n const {\n enableKeyboardShortcuts: t1\n } = options;\n t1 === undefined ? false : t1;\n const canUndo = useAtomValue(canUndoAtom);\n const canRedo = useAtomValue(canRedoAtom);\n const undoCount = useAtomValue(undoCountAtom);\n const redoCount = useAtomValue(redoCountAtom);\n const historyLabels = useAtomValue(historyLabelsAtom);\n const undoAction = useSetAtom(undoAtom);\n const redoAction = useSetAtom(redoAtom);\n const pushHistory = useSetAtom(pushHistoryAtom);\n const clearHistory = useSetAtom(clearHistoryAtom);\n const showToast = useSetAtom(showToastAtom);\n const store = useStore();\n let t2;\n if ($[0] !== showToast || $[1] !== store || $[2] !== undoAction) {\n t2 = () => {\n const state = store.get(historyStateAtom);\n const label = state.past[state.past.length - 1]?.label;\n const result = undoAction();\n if (result && label) {\n showToast(`Undo: ${label}`);\n }\n return result;\n };\n $[0] = showToast;\n $[1] = store;\n $[2] = undoAction;\n $[3] = t2;\n } else {\n t2 = $[3];\n }\n const undo = t2;\n let t3;\n if ($[4] !== redoAction || $[5] !== showToast || $[6] !== store) {\n t3 = () => {\n const state_0 = store.get(historyStateAtom);\n const label_0 = state_0.future[0]?.label;\n const result_0 = redoAction();\n if (result_0 && label_0) {\n showToast(`Redo: ${label_0}`);\n }\n return result_0;\n };\n $[4] = redoAction;\n $[5] = showToast;\n $[6] = store;\n $[7] = t3;\n } else {\n t3 = $[7];\n }\n const redo = t3;\n let t4;\n if ($[8] !== pushHistory) {\n t4 = label_1 => {\n pushHistory(label_1);\n };\n $[8] = pushHistory;\n $[9] = t4;\n } else {\n t4 = $[9];\n }\n const recordSnapshot = t4;\n let t5;\n if ($[10] !== clearHistory) {\n t5 = () => {\n clearHistory();\n };\n $[10] = clearHistory;\n $[11] = t5;\n } else {\n t5 = $[11];\n }\n const clear = t5;\n let t6;\n if ($[12] !== canRedo || $[13] !== canUndo || $[14] !== clear || $[15] !== historyLabels || $[16] !== recordSnapshot || $[17] !== redo || $[18] !== redoCount || $[19] !== undo || $[20] !== undoCount) {\n t6 = {\n undo,\n redo,\n canUndo,\n canRedo,\n undoCount,\n redoCount,\n historyLabels,\n recordSnapshot,\n clear\n };\n $[12] = canRedo;\n $[13] = canUndo;\n $[14] = clear;\n $[15] = historyLabels;\n $[16] = recordSnapshot;\n $[17] = redo;\n $[18] = redoCount;\n $[19] = undo;\n $[20] = undoCount;\n $[21] = t6;\n } else {\n t6 = $[21];\n }\n return t6;\n}","/**\n * Toast Store\n *\n * Lightweight toast notification system for canvas operations.\n * Shows brief, non-interactive messages (e.g., \"Undo: Split node\").\n */\n\nimport { atom } from 'jotai';\n/** Current visible toast, or null */\nexport const canvasToastAtom = atom(null);\n\n/** Show a toast message that auto-clears after 2 seconds */\nexport const showToastAtom = atom(null, (_get, set, message) => {\n const id = `toast-${Date.now()}`;\n set(canvasToastAtom, {\n id,\n message,\n timestamp: Date.now()\n });\n setTimeout(() => {\n set(canvasToastAtom, current => current?.id === id ? null : current);\n }, 2000);\n});","import { c as _c } from \"react/compiler-runtime\";\n/**\n * Canvas Selection Subscription Hook\n *\n * Provides reactive access to canvas selection state.\n * Use this to respond to selection changes in your components.\n *\n * @since 0.2.0\n * @example\n * ```tsx\n * function SelectionInfo() {\n * const { selectedNodeIds, selectedEdgeId, hasSelection } = useCanvasSelection();\n * return
Rendering {visibleNodes}/{totalNodes} nodes ({culledNodes} culled)
\n * \n *