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/core/graph-store.ts import { atom } from "jotai"; import Graph from "graphology"; var graphOptions = { type: "directed", multi: true, allowSelfLoops: true }; var currentGraphIdAtom = atom(null); var graphAtom = atom(new Graph(graphOptions)); var graphUpdateVersionAtom = atom(0); var edgeCreationAtom = atom({ isCreating: false, sourceNodeId: null, sourceNodePosition: null, targetPosition: null, hoveredTargetNodeId: null, sourceHandle: null, targetHandle: null, sourcePort: null, targetPort: null, snappedTargetPosition: null }); var draggingNodeIdAtom = atom(null); var preDragNodeAttributesAtom = atom(null); // src/core/graph-position.ts import { atom as atom3 } from "jotai"; import { atomFamily } from "jotai-family"; import Graph2 from "graphology"; // 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/utils/mutation-queue.ts var pendingNodeMutations = /* @__PURE__ */ new Map(); function clearAllPendingMutations() { pendingNodeMutations.clear(); } // src/core/perf.ts import { atom as atom2 } from "jotai"; var perfEnabledAtom = atom2(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() { } function canvasWrap(name, fn) { const end = canvasMark(name); try { return fn(); } finally { end(); } } // src/core/graph-position.ts var debug2 = 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 = atom3(0); var nodePositionAtomFamily = atomFamily((nodeId) => atom3((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 = atom3(null, (get, set, { nodeId, position }) => { const end = canvasMark("drag-frame"); const graph = get(graphAtom); if (graph.hasNode(nodeId)) { debug2("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 = atom3(null, (get, _set, nodeId) => { nodePositionAtomFamily.remove(nodeId); const graph = get(graphAtom); getPositionCache(graph).delete(nodeId); debug2("Removed position atom for node: %s", nodeId); }); var cleanupAllNodePositionsAtom = atom3(null, (get, _set) => { const graph = get(graphAtom); const nodeIds = graph.nodes(); nodeIds.forEach((nodeId) => { nodePositionAtomFamily.remove(nodeId); }); _positionCacheByGraph.delete(graph); debug2("Removed %d position atoms", nodeIds.length); }); var clearGraphOnSwitchAtom = atom3(null, (get, set) => { debug2("Clearing graph for switch"); set(cleanupAllNodePositionsAtom); clearAllPendingMutations(); const emptyGraph = new Graph2(graphOptions); set(graphAtom, emptyGraph); set(graphUpdateVersionAtom, (v) => v + 1); }); // 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/core/selection-store.ts import { atom as atom4 } from "jotai"; var debug3 = createDebug("selection"); var selectedNodeIdsAtom = atom4(/* @__PURE__ */ new Set()); var selectedEdgeIdAtom = atom4(null); var handleNodePointerDownSelectionAtom = atom4(null, (get, set, { nodeId, isShiftPressed }) => { const currentSelection = get(selectedNodeIdsAtom); debug3("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); } debug3("Shift-click, setting selection to: %o", Array.from(newSelection)); set(selectedNodeIdsAtom, newSelection); } else { if (!currentSelection.has(nodeId)) { debug3("Node not in selection, selecting: %s", nodeId); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set([nodeId])); } else { debug3("Node already selected, preserving multi-select"); } } }); var selectSingleNodeAtom = atom4(null, (get, set, nodeId) => { debug3("selectSingleNode: %s", nodeId); set(selectedEdgeIdAtom, null); if (nodeId === null || nodeId === void 0) { debug3("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 = atom4(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 = atom4(null, (_get, set) => { debug3("clearSelection"); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); }); var addNodesToSelectionAtom = atom4(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 = atom4(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 = atom4(null, (get, set, edgeId) => { set(selectedEdgeIdAtom, edgeId); if (edgeId !== null) { set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); } }); var clearEdgeSelectionAtom = atom4(null, (_get, set) => { set(selectedEdgeIdAtom, null); }); var focusedNodeIdAtom = atom4(null); var setFocusedNodeAtom = atom4(null, (_get, set, nodeId) => { set(focusedNodeIdAtom, nodeId); }); var hasFocusedNodeAtom = atom4((get) => get(focusedNodeIdAtom) !== null); var selectedNodesCountAtom = atom4((get) => get(selectedNodeIdsAtom).size); var hasSelectionAtom = atom4((get) => get(selectedNodeIdsAtom).size > 0); // 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 }; }; // 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); }); function isNodeCollapsed(nodeId, getParentId, collapsed) { let current = nodeId; while (true) { const parentId = getParentId(current); if (!parentId) return false; if (collapsed.has(parentId)) return true; current = parentId; } } // 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.ts import { atom as atom12 } from "jotai"; import Graph3 from "graphology"; // 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/core/interaction-store.ts import { atom as atom14 } from "jotai"; var inputModeAtom = atom14({ type: "normal" }); var keyboardInteractionModeAtom = atom14("navigate"); var interactionFeedbackAtom = atom14(null); var pendingInputResolverAtom = atom14(null); var resetInputModeAtom = atom14(null, (_get, set) => { set(inputModeAtom, { type: "normal" }); set(interactionFeedbackAtom, null); set(pendingInputResolverAtom, null); }); var resetKeyboardInteractionModeAtom = atom14(null, (_get, set) => { set(keyboardInteractionModeAtom, "navigate"); }); var setKeyboardInteractionModeAtom = atom14(null, (_get, set, mode) => { set(keyboardInteractionModeAtom, mode); }); var startPickNodeAtom = atom14(null, (_get, set, options) => { set(inputModeAtom, { type: "pickNode", ...options }); }); var startPickNodesAtom = atom14(null, (_get, set, options) => { set(inputModeAtom, { type: "pickNodes", ...options }); }); var startPickPointAtom = atom14(null, (_get, set, options) => { set(inputModeAtom, { type: "pickPoint", ...options }); }); var provideInputAtom = atom14(null, (get, set, value) => { set(pendingInputResolverAtom, value); }); var updateInteractionFeedbackAtom = atom14(null, (get, set, feedback) => { const current = get(interactionFeedbackAtom); set(interactionFeedbackAtom, { ...current, ...feedback }); }); var isPickingModeAtom = atom14((get) => { const mode = get(inputModeAtom); return mode.type !== "normal"; }); var isPickNodeModeAtom = atom14((get) => { const mode = get(inputModeAtom); return mode.type === "pickNode" || mode.type === "pickNodes"; }); // src/core/locked-node-store.ts import { atom as atom15 } from "jotai"; var lockedNodeIdAtom = atom15(null); var lockedNodeDataAtom = atom15((get) => { const id = get(lockedNodeIdAtom); if (!id) return null; const nodes = get(uiNodesAtom); return nodes.find((n) => n.id === id) || null; }); var lockedNodePageIndexAtom = atom15(0); var lockedNodePageCountAtom = atom15(1); var lockNodeAtom = atom15(null, (_get, set, payload) => { set(lockedNodeIdAtom, payload.nodeId); set(lockedNodePageIndexAtom, 0); }); var unlockNodeAtom = atom15(null, (_get, set) => { set(lockedNodeIdAtom, null); }); var nextLockedPageAtom = atom15(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current + 1) % pageCount); }); var prevLockedPageAtom = atom15(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current - 1 + pageCount) % pageCount); }); var goToLockedPageAtom = atom15(null, (get, set, index) => { const pageCount = get(lockedNodePageCountAtom); if (index >= 0 && index < pageCount) { set(lockedNodePageIndexAtom, index); } }); var hasLockedNodeAtom = atom15((get) => get(lockedNodeIdAtom) !== null); // src/core/node-type-registry.tsx import { c as _c } 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 registerNodeType(nodeType, component) { nodeTypeRegistry.set(nodeType, component); } function registerNodeTypes(types) { for (const [nodeType, component] of Object.entries(types)) { nodeTypeRegistry.set(nodeType, component); } } function unregisterNodeType(nodeType) { return nodeTypeRegistry.delete(nodeType); } function getNodeTypeComponent(nodeType) { if (!nodeType) return void 0; return nodeTypeRegistry.get(nodeType); } function hasNodeTypeComponent(nodeType) { if (!nodeType) return false; return nodeTypeRegistry.has(nodeType); } function getRegisteredNodeTypes() { return Array.from(nodeTypeRegistry.keys()); } function clearNodeTypeRegistry() { nodeTypeRegistry.clear(); } var FallbackNodeTypeComponent = (t0) => { const $ = _c(11); const { nodeData } = t0; let t1; if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t1 = { padding: "12px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", color: "#666", fontSize: "12px" }; $[0] = t1; } else { t1 = $[0]; } const t2 = nodeData.dbData.node_type || "none"; let t3; if ($[1] !== t2) { t3 = /* @__PURE__ */ _jsxs("div", { children: ["Unknown type: ", t2] }); $[1] = t2; $[2] = t3; } else { t3 = $[2]; } let t4; if ($[3] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t4 = { marginTop: "4px", opacity: 0.7 }; $[3] = t4; } else { t4 = $[3]; } let t5; if ($[4] !== nodeData.id) { t5 = nodeData.id.substring(0, 8); $[4] = nodeData.id; $[5] = t5; } else { t5 = $[5]; } let t6; if ($[6] !== t5) { t6 = /* @__PURE__ */ _jsx("div", { style: t4, children: t5 }); $[6] = t5; $[7] = t6; } else { t6 = $[7]; } let t7; if ($[8] !== t3 || $[9] !== t6) { t7 = /* @__PURE__ */ _jsxs("div", { style: t1, children: [t3, t6] }); $[8] = t3; $[9] = t6; $[10] = t7; } else { t7 = $[10]; } return t7; }; // 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/core/snap-store.ts import { atom as atom17 } from "jotai"; var snapEnabledAtom = atom17(false); var snapGridSizeAtom = atom17(20); var snapTemporaryDisableAtom = atom17(false); var isSnappingActiveAtom = atom17((get) => { return get(snapEnabledAtom) && !get(snapTemporaryDisableAtom); }); function snapToGrid(pos, gridSize) { return { x: Math.round(pos.x / gridSize) * gridSize, y: Math.round(pos.y / gridSize) * gridSize }; } function conditionalSnap(pos, gridSize, isActive) { return isActive ? snapToGrid(pos, gridSize) : pos; } function getSnapGuides(pos, gridSize, tolerance = 5) { const snappedX = Math.round(pos.x / gridSize) * gridSize; const snappedY = Math.round(pos.y / gridSize) * gridSize; return { x: Math.abs(pos.x - snappedX) < tolerance ? snappedX : null, y: Math.abs(pos.y - snappedY) < tolerance ? snappedY : null }; } var toggleSnapAtom = atom17(null, (get, set) => { set(snapEnabledAtom, !get(snapEnabledAtom)); }); var setGridSizeAtom = atom17(null, (_get, set, size) => { set(snapGridSizeAtom, Math.max(5, Math.min(200, size))); }); var snapAlignmentEnabledAtom = atom17(true); var toggleAlignmentGuidesAtom = atom17(null, (get, set) => { set(snapAlignmentEnabledAtom, !get(snapAlignmentEnabledAtom)); }); var alignmentGuidesAtom = atom17({ verticalGuides: [], horizontalGuides: [] }); var clearAlignmentGuidesAtom = atom17(null, (_get, set) => { set(alignmentGuidesAtom, { verticalGuides: [], horizontalGuides: [] }); }); function findAlignmentGuides(dragged, others, tolerance = 5) { const verticals = /* @__PURE__ */ new Set(); const horizontals = /* @__PURE__ */ new Set(); const dragCX = dragged.x + dragged.width / 2; const dragCY = dragged.y + dragged.height / 2; const dragRight = dragged.x + dragged.width; const dragBottom = dragged.y + dragged.height; for (const other of others) { const otherCX = other.x + other.width / 2; const otherCY = other.y + other.height / 2; const otherRight = other.x + other.width; const otherBottom = other.y + other.height; if (Math.abs(dragCX - otherCX) < tolerance) verticals.add(otherCX); if (Math.abs(dragged.x - other.x) < tolerance) verticals.add(other.x); if (Math.abs(dragRight - otherRight) < tolerance) verticals.add(otherRight); if (Math.abs(dragged.x - otherRight) < tolerance) verticals.add(otherRight); if (Math.abs(dragRight - other.x) < tolerance) verticals.add(other.x); if (Math.abs(dragCX - other.x) < tolerance) verticals.add(other.x); if (Math.abs(dragCX - otherRight) < tolerance) verticals.add(otherRight); if (Math.abs(dragCY - otherCY) < tolerance) horizontals.add(otherCY); if (Math.abs(dragged.y - other.y) < tolerance) horizontals.add(other.y); if (Math.abs(dragBottom - otherBottom) < tolerance) horizontals.add(otherBottom); if (Math.abs(dragged.y - otherBottom) < tolerance) horizontals.add(otherBottom); if (Math.abs(dragBottom - other.y) < tolerance) horizontals.add(other.y); if (Math.abs(dragCY - other.y) < tolerance) horizontals.add(other.y); if (Math.abs(dragCY - otherBottom) < tolerance) horizontals.add(otherBottom); } return { verticalGuides: Array.from(verticals), horizontalGuides: Array.from(horizontals) }; } // 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/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 hasAction(id) { return actionRegistry.has(id); } function getAllActions() { return Array.from(actionRegistry.values()); } function getActionsByCategory(category) { return getAllActions().filter((action) => action.category === category); } function unregisterAction(id) { return actionRegistry.delete(id); } function clearActions() { actionRegistry.clear(); } registerBuiltInActions(); function getActionsByCategories() { const categoryLabels = { [ActionCategory.None]: "None", [ActionCategory.Selection]: "Selection", [ActionCategory.Viewport]: "Viewport", [ActionCategory.Node]: "Node", [ActionCategory.Layout]: "Layout", [ActionCategory.History]: "History", [ActionCategory.Custom]: "Custom" }; const categoryOrder = [ActionCategory.None, ActionCategory.Selection, ActionCategory.Viewport, ActionCategory.Node, ActionCategory.Layout, ActionCategory.History, ActionCategory.Custom]; return categoryOrder.map((category) => ({ category, label: categoryLabels[category], actions: getActionsByCategory(category) })).filter((group) => group.actions.length > 0); } // src/core/action-executor.ts var debug9 = createDebug("actions"); async function executeAction(actionId, context, helpers) { if (actionId === BuiltInActionId.None) { return { success: true, actionId }; } const action = getAction(actionId); if (!action) { debug9.warn("Action not found: %s", actionId); return { success: false, actionId, error: new Error(`Action not found: ${actionId}`) }; } if (action.requiresNode && !context.nodeId) { debug9.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) { debug9.error("Error executing action %s: %O", actionId, error); return { success: false, actionId, error: error instanceof Error ? error : new Error(String(error)) }; } } function createActionContext(eventType, screenEvent, worldPosition, options) { return { eventType, nodeId: options?.nodeId, nodeData: options?.nodeData, edgeId: options?.edgeId, edgeData: options?.edgeData, worldPosition, screenPosition: { x: screenEvent.clientX, y: screenEvent.clientY }, modifiers: { shift: false, ctrl: false, alt: false, meta: false } }; } function createActionContextFromReactEvent(eventType, event, worldPosition, options) { return { eventType, nodeId: options?.nodeId, nodeData: options?.nodeData, edgeId: options?.edgeId, edgeData: options?.edgeData, worldPosition, screenPosition: { x: event.clientX, y: event.clientY }, modifiers: { shift: event.shiftKey, ctrl: event.ctrlKey, alt: event.altKey, meta: event.metaKey } }; } function createActionContextFromTouchEvent(eventType, touch, worldPosition, options) { return { eventType, nodeId: options?.nodeId, nodeData: options?.nodeData, edgeId: options?.edgeId, edgeData: options?.edgeData, worldPosition, screenPosition: { x: touch.clientX, y: touch.clientY }, modifiers: { shift: false, ctrl: false, alt: false, meta: false } }; } 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 { debug9.warn("deleteNode called but onDeleteNode callback not provided"); } }, isNodeLocked: (nodeId) => store.get(lockedNodeIdAtom) === nodeId, applyForceLayout: async () => { if (options.onApplyForceLayout) { await options.onApplyForceLayout(); } else { debug9.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/core/settings-store.ts import { atom as atom18 } from "jotai"; import { atomWithStorage } from "jotai/utils"; // 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 debug10 = 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 = atom18((get) => get(canvasSettingsAtom).mappings); var activePresetIdAtom = atom18((get) => get(canvasSettingsAtom).activePresetId); var allPresetsAtom = atom18((get) => { const state = get(canvasSettingsAtom); return [...BUILT_IN_PRESETS, ...state.customPresets]; }); var activePresetAtom = atom18((get) => { const presetId = get(activePresetIdAtom); if (!presetId) return null; const allPresets = get(allPresetsAtom); return allPresets.find((p) => p.id === presetId) || null; }); var isPanelOpenAtom = atom18((get) => get(canvasSettingsAtom).isPanelOpen); var virtualizationEnabledAtom = atom18((get) => get(canvasSettingsAtom).virtualizationEnabled ?? true); var hasUnsavedChangesAtom = atom18((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 = atom18(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 = atom18(null, (get, set, presetId) => { const allPresets = get(allPresetsAtom); const preset = allPresets.find((p) => p.id === presetId); if (!preset) { debug10.warn("Preset not found: %s", presetId); return; } const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: { ...preset.mappings }, activePresetId: presetId }); }); var saveAsPresetAtom = atom18(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 = atom18(null, (get, set, presetId) => { const current = get(canvasSettingsAtom); const presetIndex = current.customPresets.findIndex((p) => p.id === presetId); if (presetIndex === -1) { debug10.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 = atom18(null, (get, set, presetId) => { const current = get(canvasSettingsAtom); const newCustomPresets = current.customPresets.filter((p) => p.id !== presetId); if (newCustomPresets.length === current.customPresets.length) { debug10.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 = atom18(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: DEFAULT_MAPPINGS, activePresetId: "default" }); }); var togglePanelAtom = atom18(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: !current.isPanelOpen }); }); var setPanelOpenAtom = atom18(null, (get, set, isOpen) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: isOpen }); }); var setVirtualizationEnabledAtom = atom18(null, (get, set, enabled) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: enabled }); }); var toggleVirtualizationAtom = atom18(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: !(current.virtualizationEnabled ?? true) }); }); // src/core/canvas-serializer.ts import Graph4 from "graphology"; var SNAPSHOT_VERSION = 1; function exportGraph(store, metadata) { const graph = store.get(graphAtom); const zoom = store.get(zoomAtom); const pan = store.get(panAtom); const collapsed = store.get(collapsedGroupsAtom); const nodes = []; const groups = []; const seenGroupParents = /* @__PURE__ */ new Set(); graph.forEachNode((nodeId, attrs) => { const a = attrs; nodes.push({ id: nodeId, position: { x: a.x, y: a.y }, dimensions: { width: a.width, height: a.height }, size: a.size, color: a.color, zIndex: a.zIndex, label: a.label, parentId: a.parentId, dbData: a.dbData }); if (a.parentId) { const key = `${nodeId}:${a.parentId}`; if (!seenGroupParents.has(key)) { seenGroupParents.add(key); groups.push({ nodeId, parentId: a.parentId, isCollapsed: collapsed.has(a.parentId) }); } } }); const edges = []; graph.forEachEdge((key, attrs, source, target) => { const a = attrs; edges.push({ key, sourceId: source, targetId: target, attributes: { weight: a.weight, type: a.type, color: a.color, label: a.label }, dbData: a.dbData }); }); return { version: SNAPSHOT_VERSION, exportedAt: (/* @__PURE__ */ new Date()).toISOString(), nodes, edges, groups, viewport: { zoom, pan: { ...pan } }, metadata }; } function importGraph(store, snapshot, options = {}) { const { clearExisting = true, offsetPosition, remapIds = false } = options; const idMap = /* @__PURE__ */ new Map(); if (remapIds) { for (const node of snapshot.nodes) { idMap.set(node.id, crypto.randomUUID()); } for (const edge of snapshot.edges) { idMap.set(edge.key, crypto.randomUUID()); } } const remap = (id) => idMap.get(id) ?? id; let graph; if (clearExisting) { graph = new Graph4(graphOptions); } else { graph = store.get(graphAtom); } const ox = offsetPosition?.x ?? 0; const oy = offsetPosition?.y ?? 0; for (const node of snapshot.nodes) { const nodeId = remap(node.id); const parentId = node.parentId ? remap(node.parentId) : void 0; const dbData = remapIds ? { ...node.dbData, id: nodeId } : node.dbData; const attrs = { x: node.position.x + ox, y: node.position.y + oy, width: node.dimensions.width, height: node.dimensions.height, size: node.size, color: node.color, zIndex: node.zIndex, label: node.label, parentId, dbData }; graph.addNode(nodeId, attrs); } for (const edge of snapshot.edges) { const edgeKey = remap(edge.key); const sourceId = remap(edge.sourceId); const targetId = remap(edge.targetId); if (!graph.hasNode(sourceId) || !graph.hasNode(targetId)) continue; const dbData = remapIds ? { ...edge.dbData, id: edgeKey, source_node_id: sourceId, target_node_id: targetId } : edge.dbData; const attrs = { weight: edge.attributes.weight, type: edge.attributes.type, color: edge.attributes.color, label: edge.attributes.label, dbData }; graph.addEdgeWithKey(edgeKey, sourceId, targetId, attrs); } store.set(graphAtom, graph); store.set(graphUpdateVersionAtom, (v) => v + 1); store.set(nodePositionUpdateCounterAtom, (c) => c + 1); const collapsedSet = /* @__PURE__ */ new Set(); for (const group of snapshot.groups) { if (group.isCollapsed) { collapsedSet.add(remap(group.parentId)); } } store.set(collapsedGroupsAtom, collapsedSet); store.set(zoomAtom, snapshot.viewport.zoom); store.set(panAtom, { ...snapshot.viewport.pan }); } function validateSnapshot(data) { const errors = []; if (!data || typeof data !== "object") { return { valid: false, errors: ["Snapshot must be a non-null object"] }; } const obj = data; if (obj.version !== SNAPSHOT_VERSION) { errors.push(`Expected version ${SNAPSHOT_VERSION}, got ${String(obj.version)}`); } if (typeof obj.exportedAt !== "string") { errors.push('Missing or invalid "exportedAt" (expected ISO string)'); } if (!Array.isArray(obj.nodes)) { errors.push('Missing or invalid "nodes" (expected array)'); } else { for (let i = 0; i < obj.nodes.length; i++) { const node = obj.nodes[i]; if (!node || typeof node !== "object") { errors.push(`nodes[${i}]: expected object`); continue; } if (typeof node.id !== "string") errors.push(`nodes[${i}]: missing "id"`); if (!node.position || typeof node.position !== "object") errors.push(`nodes[${i}]: missing "position"`); if (!node.dimensions || typeof node.dimensions !== "object") errors.push(`nodes[${i}]: missing "dimensions"`); if (!node.dbData || typeof node.dbData !== "object") errors.push(`nodes[${i}]: missing "dbData"`); } } if (!Array.isArray(obj.edges)) { errors.push('Missing or invalid "edges" (expected array)'); } else { for (let i = 0; i < obj.edges.length; i++) { const edge = obj.edges[i]; if (!edge || typeof edge !== "object") { errors.push(`edges[${i}]: expected object`); continue; } if (typeof edge.key !== "string") errors.push(`edges[${i}]: missing "key"`); if (typeof edge.sourceId !== "string") errors.push(`edges[${i}]: missing "sourceId"`); if (typeof edge.targetId !== "string") errors.push(`edges[${i}]: missing "targetId"`); if (!edge.dbData || typeof edge.dbData !== "object") errors.push(`edges[${i}]: missing "dbData"`); } } if (!Array.isArray(obj.groups)) { errors.push('Missing or invalid "groups" (expected array)'); } if (!obj.viewport || typeof obj.viewport !== "object") { errors.push('Missing or invalid "viewport" (expected object)'); } else { const vp = obj.viewport; if (typeof vp.zoom !== "number") errors.push('viewport: missing "zoom"'); if (!vp.pan || typeof vp.pan !== "object") errors.push('viewport: missing "pan"'); } return { valid: errors.length === 0, errors }; } // src/core/clipboard-store.ts import { atom as atom19 } from "jotai"; var debug11 = createDebug("clipboard"); var PASTE_OFFSET = { x: 50, y: 50 }; var clipboardAtom = atom19(null); var hasClipboardContentAtom = atom19((get) => get(clipboardAtom) !== null); var clipboardNodeCountAtom = atom19((get) => { const clipboard = get(clipboardAtom); return clipboard?.nodes.length ?? 0; }); function calculateBounds2(nodes) { if (nodes.length === 0) { return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; } let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (const node of nodes) { minX = Math.min(minX, node.attrs.x); minY = Math.min(minY, node.attrs.y); maxX = Math.max(maxX, node.attrs.x + node.attrs.width); maxY = Math.max(maxY, node.attrs.y + node.attrs.height); } return { minX, minY, maxX, maxY }; } function generatePasteId(index) { return `paste-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 8)}`; } var copyToClipboardAtom = atom19(null, (get, set, nodeIds) => { const selectedIds = nodeIds ?? Array.from(get(selectedNodeIdsAtom)); if (selectedIds.length === 0) { debug11("Nothing to copy - no nodes selected"); return; } const graph = get(graphAtom); const selectedSet = new Set(selectedIds); const nodes = []; const edges = []; for (const nodeId of selectedIds) { if (!graph.hasNode(nodeId)) { debug11("Node %s not found in graph, skipping", nodeId); continue; } const attrs = graph.getNodeAttributes(nodeId); nodes.push({ attrs: { ...attrs }, dbData: { ...attrs.dbData } }); } graph.forEachEdge((edgeKey, attrs, source, target) => { if (selectedSet.has(source) && selectedSet.has(target)) { edges.push({ source, target, attrs: { ...attrs }, dbData: { ...attrs.dbData } }); } }); const bounds = calculateBounds2(nodes); const clipboardData = { nodes, edges, bounds, timestamp: Date.now() }; set(clipboardAtom, clipboardData); debug11("Copied %d nodes and %d edges to clipboard", nodes.length, edges.length); }); var cutToClipboardAtom = atom19(null, (get, set, nodeIds) => { const selectedIds = nodeIds ?? Array.from(get(selectedNodeIdsAtom)); if (selectedIds.length === 0) return; set(copyToClipboardAtom, selectedIds); set(pushHistoryAtom, "Cut nodes"); for (const nodeId of selectedIds) { set(optimisticDeleteNodeAtom, { nodeId }); } set(clearSelectionAtom); debug11("Cut %d nodes \u2014 copied to clipboard and deleted from graph", selectedIds.length); }); var pasteFromClipboardAtom = atom19(null, (get, set, offset) => { const clipboard = get(clipboardAtom); if (!clipboard || clipboard.nodes.length === 0) { debug11("Nothing to paste - clipboard empty"); return []; } const pasteOffset = offset ?? PASTE_OFFSET; const graph = get(graphAtom); set(pushHistoryAtom, "Paste nodes"); const idMap = /* @__PURE__ */ new Map(); const newNodeIds = []; for (let i = 0; i < clipboard.nodes.length; i++) { const nodeData = clipboard.nodes[i]; const newId = generatePasteId(i); idMap.set(nodeData.dbData.id, newId); newNodeIds.push(newId); const newDbNode = { ...nodeData.dbData, id: newId, created_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString(), ui_properties: { ...nodeData.dbData.ui_properties || {}, x: nodeData.attrs.x + pasteOffset.x, y: nodeData.attrs.y + pasteOffset.y } }; debug11("Pasting node %s -> %s at (%d, %d)", nodeData.dbData.id, newId, nodeData.attrs.x + pasteOffset.x, nodeData.attrs.y + pasteOffset.y); set(addNodeToLocalGraphAtom, newDbNode); } for (const edgeData of clipboard.edges) { const newSourceId = idMap.get(edgeData.source); const newTargetId = idMap.get(edgeData.target); if (!newSourceId || !newTargetId) { debug11("Edge %s: source or target not found in id map, skipping", edgeData.dbData.id); continue; } const newEdgeId = generatePasteId(clipboard.edges.indexOf(edgeData) + clipboard.nodes.length); const newDbEdge = { ...edgeData.dbData, id: newEdgeId, source_node_id: newSourceId, target_node_id: newTargetId, created_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() }; debug11("Pasting edge %s -> %s (from %s to %s)", edgeData.dbData.id, newEdgeId, newSourceId, newTargetId); set(addEdgeToLocalGraphAtom, newDbEdge); } set(clearSelectionAtom); set(addNodesToSelectionAtom, newNodeIds); debug11("Pasted %d nodes and %d edges", newNodeIds.length, clipboard.edges.length); return newNodeIds; }); var duplicateSelectionAtom = atom19(null, (get, set) => { set(copyToClipboardAtom); return set(pasteFromClipboardAtom); }); var clearClipboardAtom = atom19(null, (_get, set) => { set(clipboardAtom, null); debug11("Clipboard cleared"); }); // src/core/virtualization-store.ts import { atom as atom20 } 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 = atom20((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 = atom20((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 = atom20((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 = atom20((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 = atom20((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/core/canvas-api.ts function createCanvasAPI(store, options = {}) { const helpers = buildActionHelpers(store, options); const api = { // Selection selectNode: (id) => store.set(selectSingleNodeAtom, id), addToSelection: (ids) => store.set(addNodesToSelectionAtom, ids), clearSelection: () => store.set(clearSelectionAtom), getSelectedNodeIds: () => Array.from(store.get(selectedNodeIdsAtom)), selectEdge: (edgeId) => store.set(selectEdgeAtom, edgeId), clearEdgeSelection: () => store.set(clearEdgeSelectionAtom), getSelectedEdgeId: () => store.get(selectedEdgeIdAtom), // Viewport getZoom: () => store.get(zoomAtom), setZoom: (zoom) => store.set(zoomAtom, zoom), getPan: () => store.get(panAtom), setPan: (pan) => store.set(panAtom, pan), resetViewport: () => store.set(resetViewportAtom), fitToBounds: (mode, padding) => { const fitMode = mode === "graph" ? FitToBoundsMode.Graph : FitToBoundsMode.Selection; store.set(fitToBoundsAtom, { mode: fitMode, padding }); }, centerOnNode: (nodeId) => store.set(centerOnNodeAtom, nodeId), // Graph addNode: (node) => store.set(addNodeToLocalGraphAtom, node), removeNode: (nodeId) => store.set(optimisticDeleteNodeAtom, { nodeId }), addEdge: (edge) => store.set(addEdgeToLocalGraphAtom, edge), removeEdge: (edgeKey) => store.set(optimisticDeleteEdgeAtom, { edgeKey }), getNodeKeys: () => store.get(nodeKeysAtom), getEdgeKeys: () => store.get(edgeKeysAtom), getNodeAttributes: (id) => { const graph = store.get(graphAtom); return graph.hasNode(id) ? graph.getNodeAttributes(id) : void 0; }, // History undo: () => store.set(undoAtom), redo: () => store.set(redoAtom), canUndo: () => store.get(canUndoAtom), canRedo: () => store.get(canRedoAtom), recordSnapshot: (label) => store.set(pushHistoryAtom, label), clearHistory: () => store.set(clearHistoryAtom), // Clipboard copy: () => store.set(copyToClipboardAtom), cut: () => store.set(cutToClipboardAtom), paste: () => store.set(pasteFromClipboardAtom), duplicate: () => store.set(duplicateSelectionAtom), hasClipboardContent: () => store.get(clipboardAtom) !== null, // Snap isSnapEnabled: () => store.get(snapEnabledAtom), toggleSnap: () => store.set(toggleSnapAtom), getSnapGridSize: () => store.get(snapGridSizeAtom), // Virtualization isVirtualizationEnabled: () => store.get(virtualizationEnabledAtom), getVisibleNodeKeys: () => store.get(visibleNodeKeysAtom), getVisibleEdgeKeys: () => store.get(visibleEdgeKeysAtom), // Actions executeAction: (actionId, context) => executeAction(actionId, context, helpers), executeEventAction: (event, context) => { const mappings = store.get(eventMappingsAtom); const actionId = getActionForEvent(mappings, event); return executeAction(actionId, context, helpers); }, // Serialization exportSnapshot: (metadata) => exportGraph(store, metadata), importSnapshot: (snapshot, options2) => importGraph(store, snapshot, options2), validateSnapshot: (data) => validateSnapshot(data) }; return api; } // src/core/port-types.ts function calculatePortPosition(nodeX, nodeY, nodeWidth, nodeHeight, port) { switch (port.side) { case "left": return { x: nodeX, y: nodeY + nodeHeight * port.position }; case "right": return { x: nodeX + nodeWidth, y: nodeY + nodeHeight * port.position }; case "top": return { x: nodeX + nodeWidth * port.position, y: nodeY }; case "bottom": return { x: nodeX + nodeWidth * port.position, y: nodeY + nodeHeight }; } } var DEFAULT_PORT = { id: "default", type: "bidirectional", side: "right", position: 0.5 }; function getNodePorts(ports) { if (ports && ports.length > 0) { return ports; } return [DEFAULT_PORT]; } function canPortAcceptConnection(port, currentConnections, isSource) { if (isSource && port.type === "input") { return false; } if (!isSource && port.type === "output") { return false; } if (port.maxConnections !== void 0 && currentConnections >= port.maxConnections) { return false; } return true; } function arePortsCompatible(sourcePort, targetPort) { if (sourcePort.type === "input") { return false; } if (targetPort.type === "output") { return false; } return true; } // src/core/input-classifier.ts function classifyPointer(e) { const source = pointerTypeToSource(e.pointerType); return { source, pointerId: e.pointerId, pressure: e.pressure, tiltX: e.tiltX, tiltY: e.tiltY, isPrimary: e.isPrimary, rawPointerType: e.pointerType }; } function pointerTypeToSource(pointerType) { switch (pointerType) { case "pen": return "pencil"; case "touch": return "finger"; case "mouse": return "mouse"; default: return "mouse"; } } 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 }; } function getGestureThresholds(source) { switch (source) { case "finger": return { dragThreshold: 10, tapThreshold: 10, longPressDuration: 600, longPressMoveLimit: 10 }; case "pencil": return { dragThreshold: 2, tapThreshold: 3, longPressDuration: 500, longPressMoveLimit: 5 }; case "mouse": return { dragThreshold: 3, tapThreshold: 5, longPressDuration: 0, // Mouse uses right-click instead longPressMoveLimit: 0 }; } } var HIT_TARGET_SIZES = { /** Minimum touch target (Apple HIG: 44pt) */ finger: 44, /** Stylus target (precise, can use smaller targets) */ pencil: 24, /** Mouse target (hover-discoverable, smallest) */ mouse: 16 }; function getHitTargetSize(source) { return HIT_TARGET_SIZES[source]; } // src/core/input-store.ts import { atom as atom21 } from "jotai"; var activePointersAtom = atom21(/* @__PURE__ */ new Map()); var primaryInputSourceAtom = atom21("mouse"); var inputCapabilitiesAtom = atom21(detectInputCapabilities()); var isStylusActiveAtom = atom21((get) => { const pointers = get(activePointersAtom); for (const [, pointer] of pointers) { if (pointer.source === "pencil") return true; } return false; }); var isMultiTouchAtom = atom21((get) => { const pointers = get(activePointersAtom); let fingerCount = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") fingerCount++; } return fingerCount > 1; }); var fingerCountAtom = atom21((get) => { const pointers = get(activePointersAtom); let count = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") count++; } return count; }); var isTouchDeviceAtom = atom21((get) => { const caps = get(inputCapabilitiesAtom); return caps.hasTouch; }); var pointerDownAtom = atom21(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 = atom21(null, (get, set, pointerId) => { const pointers = new Map(get(activePointersAtom)); pointers.delete(pointerId); set(activePointersAtom, pointers); }); var clearPointersAtom = atom21(null, (_get, set) => { set(activePointersAtom, /* @__PURE__ */ new Map()); }); // src/core/selection-path-store.ts import { atom as atom22 } from "jotai"; var selectionPathAtom = atom22(null); var isSelectingAtom = atom22((get) => get(selectionPathAtom) !== null); var startSelectionAtom = atom22(null, (_get, set, { type, point }) => { set(selectionPathAtom, { type, points: [point] }); }); var updateSelectionAtom = atom22(null, (get, set, point) => { const current = get(selectionPathAtom); if (!current) return; if (current.type === "rect") { set(selectionPathAtom, { ...current, points: [current.points[0], point] }); } else { set(selectionPathAtom, { ...current, points: [...current.points, point] }); } }); var cancelSelectionAtom = atom22(null, (_get, set) => { set(selectionPathAtom, null); }); var endSelectionAtom = atom22(null, (get, set) => { const path = get(selectionPathAtom); if (!path || path.points.length < 2) { set(selectionPathAtom, null); return; } const nodes = get(uiNodesAtom); const selectedIds = []; if (path.type === "rect") { const [p1, p2] = [path.points[0], path.points[path.points.length - 1]]; const minX = Math.min(p1.x, p2.x); const maxX = Math.max(p1.x, p2.x); const minY = Math.min(p1.y, p2.y); const maxY = Math.max(p1.y, p2.y); for (const node of nodes) { const nodeRight = node.position.x + (node.width ?? 200); const nodeBottom = node.position.y + (node.height ?? 100); if (node.position.x < maxX && nodeRight > minX && node.position.y < maxY && nodeBottom > minY) { selectedIds.push(node.id); } } } else { const polygon = path.points; for (const node of nodes) { const cx = node.position.x + (node.width ?? 200) / 2; const cy = node.position.y + (node.height ?? 100) / 2; if (pointInPolygon(cx, cy, polygon)) { selectedIds.push(node.id); } } } set(selectedNodeIdsAtom, new Set(selectedIds)); set(selectionPathAtom, null); }); var selectionRectAtom = atom22((get) => { const path = get(selectionPathAtom); if (!path || path.type !== "rect" || path.points.length < 2) return null; const [p1, p2] = [path.points[0], path.points[path.points.length - 1]]; return { x: Math.min(p1.x, p2.x), y: Math.min(p1.y, p2.y), width: Math.abs(p2.x - p1.x), height: Math.abs(p2.y - p1.y) }; }); function pointInPolygon(px, py, polygon) { let inside = false; const n = polygon.length; for (let i = 0, j = n - 1; i < n; j = i++) { const xi = polygon[i].x; const yi = polygon[i].y; const xj = polygon[j].x; const yj = polygon[j].y; if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) { inside = !inside; } } return inside; } // src/core/search-store.ts import { atom as atom23 } from "jotai"; var searchQueryAtom = atom23(""); var setSearchQueryAtom = atom23(null, (_get, set, query) => { set(searchQueryAtom, query); set(highlightedSearchIndexAtom, 0); }); var clearSearchAtom = atom23(null, (_get, set) => { set(searchQueryAtom, ""); set(highlightedSearchIndexAtom, 0); }); function fuzzyMatch(query, ...haystacks) { const tokens = query.toLowerCase().split(/\s+/).filter(Boolean); if (tokens.length === 0) return false; const combined = haystacks.join(" ").toLowerCase(); return tokens.every((token) => combined.includes(token)); } var searchResultsAtom = atom23((get) => { const query = get(searchQueryAtom).trim(); if (!query) return /* @__PURE__ */ new Set(); const nodes = get(uiNodesAtom); const matches = /* @__PURE__ */ new Set(); for (const node of nodes) { if (fuzzyMatch(query, node.label || "", node.dbData.node_type || "", node.id)) { matches.add(node.id); } } return matches; }); var searchResultsArrayAtom = atom23((get) => { return Array.from(get(searchResultsAtom)); }); var searchResultCountAtom = atom23((get) => { return get(searchResultsAtom).size; }); var searchEdgeResultsAtom = atom23((get) => { const query = get(searchQueryAtom).trim(); if (!query) return /* @__PURE__ */ new Set(); get(graphUpdateVersionAtom); const graph = get(graphAtom); const matches = /* @__PURE__ */ new Set(); graph.forEachEdge((edgeKey, attrs) => { const label = attrs.label || ""; const edgeType = attrs.dbData?.edge_type || ""; if (fuzzyMatch(query, label, edgeType, edgeKey)) { matches.add(edgeKey); } }); return matches; }); var searchEdgeResultCountAtom = atom23((get) => { return get(searchEdgeResultsAtom).size; }); var isFilterActiveAtom = atom23((get) => { return get(searchQueryAtom).trim().length > 0; }); var searchTotalResultCountAtom = atom23((get) => { return get(searchResultCountAtom) + get(searchEdgeResultCountAtom); }); var highlightedSearchIndexAtom = atom23(0); var nextSearchResultAtom = atom23(null, (get, set) => { const results = get(searchResultsArrayAtom); if (results.length === 0) return; const currentIndex = get(highlightedSearchIndexAtom); const nextIndex = (currentIndex + 1) % results.length; set(highlightedSearchIndexAtom, nextIndex); const nodeId = results[nextIndex]; set(centerOnNodeAtom, nodeId); set(selectSingleNodeAtom, nodeId); }); var prevSearchResultAtom = atom23(null, (get, set) => { const results = get(searchResultsArrayAtom); if (results.length === 0) return; const currentIndex = get(highlightedSearchIndexAtom); const prevIndex = (currentIndex - 1 + results.length) % results.length; set(highlightedSearchIndexAtom, prevIndex); const nodeId = results[prevIndex]; set(centerOnNodeAtom, nodeId); set(selectSingleNodeAtom, nodeId); }); var highlightedSearchNodeIdAtom = atom23((get) => { const results = get(searchResultsArrayAtom); if (results.length === 0) return null; const index = get(highlightedSearchIndexAtom); return results[index] ?? null; }); // src/core/gesture-rules-defaults.ts var MODIFIER_KEYS = ["shift", "ctrl", "alt", "meta"]; var SOURCE_LABELS = { mouse: "Mouse", pencil: "Pencil", finger: "Touch" }; var GESTURE_LABELS = { tap: "Tap", "double-tap": "Double-tap", "triple-tap": "Triple-tap", drag: "Drag", "long-press": "Long-press", "right-click": "Right-click", pinch: "Pinch", scroll: "Scroll" }; var TARGET_LABELS = { node: "node", edge: "edge", port: "port", "resize-handle": "resize handle", background: "background" }; var BUTTON_LABELS = { 0: "Left", 1: "Middle", 2: "Right" }; function formatRuleLabel(pattern) { const parts = []; if (pattern.modifiers) { const mods = MODIFIER_KEYS.filter((k) => pattern.modifiers[k]).map((k) => k.charAt(0).toUpperCase() + k.slice(1)); if (mods.length) parts.push(mods.join("+")); } if (pattern.button !== void 0 && pattern.button !== 0) { parts.push(BUTTON_LABELS[pattern.button]); } if (pattern.source) { parts.push(SOURCE_LABELS[pattern.source]); } if (pattern.gesture) { parts.push(GESTURE_LABELS[pattern.gesture] ?? pattern.gesture); } if (pattern.target) { parts.push("on " + (TARGET_LABELS[pattern.target] ?? pattern.target)); } if (parts.length === 0) return "Any gesture"; if (pattern.modifiers) { const modCount = MODIFIER_KEYS.filter((k) => pattern.modifiers[k]).length; if (modCount > 0 && parts.length > modCount) { const modPart = parts.slice(0, 1).join(""); const rest = parts.slice(1).join(" ").toLowerCase(); return `${modPart} + ${rest}`; } } return parts.join(" "); } 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_KEYS2 = ["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_KEYS2) { 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 atom24 } 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 = atom24([]); var gestureRulesAtom = atom24((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 = atom24((get) => { return buildRuleIndex(get(gestureRulesAtom)); }); var palmRejectionEnabledAtom = atom24((get) => get(gestureRuleSettingsAtom).palmRejection, (get, set, enabled) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, palmRejection: enabled }); }); var addGestureRuleAtom = atom24(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 = atom24(null, (get, set, ruleId) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: current.customRules.filter((r) => r.id !== ruleId) }); }); var updateGestureRuleAtom = atom24(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 = atom24(null, (get, set) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: [] }); }); // src/core/external-keyboard-store.ts import { atom as atom25 } from "jotai"; var hasExternalKeyboardAtom = atom25(false); var watchExternalKeyboardAtom = atom25(null, (get, set) => { if (typeof window === "undefined") return; const handler = (e) => { if (e.key && e.key.length === 1 || ["Tab", "Escape", "Enter", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { set(hasExternalKeyboardAtom, true); window.removeEventListener("keydown", handler); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }); // 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/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/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/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 debug12 = createDebug("plugins"); var plugins = /* @__PURE__ */ new Map(); function registerPlugin(plugin) { debug12("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() }); debug12("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); debug12("Plugin unregistered: %s", pluginId); } function getPlugin(id) { return plugins.get(id)?.plugin; } function hasPlugin(id) { return plugins.has(id); } function getAllPlugins() { return Array.from(plugins.values()).map((r) => r.plugin); } function getPluginIds() { return Array.from(plugins.keys()); } function getPluginGestureContexts() { const contexts = []; for (const registration of plugins.values()) { if (registration.plugin.gestureContexts) { contexts.push(...registration.plugin.gestureContexts); } } return contexts; } function clearPlugins() { const ids = Array.from(plugins.keys()).reverse(); for (const id of ids) { const reg = plugins.get(id); if (reg?.cleanup) { try { reg.cleanup(); } catch { } } plugins.delete(id); } debug12("All plugins cleared"); } 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 }; } export { ActionCategory, BUILT_IN_PRESETS, BuiltInActionId, CanvasEventType, DEFAULT_GESTURE_RULES, DEFAULT_MAPPINGS, DEFAULT_PORT, EDGE_ANIMATION_DURATION, EVENT_TYPE_INFO, FallbackNodeTypeComponent, HIT_TARGET_SIZES, PASTE_OFFSET, PluginError, SNAPSHOT_VERSION, SpatialGrid, VIRTUALIZATION_BUFFER, ZOOM_EXIT_THRESHOLD, ZOOM_TRANSITION_THRESHOLD, activePointersAtom, activePresetAtom, activePresetIdAtom, addGestureRuleAtom, addNodeToLocalGraphAtom, addNodesToSelectionAtom, alignmentGuidesAtom, allPresetsAtom, animateFitToBoundsAtom, animateZoomToNodeAtom, applyDelta, applyPresetAtom, arePortsCompatible, autoResizeGroupAtom, buildActionHelpers, buildRuleIndex, calculatePortPosition, canPortAcceptConnection, canRedoAtom, canUndoAtom, cancelSelectionAtom, canvasMark, canvasSettingsAtom, canvasToastAtom, canvasWrap, centerOnNodeAtom, classifyPointer, cleanupAllNodePositionsAtom, cleanupNodePositionAtom, clearActions, clearAlignmentGuidesAtom, clearClipboardAtom, clearEdgeSelectionAtom, clearGraphOnSwitchAtom, clearHistoryAtom, clearMutationQueueAtom, clearNodeTypeRegistry, clearPlugins, clearPointersAtom, clearSearchAtom, clearSelectionAtom, clipboardAtom, clipboardNodeCountAtom, collapseGroupAtom, collapsedEdgeRemapAtom, collapsedGroupsAtom, completeMutationAtom, conditionalSnap, consumerGestureRulesAtom, copyToClipboardAtom, createActionContext, createActionContextFromReactEvent, createActionContextFromTouchEvent, createCanvasAPI, createSnapshot, currentGraphIdAtom, cutToClipboardAtom, deletePresetAtom, departingEdgesAtom, dequeueMutationAtom, detectInputCapabilities, draggingNodeIdAtom, dropTargetNodeIdAtom, duplicateSelectionAtom, edgeCreationAtom, edgeFamilyAtom, edgeKeysAtom, edgeKeysWithTempEdgeAtom, editingEdgeLabelAtom, endNodeDragAtom, endSelectionAtom, eventMappingsAtom, executeAction, expandGroupAtom, exportGraph, findAlignmentGuides, fingerCountAtom, fitToBoundsAtom, focusedNodeIdAtom, formatRuleLabel, fuzzyMatch, gestureRuleIndexAtom, gestureRuleSettingsAtom, gestureRulesAtom, getAction, getActionForEvent, getActionsByCategories, getActionsByCategory, getAllActions, getAllPlugins, getGestureThresholds, getHitTargetSize, getNextQueuedMutationAtom, getNodeDescendants, getNodePorts, getNodeTypeComponent, getPlugin, getPluginGestureContexts, getPluginIds, getRegisteredNodeTypes, getSnapGuides, goToLockedPageAtom, graphAtom, graphOptions, graphUpdateVersionAtom, groupChildCountAtom, groupSelectedNodesAtom, handleNodePointerDownSelectionAtom, hasAction, hasClipboardContentAtom, hasExternalKeyboardAtom, hasFocusedNodeAtom, hasLockedNodeAtom, hasNodeTypeComponent, hasPlugin, hasSelectionAtom, hasUnsavedChangesAtom, highestZIndexAtom, highlightedSearchIndexAtom, highlightedSearchNodeIdAtom, historyLabelsAtom, historyStateAtom, importGraph, incrementRetryCountAtom, inputCapabilitiesAtom, inputModeAtom, interactionFeedbackAtom, invertDelta, isFilterActiveAtom, isGroupNodeAtom, isMultiTouchAtom, isNodeCollapsed, isOnlineAtom, isPanelOpenAtom, isPickNodeModeAtom, isPickingModeAtom, isSelectingAtom, isSnappingActiveAtom, isStylusActiveAtom, isTouchDeviceAtom, isZoomTransitioningAtom, keyboardInteractionModeAtom, lastSyncErrorAtom, lastSyncTimeAtom, loadGraphFromDbAtom, lockNodeAtom, lockedNodeDataAtom, lockedNodeIdAtom, lockedNodePageCountAtom, lockedNodePageIndexAtom, matchSpecificity, mergeNodesAtom, mergeRules, moveNodesToGroupAtom, mutationQueueAtom, nestNodesOnDropAtom, nextLockedPageAtom, nextSearchResultAtom, nodeChildrenAtom, nodeFamilyAtom, nodeKeysAtom, nodeParentAtom, nodePositionAtomFamily, nodePositionUpdateCounterAtom, optimisticDeleteEdgeAtom, optimisticDeleteNodeAtom, palmRejectionEnabledAtom, panAtom, pasteFromClipboardAtom, pendingInputResolverAtom, pendingMutationsCountAtom, perfEnabledAtom, pointInPolygon, pointerDownAtom, pointerUpAtom, preDragNodeAttributesAtom, prefersReducedMotionAtom, prevLockedPageAtom, prevSearchResultAtom, primaryInputSourceAtom, provideInputAtom, pushDeltaAtom, pushHistoryAtom, queueMutationAtom, redoAtom, redoCountAtom, registerAction, registerNodeType, registerNodeTypes, registerPlugin, removeEdgeWithAnimationAtom, removeFromGroupAtom, removeGestureRuleAtom, removeNodesFromSelectionAtom, resetGestureRulesAtom, resetInputModeAtom, resetKeyboardInteractionModeAtom, resetSettingsAtom, resetViewportAtom, resolveGesture, resolveGestureIndexed, saveAsPresetAtom, screenToWorldAtom, searchEdgeResultCountAtom, searchEdgeResultsAtom, searchQueryAtom, searchResultCountAtom, searchResultsArrayAtom, searchResultsAtom, searchTotalResultCountAtom, selectEdgeAtom, selectSingleNodeAtom, selectedEdgeIdAtom, selectedNodeIdsAtom, selectedNodesCountAtom, selectionPathAtom, selectionRectAtom, setEventMappingAtom, setFocusedNodeAtom, setGridSizeAtom, setKeyboardInteractionModeAtom, setNodeParentAtom, setOnlineStatusAtom, setPanelOpenAtom, setPerfEnabled, setSearchQueryAtom, setVirtualizationEnabledAtom, setZoomAtom, showToastAtom, snapAlignmentEnabledAtom, snapEnabledAtom, snapGridSizeAtom, snapTemporaryDisableAtom, snapToGrid, spatialIndexAtom, splitNodeAtom, startMutationAtom, startNodeDragAtom, startPickNodeAtom, startPickNodesAtom, startPickPointAtom, startSelectionAtom, swapEdgeAtomicAtom, syncStateAtom, syncStatusAtom, toggleAlignmentGuidesAtom, toggleGroupCollapseAtom, toggleNodeInSelectionAtom, togglePanelAtom, toggleSnapAtom, toggleVirtualizationAtom, trackMutationErrorAtom, uiNodesAtom, undoAtom, undoCountAtom, ungroupNodesAtom, unlockNodeAtom, unregisterAction, unregisterNodeType, unregisterPlugin, updateEdgeLabelAtom, updateGestureRuleAtom, updateInteractionFeedbackAtom, updateNodePositionAtom, updatePresetAtom, updateSelectionAtom, validateSnapshot, viewportRectAtom, virtualizationEnabledAtom, virtualizationMetricsAtom, visibleBoundsAtom, visibleEdgeKeysAtom, visibleNodeKeysAtom, watchExternalKeyboardAtom, watchReducedMotionAtom, worldToScreenAtom, zoomAnimationTargetAtom, zoomAtom, zoomFocusNodeIdAtom, zoomTransitionProgressAtom }; //# sourceMappingURL=index.mjs.map