var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/hooks/useNodeSelection.ts import { c as _c } from "react/compiler-runtime"; import { useAtom, useSetAtom } from "jotai"; // src/core/selection-store.ts import { atom } from "jotai"; // src/utils/debug.ts import debugFactory from "debug"; var NAMESPACE = "canvas"; function createDebug(module) { const base = debugFactory(`${NAMESPACE}:${module}`); const warn = debugFactory(`${NAMESPACE}:${module}:warn`); const error = debugFactory(`${NAMESPACE}:${module}:error`); warn.enabled = true; error.enabled = true; warn.log = console.warn.bind(console); error.log = console.error.bind(console); const debugFn = Object.assign(base, { warn, error }); return debugFn; } var debug = { graph: { node: createDebug("graph:node"), edge: createDebug("graph:edge"), sync: createDebug("graph:sync") }, ui: { selection: createDebug("ui:selection"), drag: createDebug("ui:drag"), resize: createDebug("ui:resize") }, sync: { status: createDebug("sync:status"), mutations: createDebug("sync:mutations"), queue: createDebug("sync:queue") }, viewport: createDebug("viewport") }; // src/core/selection-store.ts var debug2 = createDebug("selection"); var selectedNodeIdsAtom = atom(/* @__PURE__ */ new Set()); var selectedEdgeIdAtom = atom(null); var handleNodePointerDownSelectionAtom = atom(null, (get, set, { nodeId, isShiftPressed }) => { const currentSelection = get(selectedNodeIdsAtom); debug2("handleNodePointerDownSelection: nodeId=%s, shift=%s, current=%o", nodeId, isShiftPressed, Array.from(currentSelection)); set(selectedEdgeIdAtom, null); if (isShiftPressed) { const newSelection = new Set(currentSelection); if (newSelection.has(nodeId)) { newSelection.delete(nodeId); } else { newSelection.add(nodeId); } debug2("Shift-click, setting selection to: %o", Array.from(newSelection)); set(selectedNodeIdsAtom, newSelection); } else { if (!currentSelection.has(nodeId)) { debug2("Node not in selection, selecting: %s", nodeId); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set([nodeId])); } else { debug2("Node already selected, preserving multi-select"); } } }); var selectSingleNodeAtom = atom(null, (get, set, nodeId) => { debug2("selectSingleNode: %s", nodeId); set(selectedEdgeIdAtom, null); if (nodeId === null || nodeId === void 0) { debug2("Clearing selection"); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); } else { const currentSelection = get(selectedNodeIdsAtom); if (currentSelection.has(nodeId) && currentSelection.size === 1) { return; } set(selectedNodeIdsAtom, /* @__PURE__ */ new Set([nodeId])); } }); var toggleNodeInSelectionAtom = atom(null, (get, set, nodeId) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); if (newSelection.has(nodeId)) { newSelection.delete(nodeId); } else { newSelection.add(nodeId); } set(selectedNodeIdsAtom, newSelection); }); var clearSelectionAtom = atom(null, (_get, set) => { debug2("clearSelection"); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); }); var addNodesToSelectionAtom = atom(null, (get, set, nodeIds) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); for (const nodeId of nodeIds) { newSelection.add(nodeId); } set(selectedNodeIdsAtom, newSelection); }); var removeNodesFromSelectionAtom = atom(null, (get, set, nodeIds) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); for (const nodeId of nodeIds) { newSelection.delete(nodeId); } set(selectedNodeIdsAtom, newSelection); }); var selectEdgeAtom = atom(null, (get, set, edgeId) => { set(selectedEdgeIdAtom, edgeId); if (edgeId !== null) { set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); } }); var clearEdgeSelectionAtom = atom(null, (_get, set) => { set(selectedEdgeIdAtom, null); }); var focusedNodeIdAtom = atom(null); var setFocusedNodeAtom = atom(null, (_get, set, nodeId) => { set(focusedNodeIdAtom, nodeId); }); var hasFocusedNodeAtom = atom((get) => get(focusedNodeIdAtom) !== null); var selectedNodesCountAtom = atom((get) => get(selectedNodeIdsAtom).size); var hasSelectionAtom = atom((get) => get(selectedNodeIdsAtom).size > 0); // src/hooks/useNodeSelection.ts function useNodeSelection(nodeId) { const $ = _c(13); const [selectedIds] = useAtom(selectedNodeIdsAtom); const selectSingle = useSetAtom(selectSingleNodeAtom); const toggleNode = useSetAtom(toggleNodeInSelectionAtom); let t0; if ($[0] !== nodeId || $[1] !== selectedIds) { t0 = selectedIds.has(nodeId); $[0] = nodeId; $[1] = selectedIds; $[2] = t0; } else { t0 = $[2]; } let t1; if ($[3] !== nodeId || $[4] !== selectSingle) { t1 = () => selectSingle(nodeId); $[3] = nodeId; $[4] = selectSingle; $[5] = t1; } else { t1 = $[5]; } let t2; if ($[6] !== nodeId || $[7] !== toggleNode) { t2 = () => toggleNode(nodeId); $[6] = nodeId; $[7] = toggleNode; $[8] = t2; } else { t2 = $[8]; } let t3; if ($[9] !== t0 || $[10] !== t1 || $[11] !== t2) { t3 = { isSelected: t0, selectNode: t1, toggleNode: t2 }; $[9] = t0; $[10] = t1; $[11] = t2; $[12] = t3; } else { t3 = $[12]; } return t3; } // src/hooks/useNodeDrag.ts import { c as _c2 } from "react/compiler-runtime"; import { useAtom as useAtom2, useAtomValue, useSetAtom as useSetAtom2, atom as atom15 } from "jotai"; import { useGesture } from "@use-gesture/react"; import { useRef } from "react"; // src/core/graph-store.ts import { atom as atom2 } from "jotai"; import Graph from "graphology"; var graphOptions = { type: "directed", multi: true, allowSelfLoops: true }; var currentGraphIdAtom = atom2(null); var graphAtom = atom2(new Graph(graphOptions)); var graphUpdateVersionAtom = atom2(0); var edgeCreationAtom = atom2({ isCreating: false, sourceNodeId: null, sourceNodePosition: null, targetPosition: null, hoveredTargetNodeId: null, sourceHandle: null, targetHandle: null, sourcePort: null, targetPort: null, snappedTargetPosition: null }); var draggingNodeIdAtom = atom2(null); var preDragNodeAttributesAtom = atom2(null); // src/core/graph-position.ts import { atom as atom4 } from "jotai"; import { atomFamily } from "jotai-family"; import Graph2 from "graphology"; // src/utils/mutation-queue.ts var pendingNodeMutations = /* @__PURE__ */ new Map(); function getPendingState(nodeId) { let state = pendingNodeMutations.get(nodeId); if (!state) { state = { inFlight: false, queuedPosition: null, queuedUiProperties: null, graphId: null }; pendingNodeMutations.set(nodeId, state); } return state; } function clearAllPendingMutations() { pendingNodeMutations.clear(); } // src/core/perf.ts import { atom as atom3 } from "jotai"; var perfEnabledAtom = atom3(false); var _enabled = false; function setPerfEnabled(enabled) { _enabled = enabled; } if (typeof window !== "undefined") { window.__canvasPerf = setPerfEnabled; } function canvasMark(name) { if (!_enabled) return _noop; const markName = `canvas:${name}`; try { performance.mark(markName); } catch { return _noop; } return () => { try { performance.measure(`canvas:${name}`, markName); } catch { } }; } function _noop() { } // src/core/graph-position.ts var debug3 = createDebug("graph:position"); var _positionCacheByGraph = /* @__PURE__ */ new WeakMap(); function getPositionCache(graph) { let cache = _positionCacheByGraph.get(graph); if (!cache) { cache = /* @__PURE__ */ new Map(); _positionCacheByGraph.set(graph, cache); } return cache; } var nodePositionUpdateCounterAtom = atom4(0); var nodePositionAtomFamily = atomFamily((nodeId) => atom4((get) => { get(nodePositionUpdateCounterAtom); const graph = get(graphAtom); if (!graph.hasNode(nodeId)) { return { x: 0, y: 0 }; } const x = graph.getNodeAttribute(nodeId, "x"); const y = graph.getNodeAttribute(nodeId, "y"); const cache = getPositionCache(graph); const prev = cache.get(nodeId); if (prev && prev.x === x && prev.y === y) { return prev; } const pos = { x, y }; cache.set(nodeId, pos); return pos; })); var updateNodePositionAtom = atom4(null, (get, set, { nodeId, position }) => { const end = canvasMark("drag-frame"); const graph = get(graphAtom); if (graph.hasNode(nodeId)) { debug3("Updating node %s position to %o", nodeId, position); graph.setNodeAttribute(nodeId, "x", position.x); graph.setNodeAttribute(nodeId, "y", position.y); set(nodePositionUpdateCounterAtom, (c) => c + 1); } end(); }); var cleanupNodePositionAtom = atom4(null, (get, _set, nodeId) => { nodePositionAtomFamily.remove(nodeId); const graph = get(graphAtom); getPositionCache(graph).delete(nodeId); debug3("Removed position atom for node: %s", nodeId); }); var cleanupAllNodePositionsAtom = atom4(null, (get, _set) => { const graph = get(graphAtom); const nodeIds = graph.nodes(); nodeIds.forEach((nodeId) => { nodePositionAtomFamily.remove(nodeId); }); _positionCacheByGraph.delete(graph); debug3("Removed %d position atoms", nodeIds.length); }); var clearGraphOnSwitchAtom = atom4(null, (get, set) => { debug3("Clearing graph for switch"); set(cleanupAllNodePositionsAtom); clearAllPendingMutations(); const emptyGraph = new Graph2(graphOptions); set(graphAtom, emptyGraph); set(graphUpdateVersionAtom, (v) => v + 1); }); // src/core/graph-mutations.ts import { atom as atom12 } from "jotai"; import Graph3 from "graphology"; // src/core/graph-derived.ts import { atom as atom8 } from "jotai"; import { atomFamily as atomFamily2 } from "jotai-family"; // src/core/viewport-store.ts import { atom as atom5 } from "jotai"; // src/utils/layout.ts var FitToBoundsMode = /* @__PURE__ */ (function(FitToBoundsMode2) { FitToBoundsMode2["Graph"] = "graph"; FitToBoundsMode2["Selection"] = "selection"; return FitToBoundsMode2; })({}); var calculateBounds = (nodes) => { if (nodes.length === 0) { return { x: 0, y: 0, width: 0, height: 0 }; } const minX = Math.min(...nodes.map((node) => node.x)); const minY = Math.min(...nodes.map((node) => node.y)); const maxX = Math.max(...nodes.map((node) => node.x + node.width)); const maxY = Math.max(...nodes.map((node) => node.y + node.height)); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }; function getNodeCenter(node) { return { x: node.x + node.width / 2, y: node.y + node.height / 2 }; } function checkNodesOverlap(node1, node2) { const center1 = getNodeCenter(node1); const center2 = getNodeCenter(node2); const dx = Math.abs(center1.x - center2.x); const dy = Math.abs(center1.y - center2.y); const minDistanceX = (node1.width + node2.width) / 2; const minDistanceY = (node1.height + node2.height) / 2; return dx < minDistanceX && dy < minDistanceY; } // src/core/viewport-store.ts var zoomAtom = atom5(1); var panAtom = atom5({ x: 0, y: 0 }); var viewportRectAtom = atom5(null); var screenToWorldAtom = atom5((get) => { return (screenX, screenY) => { const pan = get(panAtom); const zoom = get(zoomAtom); const rect = get(viewportRectAtom); if (!rect) { return { x: screenX, y: screenY }; } const relativeX = screenX - rect.left; const relativeY = screenY - rect.top; return { x: (relativeX - pan.x) / zoom, y: (relativeY - pan.y) / zoom }; }; }); var worldToScreenAtom = atom5((get) => { return (worldX, worldY) => { const pan = get(panAtom); const zoom = get(zoomAtom); const rect = get(viewportRectAtom); if (!rect) { return { x: worldX, y: worldY }; } return { x: worldX * zoom + pan.x + rect.left, y: worldY * zoom + pan.y + rect.top }; }; }); var setZoomAtom = atom5(null, (get, set, { zoom, centerX, centerY }) => { const currentZoom = get(zoomAtom); const pan = get(panAtom); const rect = get(viewportRectAtom); const newZoom = Math.max(0.1, Math.min(5, zoom)); if (centerX !== void 0 && centerY !== void 0 && rect) { const relativeX = centerX - rect.left; const relativeY = centerY - rect.top; const worldX = (relativeX - pan.x) / currentZoom; const worldY = (relativeY - pan.y) / currentZoom; const newPanX = relativeX - worldX * newZoom; const newPanY = relativeY - worldY * newZoom; set(panAtom, { x: newPanX, y: newPanY }); } set(zoomAtom, newZoom); }); var resetViewportAtom = atom5(null, (_get, set) => { set(zoomAtom, 1); set(panAtom, { x: 0, y: 0 }); }); var fitToBoundsAtom = atom5(null, (get, set, { mode, padding = 20 }) => { const normalizedMode = typeof mode === "string" ? mode === "graph" ? FitToBoundsMode.Graph : FitToBoundsMode.Selection : mode; const viewportSize = get(viewportRectAtom); if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return; get(nodePositionUpdateCounterAtom); let bounds; if (normalizedMode === FitToBoundsMode.Graph) { const graph = get(graphAtom); const nodes = graph.nodes().map((node) => { const attrs = graph.getNodeAttributes(node); return { x: attrs.x, y: attrs.y, width: attrs.width || 500, height: attrs.height || 500 }; }); bounds = calculateBounds(nodes); } else { const selectedIds = get(selectedNodeIdsAtom); const allNodes = get(uiNodesAtom); const selectedNodes = allNodes.filter((n) => selectedIds.has(n.id)).map((n) => ({ x: n.position.x, y: n.position.y, width: n.width ?? 500, height: n.height ?? 500 })); bounds = calculateBounds(selectedNodes); } if (bounds.width <= 0 || bounds.height <= 0) return; const maxHPad = Math.max(0, viewportSize.width / 2 - 1); const maxVPad = Math.max(0, viewportSize.height / 2 - 1); const safePadding = Math.max(0, Math.min(padding, maxHPad, maxVPad)); const effW = Math.max(1, viewportSize.width - 2 * safePadding); const effH = Math.max(1, viewportSize.height - 2 * safePadding); const scale = Math.min(effW / bounds.width, effH / bounds.height); if (scale <= 0 || !isFinite(scale)) return; set(zoomAtom, scale); const scaledW = bounds.width * scale; const scaledH = bounds.height * scale; const startX = safePadding + (effW - scaledW) / 2; const startY = safePadding + (effH - scaledH) / 2; set(panAtom, { x: startX - bounds.x * scale, y: startY - bounds.y * scale }); }); var centerOnNodeAtom = atom5(null, (get, set, nodeId) => { const nodes = get(uiNodesAtom); const node = nodes.find((n) => n.id === nodeId); if (!node) return; const { x, y, width = 200, height = 100 } = node; const zoom = get(zoomAtom); const centerX = x + width / 2; const centerY = y + height / 2; const rect = get(viewportRectAtom); const halfWidth = rect ? rect.width / 2 : 400; const halfHeight = rect ? rect.height / 2 : 300; set(panAtom, { x: halfWidth - centerX * zoom, y: halfHeight - centerY * zoom }); }); var ZOOM_TRANSITION_THRESHOLD = 3.5; var ZOOM_EXIT_THRESHOLD = 2; var zoomFocusNodeIdAtom = atom5(null); var zoomTransitionProgressAtom = atom5(0); var isZoomTransitioningAtom = atom5((get) => { const progress = get(zoomTransitionProgressAtom); return progress > 0 && progress < 1; }); var zoomAnimationTargetAtom = atom5(null); var animateZoomToNodeAtom = atom5(null, (get, set, { nodeId, targetZoom, duration = 300 }) => { const nodes = get(uiNodesAtom); const node = nodes.find((n) => n.id === nodeId); if (!node) return; const { x, y, width = 200, height = 100 } = node; const centerX = x + width / 2; const centerY = y + height / 2; const rect = get(viewportRectAtom); const halfWidth = rect ? rect.width / 2 : 400; const halfHeight = rect ? rect.height / 2 : 300; const finalZoom = targetZoom ?? get(zoomAtom); const targetPan = { x: halfWidth - centerX * finalZoom, y: halfHeight - centerY * finalZoom }; set(zoomFocusNodeIdAtom, nodeId); set(zoomAnimationTargetAtom, { targetZoom: finalZoom, targetPan, startZoom: get(zoomAtom), startPan: { ...get(panAtom) }, duration, startTime: performance.now() }); }); var animateFitToBoundsAtom = atom5(null, (get, set, { mode, padding = 20, duration = 300 }) => { const viewportSize = get(viewportRectAtom); if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return; get(nodePositionUpdateCounterAtom); let bounds; if (mode === "graph") { const graph = get(graphAtom); const nodes = graph.nodes().map((node) => { const attrs = graph.getNodeAttributes(node); return { x: attrs.x, y: attrs.y, width: attrs.width || 500, height: attrs.height || 500 }; }); bounds = calculateBounds(nodes); } else { const selectedIds = get(selectedNodeIdsAtom); const allNodes = get(uiNodesAtom); const selectedNodes = allNodes.filter((n) => selectedIds.has(n.id)).map((n) => ({ x: n.position.x, y: n.position.y, width: n.width ?? 500, height: n.height ?? 500 })); bounds = calculateBounds(selectedNodes); } if (bounds.width <= 0 || bounds.height <= 0) return; const safePadding = Math.max(0, Math.min(padding, viewportSize.width / 2 - 1, viewportSize.height / 2 - 1)); const effW = Math.max(1, viewportSize.width - 2 * safePadding); const effH = Math.max(1, viewportSize.height - 2 * safePadding); const scale = Math.min(effW / bounds.width, effH / bounds.height); if (scale <= 0 || !isFinite(scale)) return; const scaledW = bounds.width * scale; const scaledH = bounds.height * scale; const startX = safePadding + (effW - scaledW) / 2; const startY = safePadding + (effH - scaledH) / 2; const targetPan = { x: startX - bounds.x * scale, y: startY - bounds.y * scale }; set(zoomAnimationTargetAtom, { targetZoom: scale, targetPan, startZoom: get(zoomAtom), startPan: { ...get(panAtom) }, duration, startTime: performance.now() }); }); // src/core/group-store.ts import { atom as atom7 } from "jotai"; // src/core/history-store.ts import { atom as atom6 } from "jotai"; // src/core/history-actions.ts function applyDelta(graph, delta) { switch (delta.type) { case "move-node": { if (!graph.hasNode(delta.nodeId)) return false; graph.setNodeAttribute(delta.nodeId, "x", delta.to.x); graph.setNodeAttribute(delta.nodeId, "y", delta.to.y); return false; } case "resize-node": { if (!graph.hasNode(delta.nodeId)) return false; graph.setNodeAttribute(delta.nodeId, "width", delta.to.width); graph.setNodeAttribute(delta.nodeId, "height", delta.to.height); return false; } case "add-node": { if (graph.hasNode(delta.nodeId)) return false; graph.addNode(delta.nodeId, delta.attributes); return true; } case "remove-node": { if (!graph.hasNode(delta.nodeId)) return false; graph.dropNode(delta.nodeId); return true; } case "add-edge": { if (graph.hasEdge(delta.edgeId)) return false; if (!graph.hasNode(delta.source) || !graph.hasNode(delta.target)) return false; graph.addEdgeWithKey(delta.edgeId, delta.source, delta.target, delta.attributes); return true; } case "remove-edge": { if (!graph.hasEdge(delta.edgeId)) return false; graph.dropEdge(delta.edgeId); return true; } case "update-node-attr": { if (!graph.hasNode(delta.nodeId)) return false; graph.setNodeAttribute(delta.nodeId, delta.key, delta.to); return false; } case "batch": { let structuralChange = false; for (const d of delta.deltas) { if (applyDelta(graph, d)) structuralChange = true; } return structuralChange; } case "full-snapshot": { graph.clear(); for (const node of delta.nodes) { graph.addNode(node.id, node.attributes); } for (const edge of delta.edges) { if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) { graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge.attributes); } } return true; } } } function invertDelta(delta) { switch (delta.type) { case "move-node": return { ...delta, from: delta.to, to: delta.from }; case "resize-node": return { ...delta, from: delta.to, to: delta.from }; case "add-node": return { type: "remove-node", nodeId: delta.nodeId, attributes: delta.attributes, connectedEdges: [] }; case "remove-node": { const batch = [{ type: "add-node", nodeId: delta.nodeId, attributes: delta.attributes }, ...delta.connectedEdges.map((e) => ({ type: "add-edge", edgeId: e.id, source: e.source, target: e.target, attributes: e.attributes }))]; return batch.length === 1 ? batch[0] : { type: "batch", deltas: batch }; } case "add-edge": return { type: "remove-edge", edgeId: delta.edgeId, source: delta.source, target: delta.target, attributes: delta.attributes }; case "remove-edge": return { type: "add-edge", edgeId: delta.edgeId, source: delta.source, target: delta.target, attributes: delta.attributes }; case "update-node-attr": return { ...delta, from: delta.to, to: delta.from }; case "batch": return { type: "batch", deltas: delta.deltas.map(invertDelta).reverse() }; case "full-snapshot": return delta; } } function createSnapshot(graph, label) { const nodes = []; const edges = []; graph.forEachNode((nodeId, attributes) => { nodes.push({ id: nodeId, attributes: { ...attributes } }); }); graph.forEachEdge((edgeId, attributes, source, target) => { edges.push({ id: edgeId, source, target, attributes: { ...attributes } }); }); return { timestamp: Date.now(), label, nodes, edges }; } // src/core/history-store.ts var debug4 = createDebug("history"); var MAX_HISTORY_SIZE = 50; var historyStateAtom = atom6({ past: [], future: [], isApplying: false }); var canUndoAtom = atom6((get) => { const history = get(historyStateAtom); return history.past.length > 0 && !history.isApplying; }); var canRedoAtom = atom6((get) => { const history = get(historyStateAtom); return history.future.length > 0 && !history.isApplying; }); var undoCountAtom = atom6((get) => get(historyStateAtom).past.length); var redoCountAtom = atom6((get) => get(historyStateAtom).future.length); var pushDeltaAtom = atom6(null, (get, set, delta) => { const history = get(historyStateAtom); if (history.isApplying) return; const { label, ...cleanDelta } = delta; const entry = { forward: cleanDelta, reverse: invertDelta(cleanDelta), timestamp: Date.now(), label }; const newPast = [...history.past, entry]; if (newPast.length > MAX_HISTORY_SIZE) newPast.shift(); set(historyStateAtom, { past: newPast, future: [], // Clear redo stack isApplying: false }); debug4("Pushed delta: %s (past: %d)", label || delta.type, newPast.length); }); var pushHistoryAtom = atom6(null, (get, set, label) => { const history = get(historyStateAtom); if (history.isApplying) return; const graph = get(graphAtom); const snapshot = createSnapshot(graph, label); const forward = { type: "full-snapshot", nodes: snapshot.nodes, edges: snapshot.edges }; const entry = { forward, reverse: forward, // For full snapshots, reverse IS the current state timestamp: Date.now(), label }; const newPast = [...history.past, entry]; if (newPast.length > MAX_HISTORY_SIZE) newPast.shift(); set(historyStateAtom, { past: newPast, future: [], isApplying: false }); debug4("Pushed snapshot: %s (past: %d)", label || "unnamed", newPast.length); }); var undoAtom = atom6(null, (get, set) => { const history = get(historyStateAtom); if (history.past.length === 0 || history.isApplying) return false; set(historyStateAtom, { ...history, isApplying: true }); try { const graph = get(graphAtom); const newPast = [...history.past]; const entry = newPast.pop(); let forwardForRedo = entry.forward; if (entry.reverse.type === "full-snapshot") { const currentSnapshot = createSnapshot(graph, "current"); forwardForRedo = { type: "full-snapshot", nodes: currentSnapshot.nodes, edges: currentSnapshot.edges }; } const structuralChange = applyDelta(graph, entry.reverse); if (structuralChange) { set(graphAtom, graph); set(graphUpdateVersionAtom, (v) => v + 1); } set(nodePositionUpdateCounterAtom, (c) => c + 1); const redoEntry = { forward: forwardForRedo, reverse: entry.reverse, timestamp: entry.timestamp, label: entry.label }; set(historyStateAtom, { past: newPast, future: [redoEntry, ...history.future], isApplying: false }); debug4("Undo: %s (past: %d, future: %d)", entry.label, newPast.length, history.future.length + 1); return true; } catch (error) { debug4.error("Undo failed: %O", error); set(historyStateAtom, { ...history, isApplying: false }); return false; } }); var redoAtom = atom6(null, (get, set) => { const history = get(historyStateAtom); if (history.future.length === 0 || history.isApplying) return false; set(historyStateAtom, { ...history, isApplying: true }); try { const graph = get(graphAtom); const newFuture = [...history.future]; const entry = newFuture.shift(); let reverseForUndo = entry.reverse; if (entry.forward.type === "full-snapshot") { const currentSnapshot = createSnapshot(graph, "current"); reverseForUndo = { type: "full-snapshot", nodes: currentSnapshot.nodes, edges: currentSnapshot.edges }; } const structuralChange = applyDelta(graph, entry.forward); if (structuralChange) { set(graphAtom, graph); set(graphUpdateVersionAtom, (v) => v + 1); } set(nodePositionUpdateCounterAtom, (c) => c + 1); const undoEntry = { forward: entry.forward, reverse: reverseForUndo, timestamp: entry.timestamp, label: entry.label }; set(historyStateAtom, { past: [...history.past, undoEntry], future: newFuture, isApplying: false }); debug4("Redo: %s (past: %d, future: %d)", entry.label, history.past.length + 1, newFuture.length); return true; } catch (error) { debug4.error("Redo failed: %O", error); set(historyStateAtom, { ...history, isApplying: false }); return false; } }); var clearHistoryAtom = atom6(null, (_get, set) => { set(historyStateAtom, { past: [], future: [], isApplying: false }); debug4("History cleared"); }); var historyLabelsAtom = atom6((get) => { const history = get(historyStateAtom); return { past: history.past.map((e) => e.label || "Unnamed"), future: history.future.map((e) => e.label || "Unnamed") }; }); // src/core/group-store.ts var collapsedGroupsAtom = atom7(/* @__PURE__ */ new Set()); var toggleGroupCollapseAtom = atom7(null, (get, set, groupId) => { const current = get(collapsedGroupsAtom); const next = new Set(current); if (next.has(groupId)) { next.delete(groupId); } else { next.add(groupId); } set(collapsedGroupsAtom, next); }); var collapseGroupAtom = atom7(null, (get, set, groupId) => { const current = get(collapsedGroupsAtom); if (!current.has(groupId)) { const next = new Set(current); next.add(groupId); set(collapsedGroupsAtom, next); } }); var expandGroupAtom = atom7(null, (get, set, groupId) => { const current = get(collapsedGroupsAtom); if (current.has(groupId)) { const next = new Set(current); next.delete(groupId); set(collapsedGroupsAtom, next); } }); var nodeChildrenAtom = atom7((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return (parentId) => { const children = []; graph.forEachNode((nodeId, attrs) => { if (attrs.parentId === parentId) { children.push(nodeId); } }); return children; }; }); var nodeParentAtom = atom7((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return (nodeId) => { if (!graph.hasNode(nodeId)) return void 0; return graph.getNodeAttribute(nodeId, "parentId"); }; }); var isGroupNodeAtom = atom7((get) => { const getChildren = get(nodeChildrenAtom); return (nodeId) => getChildren(nodeId).length > 0; }); var groupChildCountAtom = atom7((get) => { const getChildren = get(nodeChildrenAtom); return (groupId) => getChildren(groupId).length; }); var setNodeParentAtom = atom7(null, (get, set, { nodeId, parentId }) => { const graph = get(graphAtom); if (!graph.hasNode(nodeId)) return; if (parentId) { if (parentId === nodeId) return; let current = parentId; while (current) { if (current === nodeId) return; if (!graph.hasNode(current)) break; current = graph.getNodeAttribute(current, "parentId"); } } graph.setNodeAttribute(nodeId, "parentId", parentId); set(graphUpdateVersionAtom, (v) => v + 1); }); var moveNodesToGroupAtom = atom7(null, (get, set, { nodeIds, groupId }) => { for (const nodeId of nodeIds) { set(setNodeParentAtom, { nodeId, parentId: groupId }); } }); var removeFromGroupAtom = atom7(null, (get, set, nodeId) => { set(setNodeParentAtom, { nodeId, parentId: void 0 }); }); var groupSelectedNodesAtom = atom7(null, (get, set, { nodeIds, groupNodeId }) => { set(pushHistoryAtom, `Group ${nodeIds.length} nodes`); const graph = get(graphAtom); let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const nodeId of nodeIds) { if (!graph.hasNode(nodeId)) continue; const attrs = graph.getNodeAttributes(nodeId); minX = Math.min(minX, attrs.x); minY = Math.min(minY, attrs.y); maxX = Math.max(maxX, attrs.x + (attrs.width || 200)); maxY = Math.max(maxY, attrs.y + (attrs.height || 100)); } const padding = 20; if (graph.hasNode(groupNodeId)) { graph.setNodeAttribute(groupNodeId, "x", minX - padding); graph.setNodeAttribute(groupNodeId, "y", minY - padding - 30); graph.setNodeAttribute(groupNodeId, "width", maxX - minX + 2 * padding); graph.setNodeAttribute(groupNodeId, "height", maxY - minY + 2 * padding + 30); } for (const nodeId of nodeIds) { if (nodeId !== groupNodeId && graph.hasNode(nodeId)) { graph.setNodeAttribute(nodeId, "parentId", groupNodeId); } } set(graphUpdateVersionAtom, (v) => v + 1); set(nodePositionUpdateCounterAtom, (c) => c + 1); }); var ungroupNodesAtom = atom7(null, (get, set, groupId) => { set(pushHistoryAtom, "Ungroup nodes"); const graph = get(graphAtom); graph.forEachNode((nodeId, attrs) => { if (attrs.parentId === groupId) { graph.setNodeAttribute(nodeId, "parentId", void 0); } }); set(graphUpdateVersionAtom, (v) => v + 1); }); var nestNodesOnDropAtom = atom7(null, (get, set, { nodeIds, targetId }) => { set(pushHistoryAtom, "Nest nodes"); for (const nodeId of nodeIds) { if (nodeId === targetId) continue; set(setNodeParentAtom, { nodeId, parentId: targetId }); } set(autoResizeGroupAtom, targetId); }); function getNodeDescendants(graph, groupId) { const descendants = []; const stack = [groupId]; while (stack.length > 0) { const current = stack.pop(); graph.forEachNode((nodeId, attrs) => { if (attrs.parentId === current) { descendants.push(nodeId); stack.push(nodeId); } }); } return descendants; } var collapsedEdgeRemapAtom = atom7((get) => { const collapsed = get(collapsedGroupsAtom); if (collapsed.size === 0) return /* @__PURE__ */ new Map(); get(graphUpdateVersionAtom); const graph = get(graphAtom); const remap = /* @__PURE__ */ new Map(); for (const nodeId of graph.nodes()) { let current = nodeId; let outermost = null; while (true) { if (!graph.hasNode(current)) break; const parent = graph.getNodeAttribute(current, "parentId"); if (!parent) break; if (collapsed.has(parent)) outermost = parent; current = parent; } if (outermost) remap.set(nodeId, outermost); } return remap; }); var autoResizeGroupAtom = atom7(null, (get, set, groupId) => { const graph = get(graphAtom); if (!graph.hasNode(groupId)) return; const children = []; graph.forEachNode((nodeId, attrs) => { if (attrs.parentId === groupId) { children.push(nodeId); } }); if (children.length === 0) return; const padding = 20; const headerHeight = 30; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const childId of children) { const attrs = graph.getNodeAttributes(childId); minX = Math.min(minX, attrs.x); minY = Math.min(minY, attrs.y); maxX = Math.max(maxX, attrs.x + (attrs.width || 200)); maxY = Math.max(maxY, attrs.y + (attrs.height || 100)); } graph.setNodeAttribute(groupId, "x", minX - padding); graph.setNodeAttribute(groupId, "y", minY - padding - headerHeight); graph.setNodeAttribute(groupId, "width", maxX - minX + 2 * padding); graph.setNodeAttribute(groupId, "height", maxY - minY + 2 * padding + headerHeight); set(nodePositionUpdateCounterAtom, (c) => c + 1); }); // src/core/graph-derived.ts var highestZIndexAtom = atom8((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); let maxZ = 0; graph.forEachNode((_node, attributes) => { if (attributes.zIndex > maxZ) { maxZ = attributes.zIndex; } }); return maxZ; }); var _prevUiNodesByGraph = /* @__PURE__ */ new WeakMap(); var uiNodesAtom = atom8((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); const currentDraggingId = get(draggingNodeIdAtom); const collapsed = get(collapsedGroupsAtom); const nodes = []; graph.forEachNode((nodeId, attributes) => { if (collapsed.size > 0) { let current = nodeId; let hidden = false; while (true) { if (!graph.hasNode(current)) break; const pid = graph.getNodeAttributes(current).parentId; if (!pid) break; if (collapsed.has(pid)) { hidden = true; break; } current = pid; } if (hidden) return; } const position = get(nodePositionAtomFamily(nodeId)); nodes.push({ ...attributes, id: nodeId, position, isDragging: nodeId === currentDraggingId }); }); const prev = _prevUiNodesByGraph.get(graph) ?? []; if (nodes.length === prev.length && nodes.every((n, i) => n.id === prev[i].id && n.position === prev[i].position && n.isDragging === prev[i].isDragging)) { return prev; } _prevUiNodesByGraph.set(graph, nodes); return nodes; }); var nodeKeysAtom = atom8((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return graph.nodes(); }); var nodeFamilyAtom = atomFamily2((nodeId) => atom8((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); if (!graph.hasNode(nodeId)) { return null; } const attributes = graph.getNodeAttributes(nodeId); const position = get(nodePositionAtomFamily(nodeId)); const currentDraggingId = get(draggingNodeIdAtom); return { ...attributes, id: nodeId, position, isDragging: nodeId === currentDraggingId }; }), (a, b) => a === b); var edgeKeysAtom = atom8((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return graph.edges(); }); var edgeKeysWithTempEdgeAtom = atom8((get) => { const keys = get(edgeKeysAtom); const edgeCreation = get(edgeCreationAtom); if (edgeCreation.isCreating) { return [...keys, "temp-creating-edge"]; } return keys; }); var _edgeCacheByGraph = /* @__PURE__ */ new WeakMap(); function getEdgeCache(graph) { let cache = _edgeCacheByGraph.get(graph); if (!cache) { cache = /* @__PURE__ */ new Map(); _edgeCacheByGraph.set(graph, cache); } return cache; } var edgeFamilyAtom = atomFamily2((key) => atom8((get) => { get(graphUpdateVersionAtom); if (key === "temp-creating-edge") { const edgeCreationState = get(edgeCreationAtom); const graph2 = get(graphAtom); if (edgeCreationState.isCreating && edgeCreationState.sourceNodeId && edgeCreationState.targetPosition) { const sourceNodeAttrs = graph2.getNodeAttributes(edgeCreationState.sourceNodeId); const sourceNodePosition = get(nodePositionAtomFamily(edgeCreationState.sourceNodeId)); const pan = get(panAtom); const zoom = get(zoomAtom); const viewportRect = get(viewportRectAtom); if (sourceNodeAttrs && viewportRect) { const mouseX = edgeCreationState.targetPosition.x - viewportRect.left; const mouseY = edgeCreationState.targetPosition.y - viewportRect.top; const worldTargetX = (mouseX - pan.x) / zoom; const worldTargetY = (mouseY - pan.y) / zoom; const tempEdge = { key: "temp-creating-edge", sourceId: edgeCreationState.sourceNodeId, targetId: "temp-cursor", sourcePosition: sourceNodePosition, targetPosition: { x: worldTargetX, y: worldTargetY }, sourceNodeSize: sourceNodeAttrs.size, sourceNodeWidth: sourceNodeAttrs.width, sourceNodeHeight: sourceNodeAttrs.height, targetNodeSize: 0, targetNodeWidth: 0, targetNodeHeight: 0, type: "dashed", color: "#FF9800", weight: 2, label: void 0, dbData: { id: "temp-creating-edge", graph_id: get(currentGraphIdAtom) || "", source_node_id: edgeCreationState.sourceNodeId, target_node_id: "temp-cursor", edge_type: "temp", filter_condition: null, ui_properties: null, data: null, created_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() } }; return tempEdge; } } return null; } const graph = get(graphAtom); if (!graph.hasEdge(key)) { getEdgeCache(graph).delete(key); return null; } const sourceId = graph.source(key); const targetId = graph.target(key); const attributes = graph.getEdgeAttributes(key); const remap = get(collapsedEdgeRemapAtom); const effectiveSourceId = remap.get(sourceId) ?? sourceId; const effectiveTargetId = remap.get(targetId) ?? targetId; if (!graph.hasNode(effectiveSourceId) || !graph.hasNode(effectiveTargetId)) { getEdgeCache(graph).delete(key); return null; } const sourceAttributes = graph.getNodeAttributes(effectiveSourceId); const targetAttributes = graph.getNodeAttributes(effectiveTargetId); const sourcePosition = get(nodePositionAtomFamily(effectiveSourceId)); const targetPosition = get(nodePositionAtomFamily(effectiveTargetId)); if (sourceAttributes && targetAttributes) { const next = { ...attributes, key, sourceId: effectiveSourceId, targetId: effectiveTargetId, sourcePosition, targetPosition, sourceNodeSize: sourceAttributes.size, targetNodeSize: targetAttributes.size, sourceNodeWidth: sourceAttributes.width ?? sourceAttributes.size, sourceNodeHeight: sourceAttributes.height ?? sourceAttributes.size, targetNodeWidth: targetAttributes.width ?? targetAttributes.size, targetNodeHeight: targetAttributes.height ?? targetAttributes.size }; const edgeCache = getEdgeCache(graph); const prev = edgeCache.get(key); 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) { return prev; } edgeCache.set(key, next); return next; } getEdgeCache(graph).delete(key); return null; }), (a, b) => a === b); // src/core/graph-mutations-edges.ts import { atom as atom10 } from "jotai"; // src/core/reduced-motion-store.ts import { atom as atom9 } from "jotai"; var prefersReducedMotionAtom = atom9(typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false); var watchReducedMotionAtom = atom9(null, (_get, set) => { if (typeof window === "undefined" || typeof window.matchMedia !== "function") return; const mql = window.matchMedia("(prefers-reduced-motion: reduce)"); const handler = (e) => { set(prefersReducedMotionAtom, e.matches); }; set(prefersReducedMotionAtom, mql.matches); mql.addEventListener("change", handler); return () => mql.removeEventListener("change", handler); }); // src/core/graph-mutations-edges.ts var debug5 = createDebug("graph:mutations:edges"); var addEdgeToLocalGraphAtom = atom10(null, (get, set, newEdge) => { const graph = get(graphAtom); if (graph.hasNode(newEdge.source_node_id) && graph.hasNode(newEdge.target_node_id)) { const uiProps = newEdge.ui_properties || {}; const attributes = { type: typeof uiProps.style === "string" ? uiProps.style : "solid", color: typeof uiProps.color === "string" ? uiProps.color : "#999", label: newEdge.edge_type ?? void 0, weight: typeof uiProps.weight === "number" ? uiProps.weight : 1, dbData: newEdge }; if (!graph.hasEdge(newEdge.id)) { try { debug5("Adding edge %s to local graph", newEdge.id); graph.addEdgeWithKey(newEdge.id, newEdge.source_node_id, newEdge.target_node_id, attributes); set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); } catch (e) { debug5("Failed to add edge %s: %o", newEdge.id, e); } } } }); var removeEdgeFromLocalGraphAtom = atom10(null, (get, set, edgeId) => { const graph = get(graphAtom); if (graph.hasEdge(edgeId)) { graph.dropEdge(edgeId); set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); } }); var swapEdgeAtomicAtom = atom10(null, (get, set, { tempEdgeId, newEdge }) => { const graph = get(graphAtom); if (graph.hasEdge(tempEdgeId)) { graph.dropEdge(tempEdgeId); } if (graph.hasNode(newEdge.source_node_id) && graph.hasNode(newEdge.target_node_id)) { const uiProps = newEdge.ui_properties || {}; const attributes = { type: typeof uiProps.style === "string" ? uiProps.style : "solid", color: typeof uiProps.color === "string" ? uiProps.color : "#999", label: newEdge.edge_type ?? void 0, weight: typeof uiProps.weight === "number" ? uiProps.weight : 1, dbData: newEdge }; if (!graph.hasEdge(newEdge.id)) { try { debug5("Atomically swapping temp edge %s with real edge %s", tempEdgeId, newEdge.id); graph.addEdgeWithKey(newEdge.id, newEdge.source_node_id, newEdge.target_node_id, attributes); } catch (e) { debug5("Failed to add edge %s: %o", newEdge.id, e); } } } set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); }); var departingEdgesAtom = atom10(/* @__PURE__ */ new Map()); var EDGE_ANIMATION_DURATION = 300; var removeEdgeWithAnimationAtom = atom10(null, (get, set, edgeKey) => { const edgeState = get(edgeFamilyAtom(edgeKey)); if (edgeState) { const departing = new Map(get(departingEdgesAtom)); departing.set(edgeKey, edgeState); set(departingEdgesAtom, departing); set(removeEdgeFromLocalGraphAtom, edgeKey); const duration = get(prefersReducedMotionAtom) ? 0 : EDGE_ANIMATION_DURATION; setTimeout(() => { const current = new Map(get(departingEdgesAtom)); current.delete(edgeKey); set(departingEdgesAtom, current); }, duration); } }); var editingEdgeLabelAtom = atom10(null); var updateEdgeLabelAtom = atom10(null, (get, set, { edgeKey, label }) => { const graph = get(graphAtom); if (graph.hasEdge(edgeKey)) { graph.setEdgeAttribute(edgeKey, "label", label || void 0); set(graphUpdateVersionAtom, (v) => v + 1); set(nodePositionUpdateCounterAtom, (c) => c + 1); } }); // src/core/graph-mutations-advanced.ts import { atom as atom11 } from "jotai"; var debug6 = createDebug("graph:mutations:advanced"); var dropTargetNodeIdAtom = atom11(null); var splitNodeAtom = atom11(null, (get, set, { nodeId, position1, position2 }) => { const graph = get(graphAtom); if (!graph.hasNode(nodeId)) return; const attrs = graph.getNodeAttributes(nodeId); const graphId = get(currentGraphIdAtom) || attrs.dbData.graph_id; set(pushHistoryAtom, "Split node"); graph.setNodeAttribute(nodeId, "x", position1.x); graph.setNodeAttribute(nodeId, "y", position1.y); const edges = []; graph.forEachEdge(nodeId, (_key, eAttrs, source, target) => { edges.push({ source, target, attrs: eAttrs }); }); const cloneId = `split-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const cloneDbNode = { ...attrs.dbData, id: cloneId, graph_id: graphId, ui_properties: { ...attrs.dbData.ui_properties || {}, x: position2.x, y: position2.y }, created_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() }; set(addNodeToLocalGraphAtom, cloneDbNode); for (const edge of edges) { const newSource = edge.source === nodeId ? cloneId : edge.source; const newTarget = edge.target === nodeId ? cloneId : edge.target; set(addEdgeToLocalGraphAtom, { ...edge.attrs.dbData, id: `split-e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, source_node_id: newSource, target_node_id: newTarget }); } set(graphUpdateVersionAtom, (v) => v + 1); set(nodePositionUpdateCounterAtom, (c) => c + 1); debug6("Split node %s \u2192 clone %s", nodeId, cloneId); }); var mergeNodesAtom = atom11(null, (get, set, { nodeIds }) => { if (nodeIds.length < 2) return; const graph = get(graphAtom); const [survivorId, ...doomed] = nodeIds; if (!graph.hasNode(survivorId)) return; set(pushHistoryAtom, `Merge ${nodeIds.length} nodes`); const doomedSet = new Set(doomed); for (const doomedId of doomed) { if (!graph.hasNode(doomedId)) continue; const edges = []; graph.forEachEdge(doomedId, (_key, eAttrs, source, target) => { edges.push({ source, target, attrs: eAttrs }); }); for (const edge of edges) { const newSource = doomedSet.has(edge.source) ? survivorId : edge.source; const newTarget = doomedSet.has(edge.target) ? survivorId : edge.target; if (newSource === newTarget) continue; set(addEdgeToLocalGraphAtom, { ...edge.attrs.dbData, id: `merge-e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, source_node_id: newSource, target_node_id: newTarget }); } set(optimisticDeleteNodeAtom, { nodeId: doomedId }); } set(graphUpdateVersionAtom, (v) => v + 1); debug6("Merged nodes %o \u2192 survivor %s", nodeIds, survivorId); }); // src/core/graph-mutations.ts var debug7 = createDebug("graph:mutations"); var startNodeDragAtom = atom12(null, (get, set, { nodeId }) => { const graph = get(graphAtom); if (!graph.hasNode(nodeId)) return; const currentAttributes = graph.getNodeAttributes(nodeId); set(preDragNodeAttributesAtom, JSON.parse(JSON.stringify(currentAttributes))); const currentHighestZIndex = get(highestZIndexAtom); const newZIndex = currentHighestZIndex + 1; graph.setNodeAttribute(nodeId, "zIndex", newZIndex); set(draggingNodeIdAtom, nodeId); }); var endNodeDragAtom = atom12(null, (get, set, _payload) => { const currentDraggingId = get(draggingNodeIdAtom); if (currentDraggingId) { debug7("Node %s drag ended", currentDraggingId); const graph = get(graphAtom); if (graph.hasNode(currentDraggingId)) { const parentId = graph.getNodeAttribute(currentDraggingId, "parentId"); if (parentId) { set(autoResizeGroupAtom, parentId); } } } set(draggingNodeIdAtom, null); set(preDragNodeAttributesAtom, null); }); var optimisticDeleteNodeAtom = atom12(null, (get, set, { nodeId }) => { const graph = get(graphAtom); if (graph.hasNode(nodeId)) { graph.dropNode(nodeId); set(cleanupNodePositionAtom, nodeId); set(graphAtom, graph.copy()); debug7("Optimistically deleted node %s", nodeId); } }); var optimisticDeleteEdgeAtom = atom12(null, (get, set, { edgeKey }) => { const graph = get(graphAtom); if (graph.hasEdge(edgeKey)) { graph.dropEdge(edgeKey); set(graphAtom, graph.copy()); debug7("Optimistically deleted edge %s", edgeKey); } }); var addNodeToLocalGraphAtom = atom12(null, (get, set, newNode) => { const graph = get(graphAtom); if (graph.hasNode(newNode.id)) { debug7("Node %s already exists, skipping", newNode.id); return; } const uiProps = newNode.ui_properties || {}; const attributes = { x: typeof uiProps.x === "number" ? uiProps.x : Math.random() * 800, y: typeof uiProps.y === "number" ? uiProps.y : Math.random() * 600, size: typeof uiProps.size === "number" ? uiProps.size : 15, width: typeof uiProps.width === "number" ? uiProps.width : 500, height: typeof uiProps.height === "number" ? uiProps.height : 500, color: typeof uiProps.color === "string" ? uiProps.color : "#ccc", label: newNode.label || newNode.node_type || newNode.id, zIndex: typeof uiProps.zIndex === "number" ? uiProps.zIndex : 0, dbData: newNode }; debug7("Adding node %s to local graph at (%d, %d)", newNode.id, attributes.x, attributes.y); graph.addNode(newNode.id, attributes); set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); set(nodePositionUpdateCounterAtom, (c) => c + 1); }); var loadGraphFromDbAtom = atom12(null, (get, set, fetchedNodes, fetchedEdges) => { debug7("========== START SYNC =========="); debug7("Fetched nodes: %d, edges: %d", fetchedNodes.length, fetchedEdges.length); const currentGraphId = get(currentGraphIdAtom); if (fetchedNodes.length > 0 && fetchedNodes[0].graph_id !== currentGraphId) { debug7("Skipping sync - data belongs to different graph"); return; } const existingGraph = get(graphAtom); const isDragging = get(draggingNodeIdAtom) !== null; if (isDragging) { debug7("Skipping sync - drag in progress"); return; } const existingNodeIds = new Set(existingGraph.nodes()); const fetchedNodeIds = new Set(fetchedNodes.map((n) => n.id)); const hasAnyCommonNodes = Array.from(existingNodeIds).some((id) => fetchedNodeIds.has(id)); let graph; if (hasAnyCommonNodes && existingNodeIds.size > 0) { debug7("Merging DB data into existing graph"); graph = existingGraph.copy(); } else { debug7("Creating fresh graph (graph switch detected)"); graph = new Graph3(graphOptions); } const fetchedEdgeIds = new Set(fetchedEdges.map((e) => e.id)); if (hasAnyCommonNodes && existingNodeIds.size > 0) { graph.forEachNode((nodeId) => { if (!fetchedNodeIds.has(nodeId)) { debug7("Removing deleted node: %s", nodeId); graph.dropNode(nodeId); nodePositionAtomFamily.remove(nodeId); } }); } fetchedNodes.forEach((node) => { const uiProps = node.ui_properties || {}; const newX = typeof uiProps.x === "number" ? uiProps.x : Math.random() * 800; const newY = typeof uiProps.y === "number" ? uiProps.y : Math.random() * 600; if (graph.hasNode(node.id)) { const currentAttrs = graph.getNodeAttributes(node.id); const attributes = { x: newX, y: newY, size: typeof uiProps.size === "number" ? uiProps.size : currentAttrs.size, width: typeof uiProps.width === "number" ? uiProps.width : currentAttrs.width ?? 500, height: typeof uiProps.height === "number" ? uiProps.height : currentAttrs.height ?? 500, color: typeof uiProps.color === "string" ? uiProps.color : currentAttrs.color, label: node.label || node.node_type || node.id, zIndex: typeof uiProps.zIndex === "number" ? uiProps.zIndex : currentAttrs.zIndex, dbData: node }; graph.replaceNodeAttributes(node.id, attributes); } else { const attributes = { x: newX, y: newY, size: typeof uiProps.size === "number" ? uiProps.size : 15, width: typeof uiProps.width === "number" ? uiProps.width : 500, height: typeof uiProps.height === "number" ? uiProps.height : 500, color: typeof uiProps.color === "string" ? uiProps.color : "#ccc", label: node.label || node.node_type || node.id, zIndex: typeof uiProps.zIndex === "number" ? uiProps.zIndex : 0, dbData: node }; graph.addNode(node.id, attributes); } }); graph.forEachEdge((edgeId) => { if (!fetchedEdgeIds.has(edgeId)) { debug7("Removing deleted edge: %s", edgeId); graph.dropEdge(edgeId); } }); fetchedEdges.forEach((edge) => { if (graph.hasNode(edge.source_node_id) && graph.hasNode(edge.target_node_id)) { const uiProps = edge.ui_properties || {}; const attributes = { type: typeof uiProps.style === "string" ? uiProps.style : "solid", color: typeof uiProps.color === "string" ? uiProps.color : "#999", label: edge.edge_type ?? void 0, weight: typeof uiProps.weight === "number" ? uiProps.weight : 1, dbData: edge }; if (graph.hasEdge(edge.id)) { graph.replaceEdgeAttributes(edge.id, attributes); } else { try { graph.addEdgeWithKey(edge.id, edge.source_node_id, edge.target_node_id, attributes); } catch (e) { debug7("Failed to add edge %s: %o", edge.id, e); } } } }); set(graphAtom, graph); set(graphUpdateVersionAtom, (v) => v + 1); debug7("========== SYNC COMPLETE =========="); debug7("Final graph: %d nodes, %d edges", graph.order, graph.size); }); // src/core/sync-store.ts import { atom as atom13 } from "jotai"; var debug8 = createDebug("sync"); var syncStatusAtom = atom13("synced"); var pendingMutationsCountAtom = atom13(0); var isOnlineAtom = atom13(typeof navigator !== "undefined" ? navigator.onLine : true); var lastSyncErrorAtom = atom13(null); var lastSyncTimeAtom = atom13(Date.now()); var mutationQueueAtom = atom13([]); var syncStateAtom = atom13((get) => ({ status: get(syncStatusAtom), pendingMutations: get(pendingMutationsCountAtom), lastError: get(lastSyncErrorAtom), lastSyncTime: get(lastSyncTimeAtom), isOnline: get(isOnlineAtom), queuedMutations: get(mutationQueueAtom).length })); var startMutationAtom = atom13(null, (get, set) => { const currentCount = get(pendingMutationsCountAtom); const newCount = currentCount + 1; set(pendingMutationsCountAtom, newCount); debug8("Mutation started. Pending count: %d -> %d", currentCount, newCount); if (newCount > 0 && get(syncStatusAtom) !== "syncing") { set(syncStatusAtom, "syncing"); debug8("Status -> syncing"); } }); var completeMutationAtom = atom13(null, (get, set, success = true) => { const currentCount = get(pendingMutationsCountAtom); const newCount = Math.max(0, currentCount - 1); set(pendingMutationsCountAtom, newCount); debug8("Mutation completed (success: %s). Pending count: %d -> %d", success, currentCount, newCount); if (success) { set(lastSyncTimeAtom, Date.now()); if (newCount === 0) { set(lastSyncErrorAtom, null); } } if (newCount === 0) { const isOnline = get(isOnlineAtom); const hasError = get(lastSyncErrorAtom) !== null; if (hasError) { set(syncStatusAtom, "error"); debug8("Status -> error"); } else if (!isOnline) { set(syncStatusAtom, "offline"); debug8("Status -> offline"); } else { set(syncStatusAtom, "synced"); debug8("Status -> synced"); } } }); var trackMutationErrorAtom = atom13(null, (_get, set, error) => { set(lastSyncErrorAtom, error); debug8("Mutation failed: %s", error); }); var setOnlineStatusAtom = atom13(null, (get, set, isOnline) => { set(isOnlineAtom, isOnline); const pendingCount = get(pendingMutationsCountAtom); const hasError = get(lastSyncErrorAtom) !== null; const queueLength = get(mutationQueueAtom).length; if (pendingCount === 0) { if (hasError || queueLength > 0) { set(syncStatusAtom, "error"); } else { set(syncStatusAtom, isOnline ? "synced" : "offline"); } } }); var queueMutationAtom = atom13(null, (get, set, mutation) => { const queue = get(mutationQueueAtom); const newMutation = { ...mutation, id: crypto.randomUUID(), timestamp: Date.now(), retryCount: 0, maxRetries: mutation.maxRetries ?? 3 }; const newQueue = [...queue, newMutation]; set(mutationQueueAtom, newQueue); debug8("Queued mutation: %s. Queue size: %d", mutation.type, newQueue.length); if (get(pendingMutationsCountAtom) === 0) { set(syncStatusAtom, "error"); } return newMutation.id; }); var dequeueMutationAtom = atom13(null, (get, set, mutationId) => { const queue = get(mutationQueueAtom); const newQueue = queue.filter((m) => m.id !== mutationId); set(mutationQueueAtom, newQueue); debug8("Dequeued mutation: %s. Queue size: %d", mutationId, newQueue.length); if (newQueue.length === 0 && get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) { set(syncStatusAtom, get(isOnlineAtom) ? "synced" : "offline"); } }); var incrementRetryCountAtom = atom13(null, (get, set, mutationId) => { const queue = get(mutationQueueAtom); const newQueue = queue.map((m) => m.id === mutationId ? { ...m, retryCount: m.retryCount + 1 } : m); set(mutationQueueAtom, newQueue); }); var getNextQueuedMutationAtom = atom13((get) => { const queue = get(mutationQueueAtom); return queue.find((m) => m.retryCount < m.maxRetries) ?? null; }); var clearMutationQueueAtom = atom13(null, (get, set) => { set(mutationQueueAtom, []); debug8("Cleared mutation queue"); if (get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) { set(syncStatusAtom, get(isOnlineAtom) ? "synced" : "offline"); } }); // src/utils/gesture-configs.ts var fingerGestureConfig = { eventOptions: { passive: false, capture: false }, drag: { pointer: { touch: true, keys: false, capture: false, buttons: -1 }, filterTaps: true, tapsThreshold: 10, // Was 3 — too strict for fingers threshold: 10 // Was 3 — needs larger dead zone } }; var pencilGestureConfig = { eventOptions: { passive: false, capture: false }, drag: { pointer: { touch: true, keys: false, capture: false, buttons: -1 }, filterTaps: true, tapsThreshold: 3, threshold: 2 // Very precise — small dead zone } }; var mouseGestureConfig = { eventOptions: { passive: false, capture: false }, drag: { pointer: { touch: true, keys: false, capture: false, buttons: -1 }, filterTaps: true, tapsThreshold: 5, // Was 3 threshold: 3 } }; function getNodeGestureConfig(source) { switch (source) { case "finger": return fingerGestureConfig; case "pencil": return pencilGestureConfig; case "mouse": return mouseGestureConfig; } } // src/core/input-store.ts import { atom as atom14 } from "jotai"; // src/core/input-classifier.ts function detectInputCapabilities() { if (typeof window === "undefined") { return { hasTouch: false, hasStylus: false, hasMouse: true, hasCoarsePointer: false }; } const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0; const supportsMatchMedia = typeof window.matchMedia === "function"; const hasCoarsePointer = supportsMatchMedia ? window.matchMedia("(pointer: coarse)").matches : false; const hasFinePointer = supportsMatchMedia ? window.matchMedia("(pointer: fine)").matches : true; const hasMouse = hasFinePointer || !hasTouch; return { hasTouch, hasStylus: false, // Set to true on first pen event hasMouse, hasCoarsePointer }; } // src/core/input-store.ts var activePointersAtom = atom14(/* @__PURE__ */ new Map()); var primaryInputSourceAtom = atom14("mouse"); var inputCapabilitiesAtom = atom14(detectInputCapabilities()); var isStylusActiveAtom = atom14((get) => { const pointers = get(activePointersAtom); for (const [, pointer] of pointers) { if (pointer.source === "pencil") return true; } return false; }); var isMultiTouchAtom = atom14((get) => { const pointers = get(activePointersAtom); let fingerCount = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") fingerCount++; } return fingerCount > 1; }); var fingerCountAtom = atom14((get) => { const pointers = get(activePointersAtom); let count = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") count++; } return count; }); var isTouchDeviceAtom = atom14((get) => { const caps = get(inputCapabilitiesAtom); return caps.hasTouch; }); var pointerDownAtom = atom14(null, (get, set, pointer) => { const pointers = new Map(get(activePointersAtom)); pointers.set(pointer.pointerId, pointer); set(activePointersAtom, pointers); set(primaryInputSourceAtom, pointer.source); if (pointer.source === "pencil") { const caps = get(inputCapabilitiesAtom); if (!caps.hasStylus) { set(inputCapabilitiesAtom, { ...caps, hasStylus: true }); } } }); var pointerUpAtom = atom14(null, (get, set, pointerId) => { const pointers = new Map(get(activePointersAtom)); pointers.delete(pointerId); set(activePointersAtom, pointers); }); var clearPointersAtom = atom14(null, (_get, set) => { set(activePointersAtom, /* @__PURE__ */ new Map()); }); // src/utils/hit-test.ts var defaultProvider = { elementFromPoint: (x, y) => document.elementFromPoint(x, y), elementsFromPoint: (x, y) => document.elementsFromPoint(x, y) }; var _provider = defaultProvider; function hitTestNode(screenX, screenY) { const element = _provider.elementFromPoint(screenX, screenY); const nodeElement = element?.closest("[data-node-id]") ?? null; const nodeId = nodeElement?.getAttribute("data-node-id") ?? null; return { nodeId, element: nodeElement }; } // src/hooks/useDragStateMachine.ts function buildDragPositions(graph, selectedNodeIds) { const positions = /* @__PURE__ */ new Map(); for (const nodeId of selectedNodeIds) { if (graph.hasNode(nodeId)) { const attrs = graph.getNodeAttributes(nodeId); positions.set(nodeId, { x: attrs.x, y: attrs.y }); } } const currentKeys = Array.from(positions.keys()); for (const nodeId of currentKeys) { const descendants = getNodeDescendants(graph, nodeId); for (const descId of descendants) { if (!positions.has(descId) && graph.hasNode(descId)) { const attrs = graph.getNodeAttributes(descId); positions.set(descId, { x: attrs.x, y: attrs.y }); } } } return positions; } function computeDragUpdates(initialPositions, movementX, movementY, zoom, graph) { const deltaX = movementX / zoom; const deltaY = movementY / zoom; const updates = []; initialPositions.forEach((initialPos, nodeId) => { if (graph.hasNode(nodeId)) { updates.push({ id: nodeId, pos: { x: initialPos.x + deltaX, y: initialPos.y + deltaY } }); } }); return updates; } function isDragPrevented(target) { return !!target.closest('[data-no-drag="true"]') || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT"; } // src/hooks/useNodeDrag.ts var debug9 = createDebug("drag"); function useNodeDrag(id, t0) { const $ = _c2(49); let t1; if ($[0] !== t0) { t1 = t0 === void 0 ? {} : t0; $[0] = t0; $[1] = t1; } else { t1 = $[1]; } const options = t1; const { onPersist, onPersistError, heldKeys } = options; const graph = useAtomValue(graphAtom); const [pan, setPan] = useAtom2(panAtom); const startMutation = useSetAtom2(startMutationAtom); const completeMutation = useSetAtom2(completeMutationAtom); const setStartDrag = useSetAtom2(startNodeDragAtom); const setEndDrag = useSetAtom2(endNodeDragAtom); const getPreDragAttributes = useAtomValue(preDragNodeAttributesAtom); const currentZoom = useAtomValue(zoomAtom); const getSelectedNodeIds = useAtomValue(selectedNodeIdsAtom); const currentGraphId = useAtomValue(currentGraphIdAtom); const edgeCreation = useAtomValue(edgeCreationAtom); const setGraph = useSetAtom2(graphAtom); useSetAtom2(nodePositionUpdateCounterAtom); const pushHistory = useSetAtom2(pushHistoryAtom); const setDropTarget = useSetAtom2(dropTargetNodeIdAtom); const nestNodesOnDrop = useSetAtom2(nestNodesOnDropAtom); const inputSource = useAtomValue(primaryInputSourceAtom); let t2; if ($[2] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t2 = atom15(null, _temp2); $[2] = t2; } else { t2 = $[2]; } const batchUpdatePositions = useSetAtom2(t2); let t3; if ($[3] !== batchUpdatePositions) { t3 = (updates_0) => { batchUpdatePositions(updates_0); }; $[3] = batchUpdatePositions; $[4] = t3; } else { t3 = $[4]; } const updateNodePositions = t3; const gestureInstanceRef = useRef(0); let t4; if ($[5] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t4 = { x: 0, y: 0 }; $[5] = t4; } else { t4 = $[5]; } const panStartRef = useRef(t4); const isSpaceHeld = Boolean(heldKeys?.byCode.Space || heldKeys?.byKey[" "] || heldKeys?.byKey.Spacebar); let t5; if ($[6] !== isSpaceHeld || $[7] !== pan) { t5 = (state) => { if (isDragPrevented(state.event.target)) { return; } gestureInstanceRef.current = gestureInstanceRef.current + 1; if (isSpaceHeld) { panStartRef.current = pan; } }; $[6] = isSpaceHeld; $[7] = pan; $[8] = t5; } else { t5 = $[8]; } let t6; 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) { t6 = (state_0) => { if (isDragPrevented(state_0.event.target)) { return; } if (edgeCreation.isCreating) { return; } state_0.event.stopPropagation(); if (isSpaceHeld) { if (!state_0.tap && state_0.active) { const [mx, my] = state_0.movement; setPan({ x: panStartRef.current.x + mx, y: panStartRef.current.y + my }); } return state_0.memo; } const { movement: t72, first, active, down, pinching, cancel, tap } = state_0; const [mx_0, my_0] = t72; let currentMemo = state_0.memo; if (tap || !active) { return currentMemo; } const currentGestureInstance = gestureInstanceRef.current; if (first) { const selectionSize = getSelectedNodeIds.size; const label = selectionSize > 1 ? `Move ${selectionSize} nodes` : "Move node"; pushHistory(label); setStartDrag({ nodeId: id }); const initialPositions = buildDragPositions(graph, getSelectedNodeIds); initialPositions.forEach(() => startMutation()); currentMemo = { initialPositions, gestureInstance: currentGestureInstance }; } if (!currentMemo || currentMemo.gestureInstance !== currentGestureInstance || !currentMemo.initialPositions) { if (cancel && !pinching && !down && !tap && !active) { cancel(); } return currentMemo; } const updates_1 = computeDragUpdates(currentMemo.initialPositions, mx_0, my_0, currentZoom, graph); if (updates_1.length > 0) { updateNodePositions(updates_1); } if (state_0.event && "clientX" in state_0.event) { const { nodeId: hoveredId } = hitTestNode(state_0.event.clientX, state_0.event.clientY); const validTarget = hoveredId && !currentMemo.initialPositions.has(hoveredId) ? hoveredId : null; setDropTarget(validTarget); } return currentMemo; }; $[9] = currentZoom; $[10] = edgeCreation; $[11] = getSelectedNodeIds; $[12] = graph; $[13] = id; $[14] = isSpaceHeld; $[15] = pushHistory; $[16] = setDropTarget; $[17] = setPan; $[18] = setStartDrag; $[19] = startMutation; $[20] = updateNodePositions; $[21] = t6; } else { t6 = $[21]; } let t7; 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) { t7 = (state_1) => { if (isDragPrevented(state_1.event.target)) { return; } if (edgeCreation.isCreating) { setEndDrag({ nodeId: id }); return; } if (isSpaceHeld) { return; } state_1.event.stopPropagation(); const memo = state_1.memo; setDropTarget(null); if (state_1.event && "clientX" in state_1.event && memo?.initialPositions) { const { nodeId: hoveredId_0 } = hitTestNode(state_1.event.clientX, state_1.event.clientY); if (hoveredId_0 && !memo.initialPositions.has(hoveredId_0)) { const draggedNodeIds = Array.from(memo.initialPositions.keys()).filter((nid) => getSelectedNodeIds.has(nid)); if (draggedNodeIds.length > 0) { nestNodesOnDrop({ nodeIds: draggedNodeIds, targetId: hoveredId_0 }); } } } if (!currentGraphId) { debug9("Cannot update node position: currentGraphId is not set"); setEndDrag({ nodeId: id }); return; } const nodesToUpdate = memo?.initialPositions ? Array.from(memo.initialPositions.keys()) : [id]; nodesToUpdate.forEach((draggedNodeId) => { if (!graph.hasNode(draggedNodeId)) { completeMutation(false); return; } const finalAttrs = graph.getNodeAttributes(draggedNodeId); const initialPos = memo?.initialPositions.get(draggedNodeId); if (!initialPos) { completeMutation(false); return; } const [mx_1, my_1] = state_1.movement; const deltaX = mx_1 / currentZoom; const deltaY = my_1 / currentZoom; const finalPosition = { x: initialPos.x + deltaX, y: initialPos.y + deltaY }; updateNodePositions([{ id: draggedNodeId, pos: finalPosition }]); if (!onPersist) { completeMutation(true); setEndDrag({ nodeId: draggedNodeId }); return; } const existingDbUiProps = typeof finalAttrs.dbData.ui_properties === "object" && finalAttrs.dbData.ui_properties !== null && !Array.isArray(finalAttrs.dbData.ui_properties) ? finalAttrs.dbData.ui_properties : {}; const newUiProperties = { ...existingDbUiProps, x: finalPosition.x, y: finalPosition.y, zIndex: finalAttrs.zIndex }; const pendingState = getPendingState(draggedNodeId); if (pendingState.inFlight) { pendingState.queuedPosition = finalPosition; pendingState.queuedUiProperties = newUiProperties; pendingState.graphId = currentGraphId; return; } pendingState.inFlight = true; pendingState.graphId = currentGraphId; const processQueuedUpdate = async (nodeId) => { const state_2 = getPendingState(nodeId); if (state_2 && state_2.queuedPosition && state_2.queuedUiProperties && state_2.graphId) { const queuedProps = state_2.queuedUiProperties; const queuedGraphId = state_2.graphId; state_2.queuedPosition = null; state_2.queuedUiProperties = null; startMutation(); ; try { await onPersist(nodeId, queuedGraphId, queuedProps); completeMutation(true); } catch (t82) { const error = t82; completeMutation(false); onPersistError?.(nodeId, error); } state_2.inFlight = false; processQueuedUpdate(nodeId); } else { if (state_2) { state_2.inFlight = false; } } }; onPersist(draggedNodeId, currentGraphId, newUiProperties).then(() => { completeMutation(true); processQueuedUpdate(draggedNodeId); }).catch((error_0) => { completeMutation(false); const state_3 = getPendingState(draggedNodeId); if (state_3) { state_3.inFlight = false; } const preDragAttrsForNode = getPreDragAttributes; if (preDragAttrsForNode && preDragAttrsForNode.dbData.id === draggedNodeId && graph.hasNode(draggedNodeId)) { graph.replaceNodeAttributes(draggedNodeId, preDragAttrsForNode); setGraph(graph); } onPersistError?.(draggedNodeId, error_0); processQueuedUpdate(draggedNodeId); }).finally(() => { setEndDrag({ nodeId: draggedNodeId }); }); }); }; $[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; $[39] = t7; } else { t7 = $[39]; } let t8; if ($[40] !== t5 || $[41] !== t6 || $[42] !== t7) { t8 = { onPointerDown: t5, onDrag: t6, onDragEnd: t7 }; $[40] = t5; $[41] = t6; $[42] = t7; $[43] = t8; } else { t8 = $[43]; } let t9; if ($[44] !== inputSource) { t9 = getNodeGestureConfig(inputSource); $[44] = inputSource; $[45] = t9; } else { t9 = $[45]; } const bind = useGesture(t8, t9); let t10; if ($[46] !== bind || $[47] !== updateNodePositions) { t10 = { bind, updateNodePositions }; $[46] = bind; $[47] = updateNodePositions; $[48] = t10; } else { t10 = $[48]; } return t10; } function _temp2(get, set, updates) { const graph_0 = get(graphAtom); updates.forEach((u) => { if (graph_0.hasNode(u.id)) { graph_0.setNodeAttribute(u.id, "x", u.pos.x); graph_0.setNodeAttribute(u.id, "y", u.pos.y); } }); set(nodePositionUpdateCounterAtom, _temp); } function _temp(c) { return c + 1; } // src/hooks/useNodeResize.ts import { c as _c3 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue2, useSetAtom as useSetAtom3 } from "jotai"; import { useRef as useRef2, useState, useEffect } from "react"; import { flushSync } from "react-dom"; var debug10 = createDebug("resize"); function useNodeResize(t0) { const $ = _c3(38); const { id, nodeData, updateNodePositions, options: t1 } = t0; let t2; if ($[0] !== t1) { t2 = t1 === void 0 ? {} : t1; $[0] = t1; $[1] = t2; } else { t2 = $[1]; } const options = t2; const { onPersist, onPersistError, minWidth: t3, minHeight: t4 } = options; const minWidth = t3 === void 0 ? 200 : t3; const minHeight = t4 === void 0 ? 150 : t4; const [localWidth, setLocalWidth] = useState(nodeData.width || 500); const [localHeight, setLocalHeight] = useState(nodeData.height || 500); const [isResizing, setIsResizing] = useState(false); const resizeStartRef = useRef2(null); const graph = useAtomValue2(graphAtom); const currentZoom = useAtomValue2(zoomAtom); const currentGraphId = useAtomValue2(currentGraphIdAtom); const startMutation = useSetAtom3(startMutationAtom); const completeMutation = useSetAtom3(completeMutationAtom); const setGraphUpdateVersion = useSetAtom3(graphUpdateVersionAtom); const setNodePositionUpdateCounter = useSetAtom3(nodePositionUpdateCounterAtom); let t5; let t6; if ($[2] !== isResizing || $[3] !== nodeData.height || $[4] !== nodeData.width) { t5 = () => { if (!isResizing) { setLocalWidth(nodeData.width || 500); setLocalHeight(nodeData.height || 500); } }; t6 = [nodeData.width, nodeData.height, isResizing]; $[2] = isResizing; $[3] = nodeData.height; $[4] = nodeData.width; $[5] = t5; $[6] = t6; } else { t5 = $[5]; t6 = $[6]; } useEffect(t5, t6); let t7; if ($[7] !== graph || $[8] !== id || $[9] !== localHeight || $[10] !== localWidth) { t7 = (direction) => (e) => { e.stopPropagation(); e.preventDefault(); setIsResizing(true); const nodeAttrs = graph.hasNode(id) ? graph.getNodeAttributes(id) : { x: 0, y: 0 }; resizeStartRef.current = { width: localWidth, height: localHeight, startX: e.clientX, startY: e.clientY, startNodeX: nodeAttrs.x, startNodeY: nodeAttrs.y, direction }; e.target.setPointerCapture(e.pointerId); }; $[7] = graph; $[8] = id; $[9] = localHeight; $[10] = localWidth; $[11] = t7; } else { t7 = $[11]; } const createResizeStart = t7; let t8; if ($[12] !== currentZoom || $[13] !== graph || $[14] !== id || $[15] !== minHeight || $[16] !== minWidth || $[17] !== setGraphUpdateVersion || $[18] !== setNodePositionUpdateCounter || $[19] !== updateNodePositions) { t8 = (e_0) => { if (!resizeStartRef.current) { return; } e_0.stopPropagation(); e_0.preventDefault(); const deltaX = (e_0.clientX - resizeStartRef.current.startX) / currentZoom; const deltaY = (e_0.clientY - resizeStartRef.current.startY) / currentZoom; const { direction: direction_0, width: startWidth, height: startHeight, startNodeX, startNodeY } = resizeStartRef.current; let newWidth = startWidth; let newHeight = startHeight; let newX = startNodeX; let newY = startNodeY; if (direction_0.includes("e")) { newWidth = Math.max(minWidth, startWidth + deltaX); } if (direction_0.includes("w")) { newWidth = Math.max(minWidth, startWidth - deltaX); newX = startNodeX + (startWidth - newWidth); } if (direction_0.includes("s")) { newHeight = Math.max(minHeight, startHeight + deltaY); } if (direction_0.includes("n")) { newHeight = Math.max(minHeight, startHeight - deltaY); newY = startNodeY + (startHeight - newHeight); } if (graph.hasNode(id)) { graph.setNodeAttribute(id, "width", newWidth); graph.setNodeAttribute(id, "height", newHeight); graph.setNodeAttribute(id, "x", newX); graph.setNodeAttribute(id, "y", newY); } flushSync(() => { setLocalWidth(newWidth); setLocalHeight(newHeight); setGraphUpdateVersion(_temp3); }); if (direction_0.includes("n") || direction_0.includes("w")) { updateNodePositions([{ id, pos: { x: newX, y: newY } }]); } else { setNodePositionUpdateCounter(_temp22); } }; $[12] = currentZoom; $[13] = graph; $[14] = id; $[15] = minHeight; $[16] = minWidth; $[17] = setGraphUpdateVersion; $[18] = setNodePositionUpdateCounter; $[19] = updateNodePositions; $[20] = t8; } else { t8 = $[20]; } const handleResizeMove = t8; let t9; if ($[21] !== completeMutation || $[22] !== currentGraphId || $[23] !== graph || $[24] !== id || $[25] !== localHeight || $[26] !== localWidth || $[27] !== onPersist || $[28] !== onPersistError || $[29] !== startMutation) { t9 = (e_1) => { if (!resizeStartRef.current) { return; } e_1.stopPropagation(); e_1.target.releasePointerCapture(e_1.pointerId); setIsResizing(false); if (!currentGraphId || !resizeStartRef.current) { resizeStartRef.current = null; return; } const finalAttrs = graph.hasNode(id) ? graph.getNodeAttributes(id) : null; if (!finalAttrs) { resizeStartRef.current = null; return; } const finalWidth = finalAttrs.width || localWidth; const finalHeight = finalAttrs.height || localHeight; const finalX = finalAttrs.x; const finalY = finalAttrs.y; setLocalWidth(finalWidth); setLocalHeight(finalHeight); if (!onPersist) { resizeStartRef.current = null; return; } const existingDbUiProps = typeof finalAttrs.dbData.ui_properties === "object" && finalAttrs.dbData.ui_properties !== null && !Array.isArray(finalAttrs.dbData.ui_properties) ? finalAttrs.dbData.ui_properties : {}; const newUiProperties = { ...existingDbUiProps, width: finalWidth, height: finalHeight, x: finalX, y: finalY }; startMutation(); onPersist(id, currentGraphId, newUiProperties).then(() => { completeMutation(true); }).catch((error) => { completeMutation(false); if (resizeStartRef.current) { setLocalWidth(resizeStartRef.current.width); setLocalHeight(resizeStartRef.current.height); } onPersistError?.(id, error); }).finally(() => { resizeStartRef.current = null; }); }; $[21] = completeMutation; $[22] = currentGraphId; $[23] = graph; $[24] = id; $[25] = localHeight; $[26] = localWidth; $[27] = onPersist; $[28] = onPersistError; $[29] = startMutation; $[30] = t9; } else { t9 = $[30]; } const handleResizeEnd = t9; let t10; if ($[31] !== createResizeStart || $[32] !== handleResizeEnd || $[33] !== handleResizeMove || $[34] !== isResizing || $[35] !== localHeight || $[36] !== localWidth) { t10 = { localWidth, localHeight, isResizing, createResizeStart, handleResizeMove, handleResizeEnd }; $[31] = createResizeStart; $[32] = handleResizeEnd; $[33] = handleResizeMove; $[34] = isResizing; $[35] = localHeight; $[36] = localWidth; $[37] = t10; } else { t10 = $[37]; } return t10; } function _temp22(c) { return c + 1; } function _temp3(v) { return v + 1; } // src/hooks/useCanvasHistory.ts import { c as _c4 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue3, useSetAtom as useSetAtom4, useStore } from "jotai"; // src/core/toast-store.ts import { atom as atom16 } from "jotai"; var canvasToastAtom = atom16(null); var showToastAtom = atom16(null, (_get, set, message) => { const id = `toast-${Date.now()}`; set(canvasToastAtom, { id, message, timestamp: Date.now() }); setTimeout(() => { set(canvasToastAtom, (current) => current?.id === id ? null : current); }, 2e3); }); // src/hooks/useCanvasHistory.ts function useCanvasHistory(t0) { const $ = _c4(22); const options = t0 === void 0 ? {} : t0; const { enableKeyboardShortcuts: t1 } = options; t1 === void 0 ? false : t1; const canUndo = useAtomValue3(canUndoAtom); const canRedo = useAtomValue3(canRedoAtom); const undoCount = useAtomValue3(undoCountAtom); const redoCount = useAtomValue3(redoCountAtom); const historyLabels = useAtomValue3(historyLabelsAtom); const undoAction = useSetAtom4(undoAtom); const redoAction = useSetAtom4(redoAtom); const pushHistory = useSetAtom4(pushHistoryAtom); const clearHistory = useSetAtom4(clearHistoryAtom); const showToast = useSetAtom4(showToastAtom); const store = useStore(); let t2; if ($[0] !== showToast || $[1] !== store || $[2] !== undoAction) { t2 = () => { const state = store.get(historyStateAtom); const label = state.past[state.past.length - 1]?.label; const result = undoAction(); if (result && label) { showToast(`Undo: ${label}`); } return result; }; $[0] = showToast; $[1] = store; $[2] = undoAction; $[3] = t2; } else { t2 = $[3]; } const undo = t2; let t3; if ($[4] !== redoAction || $[5] !== showToast || $[6] !== store) { t3 = () => { const state_0 = store.get(historyStateAtom); const label_0 = state_0.future[0]?.label; const result_0 = redoAction(); if (result_0 && label_0) { showToast(`Redo: ${label_0}`); } return result_0; }; $[4] = redoAction; $[5] = showToast; $[6] = store; $[7] = t3; } else { t3 = $[7]; } const redo = t3; let t4; if ($[8] !== pushHistory) { t4 = (label_1) => { pushHistory(label_1); }; $[8] = pushHistory; $[9] = t4; } else { t4 = $[9]; } const recordSnapshot = t4; let t5; if ($[10] !== clearHistory) { t5 = () => { clearHistory(); }; $[10] = clearHistory; $[11] = t5; } else { t5 = $[11]; } const clear = t5; let t6; if ($[12] !== canRedo || $[13] !== canUndo || $[14] !== clear || $[15] !== historyLabels || $[16] !== recordSnapshot || $[17] !== redo || $[18] !== redoCount || $[19] !== undo || $[20] !== undoCount) { t6 = { undo, redo, canUndo, canRedo, undoCount, redoCount, historyLabels, recordSnapshot, clear }; $[12] = canRedo; $[13] = canUndo; $[14] = clear; $[15] = historyLabels; $[16] = recordSnapshot; $[17] = redo; $[18] = redoCount; $[19] = undo; $[20] = undoCount; $[21] = t6; } else { t6 = $[21]; } return t6; } // src/hooks/useCanvasSelection.ts import { c as _c5 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue4 } from "jotai"; function useCanvasSelection() { const $ = _c5(6); const selectedNodeIds = useAtomValue4(selectedNodeIdsAtom); const selectedEdgeId = useAtomValue4(selectedEdgeIdAtom); const count = useAtomValue4(selectedNodesCountAtom); const hasSelection = useAtomValue4(hasSelectionAtom); const t0 = selectedEdgeId !== null; let t1; if ($[0] !== count || $[1] !== hasSelection || $[2] !== selectedEdgeId || $[3] !== selectedNodeIds || $[4] !== t0) { t1 = { selectedNodeIds, selectedEdgeId, count, hasSelection, hasEdgeSelection: t0 }; $[0] = count; $[1] = hasSelection; $[2] = selectedEdgeId; $[3] = selectedNodeIds; $[4] = t0; $[5] = t1; } else { t1 = $[5]; } return t1; } // src/hooks/useCanvasViewport.ts import { c as _c6 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue5 } from "jotai"; function useCanvasViewport() { const $ = _c6(9); const zoom = useAtomValue5(zoomAtom); const pan = useAtomValue5(panAtom); const viewportRect = useAtomValue5(viewportRectAtom); const screenToWorld = useAtomValue5(screenToWorldAtom); const worldToScreen = useAtomValue5(worldToScreenAtom); const zoomFocusNodeId = useAtomValue5(zoomFocusNodeIdAtom); const zoomTransitionProgress = useAtomValue5(zoomTransitionProgressAtom); const isZoomTransitioning = useAtomValue5(isZoomTransitioningAtom); let t0; if ($[0] !== isZoomTransitioning || $[1] !== pan || $[2] !== screenToWorld || $[3] !== viewportRect || $[4] !== worldToScreen || $[5] !== zoom || $[6] !== zoomFocusNodeId || $[7] !== zoomTransitionProgress) { t0 = { zoom, pan, viewportRect, screenToWorld, worldToScreen, zoomFocusNodeId, zoomTransitionProgress, isZoomTransitioning, zoomTransitionThreshold: ZOOM_TRANSITION_THRESHOLD, zoomExitThreshold: ZOOM_EXIT_THRESHOLD }; $[0] = isZoomTransitioning; $[1] = pan; $[2] = screenToWorld; $[3] = viewportRect; $[4] = worldToScreen; $[5] = zoom; $[6] = zoomFocusNodeId; $[7] = zoomTransitionProgress; $[8] = t0; } else { t0 = $[8]; } return t0; } // src/hooks/useCanvasDrag.ts import { c as _c7 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue6 } from "jotai"; function useCanvasDrag() { const $ = _c7(3); const draggingNodeId = useAtomValue6(draggingNodeIdAtom); const t0 = draggingNodeId !== null; let t1; if ($[0] !== draggingNodeId || $[1] !== t0) { t1 = { draggingNodeId, isDragging: t0 }; $[0] = draggingNodeId; $[1] = t0; $[2] = t1; } else { t1 = $[2]; } return t1; } // src/hooks/useLayout.ts import { c as _c8 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue7, useSetAtom as useSetAtom5 } from "jotai"; var useGetGraphBounds = () => { const $ = _c8(6); const graph = useAtomValue7(graphAtom); useAtomValue7(nodePositionUpdateCounterAtom); let nodes; let t0; if ($[0] !== graph) { nodes = graph.nodes().map((node) => { const nodeAttributes = graph.getNodeAttributes(node); return { x: nodeAttributes.x, y: nodeAttributes.y, width: nodeAttributes.width || 500, height: nodeAttributes.height || 500 }; }); t0 = calculateBounds(nodes); $[0] = graph; $[1] = nodes; $[2] = t0; } else { nodes = $[1]; t0 = $[2]; } const bounds = t0; let t1; if ($[3] !== bounds || $[4] !== nodes) { t1 = { bounds, nodes }; $[3] = bounds; $[4] = nodes; $[5] = t1; } else { t1 = $[5]; } return t1; }; var useSelectionBounds = () => { const $ = _c8(5); const selectedNodeIds = useAtomValue7(selectedNodeIdsAtom); const nodes = useAtomValue7(uiNodesAtom); let t0; if ($[0] !== nodes || $[1] !== selectedNodeIds) { let t1; if ($[3] !== selectedNodeIds) { t1 = (node) => selectedNodeIds.has(node.id); $[3] = selectedNodeIds; $[4] = t1; } else { t1 = $[4]; } const selectedNodes = nodes.filter(t1).map(_temp4); t0 = calculateBounds(selectedNodes); $[0] = nodes; $[1] = selectedNodeIds; $[2] = t0; } else { t0 = $[2]; } return t0; }; var useFitToBounds = () => { const $ = _c8(2); const setFitToBounds = useSetAtom5(fitToBoundsAtom); let t0; if ($[0] !== setFitToBounds) { const fitToBounds = (mode, t1) => { const padding = t1 === void 0 ? 20 : t1; setFitToBounds({ mode, padding }); }; t0 = { fitToBounds }; $[0] = setFitToBounds; $[1] = t0; } else { t0 = $[1]; } return t0; }; var useLayout = () => { const $ = _c8(5); const { fitToBounds } = useFitToBounds(); const { bounds: graphBounds, nodes: graphNodes } = useGetGraphBounds(); const selectionBounds = useSelectionBounds(); let t0; if ($[0] !== fitToBounds || $[1] !== graphBounds || $[2] !== graphNodes || $[3] !== selectionBounds) { t0 = { fitToBounds, graphBounds, graphNodes, selectionBounds }; $[0] = fitToBounds; $[1] = graphBounds; $[2] = graphNodes; $[3] = selectionBounds; $[4] = t0; } else { t0 = $[4]; } return t0; }; function _temp4(node_0) { return { x: node_0.position.x, y: node_0.position.y, width: node_0.width ?? 500, height: node_0.height ?? 500 }; } // src/hooks/useForceLayout.ts import * as d3 from "d3-force"; import { useAtomValue as useAtomValue8, useSetAtom as useSetAtom6 } from "jotai"; import { useRef as useRef3 } from "react"; var debug11 = createDebug("force-layout"); var useForceLayout = (options = {}) => { const { onPositionsChanged, maxIterations = 1e3, chargeStrength = -6e3, linkStrength = 0.03 } = options; const nodes = useAtomValue8(uiNodesAtom); const graph = useAtomValue8(graphAtom); const updateNodePosition = useSetAtom6(updateNodePositionAtom); const isRunningRef = useRef3(false); const createVirtualLinks = () => { const links = []; for (let i = 0; i < nodes.length; i++) { for (let j = 1; j <= 3; j++) { const targetIndex = (i + j) % nodes.length; links.push({ source: nodes[i].id, target: nodes[targetIndex].id, strength: 0.05 // Very weak connection }); } } return links; }; const applyForceLayout = async () => { if (isRunningRef.current) { debug11.warn("Layout already running, ignoring request."); return; } if (nodes.length === 0) { debug11.warn("No nodes to layout."); return; } isRunningRef.current = true; const simulationNodes = nodes.map((node) => { const width = node.width || 80; const height = node.height || 80; return { id: node.id, x: node.position?.x || 0, y: node.position?.y || 0, width, height, radius: Math.max(width, height) + 80 }; }); const simulation = d3.forceSimulation(simulationNodes).force("charge", d3.forceManyBody().strength(chargeStrength).distanceMax(900)).force("collide", d3.forceCollide().radius((d) => d.radius).strength(2).iterations(8)).force("link", d3.forceLink(createVirtualLinks()).id((d_0) => d_0.id).strength(linkStrength)).force("center", d3.forceCenter(0, 0)).stop(); debug11("Starting simulation..."); return new Promise((resolve) => { let iterations = 0; function runSimulationStep() { if (iterations >= maxIterations) { debug11("Reached max iterations (%d), finalizing.", maxIterations); finalizeLayout(); return; } simulation.tick(); iterations++; let hasOverlaps = false; for (let i_0 = 0; i_0 < simulationNodes.length; i_0++) { for (let j_0 = i_0 + 1; j_0 < simulationNodes.length; j_0++) { if (checkNodesOverlap(simulationNodes[i_0], simulationNodes[j_0])) { hasOverlaps = true; break; } } if (hasOverlaps) break; } if (!hasOverlaps) { debug11("No overlaps after %d iterations.", iterations); finalizeLayout(); return; } requestAnimationFrame(runSimulationStep); } function finalizeLayout() { const positionUpdates = []; simulationNodes.forEach((simNode) => { if (graph.hasNode(simNode.id)) { const newPosition = { x: Math.round(simNode.x), y: Math.round(simNode.y) }; updateNodePosition({ nodeId: simNode.id, position: newPosition }); positionUpdates.push({ nodeId: simNode.id, position: newPosition }); } }); if (onPositionsChanged && positionUpdates.length > 0) { debug11("Saving %d positions via callback...", positionUpdates.length); Promise.resolve(onPositionsChanged(positionUpdates)).then(() => debug11("Positions saved successfully.")).catch((err) => debug11.error("Error saving positions: %O", err)); } debug11("Layout complete."); isRunningRef.current = false; resolve(); } requestAnimationFrame(runSimulationStep); }); }; return { applyForceLayout, isRunning: isRunningRef.current }; }; // src/hooks/useCanvasSettings.ts import { c as _c9 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue9, useSetAtom as useSetAtom7 } from "jotai"; // src/core/settings-store.ts import { atom as atom17 } from "jotai"; import { atomWithStorage } from "jotai/utils"; // src/core/event-types.ts var CanvasEventType = /* @__PURE__ */ (function(CanvasEventType2) { CanvasEventType2["NodeClick"] = "node:click"; CanvasEventType2["NodeDoubleClick"] = "node:double-click"; CanvasEventType2["NodeTripleClick"] = "node:triple-click"; CanvasEventType2["NodeRightClick"] = "node:right-click"; CanvasEventType2["NodeLongPress"] = "node:long-press"; CanvasEventType2["EdgeClick"] = "edge:click"; CanvasEventType2["EdgeDoubleClick"] = "edge:double-click"; CanvasEventType2["EdgeRightClick"] = "edge:right-click"; CanvasEventType2["BackgroundClick"] = "background:click"; CanvasEventType2["BackgroundDoubleClick"] = "background:double-click"; CanvasEventType2["BackgroundRightClick"] = "background:right-click"; CanvasEventType2["BackgroundLongPress"] = "background:long-press"; return CanvasEventType2; })({}); var EVENT_TYPE_INFO = { [CanvasEventType.NodeClick]: { type: CanvasEventType.NodeClick, label: "Click Node", description: "Triggered when clicking on a node", category: "node" }, [CanvasEventType.NodeDoubleClick]: { type: CanvasEventType.NodeDoubleClick, label: "Double-click Node", description: "Triggered when double-clicking on a node", category: "node" }, [CanvasEventType.NodeTripleClick]: { type: CanvasEventType.NodeTripleClick, label: "Triple-click Node", description: "Triggered when triple-clicking on a node", category: "node" }, [CanvasEventType.NodeRightClick]: { type: CanvasEventType.NodeRightClick, label: "Right-click Node", description: "Triggered when right-clicking on a node", category: "node" }, [CanvasEventType.NodeLongPress]: { type: CanvasEventType.NodeLongPress, label: "Long-press Node", description: "Triggered when long-pressing on a node (mobile/touch)", category: "node" }, [CanvasEventType.EdgeClick]: { type: CanvasEventType.EdgeClick, label: "Click Edge", description: "Triggered when clicking on an edge", category: "edge" }, [CanvasEventType.EdgeDoubleClick]: { type: CanvasEventType.EdgeDoubleClick, label: "Double-click Edge", description: "Triggered when double-clicking on an edge", category: "edge" }, [CanvasEventType.EdgeRightClick]: { type: CanvasEventType.EdgeRightClick, label: "Right-click Edge", description: "Triggered when right-clicking on an edge", category: "edge" }, [CanvasEventType.BackgroundClick]: { type: CanvasEventType.BackgroundClick, label: "Click Background", description: "Triggered when clicking on the canvas background", category: "background" }, [CanvasEventType.BackgroundDoubleClick]: { type: CanvasEventType.BackgroundDoubleClick, label: "Double-click Background", description: "Triggered when double-clicking on the canvas background", category: "background" }, [CanvasEventType.BackgroundRightClick]: { type: CanvasEventType.BackgroundRightClick, label: "Right-click Background", description: "Triggered when right-clicking on the canvas background", category: "background" }, [CanvasEventType.BackgroundLongPress]: { type: CanvasEventType.BackgroundLongPress, label: "Long-press Background", description: "Triggered when long-pressing on the canvas background (mobile/touch)", category: "background" } }; // src/core/action-types.ts var ActionCategory = /* @__PURE__ */ (function(ActionCategory2) { ActionCategory2["None"] = "none"; ActionCategory2["Selection"] = "selection"; ActionCategory2["Viewport"] = "viewport"; ActionCategory2["Node"] = "node"; ActionCategory2["Layout"] = "layout"; ActionCategory2["History"] = "history"; ActionCategory2["Custom"] = "custom"; return ActionCategory2; })({}); var BuiltInActionId = { // None None: "none", // Selection SelectNode: "select-node", SelectEdge: "select-edge", AddToSelection: "add-to-selection", ClearSelection: "clear-selection", DeleteSelected: "delete-selected", // Viewport FitToView: "fit-to-view", FitAllToView: "fit-all-to-view", CenterOnNode: "center-on-node", ResetViewport: "reset-viewport", // Node LockNode: "lock-node", UnlockNode: "unlock-node", ToggleLock: "toggle-lock", OpenContextMenu: "open-context-menu", SplitNode: "split-node", GroupNodes: "group-nodes", MergeNodes: "merge-nodes", // Layout ApplyForceLayout: "apply-force-layout", // History Undo: "undo", Redo: "redo", // Creation CreateNode: "create-node" }; // src/core/settings-state-types.ts var DEFAULT_MAPPINGS = { [CanvasEventType.NodeClick]: BuiltInActionId.None, [CanvasEventType.NodeDoubleClick]: BuiltInActionId.FitToView, [CanvasEventType.NodeTripleClick]: BuiltInActionId.ToggleLock, [CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.NodeLongPress]: BuiltInActionId.OpenContextMenu, [CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge, [CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None, [CanvasEventType.EdgeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection, [CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.FitAllToView, [CanvasEventType.BackgroundRightClick]: BuiltInActionId.None, [CanvasEventType.BackgroundLongPress]: BuiltInActionId.CreateNode }; // src/core/settings-presets.ts var BUILT_IN_PRESETS = [{ id: "default", name: "Default", description: "Standard canvas interactions", isBuiltIn: true, mappings: DEFAULT_MAPPINGS }, { id: "minimal", name: "Minimal", description: "Only essential selection and context menu actions", isBuiltIn: true, mappings: { [CanvasEventType.NodeClick]: BuiltInActionId.None, [CanvasEventType.NodeDoubleClick]: BuiltInActionId.None, [CanvasEventType.NodeTripleClick]: BuiltInActionId.None, [CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.NodeLongPress]: BuiltInActionId.OpenContextMenu, [CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge, [CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None, [CanvasEventType.EdgeRightClick]: BuiltInActionId.None, [CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection, [CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.None, [CanvasEventType.BackgroundRightClick]: BuiltInActionId.None, [CanvasEventType.BackgroundLongPress]: BuiltInActionId.None } }, { id: "power-user", name: "Power User", description: "Quick actions for experienced users", isBuiltIn: true, mappings: { [CanvasEventType.NodeClick]: BuiltInActionId.None, [CanvasEventType.NodeDoubleClick]: BuiltInActionId.ToggleLock, [CanvasEventType.NodeTripleClick]: BuiltInActionId.DeleteSelected, [CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.NodeLongPress]: BuiltInActionId.AddToSelection, [CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge, [CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None, [CanvasEventType.EdgeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection, [CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.CreateNode, [CanvasEventType.BackgroundRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.BackgroundLongPress]: BuiltInActionId.ApplyForceLayout } }]; function getActionForEvent(mappings, event) { return mappings[event] || BuiltInActionId.None; } // src/core/settings-store.ts var debug12 = createDebug("settings"); var DEFAULT_STATE = { mappings: DEFAULT_MAPPINGS, activePresetId: "default", customPresets: [], isPanelOpen: false, virtualizationEnabled: true }; var canvasSettingsAtom = atomWithStorage("@blinksgg/canvas/settings", DEFAULT_STATE); var eventMappingsAtom = atom17((get) => get(canvasSettingsAtom).mappings); var activePresetIdAtom = atom17((get) => get(canvasSettingsAtom).activePresetId); var allPresetsAtom = atom17((get) => { const state = get(canvasSettingsAtom); return [...BUILT_IN_PRESETS, ...state.customPresets]; }); var activePresetAtom = atom17((get) => { const presetId = get(activePresetIdAtom); if (!presetId) return null; const allPresets = get(allPresetsAtom); return allPresets.find((p) => p.id === presetId) || null; }); var isPanelOpenAtom = atom17((get) => get(canvasSettingsAtom).isPanelOpen); var virtualizationEnabledAtom = atom17((get) => get(canvasSettingsAtom).virtualizationEnabled ?? true); var hasUnsavedChangesAtom = atom17((get) => { const state = get(canvasSettingsAtom); const activePreset = get(activePresetAtom); if (!activePreset) return true; const events = Object.values(CanvasEventType); return events.some((event) => state.mappings[event] !== activePreset.mappings[event]); }); var setEventMappingAtom = atom17(null, (get, set, { event, actionId }) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: { ...current.mappings, [event]: actionId }, // Clear active preset since mappings have changed activePresetId: null }); }); var applyPresetAtom = atom17(null, (get, set, presetId) => { const allPresets = get(allPresetsAtom); const preset = allPresets.find((p) => p.id === presetId); if (!preset) { debug12.warn("Preset not found: %s", presetId); return; } const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: { ...preset.mappings }, activePresetId: presetId }); }); var saveAsPresetAtom = atom17(null, (get, set, { name, description }) => { const current = get(canvasSettingsAtom); const id = `custom-${Date.now()}`; const newPreset = { id, name, description, mappings: { ...current.mappings }, isBuiltIn: false }; set(canvasSettingsAtom, { ...current, customPresets: [...current.customPresets, newPreset], activePresetId: id }); return id; }); var updatePresetAtom = atom17(null, (get, set, presetId) => { const current = get(canvasSettingsAtom); const presetIndex = current.customPresets.findIndex((p) => p.id === presetId); if (presetIndex === -1) { debug12.warn("Cannot update preset: %s (not found or built-in)", presetId); return; } const updatedPresets = [...current.customPresets]; updatedPresets[presetIndex] = { ...updatedPresets[presetIndex], mappings: { ...current.mappings } }; set(canvasSettingsAtom, { ...current, customPresets: updatedPresets, activePresetId: presetId }); }); var deletePresetAtom = atom17(null, (get, set, presetId) => { const current = get(canvasSettingsAtom); const newCustomPresets = current.customPresets.filter((p) => p.id !== presetId); if (newCustomPresets.length === current.customPresets.length) { debug12.warn("Cannot delete preset: %s (not found or built-in)", presetId); return; } const newActiveId = current.activePresetId === presetId ? "default" : current.activePresetId; const newMappings = newActiveId === "default" ? DEFAULT_MAPPINGS : current.mappings; set(canvasSettingsAtom, { ...current, customPresets: newCustomPresets, activePresetId: newActiveId, mappings: newMappings }); }); var resetSettingsAtom = atom17(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: DEFAULT_MAPPINGS, activePresetId: "default" }); }); var togglePanelAtom = atom17(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: !current.isPanelOpen }); }); var setPanelOpenAtom = atom17(null, (get, set, isOpen) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: isOpen }); }); var setVirtualizationEnabledAtom = atom17(null, (get, set, enabled) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: enabled }); }); var toggleVirtualizationAtom = atom17(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: !(current.virtualizationEnabled ?? true) }); }); // src/hooks/useCanvasSettings.ts function useCanvasSettings() { const $ = _c9(34); const mappings = useAtomValue9(eventMappingsAtom); const activePresetId = useAtomValue9(activePresetIdAtom); const activePreset = useAtomValue9(activePresetAtom); const allPresets = useAtomValue9(allPresetsAtom); const hasUnsavedChanges = useAtomValue9(hasUnsavedChangesAtom); const isPanelOpen = useAtomValue9(isPanelOpenAtom); const setEventMappingAction = useSetAtom7(setEventMappingAtom); const applyPresetAction = useSetAtom7(applyPresetAtom); const saveAsPresetAction = useSetAtom7(saveAsPresetAtom); const updatePresetAction = useSetAtom7(updatePresetAtom); const deletePresetAction = useSetAtom7(deletePresetAtom); const resetSettingsAction = useSetAtom7(resetSettingsAtom); const togglePanelAction = useSetAtom7(togglePanelAtom); const setPanelOpenAction = useSetAtom7(setPanelOpenAtom); let t0; if ($[0] !== setEventMappingAction) { t0 = (event, actionId) => { setEventMappingAction({ event, actionId }); }; $[0] = setEventMappingAction; $[1] = t0; } else { t0 = $[1]; } const setEventMapping = t0; let t1; if ($[2] !== applyPresetAction) { t1 = (presetId) => { applyPresetAction(presetId); }; $[2] = applyPresetAction; $[3] = t1; } else { t1 = $[3]; } const applyPreset = t1; let t2; if ($[4] !== saveAsPresetAction) { t2 = (name, description) => saveAsPresetAction({ name, description }); $[4] = saveAsPresetAction; $[5] = t2; } else { t2 = $[5]; } const saveAsPreset = t2; let t3; if ($[6] !== updatePresetAction) { t3 = (presetId_0) => { updatePresetAction(presetId_0); }; $[6] = updatePresetAction; $[7] = t3; } else { t3 = $[7]; } const updatePreset = t3; let t4; if ($[8] !== deletePresetAction) { t4 = (presetId_1) => { deletePresetAction(presetId_1); }; $[8] = deletePresetAction; $[9] = t4; } else { t4 = $[9]; } const deletePreset = t4; let t5; if ($[10] !== resetSettingsAction) { t5 = () => { resetSettingsAction(); }; $[10] = resetSettingsAction; $[11] = t5; } else { t5 = $[11]; } const resetSettings = t5; let t6; if ($[12] !== togglePanelAction) { t6 = () => { togglePanelAction(); }; $[12] = togglePanelAction; $[13] = t6; } else { t6 = $[13]; } const togglePanel = t6; let t7; if ($[14] !== setPanelOpenAction) { t7 = (isOpen) => { setPanelOpenAction(isOpen); }; $[14] = setPanelOpenAction; $[15] = t7; } else { t7 = $[15]; } const setPanelOpen = t7; let t8; if ($[16] !== mappings) { t8 = (event_0) => getActionForEvent(mappings, event_0); $[16] = mappings; $[17] = t8; } else { t8 = $[17]; } const getActionForEventFn = t8; let t9; if ($[18] !== activePreset || $[19] !== activePresetId || $[20] !== allPresets || $[21] !== applyPreset || $[22] !== deletePreset || $[23] !== getActionForEventFn || $[24] !== hasUnsavedChanges || $[25] !== isPanelOpen || $[26] !== mappings || $[27] !== resetSettings || $[28] !== saveAsPreset || $[29] !== setEventMapping || $[30] !== setPanelOpen || $[31] !== togglePanel || $[32] !== updatePreset) { t9 = { mappings, activePresetId, activePreset, allPresets, hasUnsavedChanges, isPanelOpen, setEventMapping, applyPreset, saveAsPreset, updatePreset, deletePreset, resetSettings, togglePanel, setPanelOpen, getActionForEvent: getActionForEventFn }; $[18] = activePreset; $[19] = activePresetId; $[20] = allPresets; $[21] = applyPreset; $[22] = deletePreset; $[23] = getActionForEventFn; $[24] = hasUnsavedChanges; $[25] = isPanelOpen; $[26] = mappings; $[27] = resetSettings; $[28] = saveAsPreset; $[29] = setEventMapping; $[30] = setPanelOpen; $[31] = togglePanel; $[32] = updatePreset; $[33] = t9; } else { t9 = $[33]; } return t9; } // src/hooks/useActionExecutor.ts import { c as _c10 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue10, useStore as useStore2 } from "jotai"; // src/core/actions-node.ts function registerSelectionActions() { registerAction({ id: BuiltInActionId.SelectNode, label: "Select Node", description: "Select this node (replacing current selection)", category: ActionCategory.Selection, icon: "pointer", requiresNode: true, isBuiltIn: true, handler: (context, helpers) => { if (context.nodeId) { helpers.selectNode(context.nodeId); } } }); registerAction({ id: BuiltInActionId.SelectEdge, label: "Select Edge", description: "Select this edge", category: ActionCategory.Selection, icon: "git-commit", isBuiltIn: true, handler: (context, helpers) => { if (context.edgeId) { helpers.selectEdge(context.edgeId); } } }); registerAction({ id: BuiltInActionId.AddToSelection, label: "Add to Selection", description: "Add this node to the current selection", category: ActionCategory.Selection, icon: "plus-square", requiresNode: true, isBuiltIn: true, handler: (context, helpers) => { if (context.nodeId) { helpers.addToSelection(context.nodeId); } } }); registerAction({ id: BuiltInActionId.ClearSelection, label: "Clear Selection", description: "Deselect all nodes", category: ActionCategory.Selection, icon: "x-square", isBuiltIn: true, handler: (_context, helpers) => { helpers.clearSelection(); } }); registerAction({ id: BuiltInActionId.DeleteSelected, label: "Delete Selected", description: "Delete all selected nodes", category: ActionCategory.Selection, icon: "trash-2", isBuiltIn: true, handler: async (_context, helpers) => { const selectedIds = helpers.getSelectedNodeIds(); for (const nodeId of selectedIds) { await helpers.deleteNode(nodeId); } } }); } function registerNodeActions() { registerAction({ id: BuiltInActionId.LockNode, label: "Lock Node", description: "Prevent this node from being moved", category: ActionCategory.Node, icon: "lock", requiresNode: true, isBuiltIn: true, handler: (context, helpers) => { if (context.nodeId) { helpers.lockNode(context.nodeId); } } }); registerAction({ id: BuiltInActionId.UnlockNode, label: "Unlock Node", description: "Allow this node to be moved", category: ActionCategory.Node, icon: "unlock", requiresNode: true, isBuiltIn: true, handler: (context, helpers) => { if (context.nodeId) { helpers.unlockNode(context.nodeId); } } }); registerAction({ id: BuiltInActionId.ToggleLock, label: "Toggle Lock", description: "Toggle whether this node can be moved", category: ActionCategory.Node, icon: "lock", requiresNode: true, isBuiltIn: true, handler: (context, helpers) => { if (context.nodeId) { helpers.toggleLock(context.nodeId); } } }); registerAction({ id: BuiltInActionId.OpenContextMenu, label: "Open Context Menu", description: "Show the context menu for this node", category: ActionCategory.Node, icon: "more-vertical", isBuiltIn: true, handler: (context, helpers) => { if (helpers.openContextMenu) { helpers.openContextMenu(context.screenPosition, context.nodeId); } } }); registerAction({ id: BuiltInActionId.CreateNode, label: "Create Node", description: "Create a new node at this position", category: ActionCategory.Node, icon: "plus", isBuiltIn: true, handler: async (context, helpers) => { if (helpers.createNode) { await helpers.createNode(context.worldPosition); } } }); registerAction({ id: BuiltInActionId.SplitNode, label: "Split Node", description: "Split a node into two separate nodes", category: ActionCategory.Node, icon: "split", isBuiltIn: true, handler: async (context, helpers) => { if (helpers.splitNode && context.nodeId) { await helpers.splitNode(context.nodeId); } } }); registerAction({ id: BuiltInActionId.GroupNodes, label: "Group Nodes", description: "Group selected nodes into a parent container", category: ActionCategory.Node, icon: "group", isBuiltIn: true, handler: async (context, helpers) => { if (helpers.groupNodes) { await helpers.groupNodes(context.selectedNodeIds ?? helpers.getSelectedNodeIds()); } } }); registerAction({ id: BuiltInActionId.MergeNodes, label: "Merge Nodes", description: "Merge selected nodes into one", category: ActionCategory.Node, icon: "merge", isBuiltIn: true, handler: async (context, helpers) => { if (helpers.mergeNodes) { await helpers.mergeNodes(context.selectedNodeIds ?? helpers.getSelectedNodeIds()); } } }); } // src/core/actions-viewport.ts function registerViewportActions() { registerAction({ id: BuiltInActionId.FitToView, label: "Fit to View", description: "Zoom and pan to fit this node in view", category: ActionCategory.Viewport, icon: "maximize-2", requiresNode: true, isBuiltIn: true, handler: (context, helpers) => { if (context.nodeId) { helpers.centerOnNode(context.nodeId); } } }); registerAction({ id: BuiltInActionId.FitAllToView, label: "Fit All to View", description: "Zoom and pan to fit all nodes in view", category: ActionCategory.Viewport, icon: "maximize", isBuiltIn: true, handler: (_context, helpers) => { helpers.fitToBounds("graph"); } }); registerAction({ id: BuiltInActionId.CenterOnNode, label: "Center on Node", description: "Center the viewport on this node", category: ActionCategory.Viewport, icon: "crosshair", requiresNode: true, isBuiltIn: true, handler: (context, helpers) => { if (context.nodeId) { helpers.centerOnNode(context.nodeId); } } }); registerAction({ id: BuiltInActionId.ResetViewport, label: "Reset Viewport", description: "Reset zoom to 100% and center on origin", category: ActionCategory.Viewport, icon: "home", isBuiltIn: true, handler: (_context, helpers) => { helpers.resetViewport(); } }); } function registerHistoryActions() { registerAction({ id: BuiltInActionId.Undo, label: "Undo", description: "Undo the last action", category: ActionCategory.History, icon: "undo-2", isBuiltIn: true, handler: (_context, helpers) => { if (helpers.canUndo()) { helpers.undo(); } } }); registerAction({ id: BuiltInActionId.Redo, label: "Redo", description: "Redo the last undone action", category: ActionCategory.History, icon: "redo-2", isBuiltIn: true, handler: (_context, helpers) => { if (helpers.canRedo()) { helpers.redo(); } } }); registerAction({ id: BuiltInActionId.ApplyForceLayout, label: "Apply Force Layout", description: "Automatically arrange nodes using force-directed layout", category: ActionCategory.Layout, icon: "layout-grid", isBuiltIn: true, handler: async (_context, helpers) => { await helpers.applyForceLayout(); } }); } // src/core/built-in-actions.ts function registerBuiltInActions() { registerAction({ id: BuiltInActionId.None, label: "None", description: "Do nothing", category: ActionCategory.None, icon: "ban", isBuiltIn: true, handler: () => { } }); registerSelectionActions(); registerNodeActions(); registerViewportActions(); registerHistoryActions(); } // src/core/action-registry.ts var actionRegistry = /* @__PURE__ */ new Map(); function registerAction(action) { actionRegistry.set(action.id, action); } function getAction(id) { return actionRegistry.get(id); } function unregisterAction(id) { return actionRegistry.delete(id); } registerBuiltInActions(); // src/core/locked-node-store.ts import { atom as atom18 } from "jotai"; var lockedNodeIdAtom = atom18(null); var lockedNodeDataAtom = atom18((get) => { const id = get(lockedNodeIdAtom); if (!id) return null; const nodes = get(uiNodesAtom); return nodes.find((n) => n.id === id) || null; }); var lockedNodePageIndexAtom = atom18(0); var lockedNodePageCountAtom = atom18(1); var lockNodeAtom = atom18(null, (_get, set, payload) => { set(lockedNodeIdAtom, payload.nodeId); set(lockedNodePageIndexAtom, 0); }); var unlockNodeAtom = atom18(null, (_get, set) => { set(lockedNodeIdAtom, null); }); var nextLockedPageAtom = atom18(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current + 1) % pageCount); }); var prevLockedPageAtom = atom18(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current - 1 + pageCount) % pageCount); }); var goToLockedPageAtom = atom18(null, (get, set, index) => { const pageCount = get(lockedNodePageCountAtom); if (index >= 0 && index < pageCount) { set(lockedNodePageIndexAtom, index); } }); var hasLockedNodeAtom = atom18((get) => get(lockedNodeIdAtom) !== null); // src/core/action-executor.ts var debug13 = createDebug("actions"); async function executeAction(actionId, context, helpers) { if (actionId === BuiltInActionId.None) { return { success: true, actionId }; } const action = getAction(actionId); if (!action) { debug13.warn("Action not found: %s", actionId); return { success: false, actionId, error: new Error(`Action not found: ${actionId}`) }; } if (action.requiresNode && !context.nodeId) { debug13.warn("Action %s requires a node context", actionId); return { success: false, actionId, error: new Error(`Action ${actionId} requires a node context`) }; } try { const result = action.handler(context, helpers); if (result instanceof Promise) { await result; } return { success: true, actionId }; } catch (error) { debug13.error("Error executing action %s: %O", actionId, error); return { success: false, actionId, error: error instanceof Error ? error : new Error(String(error)) }; } } function buildActionHelpers(store, options = {}) { return { selectNode: (nodeId) => store.set(selectSingleNodeAtom, nodeId), addToSelection: (nodeId) => store.set(addNodesToSelectionAtom, [nodeId]), clearSelection: () => store.set(clearSelectionAtom), getSelectedNodeIds: () => Array.from(store.get(selectedNodeIdsAtom)), fitToBounds: (mode, padding) => { const fitMode = mode === "graph" ? FitToBoundsMode.Graph : FitToBoundsMode.Selection; store.set(fitToBoundsAtom, { mode: fitMode, padding }); }, centerOnNode: (nodeId) => store.set(centerOnNodeAtom, nodeId), resetViewport: () => store.set(resetViewportAtom), lockNode: (nodeId) => store.set(lockNodeAtom, { nodeId }), unlockNode: (_nodeId) => store.set(unlockNodeAtom), toggleLock: (nodeId) => { const currentLockedId = store.get(lockedNodeIdAtom); if (currentLockedId === nodeId) { store.set(unlockNodeAtom); } else { store.set(lockNodeAtom, { nodeId }); } }, deleteNode: async (nodeId) => { if (options.onDeleteNode) { await options.onDeleteNode(nodeId); } else { debug13.warn("deleteNode called but onDeleteNode callback not provided"); } }, isNodeLocked: (nodeId) => store.get(lockedNodeIdAtom) === nodeId, applyForceLayout: async () => { if (options.onApplyForceLayout) { await options.onApplyForceLayout(); } else { debug13.warn("applyForceLayout called but onApplyForceLayout callback not provided"); } }, undo: () => store.set(undoAtom), redo: () => store.set(redoAtom), canUndo: () => store.get(canUndoAtom), canRedo: () => store.get(canRedoAtom), selectEdge: (edgeId) => store.set(selectEdgeAtom, edgeId), clearEdgeSelection: () => store.set(clearEdgeSelectionAtom), openContextMenu: options.onOpenContextMenu, createNode: options.onCreateNode }; } // src/hooks/useActionExecutor.ts function useActionExecutor(t0) { const $ = _c10(13); const options = t0 === void 0 ? {} : t0; const store = useStore2(); const mappings = useAtomValue10(eventMappingsAtom); const helpers = buildActionHelpers(store, { onDeleteNode: options.onDeleteNode, onOpenContextMenu: options.onOpenContextMenu, onCreateNode: options.onCreateNode, onApplyForceLayout: options.onApplyForceLayout }); let t1; if ($[0] !== helpers) { t1 = async (actionId, context) => executeAction(actionId, context, helpers); $[0] = helpers; $[1] = t1; } else { t1 = $[1]; } const executeActionById = t1; let t2; if ($[2] !== helpers || $[3] !== mappings) { t2 = async (event, context_0) => { const actionId_0 = getActionForEvent(mappings, event); return executeAction(actionId_0, context_0, helpers); }; $[2] = helpers; $[3] = mappings; $[4] = t2; } else { t2 = $[4]; } const executeEventAction = t2; let t3; if ($[5] !== mappings) { t3 = (event_0) => getActionForEvent(mappings, event_0); $[5] = mappings; $[6] = t3; } else { t3 = $[6]; } const getActionForEventFn = t3; let t4; if ($[7] !== executeActionById || $[8] !== executeEventAction || $[9] !== getActionForEventFn || $[10] !== helpers || $[11] !== mappings) { t4 = { executeActionById, executeEventAction, getActionForEvent: getActionForEventFn, mappings, helpers }; $[7] = executeActionById; $[8] = executeEventAction; $[9] = getActionForEventFn; $[10] = helpers; $[11] = mappings; $[12] = t4; } else { t4 = $[12]; } return t4; } // src/hooks/useGestureResolver.ts import { c as _c11 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue11 } from "jotai"; // src/core/gesture-rules-defaults.ts function mergeRules(defaults, overrides) { const overrideMap = new Map(overrides.map((r) => [r.id, r])); const result = []; for (const rule of defaults) { const override = overrideMap.get(rule.id); if (override) { result.push(override); overrideMap.delete(rule.id); } else { result.push(rule); } } for (const rule of overrideMap.values()) { result.push(rule); } return result; } var DEFAULT_GESTURE_RULES = [ // ── Tap gestures ────────────────────────────────────────────── { id: "tap-node", pattern: { gesture: "tap", target: "node" }, actionId: "select-node" }, { id: "tap-edge", pattern: { gesture: "tap", target: "edge" }, actionId: "select-edge" }, { id: "tap-port", pattern: { gesture: "tap", target: "port" }, actionId: "select-node" }, { id: "tap-bg", pattern: { gesture: "tap", target: "background" }, actionId: "clear-selection" }, // ── Double-tap ──────────────────────────────────────────────── { id: "dtap-node", pattern: { gesture: "double-tap", target: "node" }, actionId: "fit-to-view" }, { id: "dtap-bg", pattern: { gesture: "double-tap", target: "background" }, actionId: "fit-all-to-view" }, // ── Triple-tap ──────────────────────────────────────────────── { id: "ttap-node", pattern: { gesture: "triple-tap", target: "node" }, actionId: "toggle-lock" }, // ── Left-button drag ────────────────────────────────────────── { id: "drag-node", pattern: { gesture: "drag", target: "node" }, actionId: "move-node" }, { id: "drag-port", pattern: { gesture: "drag", target: "port" }, actionId: "create-edge" }, { id: "drag-bg-finger", pattern: { gesture: "drag", target: "background", source: "finger" }, actionId: "pan" }, { id: "drag-bg-mouse", pattern: { gesture: "drag", target: "background", source: "mouse" }, actionId: "pan" }, { id: "drag-bg-pencil", pattern: { gesture: "drag", target: "background", source: "pencil" }, actionId: "lasso-select" }, // ── Shift+drag overrides ────────────────────────────────────── { id: "shift-drag-bg", pattern: { gesture: "drag", target: "background", modifiers: { shift: true } }, actionId: "rect-select" }, // ── Right-click tap (context menu) ──────────────────────────── { id: "rc-node", pattern: { gesture: "tap", target: "node", button: 2 }, actionId: "open-context-menu" }, { id: "rc-edge", pattern: { gesture: "tap", target: "edge", button: 2 }, actionId: "open-context-menu" }, { id: "rc-bg", pattern: { gesture: "tap", target: "background", button: 2 }, actionId: "open-context-menu" }, // ── Long-press ──────────────────────────────────────────────── { id: "lp-node", pattern: { gesture: "long-press", target: "node" }, actionId: "open-context-menu" }, { id: "lp-bg-finger", pattern: { gesture: "long-press", target: "background", source: "finger" }, actionId: "create-node" }, // ── Right-button drag (defaults to none — consumers override) ─ { id: "rdrag-node", pattern: { gesture: "drag", target: "node", button: 2 }, actionId: "none" }, { id: "rdrag-bg", pattern: { gesture: "drag", target: "background", button: 2 }, actionId: "none" }, // ── Middle-button drag (defaults to none) ───────────────────── { id: "mdrag-node", pattern: { gesture: "drag", target: "node", button: 1 }, actionId: "none" }, { id: "mdrag-bg", pattern: { gesture: "drag", target: "background", button: 1 }, actionId: "none" }, // ── Zoom ────────────────────────────────────────────────────── { id: "pinch-bg", pattern: { gesture: "pinch", target: "background" }, actionId: "zoom" }, { id: "scroll-any", pattern: { gesture: "scroll" }, actionId: "zoom" }, // ── Split ───────────────────────────────────────────────────── { id: "pinch-node", pattern: { gesture: "pinch", target: "node" }, actionId: "split-node" } ]; // src/core/gesture-rules.ts var MODIFIER_KEYS = ["shift", "ctrl", "alt", "meta"]; function matchSpecificity(pattern, desc) { let score = 0; if (pattern.gesture !== void 0) { if (pattern.gesture !== desc.gesture) return -1; score += 32; } if (pattern.target !== void 0) { if (pattern.target !== desc.target) return -1; score += 16; } if (pattern.source !== void 0) { if (pattern.source !== desc.source) return -1; score += 4; } if (pattern.button !== void 0) { if (pattern.button !== (desc.button ?? 0)) return -1; score += 2; } if (pattern.modifiers !== void 0) { const dm = desc.modifiers ?? {}; for (const key of MODIFIER_KEYS) { const required = pattern.modifiers[key]; if (required === void 0) continue; const actual = dm[key] ?? false; if (required !== actual) return -1; score += 8; } } return score; } var PALM_REJECTION_RULE = { id: "__palm-rejection__", pattern: {}, actionId: "none", label: "Palm rejection" }; function resolveGesture(desc, rules, options) { const palmRejection = options?.palmRejection !== false; if (palmRejection && desc.isStylusActive && desc.source === "finger") { if (desc.gesture === "tap" || desc.gesture === "long-press" || desc.gesture === "double-tap" || desc.gesture === "triple-tap") { return { actionId: "none", rule: PALM_REJECTION_RULE, score: Infinity }; } if (desc.gesture === "drag" && desc.target !== "background") { return resolveGesture({ ...desc, target: "background", isStylusActive: false }, rules, { palmRejection: false }); } } let best = null; for (const rule of rules) { const specificity = matchSpecificity(rule.pattern, desc); if (specificity < 0) continue; const effectiveScore = specificity * 1e3 + (rule.priority ?? 0); if (!best || effectiveScore > best.score) { best = { actionId: rule.actionId, rule, score: effectiveScore }; } } return best; } function buildRuleIndex(rules) { const buckets = /* @__PURE__ */ new Map(); const wildcardRules = []; for (const rule of rules) { const key = rule.pattern.gesture; if (key === void 0) { wildcardRules.push(rule); } else { const bucket = buckets.get(key); if (bucket) { bucket.push(rule); } else { buckets.set(key, [rule]); } } } const index = /* @__PURE__ */ new Map(); if (wildcardRules.length > 0) { for (const [key, bucket] of buckets) { index.set(key, bucket.concat(wildcardRules)); } index.set("__wildcard__", wildcardRules); } else { for (const [key, bucket] of buckets) { index.set(key, bucket); } } return index; } function resolveGestureIndexed(desc, index, options) { const rules = index.get(desc.gesture) ?? index.get("__wildcard__") ?? []; return resolveGesture(desc, rules, options); } // src/core/gesture-rule-store.ts import { atom as atom19 } from "jotai"; import { atomWithStorage as atomWithStorage2 } from "jotai/utils"; var DEFAULT_RULE_STATE = { customRules: [], palmRejection: true }; var gestureRuleSettingsAtom = atomWithStorage2("canvas-gesture-rules", DEFAULT_RULE_STATE); var consumerGestureRulesAtom = atom19([]); var gestureRulesAtom = atom19((get) => { const settings = get(gestureRuleSettingsAtom); const consumerRules = get(consumerGestureRulesAtom); let rules = mergeRules(DEFAULT_GESTURE_RULES, settings.customRules); if (consumerRules.length > 0) { rules = mergeRules(rules, consumerRules); } return rules; }); var gestureRuleIndexAtom = atom19((get) => { return buildRuleIndex(get(gestureRulesAtom)); }); var palmRejectionEnabledAtom = atom19((get) => get(gestureRuleSettingsAtom).palmRejection, (get, set, enabled) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, palmRejection: enabled }); }); var addGestureRuleAtom = atom19(null, (get, set, rule) => { const current = get(gestureRuleSettingsAtom); const existing = current.customRules.findIndex((r) => r.id === rule.id); const newRules = [...current.customRules]; if (existing >= 0) { newRules[existing] = rule; } else { newRules.push(rule); } set(gestureRuleSettingsAtom, { ...current, customRules: newRules }); }); var removeGestureRuleAtom = atom19(null, (get, set, ruleId) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: current.customRules.filter((r) => r.id !== ruleId) }); }); var updateGestureRuleAtom = atom19(null, (get, set, { id, updates }) => { const current = get(gestureRuleSettingsAtom); const index = current.customRules.findIndex((r) => r.id === id); if (index < 0) return; const newRules = [...current.customRules]; newRules[index] = { ...newRules[index], ...updates }; set(gestureRuleSettingsAtom, { ...current, customRules: newRules }); }); var resetGestureRulesAtom = atom19(null, (get, set) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: [] }); }); // src/hooks/useGestureResolver.ts function useGestureResolver() { const $ = _c11(4); const index = useAtomValue11(gestureRuleIndexAtom); const palmRejection = useAtomValue11(palmRejectionEnabledAtom); const isStylusActive = useAtomValue11(isStylusActiveAtom); let t0; if ($[0] !== index || $[1] !== isStylusActive || $[2] !== palmRejection) { t0 = (desc) => resolveGestureIndexed({ ...desc, isStylusActive: desc.isStylusActive ?? isStylusActive }, index, { palmRejection }); $[0] = index; $[1] = isStylusActive; $[2] = palmRejection; $[3] = t0; } else { t0 = $[3]; } return t0; } // src/hooks/useCommandLine.ts import { c as _c12 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue12, useSetAtom as useSetAtom8 } from "jotai"; // src/commands/store.ts import { atom as atom21 } from "jotai"; // src/commands/registry.ts var CommandRegistry = class { constructor() { __publicField(this, "commands", /* @__PURE__ */ new Map()); __publicField(this, "aliases", /* @__PURE__ */ new Map()); } // alias -> command name /** * Register a command with the registry. * @param command The command definition to register * @throws Error if command name or alias already exists */ register(command) { if (this.commands.has(command.name)) { throw new Error(`Command "${command.name}" is already registered`); } this.commands.set(command.name, command); if (command.aliases) { for (const alias of command.aliases) { if (this.aliases.has(alias)) { throw new Error(`Alias "${alias}" is already registered for command "${this.aliases.get(alias)}"`); } if (this.commands.has(alias)) { throw new Error(`Alias "${alias}" conflicts with existing command name`); } this.aliases.set(alias, command.name); } } } /** * Unregister a command by name. * @param name The command name to remove */ unregister(name) { const command = this.commands.get(name); if (command) { if (command.aliases) { for (const alias of command.aliases) { this.aliases.delete(alias); } } this.commands.delete(name); } } /** * Get a command by name or alias. * @param nameOrAlias Command name or alias * @returns The command definition or undefined if not found */ get(nameOrAlias) { const direct = this.commands.get(nameOrAlias); if (direct) return direct; const commandName = this.aliases.get(nameOrAlias); if (commandName) { return this.commands.get(commandName); } return void 0; } /** * Check if a command exists by name or alias. * @param nameOrAlias Command name or alias */ has(nameOrAlias) { return this.commands.has(nameOrAlias) || this.aliases.has(nameOrAlias); } /** * Search for commands matching a query. * Searches command names, aliases, and descriptions. * @param query Search query (case-insensitive) * @returns Array of matching commands, sorted by relevance */ search(query) { if (!query.trim()) { return this.all(); } const lowerQuery = query.toLowerCase().trim(); const results = []; const commands = Array.from(this.commands.values()); for (const command of commands) { let score = 0; if (command.name.toLowerCase() === lowerQuery) { score = 100; } else if (command.name.toLowerCase().startsWith(lowerQuery)) { score = 80; } else if (command.name.toLowerCase().includes(lowerQuery)) { score = 60; } else if (command.aliases?.some((a) => a.toLowerCase() === lowerQuery)) { score = 90; } else if (command.aliases?.some((a) => a.toLowerCase().startsWith(lowerQuery))) { score = 70; } else if (command.aliases?.some((a) => a.toLowerCase().includes(lowerQuery))) { score = 50; } else if (command.description.toLowerCase().includes(lowerQuery)) { score = 30; } if (score > 0) { results.push({ command, score }); } } return results.sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name)).map((r) => r.command); } /** * Get all registered commands. * @returns Array of all commands, sorted alphabetically by name */ all() { return Array.from(this.commands.values()).sort((a, b) => a.name.localeCompare(b.name)); } /** * Get commands by category. * @param category The category to filter by * @returns Array of commands in the category */ byCategory(category) { return this.all().filter((cmd) => cmd.category === category); } /** * Get all available categories. * @returns Array of unique categories */ categories() { const categories = /* @__PURE__ */ new Set(); const commands = Array.from(this.commands.values()); for (const command of commands) { categories.add(command.category); } return Array.from(categories).sort(); } /** * Get the count of registered commands. */ get size() { return this.commands.size; } /** * Clear all registered commands. * Useful for testing. */ clear() { this.commands.clear(); this.aliases.clear(); } /** * Get a serializable list of commands for API responses. */ toJSON() { return this.all().map((cmd) => ({ name: cmd.name, aliases: cmd.aliases || [], description: cmd.description, category: cmd.category, inputs: cmd.inputs.map((input) => ({ name: input.name, type: input.type, prompt: input.prompt, required: input.required !== false })) })); } }; var commandRegistry = new CommandRegistry(); // src/commands/store-atoms.ts import { atom as atom20 } from "jotai"; import { atomWithStorage as atomWithStorage3 } from "jotai/utils"; var inputModeAtom = atom20({ type: "normal" }); var commandLineVisibleAtom = atom20(false); var commandLineStateAtom = atom20({ phase: "idle" }); var commandFeedbackAtom = atom20(null); var commandHistoryAtom = atomWithStorage3("canvas-command-history", []); var selectedSuggestionIndexAtom = atom20(0); var pendingInputResolverAtom = atom20(null); var isCommandActiveAtom = atom20((get) => { const state = get(commandLineStateAtom); return state.phase === "collecting" || state.phase === "executing"; }); var currentInputAtom = atom20((get) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return null; return state.command.inputs[state.inputIndex]; }); var commandProgressAtom = atom20((get) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return null; return { current: state.inputIndex + 1, total: state.command.inputs.length }; }); // src/commands/store.ts var openCommandLineAtom = atom21(null, (get, set) => { set(commandLineVisibleAtom, true); set(commandLineStateAtom, { phase: "searching", query: "", suggestions: commandRegistry.all() }); set(selectedSuggestionIndexAtom, 0); }); var closeCommandLineAtom = atom21(null, (get, set) => { set(commandLineVisibleAtom, false); set(commandLineStateAtom, { phase: "idle" }); set(inputModeAtom, { type: "normal" }); set(commandFeedbackAtom, null); set(pendingInputResolverAtom, null); }); var updateSearchQueryAtom = atom21(null, (get, set, query) => { const suggestions = commandRegistry.search(query); set(commandLineStateAtom, { phase: "searching", query, suggestions }); set(selectedSuggestionIndexAtom, 0); }); var selectCommandAtom = atom21(null, (get, set, command) => { const history = get(commandHistoryAtom); const newHistory = [command.name, ...history.filter((h) => h !== command.name)].slice(0, 50); set(commandHistoryAtom, newHistory); if (command.inputs.length === 0) { set(commandLineStateAtom, { phase: "executing", command }); return; } set(commandLineStateAtom, { phase: "collecting", command, inputIndex: 0, collected: {} }); const firstInput = command.inputs[0]; set(inputModeAtom, inputDefToMode(firstInput)); }); var provideInputAtom = atom21(null, (get, set, value) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return; const { command, inputIndex, collected } = state; const currentInput = command.inputs[inputIndex]; if (currentInput.validate) { const result = currentInput.validate(value, collected); if (result !== true) { set(commandLineStateAtom, { phase: "error", message: typeof result === "string" ? result : `Invalid value for ${currentInput.name}` }); return; } } const newCollected = { ...collected, [currentInput.name]: value }; if (inputIndex < command.inputs.length - 1) { const nextInputIndex = inputIndex + 1; const nextInput = command.inputs[nextInputIndex]; set(commandLineStateAtom, { phase: "collecting", command, inputIndex: nextInputIndex, collected: newCollected }); set(inputModeAtom, inputDefToMode(nextInput, newCollected)); if (command.feedback) { const feedback = command.feedback(newCollected, nextInput); if (feedback) { const feedbackState = { hoveredNodeId: feedback.highlightNodeId, ghostNode: feedback.ghostNode, crosshair: feedback.crosshair, // Handle previewEdge conversion - toCursor variant needs cursorWorldPos previewEdge: feedback.previewEdge && "to" in feedback.previewEdge ? { from: feedback.previewEdge.from, to: feedback.previewEdge.to } : void 0 }; set(commandFeedbackAtom, feedbackState); } else { set(commandFeedbackAtom, null); } } } else { set(commandLineStateAtom, { phase: "collecting", command, inputIndex, collected: newCollected }); set(inputModeAtom, { type: "normal" }); } }); var skipInputAtom = atom21(null, (get, set) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return; const { command, inputIndex } = state; const currentInput = command.inputs[inputIndex]; if (currentInput.required !== false) { return; } const value = currentInput.default; set(provideInputAtom, value); }); var goBackInputAtom = atom21(null, (get, set) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return; const { command, inputIndex, collected } = state; if (inputIndex === 0) { set(commandLineStateAtom, { phase: "searching", query: command.name, suggestions: [command] }); set(inputModeAtom, { type: "normal" }); return; } const prevInputIndex = inputIndex - 1; const prevInput = command.inputs[prevInputIndex]; const newCollected = { ...collected }; delete newCollected[prevInput.name]; set(commandLineStateAtom, { phase: "collecting", command, inputIndex: prevInputIndex, collected: newCollected }); set(inputModeAtom, inputDefToMode(prevInput, newCollected)); }); var setCommandErrorAtom = atom21(null, (get, set, message) => { set(commandLineStateAtom, { phase: "error", message }); set(inputModeAtom, { type: "normal" }); }); var clearCommandErrorAtom = atom21(null, (get, set) => { set(commandLineStateAtom, { phase: "idle" }); }); function inputDefToMode(input, collected) { switch (input.type) { case "point": return { type: "pickPoint", prompt: input.prompt, snapToGrid: input.snapToGrid }; case "node": return { type: "pickNode", prompt: input.prompt, filter: input.filter ? (node) => input.filter(node, collected || {}) : void 0 }; case "nodes": return { type: "pickNodes", prompt: input.prompt, filter: input.filter ? (node) => input.filter(node, collected || {}) : void 0 }; case "select": return { type: "select", prompt: input.prompt, options: input.options || [] }; case "text": case "number": case "color": case "boolean": default: return { type: "text", prompt: input.prompt }; } } // src/hooks/useCommandLine.ts function useCommandLine() { const $ = _c12(16); const visible = useAtomValue12(commandLineVisibleAtom); const state = useAtomValue12(commandLineStateAtom); const history = useAtomValue12(commandHistoryAtom); const currentInput = useAtomValue12(currentInputAtom); const progress = useAtomValue12(commandProgressAtom); const open = useSetAtom8(openCommandLineAtom); const close = useSetAtom8(closeCommandLineAtom); const updateQuery = useSetAtom8(updateSearchQueryAtom); const selectCommand = useSetAtom8(selectCommandAtom); const t0 = state.phase === "searching"; const t1 = state.phase === "collecting"; const t2 = state.phase === "executing"; const t3 = state.phase === "error"; const t4 = state.phase === "error" ? state.message : null; let t5; if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t5 = commandRegistry.all(); $[0] = t5; } else { t5 = $[0]; } let t6; if ($[1] !== close || $[2] !== currentInput || $[3] !== history || $[4] !== open || $[5] !== progress || $[6] !== selectCommand || $[7] !== state || $[8] !== t0 || $[9] !== t1 || $[10] !== t2 || $[11] !== t3 || $[12] !== t4 || $[13] !== updateQuery || $[14] !== visible) { t6 = { visible, state, history, currentInput, progress, open, close, updateQuery, selectCommand, isSearching: t0, isCollecting: t1, isExecuting: t2, hasError: t3, errorMessage: t4, allCommands: t5, searchCommands: _temp5 }; $[1] = close; $[2] = currentInput; $[3] = history; $[4] = open; $[5] = progress; $[6] = selectCommand; $[7] = state; $[8] = t0; $[9] = t1; $[10] = t2; $[11] = t3; $[12] = t4; $[13] = updateQuery; $[14] = visible; $[15] = t6; } else { t6 = $[15]; } return t6; } function _temp5(query) { return commandRegistry.search(query); } // src/hooks/useVirtualization.ts import { c as _c13 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue13, useSetAtom as useSetAtom9 } from "jotai"; // src/core/virtualization-store.ts import { atom as atom22 } from "jotai"; // src/core/spatial-index.ts var SpatialGrid = class { constructor(cellSize = 500) { /** cell key → set of node IDs in that cell */ __publicField(this, "cells", /* @__PURE__ */ new Map()); /** node ID → entry data (for update/remove) */ __publicField(this, "entries", /* @__PURE__ */ new Map()); this.cellSize = cellSize; } /** Number of tracked entries */ get size() { return this.entries.size; } cellKey(cx, cy) { return `${cx},${cy}`; } getCellRange(x, y, w, h) { const cs = this.cellSize; return { minCX: Math.floor(x / cs), minCY: Math.floor(y / cs), maxCX: Math.floor((x + w) / cs), maxCY: Math.floor((y + h) / cs) }; } /** * Insert a node into the index. * If the node already exists, it is updated. */ insert(id, x, y, width, height) { if (this.entries.has(id)) { this.update(id, x, y, width, height); return; } const entry = { id, x, y, width, height }; this.entries.set(id, entry); const { minCX, minCY, maxCX, maxCY } = this.getCellRange(x, y, width, height); for (let cx = minCX; cx <= maxCX; cx++) { for (let cy = minCY; cy <= maxCY; cy++) { const key = this.cellKey(cx, cy); let cell = this.cells.get(key); if (!cell) { cell = /* @__PURE__ */ new Set(); this.cells.set(key, cell); } cell.add(id); } } } /** * Update a node's position/dimensions. */ update(id, x, y, width, height) { const prev = this.entries.get(id); if (!prev) { this.insert(id, x, y, width, height); return; } const prevRange = this.getCellRange(prev.x, prev.y, prev.width, prev.height); const newRange = this.getCellRange(x, y, width, height); prev.x = x; prev.y = y; prev.width = width; prev.height = height; if (prevRange.minCX === newRange.minCX && prevRange.minCY === newRange.minCY && prevRange.maxCX === newRange.maxCX && prevRange.maxCY === newRange.maxCY) { return; } for (let cx = prevRange.minCX; cx <= prevRange.maxCX; cx++) { for (let cy = prevRange.minCY; cy <= prevRange.maxCY; cy++) { const key = this.cellKey(cx, cy); const cell = this.cells.get(key); if (cell) { cell.delete(id); if (cell.size === 0) this.cells.delete(key); } } } for (let cx = newRange.minCX; cx <= newRange.maxCX; cx++) { for (let cy = newRange.minCY; cy <= newRange.maxCY; cy++) { const key = this.cellKey(cx, cy); let cell = this.cells.get(key); if (!cell) { cell = /* @__PURE__ */ new Set(); this.cells.set(key, cell); } cell.add(id); } } } /** * Remove a node from the index. */ remove(id) { const entry = this.entries.get(id); if (!entry) return; const { minCX, minCY, maxCX, maxCY } = this.getCellRange(entry.x, entry.y, entry.width, entry.height); for (let cx = minCX; cx <= maxCX; cx++) { for (let cy = minCY; cy <= maxCY; cy++) { const key = this.cellKey(cx, cy); const cell = this.cells.get(key); if (cell) { cell.delete(id); if (cell.size === 0) this.cells.delete(key); } } } this.entries.delete(id); } /** * Query all node IDs whose bounding box overlaps the given bounds. * Returns a Set for O(1) membership checks. */ query(bounds) { const result = /* @__PURE__ */ new Set(); const { minCX, minCY, maxCX, maxCY } = this.getCellRange(bounds.minX, bounds.minY, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY); for (let cx = minCX; cx <= maxCX; cx++) { for (let cy = minCY; cy <= maxCY; cy++) { const cell = this.cells.get(this.cellKey(cx, cy)); if (!cell) continue; for (const id of cell) { if (result.has(id)) continue; const entry = this.entries.get(id); const entryRight = entry.x + entry.width; const entryBottom = entry.y + entry.height; if (entry.x <= bounds.maxX && entryRight >= bounds.minX && entry.y <= bounds.maxY && entryBottom >= bounds.minY) { result.add(id); } } } } return result; } /** * Clear all entries. */ clear() { this.cells.clear(); this.entries.clear(); } /** * Check if a node is tracked. */ has(id) { return this.entries.has(id); } }; // src/core/virtualization-store.ts var VIRTUALIZATION_BUFFER = 200; var spatialIndexAtom = atom22((get) => { get(graphUpdateVersionAtom); get(nodePositionUpdateCounterAtom); const graph = get(graphAtom); const grid = new SpatialGrid(500); graph.forEachNode((nodeId, attrs) => { const a = attrs; grid.insert(nodeId, a.x, a.y, a.width || 200, a.height || 100); }); return grid; }); var visibleBoundsAtom = atom22((get) => { const viewport = get(viewportRectAtom); const pan = get(panAtom); const zoom = get(zoomAtom); if (!viewport || zoom === 0) { return null; } const buffer = VIRTUALIZATION_BUFFER; return { minX: (-buffer - pan.x) / zoom, minY: (-buffer - pan.y) / zoom, maxX: (viewport.width + buffer - pan.x) / zoom, maxY: (viewport.height + buffer - pan.y) / zoom }; }); var visibleNodeKeysAtom = atom22((get) => { const end = canvasMark("virtualization-cull"); const enabled = get(virtualizationEnabledAtom); const allKeys = get(nodeKeysAtom); if (!enabled) { end(); return allKeys; } const bounds = get(visibleBoundsAtom); if (!bounds) { end(); return allKeys; } const grid = get(spatialIndexAtom); const visibleSet = grid.query(bounds); const result = allKeys.filter((k) => visibleSet.has(k)); end(); return result; }); var visibleEdgeKeysAtom = atom22((get) => { const enabled = get(virtualizationEnabledAtom); const allEdgeKeys = get(edgeKeysAtom); const edgeCreation = get(edgeCreationAtom); const remap = get(collapsedEdgeRemapAtom); const tempEdgeKey = edgeCreation.isCreating ? "temp-creating-edge" : null; get(graphUpdateVersionAtom); const graph = get(graphAtom); const filteredEdges = allEdgeKeys.filter((edgeKey) => { const source = graph.source(edgeKey); const target = graph.target(edgeKey); const effectiveSource = remap.get(source) ?? source; const effectiveTarget = remap.get(target) ?? target; if (effectiveSource === effectiveTarget) return false; return true; }); if (!enabled) { return tempEdgeKey ? [...filteredEdges, tempEdgeKey] : filteredEdges; } const visibleNodeKeys = get(visibleNodeKeysAtom); const visibleNodeSet = new Set(visibleNodeKeys); const visibleEdges = filteredEdges.filter((edgeKey) => { const source = graph.source(edgeKey); const target = graph.target(edgeKey); const effectiveSource = remap.get(source) ?? source; const effectiveTarget = remap.get(target) ?? target; return visibleNodeSet.has(effectiveSource) && visibleNodeSet.has(effectiveTarget); }); return tempEdgeKey ? [...visibleEdges, tempEdgeKey] : visibleEdges; }); var virtualizationMetricsAtom = atom22((get) => { const enabled = get(virtualizationEnabledAtom); const totalNodes = get(nodeKeysAtom).length; const totalEdges = get(edgeKeysAtom).length; const visibleNodes = get(visibleNodeKeysAtom).length; const visibleEdges = get(visibleEdgeKeysAtom).length; const bounds = get(visibleBoundsAtom); return { enabled, totalNodes, totalEdges, visibleNodes, visibleEdges, culledNodes: totalNodes - visibleNodes, culledEdges: totalEdges - visibleEdges, bounds }; }); // src/hooks/useVirtualization.ts function useVirtualization() { const $ = _c13(8); const metrics = useAtomValue13(virtualizationMetricsAtom); const setEnabled = useSetAtom9(setVirtualizationEnabledAtom); const toggle = useSetAtom9(toggleVirtualizationAtom); let t0; let t1; if ($[0] !== setEnabled) { t0 = () => setEnabled(true); t1 = () => setEnabled(false); $[0] = setEnabled; $[1] = t0; $[2] = t1; } else { t0 = $[1]; t1 = $[2]; } let t2; if ($[3] !== metrics || $[4] !== t0 || $[5] !== t1 || $[6] !== toggle) { t2 = { ...metrics, enable: t0, disable: t1, toggle }; $[3] = metrics; $[4] = t0; $[5] = t1; $[6] = toggle; $[7] = t2; } else { t2 = $[7]; } return t2; } // src/hooks/useTapGesture.ts import { c as _c14 } from "react/compiler-runtime"; import { useRef as useRef4 } from "react"; function useTapGesture(t0) { const $ = _c14(12); let t1; if ($[0] !== t0) { t1 = t0 === void 0 ? {} : t0; $[0] = t0; $[1] = t1; } else { t1 = $[1]; } const options = t1; const { onSingleTap, onDoubleTap, onTripleTap, tapDelay: t2, tapDistance: t3 } = options; const tapDelay = t2 === void 0 ? 300 : t2; const tapDistance = t3 === void 0 ? 25 : t3; let t4; if ($[2] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t4 = { count: 0, timer: null, lastX: 0, lastY: 0, lastEvent: null }; $[2] = t4; } else { t4 = $[2]; } const stateRef = useRef4(t4); let t5; if ($[3] !== onDoubleTap || $[4] !== onSingleTap || $[5] !== onTripleTap || $[6] !== tapDelay || $[7] !== tapDistance) { t5 = (event) => { const state = stateRef.current; const dx = Math.abs(event.clientX - state.lastX); const dy = Math.abs(event.clientY - state.lastY); const isSameSpot = state.count === 0 || dx < tapDistance && dy < tapDistance; if (!isSameSpot) { if (state.timer) { clearTimeout(state.timer); } state.count = 0; } state.count = state.count + 1; state.lastX = event.clientX; state.lastY = event.clientY; state.lastEvent = event; if (state.timer) { clearTimeout(state.timer); } if (state.count >= 3) { onTripleTap?.(event); state.count = 0; state.timer = null; return; } if (state.count === 2) { onDoubleTap?.(event); state.timer = setTimeout(() => { state.count = 0; state.timer = null; }, tapDelay); return; } state.timer = setTimeout(() => { if (state.count === 1 && state.lastEvent) { onSingleTap?.(state.lastEvent); } state.count = 0; state.timer = null; }, tapDelay); }; $[3] = onDoubleTap; $[4] = onSingleTap; $[5] = onTripleTap; $[6] = tapDelay; $[7] = tapDistance; $[8] = t5; } else { t5 = $[8]; } const handleTap = t5; let t6; if ($[9] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t6 = () => { const state_0 = stateRef.current; if (state_0.timer) { clearTimeout(state_0.timer); state_0.timer = null; } state_0.count = 0; }; $[9] = t6; } else { t6 = $[9]; } const cleanup = t6; let t7; if ($[10] !== handleTap) { t7 = { handleTap, cleanup }; $[10] = handleTap; $[11] = t7; } else { t7 = $[11]; } return t7; } // src/hooks/useArrowKeyNavigation.ts import { c as _c15 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue14 } from "jotai"; function useArrowKeyNavigation() { const $ = _c15(2); const focusedNodeId = useAtomValue14(focusedNodeIdAtom); let t0; if ($[0] !== focusedNodeId) { t0 = { focusedNodeId }; $[0] = focusedNodeId; $[1] = t0; } else { t0 = $[1]; } return t0; } // src/hooks/useCanvasGraph.ts import { c as _c16 } from "react/compiler-runtime"; import { useAtomValue as useAtomValue15 } from "jotai"; function useCanvasGraph() { const $ = _c16(9); const graph = useAtomValue15(graphAtom); const nodeKeys = useAtomValue15(nodeKeysAtom); const edgeKeys = useAtomValue15(edgeKeysAtom); let t0; if ($[0] !== graph) { t0 = (id) => graph.hasNode(id) ? graph.getNodeAttributes(id) : void 0; $[0] = graph; $[1] = t0; } else { t0 = $[1]; } const getNode = t0; let t1; if ($[2] !== graph) { t1 = (id_0) => graph.hasEdge(id_0) ? graph.getEdgeAttributes(id_0) : void 0; $[2] = graph; $[3] = t1; } else { t1 = $[3]; } const getEdge = t1; let t2; if ($[4] !== edgeKeys || $[5] !== getEdge || $[6] !== getNode || $[7] !== nodeKeys) { t2 = { nodeCount: nodeKeys.length, edgeCount: edgeKeys.length, nodeKeys, edgeKeys, getNode, getEdge }; $[4] = edgeKeys; $[5] = getEdge; $[6] = getNode; $[7] = nodeKeys; $[8] = t2; } else { t2 = $[8]; } return t2; } // src/hooks/useZoomTransition.ts import { c as _c17 } from "react/compiler-runtime"; import { useEffect as useEffect2, useRef as useRef5 } from "react"; import { useAtomValue as useAtomValue16, useSetAtom as useSetAtom10 } from "jotai"; function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } function useZoomTransition() { const $ = _c17(15); const target = useAtomValue16(zoomAnimationTargetAtom); const setZoom = useSetAtom10(zoomAtom); const setPan = useSetAtom10(panAtom); const setTarget = useSetAtom10(zoomAnimationTargetAtom); const setProgress = useSetAtom10(zoomTransitionProgressAtom); const setFocusNode = useSetAtom10(zoomFocusNodeIdAtom); const progress = useAtomValue16(zoomTransitionProgressAtom); const rafRef = useRef5(null); let t0; if ($[0] !== setFocusNode || $[1] !== setProgress || $[2] !== setTarget) { t0 = () => { if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } setTarget(null); setProgress(0); setFocusNode(null); }; $[0] = setFocusNode; $[1] = setProgress; $[2] = setTarget; $[3] = t0; } else { t0 = $[3]; } const cancel = t0; let t1; let t2; if ($[4] !== setPan || $[5] !== setProgress || $[6] !== setTarget || $[7] !== setZoom || $[8] !== target) { t1 = () => { if (!target) { return; } const animate = () => { const elapsed = performance.now() - target.startTime; const rawT = Math.min(1, elapsed / target.duration); const t = easeInOutCubic(rawT); const currentZoom = target.startZoom + (target.targetZoom - target.startZoom) * t; const currentPanX = target.startPan.x + (target.targetPan.x - target.startPan.x) * t; const currentPanY = target.startPan.y + (target.targetPan.y - target.startPan.y) * t; setZoom(currentZoom); setPan({ x: currentPanX, y: currentPanY }); setProgress(t); if (rawT < 1) { rafRef.current = requestAnimationFrame(animate); } else { rafRef.current = null; setTarget(null); } }; rafRef.current = requestAnimationFrame(animate); return () => { if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } }; }; t2 = [target, setZoom, setPan, setTarget, setProgress]; $[4] = setPan; $[5] = setProgress; $[6] = setTarget; $[7] = setZoom; $[8] = target; $[9] = t1; $[10] = t2; } else { t1 = $[9]; t2 = $[10]; } useEffect2(t1, t2); const t3 = target !== null; let t4; if ($[11] !== cancel || $[12] !== progress || $[13] !== t3) { t4 = { isAnimating: t3, progress, cancel }; $[11] = cancel; $[12] = progress; $[13] = t3; $[14] = t4; } else { t4 = $[14]; } return t4; } // src/hooks/useSplitGesture.ts import { c as _c18 } from "react/compiler-runtime"; import { useRef as useRef6 } from "react"; import { useAtomValue as useAtomValue17, useSetAtom as useSetAtom11 } from "jotai"; var SPLIT_THRESHOLD = 80; function useSplitGesture(nodeId) { const $ = _c18(9); const splitNode = useSetAtom11(splitNodeAtom); const screenToWorld = useAtomValue17(screenToWorldAtom); let t0; if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t0 = /* @__PURE__ */ new Map(); $[0] = t0; } else { t0 = $[0]; } const pointersRef = useRef6(t0); const initialDistanceRef = useRef6(null); const splitFiredRef = useRef6(false); const getDistance = _temp6; let t1; if ($[1] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t1 = (e) => { if (e.pointerType !== "touch") { return; } pointersRef.current.set(e.pointerId, { pointerId: e.pointerId, x: e.clientX, y: e.clientY }); if (pointersRef.current.size === 2) { const [p1, p2] = Array.from(pointersRef.current.values()); initialDistanceRef.current = getDistance(p1, p2); splitFiredRef.current = false; } }; $[1] = t1; } else { t1 = $[1]; } const onPointerDown = t1; let t2; if ($[2] !== nodeId || $[3] !== screenToWorld || $[4] !== splitNode) { t2 = (e_0) => { if (e_0.pointerType !== "touch") { return; } if (!pointersRef.current.has(e_0.pointerId)) { return; } pointersRef.current.set(e_0.pointerId, { pointerId: e_0.pointerId, x: e_0.clientX, y: e_0.clientY }); if (pointersRef.current.size === 2 && initialDistanceRef.current !== null && !splitFiredRef.current) { const [p1_0, p2_0] = Array.from(pointersRef.current.values()); const currentDistance = getDistance(p1_0, p2_0); const delta = currentDistance - initialDistanceRef.current; if (delta > SPLIT_THRESHOLD) { splitFiredRef.current = true; e_0.stopPropagation(); const world1 = screenToWorld(p1_0.x, p1_0.y); const world2 = screenToWorld(p2_0.x, p2_0.y); splitNode({ nodeId, position1: world1, position2: world2 }); } } }; $[2] = nodeId; $[3] = screenToWorld; $[4] = splitNode; $[5] = t2; } else { t2 = $[5]; } const onPointerMove = t2; let t3; if ($[6] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t3 = (e_1) => { pointersRef.current.delete(e_1.pointerId); if (pointersRef.current.size < 2) { initialDistanceRef.current = null; splitFiredRef.current = false; } }; $[6] = t3; } else { t3 = $[6]; } const onPointerUp = t3; let t4; if ($[7] !== onPointerMove) { t4 = { onPointerDown, onPointerMove, onPointerUp }; $[7] = onPointerMove; $[8] = t4; } else { t4 = $[8]; } return t4; } function _temp6(a, b) { return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2); } // src/hooks/useAnimatedLayout.ts import { useAtomValue as useAtomValue18, useSetAtom as useSetAtom12 } from "jotai"; import { useRef as useRef7 } from "react"; var debug14 = createDebug("animated-layout"); function easeInOutCubic2(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } function useAnimatedLayout(options = {}) { const { onPositionsChanged, duration = 400 } = options; const graph = useAtomValue18(graphAtom); const updateNodePosition = useSetAtom12(updateNodePositionAtom); const pushHistory = useSetAtom12(pushHistoryAtom); const setPositionCounter = useSetAtom12(nodePositionUpdateCounterAtom); const reducedMotion = useAtomValue18(prefersReducedMotionAtom); const isAnimatingRef = useRef7(false); const animate = async (targets, label) => { if (isAnimatingRef.current) return; if (targets.size === 0) return; if (label) pushHistory(label); isAnimatingRef.current = true; if (reducedMotion) { for (const [nodeId, target] of targets) { updateNodePosition({ nodeId, position: target }); } isAnimatingRef.current = false; setPositionCounter((c) => c + 1); if (onPositionsChanged) { const updates = []; for (const [nodeId_0, target_0] of targets) { updates.push({ nodeId: nodeId_0, position: target_0 }); } Promise.resolve(onPositionsChanged(updates)).catch((err) => debug14.error("Position change callback failed: %O", err)); } return; } const starts = /* @__PURE__ */ new Map(); for (const [nodeId_1] of targets) { if (graph.hasNode(nodeId_1)) { const attrs = graph.getNodeAttributes(nodeId_1); starts.set(nodeId_1, { x: attrs.x, y: attrs.y }); } } return new Promise((resolve) => { const startTime = performance.now(); function tick() { const elapsed = performance.now() - startTime; const t = Math.min(elapsed / duration, 1); const eased = easeInOutCubic2(t); for (const [nodeId_2, target_1] of targets) { const start = starts.get(nodeId_2); if (!start) continue; const x = Math.round(start.x + (target_1.x - start.x) * eased); const y = Math.round(start.y + (target_1.y - start.y) * eased); updateNodePosition({ nodeId: nodeId_2, position: { x, y } }); } if (t < 1) { requestAnimationFrame(tick); } else { isAnimatingRef.current = false; setPositionCounter((c_0) => c_0 + 1); if (onPositionsChanged) { const updates_0 = []; for (const [nodeId_3, target_2] of targets) { updates_0.push({ nodeId: nodeId_3, position: target_2 }); } Promise.resolve(onPositionsChanged(updates_0)).catch((err_0) => debug14.error("Position change callback failed: %O", err_0)); } resolve(); } } requestAnimationFrame(tick); }); }; return { animate, isAnimating: isAnimatingRef.current }; } // src/hooks/useTreeLayout.ts import { useAtomValue as useAtomValue19 } from "jotai"; import { useRef as useRef8 } from "react"; function useTreeLayout(options = {}) { const { direction = "top-down", levelGap = 200, nodeGap = 100, ...animateOptions } = options; const graph = useAtomValue19(graphAtom); const nodes = useAtomValue19(uiNodesAtom); const { animate, isAnimating } = useAnimatedLayout(animateOptions); const isRunningRef = useRef8(false); const applyLayout = async () => { if (isRunningRef.current || isAnimating) return; if (nodes.length === 0) return; isRunningRef.current = true; const nodeIds = new Set(nodes.map((n) => n.id)); const children = /* @__PURE__ */ new Map(); const hasIncoming = /* @__PURE__ */ new Set(); for (const nodeId of nodeIds) { children.set(nodeId, []); } graph.forEachEdge((_key, _attrs, source, target) => { if (nodeIds.has(source) && nodeIds.has(target) && source !== target) { children.get(source)?.push(target); hasIncoming.add(target); } }); const roots = [...nodeIds].filter((id) => !hasIncoming.has(id)); if (roots.length === 0) { roots.push(nodes[0].id); } const levels = /* @__PURE__ */ new Map(); const queue = [...roots]; for (const r of roots) levels.set(r, 0); while (queue.length > 0) { const current = queue.shift(); const level = levels.get(current); for (const child of children.get(current) || []) { if (!levels.has(child)) { levels.set(child, level + 1); queue.push(child); } } } for (const nodeId_0 of nodeIds) { if (!levels.has(nodeId_0)) levels.set(nodeId_0, 0); } const byLevel = /* @__PURE__ */ new Map(); for (const [nodeId_1, level_0] of levels) { if (!byLevel.has(level_0)) byLevel.set(level_0, []); byLevel.get(level_0).push(nodeId_1); } const targets = /* @__PURE__ */ new Map(); const maxLevel = Math.max(...byLevel.keys()); for (const [level_1, nodeIdsAtLevel] of byLevel) { const count = nodeIdsAtLevel.length; let maxNodeSize = 200; for (const nid of nodeIdsAtLevel) { if (graph.hasNode(nid)) { const attrs = graph.getNodeAttributes(nid); maxNodeSize = Math.max(maxNodeSize, attrs.width || 200); } } const totalWidth = (count - 1) * (maxNodeSize + nodeGap); const startX = -totalWidth / 2; for (let i = 0; i < count; i++) { const primary = level_1 * levelGap; const secondary = startX + i * (maxNodeSize + nodeGap); if (direction === "top-down") { targets.set(nodeIdsAtLevel[i], { x: secondary, y: primary }); } else { targets.set(nodeIdsAtLevel[i], { x: primary, y: secondary }); } } } await animate(targets, direction === "top-down" ? "Tree layout" : "Horizontal layout"); isRunningRef.current = false; }; return { applyLayout, isRunning: isRunningRef.current || isAnimating }; } // src/hooks/useGridLayout.ts import { useAtomValue as useAtomValue20 } from "jotai"; import { useRef as useRef9 } from "react"; function useGridLayout(options = {}) { const { columns, gap = 80, ...animateOptions } = options; const graph = useAtomValue20(graphAtom); const nodes = useAtomValue20(uiNodesAtom); const { animate, isAnimating } = useAnimatedLayout(animateOptions); const isRunningRef = useRef9(false); const applyLayout = async () => { if (isRunningRef.current || isAnimating) return; if (nodes.length === 0) return; isRunningRef.current = true; const sorted = [...nodes].sort((a, b) => { const ay = a.position?.y ?? 0; const by = b.position?.y ?? 0; if (Math.abs(ay - by) > 50) return ay - by; return (a.position?.x ?? 0) - (b.position?.x ?? 0); }); const cols = columns ?? Math.ceil(Math.sqrt(sorted.length)); let maxW = 200; let maxH = 100; for (const node of sorted) { if (graph.hasNode(node.id)) { const attrs = graph.getNodeAttributes(node.id); maxW = Math.max(maxW, attrs.width || 200); maxH = Math.max(maxH, attrs.height || 100); } } const cellW = maxW + gap; const cellH = maxH + gap; const rows = Math.ceil(sorted.length / cols); const totalW = (cols - 1) * cellW; const totalH = (rows - 1) * cellH; const offsetX = -totalW / 2; const offsetY = -totalH / 2; const targets = /* @__PURE__ */ new Map(); for (let i = 0; i < sorted.length; i++) { const col = i % cols; const row = Math.floor(i / cols); targets.set(sorted[i].id, { x: Math.round(offsetX + col * cellW), y: Math.round(offsetY + row * cellH) }); } await animate(targets, "Grid layout"); isRunningRef.current = false; }; return { applyLayout, isRunning: isRunningRef.current || isAnimating }; } // src/hooks/usePlugin.ts import { c as _c20 } from "react/compiler-runtime"; import { useEffect as useEffect3, useRef as useRef10 } from "react"; // src/core/plugin-types.ts var PluginError = class extends Error { constructor(message, pluginId, code) { super(`[Plugin "${pluginId}"] ${message}`); this.pluginId = pluginId; this.code = code; this.name = "PluginError"; } }; // src/core/node-type-registry.tsx import { c as _c19 } from "react/compiler-runtime"; import React from "react"; import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime"; var nodeTypeRegistry = /* @__PURE__ */ new Map(); function registerNodeTypes(types) { for (const [nodeType, component] of Object.entries(types)) { nodeTypeRegistry.set(nodeType, component); } } function unregisterNodeType(nodeType) { return nodeTypeRegistry.delete(nodeType); } // src/gestures/types.ts var NO_MODIFIERS = Object.freeze({ shift: false, ctrl: false, alt: false, meta: false }); var NO_HELD_KEYS = Object.freeze({ byKey: Object.freeze({}), byCode: Object.freeze({}) }); // src/gestures/dispatcher.ts var handlers = /* @__PURE__ */ new Map(); function registerAction2(actionId, handler) { handlers.set(actionId, handler); } function unregisterAction2(actionId) { handlers.delete(actionId); } // src/utils/edge-path-registry.ts var customCalculators = /* @__PURE__ */ new Map(); function registerEdgePathCalculator(name, calculator) { customCalculators.set(name, calculator); } function unregisterEdgePathCalculator(name) { return customCalculators.delete(name); } // src/core/plugin-registry.ts var debug15 = createDebug("plugins"); var plugins = /* @__PURE__ */ new Map(); function registerPlugin(plugin) { debug15("Registering plugin: %s", plugin.id); if (plugins.has(plugin.id)) { throw new PluginError("Plugin is already registered", plugin.id, "ALREADY_REGISTERED"); } if (plugin.dependencies) { for (const depId of plugin.dependencies) { if (!plugins.has(depId)) { throw new PluginError(`Missing dependency: "${depId}"`, plugin.id, "MISSING_DEPENDENCY"); } } } detectConflicts(plugin); const cleanups = []; try { if (plugin.nodeTypes) { const nodeTypeNames = Object.keys(plugin.nodeTypes); registerNodeTypes(plugin.nodeTypes); cleanups.push(() => { for (const name of nodeTypeNames) { unregisterNodeType(name); } }); } if (plugin.edgePathCalculators) { for (const [name, calc] of Object.entries(plugin.edgePathCalculators)) { registerEdgePathCalculator(name, calc); cleanups.push(() => unregisterEdgePathCalculator(name)); } } if (plugin.actionHandlers) { for (const [actionId, handler] of Object.entries(plugin.actionHandlers)) { registerAction2(actionId, handler); cleanups.push(() => unregisterAction2(actionId)); } } if (plugin.commands) { for (const cmd of plugin.commands) { commandRegistry.register(cmd); cleanups.push(() => commandRegistry.unregister(cmd.name)); } } if (plugin.actions) { for (const action of plugin.actions) { registerAction(action); cleanups.push(() => unregisterAction(action.id)); } } let lifecycleCleanup = null; if (plugin.onRegister) { const ctx = makePluginContext(plugin.id); try { const result = plugin.onRegister(ctx); if (typeof result === "function") { lifecycleCleanup = result; } } catch (err) { for (const cleanup of cleanups.reverse()) { try { cleanup(); } catch { } } throw new PluginError(`onRegister failed: ${err instanceof Error ? err.message : String(err)}`, plugin.id, "LIFECYCLE_ERROR"); } } plugins.set(plugin.id, { plugin, cleanup: () => { for (const cleanup of cleanups.reverse()) { try { cleanup(); } catch { } } if (lifecycleCleanup) { try { lifecycleCleanup(); } catch { } } }, registeredAt: Date.now() }); debug15("Plugin registered: %s (%d node types, %d commands, %d actions)", plugin.id, Object.keys(plugin.nodeTypes ?? {}).length, plugin.commands?.length ?? 0, plugin.actions?.length ?? 0); } catch (err) { if (err instanceof PluginError) throw err; for (const cleanup of cleanups.reverse()) { try { cleanup(); } catch { } } throw err; } } function unregisterPlugin(pluginId) { const registration = plugins.get(pluginId); if (!registration) { throw new PluginError("Plugin is not registered", pluginId, "NOT_FOUND"); } for (const [otherId, other] of plugins) { if (other.plugin.dependencies?.includes(pluginId)) { throw new PluginError(`Cannot unregister: plugin "${otherId}" depends on it`, pluginId, "CONFLICT"); } } if (registration.cleanup) { registration.cleanup(); } plugins.delete(pluginId); debug15("Plugin unregistered: %s", pluginId); } function getPlugin(id) { return plugins.get(id)?.plugin; } function hasPlugin(id) { return plugins.has(id); } function detectConflicts(plugin) { if (plugin.commands) { for (const cmd of plugin.commands) { if (commandRegistry.has(cmd.name)) { throw new PluginError(`Command "${cmd.name}" is already registered`, plugin.id, "CONFLICT"); } } } if (plugin.edgePathCalculators) { for (const name of Object.keys(plugin.edgePathCalculators)) { for (const [otherId, other] of plugins) { if (other.plugin.edgePathCalculators?.[name]) { throw new PluginError(`Edge path calculator "${name}" already registered by plugin "${otherId}"`, plugin.id, "CONFLICT"); } } } } if (plugin.nodeTypes) { for (const nodeType of Object.keys(plugin.nodeTypes)) { for (const [otherId, other] of plugins) { if (other.plugin.nodeTypes?.[nodeType]) { throw new PluginError(`Node type "${nodeType}" already registered by plugin "${otherId}"`, plugin.id, "CONFLICT"); } } } } if (plugin.actionHandlers) { for (const actionId of Object.keys(plugin.actionHandlers)) { for (const [otherId, other] of plugins) { if (other.plugin.actionHandlers?.[actionId]) { throw new PluginError(`Action handler "${actionId}" already registered by plugin "${otherId}"`, plugin.id, "CONFLICT"); } } } } } function makePluginContext(pluginId) { return { pluginId, getPlugin, hasPlugin }; } // src/hooks/usePlugin.ts function usePlugin(plugin) { const $ = _c20(4); const registeredRef = useRef10(false); let t0; if ($[0] !== plugin) { t0 = () => { if (!hasPlugin(plugin.id)) { registerPlugin(plugin); registeredRef.current = true; } return () => { if (registeredRef.current && hasPlugin(plugin.id)) { try { unregisterPlugin(plugin.id); } catch { } registeredRef.current = false; } }; }; $[0] = plugin; $[1] = t0; } else { t0 = $[1]; } let t1; if ($[2] !== plugin.id) { t1 = [plugin.id]; $[2] = plugin.id; $[3] = t1; } else { t1 = $[3]; } useEffect3(t0, t1); } function usePlugins(plugins2) { const $ = _c20(6); let t0; if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t0 = []; $[0] = t0; } else { t0 = $[0]; } const registeredRef = useRef10(t0); let t1; let t2; if ($[1] !== plugins2) { t1 = () => { const registered = []; for (const plugin of plugins2) { if (!hasPlugin(plugin.id)) { registerPlugin(plugin); registered.push(plugin.id); } } registeredRef.current = registered; return () => { for (const id of registeredRef.current.reverse()) { if (hasPlugin(id)) { try { unregisterPlugin(id); } catch { } } } registeredRef.current = []; }; }; t2 = plugins2.map(_temp7).join(","); $[1] = plugins2; $[2] = t1; $[3] = t2; } else { t1 = $[2]; t2 = $[3]; } let t3; if ($[4] !== t2) { t3 = [t2]; $[4] = t2; $[5] = t3; } else { t3 = $[5]; } useEffect3(t1, t3); } function _temp7(p) { return p.id; } export { FitToBoundsMode, useActionExecutor, useAnimatedLayout, useArrowKeyNavigation, useCanvasDrag, useCanvasGraph, useCanvasHistory, useCanvasSelection, useCanvasSettings, useCanvasViewport, useCommandLine, useFitToBounds, useForceLayout, useGestureResolver, useGetGraphBounds, useGridLayout, useLayout, useNodeDrag, useNodeResize, useNodeSelection, usePlugin, usePlugins, useSelectionBounds, useSplitGesture, useTapGesture, useTreeLayout, useVirtualization, useZoomTransition }; //# sourceMappingURL=index.mjs.map