"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/core/index.ts var index_exports = {}; __export(index_exports, { ActionCategory: () => ActionCategory, BUILT_IN_PRESETS: () => BUILT_IN_PRESETS, BuiltInActionId: () => BuiltInActionId, CanvasEventType: () => CanvasEventType, DEFAULT_GESTURE_RULES: () => DEFAULT_GESTURE_RULES, DEFAULT_MAPPINGS: () => DEFAULT_MAPPINGS, DEFAULT_PORT: () => DEFAULT_PORT, EDGE_ANIMATION_DURATION: () => EDGE_ANIMATION_DURATION, EVENT_TYPE_INFO: () => EVENT_TYPE_INFO, FallbackNodeTypeComponent: () => FallbackNodeTypeComponent, HIT_TARGET_SIZES: () => HIT_TARGET_SIZES, PASTE_OFFSET: () => PASTE_OFFSET, PluginError: () => PluginError, SNAPSHOT_VERSION: () => SNAPSHOT_VERSION, SpatialGrid: () => SpatialGrid, VIRTUALIZATION_BUFFER: () => VIRTUALIZATION_BUFFER, ZOOM_EXIT_THRESHOLD: () => ZOOM_EXIT_THRESHOLD, ZOOM_TRANSITION_THRESHOLD: () => ZOOM_TRANSITION_THRESHOLD, activePointersAtom: () => activePointersAtom, activePresetAtom: () => activePresetAtom, activePresetIdAtom: () => activePresetIdAtom, addGestureRuleAtom: () => addGestureRuleAtom, addNodeToLocalGraphAtom: () => addNodeToLocalGraphAtom, addNodesToSelectionAtom: () => addNodesToSelectionAtom, alignmentGuidesAtom: () => alignmentGuidesAtom, allPresetsAtom: () => allPresetsAtom, animateFitToBoundsAtom: () => animateFitToBoundsAtom, animateZoomToNodeAtom: () => animateZoomToNodeAtom, applyDelta: () => applyDelta, applyPresetAtom: () => applyPresetAtom, arePortsCompatible: () => arePortsCompatible, autoResizeGroupAtom: () => autoResizeGroupAtom, buildActionHelpers: () => buildActionHelpers, buildRuleIndex: () => buildRuleIndex, calculatePortPosition: () => calculatePortPosition, canPortAcceptConnection: () => canPortAcceptConnection, canRedoAtom: () => canRedoAtom, canUndoAtom: () => canUndoAtom, cancelSelectionAtom: () => cancelSelectionAtom, canvasMark: () => canvasMark, canvasSettingsAtom: () => canvasSettingsAtom, canvasToastAtom: () => canvasToastAtom, canvasWrap: () => canvasWrap, centerOnNodeAtom: () => centerOnNodeAtom, classifyPointer: () => classifyPointer, cleanupAllNodePositionsAtom: () => cleanupAllNodePositionsAtom, cleanupNodePositionAtom: () => cleanupNodePositionAtom, clearActions: () => clearActions, clearAlignmentGuidesAtom: () => clearAlignmentGuidesAtom, clearClipboardAtom: () => clearClipboardAtom, clearEdgeSelectionAtom: () => clearEdgeSelectionAtom, clearGraphOnSwitchAtom: () => clearGraphOnSwitchAtom, clearHistoryAtom: () => clearHistoryAtom, clearMutationQueueAtom: () => clearMutationQueueAtom, clearNodeTypeRegistry: () => clearNodeTypeRegistry, clearPlugins: () => clearPlugins, clearPointersAtom: () => clearPointersAtom, clearSearchAtom: () => clearSearchAtom, clearSelectionAtom: () => clearSelectionAtom, clipboardAtom: () => clipboardAtom, clipboardNodeCountAtom: () => clipboardNodeCountAtom, collapseGroupAtom: () => collapseGroupAtom, collapsedEdgeRemapAtom: () => collapsedEdgeRemapAtom, collapsedGroupsAtom: () => collapsedGroupsAtom, completeMutationAtom: () => completeMutationAtom, conditionalSnap: () => conditionalSnap, consumerGestureRulesAtom: () => consumerGestureRulesAtom, copyToClipboardAtom: () => copyToClipboardAtom, createActionContext: () => createActionContext, createActionContextFromReactEvent: () => createActionContextFromReactEvent, createActionContextFromTouchEvent: () => createActionContextFromTouchEvent, createCanvasAPI: () => createCanvasAPI, createSnapshot: () => createSnapshot, currentGraphIdAtom: () => currentGraphIdAtom, cutToClipboardAtom: () => cutToClipboardAtom, deletePresetAtom: () => deletePresetAtom, departingEdgesAtom: () => departingEdgesAtom, dequeueMutationAtom: () => dequeueMutationAtom, detectInputCapabilities: () => detectInputCapabilities, draggingNodeIdAtom: () => draggingNodeIdAtom, dropTargetNodeIdAtom: () => dropTargetNodeIdAtom, duplicateSelectionAtom: () => duplicateSelectionAtom, edgeCreationAtom: () => edgeCreationAtom, edgeFamilyAtom: () => edgeFamilyAtom, edgeKeysAtom: () => edgeKeysAtom, edgeKeysWithTempEdgeAtom: () => edgeKeysWithTempEdgeAtom, editingEdgeLabelAtom: () => editingEdgeLabelAtom, endNodeDragAtom: () => endNodeDragAtom, endSelectionAtom: () => endSelectionAtom, eventMappingsAtom: () => eventMappingsAtom, executeAction: () => executeAction, expandGroupAtom: () => expandGroupAtom, exportGraph: () => exportGraph, findAlignmentGuides: () => findAlignmentGuides, fingerCountAtom: () => fingerCountAtom, fitToBoundsAtom: () => fitToBoundsAtom, focusedNodeIdAtom: () => focusedNodeIdAtom, formatRuleLabel: () => formatRuleLabel, fuzzyMatch: () => fuzzyMatch, gestureRuleIndexAtom: () => gestureRuleIndexAtom, gestureRuleSettingsAtom: () => gestureRuleSettingsAtom, gestureRulesAtom: () => gestureRulesAtom, getAction: () => getAction, getActionForEvent: () => getActionForEvent, getActionsByCategories: () => getActionsByCategories, getActionsByCategory: () => getActionsByCategory, getAllActions: () => getAllActions, getAllPlugins: () => getAllPlugins, getGestureThresholds: () => getGestureThresholds, getHitTargetSize: () => getHitTargetSize, getNextQueuedMutationAtom: () => getNextQueuedMutationAtom, getNodeDescendants: () => getNodeDescendants, getNodePorts: () => getNodePorts, getNodeTypeComponent: () => getNodeTypeComponent, getPlugin: () => getPlugin, getPluginGestureContexts: () => getPluginGestureContexts, getPluginIds: () => getPluginIds, getRegisteredNodeTypes: () => getRegisteredNodeTypes, getSnapGuides: () => getSnapGuides, goToLockedPageAtom: () => goToLockedPageAtom, graphAtom: () => graphAtom, graphOptions: () => graphOptions, graphUpdateVersionAtom: () => graphUpdateVersionAtom, groupChildCountAtom: () => groupChildCountAtom, groupSelectedNodesAtom: () => groupSelectedNodesAtom, handleNodePointerDownSelectionAtom: () => handleNodePointerDownSelectionAtom, hasAction: () => hasAction, hasClipboardContentAtom: () => hasClipboardContentAtom, hasExternalKeyboardAtom: () => hasExternalKeyboardAtom, hasFocusedNodeAtom: () => hasFocusedNodeAtom, hasLockedNodeAtom: () => hasLockedNodeAtom, hasNodeTypeComponent: () => hasNodeTypeComponent, hasPlugin: () => hasPlugin, hasSelectionAtom: () => hasSelectionAtom, hasUnsavedChangesAtom: () => hasUnsavedChangesAtom, highestZIndexAtom: () => highestZIndexAtom, highlightedSearchIndexAtom: () => highlightedSearchIndexAtom, highlightedSearchNodeIdAtom: () => highlightedSearchNodeIdAtom, historyLabelsAtom: () => historyLabelsAtom, historyStateAtom: () => historyStateAtom, importGraph: () => importGraph, incrementRetryCountAtom: () => incrementRetryCountAtom, inputCapabilitiesAtom: () => inputCapabilitiesAtom, inputModeAtom: () => inputModeAtom, interactionFeedbackAtom: () => interactionFeedbackAtom, invertDelta: () => invertDelta, isFilterActiveAtom: () => isFilterActiveAtom, isGroupNodeAtom: () => isGroupNodeAtom, isMultiTouchAtom: () => isMultiTouchAtom, isNodeCollapsed: () => isNodeCollapsed, isOnlineAtom: () => isOnlineAtom, isPanelOpenAtom: () => isPanelOpenAtom, isPickNodeModeAtom: () => isPickNodeModeAtom, isPickingModeAtom: () => isPickingModeAtom, isSelectingAtom: () => isSelectingAtom, isSnappingActiveAtom: () => isSnappingActiveAtom, isStylusActiveAtom: () => isStylusActiveAtom, isTouchDeviceAtom: () => isTouchDeviceAtom, isZoomTransitioningAtom: () => isZoomTransitioningAtom, keyboardInteractionModeAtom: () => keyboardInteractionModeAtom, lastSyncErrorAtom: () => lastSyncErrorAtom, lastSyncTimeAtom: () => lastSyncTimeAtom, loadGraphFromDbAtom: () => loadGraphFromDbAtom, lockNodeAtom: () => lockNodeAtom, lockedNodeDataAtom: () => lockedNodeDataAtom, lockedNodeIdAtom: () => lockedNodeIdAtom, lockedNodePageCountAtom: () => lockedNodePageCountAtom, lockedNodePageIndexAtom: () => lockedNodePageIndexAtom, matchSpecificity: () => matchSpecificity, mergeNodesAtom: () => mergeNodesAtom, mergeRules: () => mergeRules, moveNodesToGroupAtom: () => moveNodesToGroupAtom, mutationQueueAtom: () => mutationQueueAtom, nestNodesOnDropAtom: () => nestNodesOnDropAtom, nextLockedPageAtom: () => nextLockedPageAtom, nextSearchResultAtom: () => nextSearchResultAtom, nodeChildrenAtom: () => nodeChildrenAtom, nodeFamilyAtom: () => nodeFamilyAtom, nodeKeysAtom: () => nodeKeysAtom, nodeParentAtom: () => nodeParentAtom, nodePositionAtomFamily: () => nodePositionAtomFamily, nodePositionUpdateCounterAtom: () => nodePositionUpdateCounterAtom, optimisticDeleteEdgeAtom: () => optimisticDeleteEdgeAtom, optimisticDeleteNodeAtom: () => optimisticDeleteNodeAtom, palmRejectionEnabledAtom: () => palmRejectionEnabledAtom, panAtom: () => panAtom, pasteFromClipboardAtom: () => pasteFromClipboardAtom, pendingInputResolverAtom: () => pendingInputResolverAtom, pendingMutationsCountAtom: () => pendingMutationsCountAtom, perfEnabledAtom: () => perfEnabledAtom, pointInPolygon: () => pointInPolygon, pointerDownAtom: () => pointerDownAtom, pointerUpAtom: () => pointerUpAtom, preDragNodeAttributesAtom: () => preDragNodeAttributesAtom, prefersReducedMotionAtom: () => prefersReducedMotionAtom, prevLockedPageAtom: () => prevLockedPageAtom, prevSearchResultAtom: () => prevSearchResultAtom, primaryInputSourceAtom: () => primaryInputSourceAtom, provideInputAtom: () => provideInputAtom, pushDeltaAtom: () => pushDeltaAtom, pushHistoryAtom: () => pushHistoryAtom, queueMutationAtom: () => queueMutationAtom, redoAtom: () => redoAtom, redoCountAtom: () => redoCountAtom, registerAction: () => registerAction, registerNodeType: () => registerNodeType, registerNodeTypes: () => registerNodeTypes, registerPlugin: () => registerPlugin, removeEdgeWithAnimationAtom: () => removeEdgeWithAnimationAtom, removeFromGroupAtom: () => removeFromGroupAtom, removeGestureRuleAtom: () => removeGestureRuleAtom, removeNodesFromSelectionAtom: () => removeNodesFromSelectionAtom, resetGestureRulesAtom: () => resetGestureRulesAtom, resetInputModeAtom: () => resetInputModeAtom, resetKeyboardInteractionModeAtom: () => resetKeyboardInteractionModeAtom, resetSettingsAtom: () => resetSettingsAtom, resetViewportAtom: () => resetViewportAtom, resolveGesture: () => resolveGesture, resolveGestureIndexed: () => resolveGestureIndexed, saveAsPresetAtom: () => saveAsPresetAtom, screenToWorldAtom: () => screenToWorldAtom, searchEdgeResultCountAtom: () => searchEdgeResultCountAtom, searchEdgeResultsAtom: () => searchEdgeResultsAtom, searchQueryAtom: () => searchQueryAtom, searchResultCountAtom: () => searchResultCountAtom, searchResultsArrayAtom: () => searchResultsArrayAtom, searchResultsAtom: () => searchResultsAtom, searchTotalResultCountAtom: () => searchTotalResultCountAtom, selectEdgeAtom: () => selectEdgeAtom, selectSingleNodeAtom: () => selectSingleNodeAtom, selectedEdgeIdAtom: () => selectedEdgeIdAtom, selectedNodeIdsAtom: () => selectedNodeIdsAtom, selectedNodesCountAtom: () => selectedNodesCountAtom, selectionPathAtom: () => selectionPathAtom, selectionRectAtom: () => selectionRectAtom, setEventMappingAtom: () => setEventMappingAtom, setFocusedNodeAtom: () => setFocusedNodeAtom, setGridSizeAtom: () => setGridSizeAtom, setKeyboardInteractionModeAtom: () => setKeyboardInteractionModeAtom, setNodeParentAtom: () => setNodeParentAtom, setOnlineStatusAtom: () => setOnlineStatusAtom, setPanelOpenAtom: () => setPanelOpenAtom, setPerfEnabled: () => setPerfEnabled, setSearchQueryAtom: () => setSearchQueryAtom, setVirtualizationEnabledAtom: () => setVirtualizationEnabledAtom, setZoomAtom: () => setZoomAtom, showToastAtom: () => showToastAtom, snapAlignmentEnabledAtom: () => snapAlignmentEnabledAtom, snapEnabledAtom: () => snapEnabledAtom, snapGridSizeAtom: () => snapGridSizeAtom, snapTemporaryDisableAtom: () => snapTemporaryDisableAtom, snapToGrid: () => snapToGrid, spatialIndexAtom: () => spatialIndexAtom, splitNodeAtom: () => splitNodeAtom, startMutationAtom: () => startMutationAtom, startNodeDragAtom: () => startNodeDragAtom, startPickNodeAtom: () => startPickNodeAtom, startPickNodesAtom: () => startPickNodesAtom, startPickPointAtom: () => startPickPointAtom, startSelectionAtom: () => startSelectionAtom, swapEdgeAtomicAtom: () => swapEdgeAtomicAtom, syncStateAtom: () => syncStateAtom, syncStatusAtom: () => syncStatusAtom, toggleAlignmentGuidesAtom: () => toggleAlignmentGuidesAtom, toggleGroupCollapseAtom: () => toggleGroupCollapseAtom, toggleNodeInSelectionAtom: () => toggleNodeInSelectionAtom, togglePanelAtom: () => togglePanelAtom, toggleSnapAtom: () => toggleSnapAtom, toggleVirtualizationAtom: () => toggleVirtualizationAtom, trackMutationErrorAtom: () => trackMutationErrorAtom, uiNodesAtom: () => uiNodesAtom, undoAtom: () => undoAtom, undoCountAtom: () => undoCountAtom, ungroupNodesAtom: () => ungroupNodesAtom, unlockNodeAtom: () => unlockNodeAtom, unregisterAction: () => unregisterAction, unregisterNodeType: () => unregisterNodeType, unregisterPlugin: () => unregisterPlugin, updateEdgeLabelAtom: () => updateEdgeLabelAtom, updateGestureRuleAtom: () => updateGestureRuleAtom, updateInteractionFeedbackAtom: () => updateInteractionFeedbackAtom, updateNodePositionAtom: () => updateNodePositionAtom, updatePresetAtom: () => updatePresetAtom, updateSelectionAtom: () => updateSelectionAtom, validateSnapshot: () => validateSnapshot, viewportRectAtom: () => viewportRectAtom, virtualizationEnabledAtom: () => virtualizationEnabledAtom, virtualizationMetricsAtom: () => virtualizationMetricsAtom, visibleBoundsAtom: () => visibleBoundsAtom, visibleEdgeKeysAtom: () => visibleEdgeKeysAtom, visibleNodeKeysAtom: () => visibleNodeKeysAtom, watchExternalKeyboardAtom: () => watchExternalKeyboardAtom, watchReducedMotionAtom: () => watchReducedMotionAtom, worldToScreenAtom: () => worldToScreenAtom, zoomAnimationTargetAtom: () => zoomAnimationTargetAtom, zoomAtom: () => zoomAtom, zoomFocusNodeIdAtom: () => zoomFocusNodeIdAtom, zoomTransitionProgressAtom: () => zoomTransitionProgressAtom }); module.exports = __toCommonJS(index_exports); // src/core/graph-store.ts var import_jotai = require("jotai"); var import_graphology = __toESM(require("graphology")); var graphOptions = { type: "directed", multi: true, allowSelfLoops: true }; var currentGraphIdAtom = (0, import_jotai.atom)(null); var graphAtom = (0, import_jotai.atom)(new import_graphology.default(graphOptions)); var graphUpdateVersionAtom = (0, import_jotai.atom)(0); var edgeCreationAtom = (0, import_jotai.atom)({ isCreating: false, sourceNodeId: null, sourceNodePosition: null, targetPosition: null, hoveredTargetNodeId: null, sourceHandle: null, targetHandle: null, sourcePort: null, targetPort: null, snappedTargetPosition: null }); var draggingNodeIdAtom = (0, import_jotai.atom)(null); var preDragNodeAttributesAtom = (0, import_jotai.atom)(null); // src/core/graph-position.ts var import_jotai3 = require("jotai"); var import_jotai_family = require("jotai-family"); var import_graphology2 = __toESM(require("graphology")); // src/utils/debug.ts var import_debug = __toESM(require("debug")); var NAMESPACE = "canvas"; function createDebug(module2) { const base = (0, import_debug.default)(`${NAMESPACE}:${module2}`); const warn = (0, import_debug.default)(`${NAMESPACE}:${module2}:warn`); const error = (0, import_debug.default)(`${NAMESPACE}:${module2}: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 var import_jotai2 = require("jotai"); var perfEnabledAtom = (0, import_jotai2.atom)(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 = (0, import_jotai3.atom)(0); var nodePositionAtomFamily = (0, import_jotai_family.atomFamily)((nodeId) => (0, import_jotai3.atom)((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 = (0, import_jotai3.atom)(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 = (0, import_jotai3.atom)(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 = (0, import_jotai3.atom)(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 = (0, import_jotai3.atom)(null, (get, set) => { debug2("Clearing graph for switch"); set(cleanupAllNodePositionsAtom); clearAllPendingMutations(); const emptyGraph = new import_graphology2.default(graphOptions); set(graphAtom, emptyGraph); set(graphUpdateVersionAtom, (v) => v + 1); }); // src/core/graph-derived.ts var import_jotai8 = require("jotai"); var import_jotai_family2 = require("jotai-family"); // src/core/viewport-store.ts var import_jotai5 = require("jotai"); // src/core/selection-store.ts var import_jotai4 = require("jotai"); var debug3 = createDebug("selection"); var selectedNodeIdsAtom = (0, import_jotai4.atom)(/* @__PURE__ */ new Set()); var selectedEdgeIdAtom = (0, import_jotai4.atom)(null); var handleNodePointerDownSelectionAtom = (0, import_jotai4.atom)(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 = (0, import_jotai4.atom)(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 = (0, import_jotai4.atom)(null, (get, set, nodeId) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); if (newSelection.has(nodeId)) { newSelection.delete(nodeId); } else { newSelection.add(nodeId); } set(selectedNodeIdsAtom, newSelection); }); var clearSelectionAtom = (0, import_jotai4.atom)(null, (_get, set) => { debug3("clearSelection"); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); }); var addNodesToSelectionAtom = (0, import_jotai4.atom)(null, (get, set, nodeIds) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); for (const nodeId of nodeIds) { newSelection.add(nodeId); } set(selectedNodeIdsAtom, newSelection); }); var removeNodesFromSelectionAtom = (0, import_jotai4.atom)(null, (get, set, nodeIds) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); for (const nodeId of nodeIds) { newSelection.delete(nodeId); } set(selectedNodeIdsAtom, newSelection); }); var selectEdgeAtom = (0, import_jotai4.atom)(null, (get, set, edgeId) => { set(selectedEdgeIdAtom, edgeId); if (edgeId !== null) { set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); } }); var clearEdgeSelectionAtom = (0, import_jotai4.atom)(null, (_get, set) => { set(selectedEdgeIdAtom, null); }); var focusedNodeIdAtom = (0, import_jotai4.atom)(null); var setFocusedNodeAtom = (0, import_jotai4.atom)(null, (_get, set, nodeId) => { set(focusedNodeIdAtom, nodeId); }); var hasFocusedNodeAtom = (0, import_jotai4.atom)((get) => get(focusedNodeIdAtom) !== null); var selectedNodesCountAtom = (0, import_jotai4.atom)((get) => get(selectedNodeIdsAtom).size); var hasSelectionAtom = (0, import_jotai4.atom)((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 = (0, import_jotai5.atom)(1); var panAtom = (0, import_jotai5.atom)({ x: 0, y: 0 }); var viewportRectAtom = (0, import_jotai5.atom)(null); var screenToWorldAtom = (0, import_jotai5.atom)((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 = (0, import_jotai5.atom)((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 = (0, import_jotai5.atom)(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 = (0, import_jotai5.atom)(null, (_get, set) => { set(zoomAtom, 1); set(panAtom, { x: 0, y: 0 }); }); var fitToBoundsAtom = (0, import_jotai5.atom)(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 = (0, import_jotai5.atom)(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 = (0, import_jotai5.atom)(null); var zoomTransitionProgressAtom = (0, import_jotai5.atom)(0); var isZoomTransitioningAtom = (0, import_jotai5.atom)((get) => { const progress = get(zoomTransitionProgressAtom); return progress > 0 && progress < 1; }); var zoomAnimationTargetAtom = (0, import_jotai5.atom)(null); var animateZoomToNodeAtom = (0, import_jotai5.atom)(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 = (0, import_jotai5.atom)(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 var import_jotai7 = require("jotai"); // src/core/history-store.ts var import_jotai6 = require("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 = (0, import_jotai6.atom)({ past: [], future: [], isApplying: false }); var canUndoAtom = (0, import_jotai6.atom)((get) => { const history = get(historyStateAtom); return history.past.length > 0 && !history.isApplying; }); var canRedoAtom = (0, import_jotai6.atom)((get) => { const history = get(historyStateAtom); return history.future.length > 0 && !history.isApplying; }); var undoCountAtom = (0, import_jotai6.atom)((get) => get(historyStateAtom).past.length); var redoCountAtom = (0, import_jotai6.atom)((get) => get(historyStateAtom).future.length); var pushDeltaAtom = (0, import_jotai6.atom)(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 = (0, import_jotai6.atom)(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 = (0, import_jotai6.atom)(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 = (0, import_jotai6.atom)(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 = (0, import_jotai6.atom)(null, (_get, set) => { set(historyStateAtom, { past: [], future: [], isApplying: false }); debug4("History cleared"); }); var historyLabelsAtom = (0, import_jotai6.atom)((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 = (0, import_jotai7.atom)(/* @__PURE__ */ new Set()); var toggleGroupCollapseAtom = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)((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 = (0, import_jotai7.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return (nodeId) => { if (!graph.hasNode(nodeId)) return void 0; return graph.getNodeAttribute(nodeId, "parentId"); }; }); var isGroupNodeAtom = (0, import_jotai7.atom)((get) => { const getChildren = get(nodeChildrenAtom); return (nodeId) => getChildren(nodeId).length > 0; }); var groupChildCountAtom = (0, import_jotai7.atom)((get) => { const getChildren = get(nodeChildrenAtom); return (groupId) => getChildren(groupId).length; }); var setNodeParentAtom = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)(null, (get, set, { nodeIds, groupId }) => { for (const nodeId of nodeIds) { set(setNodeParentAtom, { nodeId, parentId: groupId }); } }); var removeFromGroupAtom = (0, import_jotai7.atom)(null, (get, set, nodeId) => { set(setNodeParentAtom, { nodeId, parentId: void 0 }); }); var groupSelectedNodesAtom = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)(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 = (0, import_jotai7.atom)((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 = (0, import_jotai7.atom)(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 = (0, import_jotai8.atom)((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 = (0, import_jotai8.atom)((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 = (0, import_jotai8.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return graph.nodes(); }); var nodeFamilyAtom = (0, import_jotai_family2.atomFamily)((nodeId) => (0, import_jotai8.atom)((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 = (0, import_jotai8.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return graph.edges(); }); var edgeKeysWithTempEdgeAtom = (0, import_jotai8.atom)((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 = (0, import_jotai_family2.atomFamily)((key) => (0, import_jotai8.atom)((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 var import_jotai12 = require("jotai"); var import_graphology3 = __toESM(require("graphology")); // src/core/graph-mutations-edges.ts var import_jotai10 = require("jotai"); // src/core/reduced-motion-store.ts var import_jotai9 = require("jotai"); var prefersReducedMotionAtom = (0, import_jotai9.atom)(typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false); var watchReducedMotionAtom = (0, import_jotai9.atom)(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 = (0, import_jotai10.atom)(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 = (0, import_jotai10.atom)(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 = (0, import_jotai10.atom)(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 = (0, import_jotai10.atom)(/* @__PURE__ */ new Map()); var EDGE_ANIMATION_DURATION = 300; var removeEdgeWithAnimationAtom = (0, import_jotai10.atom)(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 = (0, import_jotai10.atom)(null); var updateEdgeLabelAtom = (0, import_jotai10.atom)(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 var import_jotai11 = require("jotai"); var debug6 = createDebug("graph:mutations:advanced"); var dropTargetNodeIdAtom = (0, import_jotai11.atom)(null); var splitNodeAtom = (0, import_jotai11.atom)(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 = (0, import_jotai11.atom)(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 = (0, import_jotai12.atom)(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 = (0, import_jotai12.atom)(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 = (0, import_jotai12.atom)(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 = (0, import_jotai12.atom)(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 = (0, import_jotai12.atom)(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 = (0, import_jotai12.atom)(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 import_graphology3.default(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 var import_jotai13 = require("jotai"); var debug8 = createDebug("sync"); var syncStatusAtom = (0, import_jotai13.atom)("synced"); var pendingMutationsCountAtom = (0, import_jotai13.atom)(0); var isOnlineAtom = (0, import_jotai13.atom)(typeof navigator !== "undefined" ? navigator.onLine : true); var lastSyncErrorAtom = (0, import_jotai13.atom)(null); var lastSyncTimeAtom = (0, import_jotai13.atom)(Date.now()); var mutationQueueAtom = (0, import_jotai13.atom)([]); var syncStateAtom = (0, import_jotai13.atom)((get) => ({ status: get(syncStatusAtom), pendingMutations: get(pendingMutationsCountAtom), lastError: get(lastSyncErrorAtom), lastSyncTime: get(lastSyncTimeAtom), isOnline: get(isOnlineAtom), queuedMutations: get(mutationQueueAtom).length })); var startMutationAtom = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(null, (_get, set, error) => { set(lastSyncErrorAtom, error); debug8("Mutation failed: %s", error); }); var setOnlineStatusAtom = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)(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 = (0, import_jotai13.atom)((get) => { const queue = get(mutationQueueAtom); return queue.find((m) => m.retryCount < m.maxRetries) ?? null; }); var clearMutationQueueAtom = (0, import_jotai13.atom)(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 var import_jotai14 = require("jotai"); var inputModeAtom = (0, import_jotai14.atom)({ type: "normal" }); var keyboardInteractionModeAtom = (0, import_jotai14.atom)("navigate"); var interactionFeedbackAtom = (0, import_jotai14.atom)(null); var pendingInputResolverAtom = (0, import_jotai14.atom)(null); var resetInputModeAtom = (0, import_jotai14.atom)(null, (_get, set) => { set(inputModeAtom, { type: "normal" }); set(interactionFeedbackAtom, null); set(pendingInputResolverAtom, null); }); var resetKeyboardInteractionModeAtom = (0, import_jotai14.atom)(null, (_get, set) => { set(keyboardInteractionModeAtom, "navigate"); }); var setKeyboardInteractionModeAtom = (0, import_jotai14.atom)(null, (_get, set, mode) => { set(keyboardInteractionModeAtom, mode); }); var startPickNodeAtom = (0, import_jotai14.atom)(null, (_get, set, options) => { set(inputModeAtom, { type: "pickNode", ...options }); }); var startPickNodesAtom = (0, import_jotai14.atom)(null, (_get, set, options) => { set(inputModeAtom, { type: "pickNodes", ...options }); }); var startPickPointAtom = (0, import_jotai14.atom)(null, (_get, set, options) => { set(inputModeAtom, { type: "pickPoint", ...options }); }); var provideInputAtom = (0, import_jotai14.atom)(null, (get, set, value) => { set(pendingInputResolverAtom, value); }); var updateInteractionFeedbackAtom = (0, import_jotai14.atom)(null, (get, set, feedback) => { const current = get(interactionFeedbackAtom); set(interactionFeedbackAtom, { ...current, ...feedback }); }); var isPickingModeAtom = (0, import_jotai14.atom)((get) => { const mode = get(inputModeAtom); return mode.type !== "normal"; }); var isPickNodeModeAtom = (0, import_jotai14.atom)((get) => { const mode = get(inputModeAtom); return mode.type === "pickNode" || mode.type === "pickNodes"; }); // src/core/locked-node-store.ts var import_jotai15 = require("jotai"); var lockedNodeIdAtom = (0, import_jotai15.atom)(null); var lockedNodeDataAtom = (0, import_jotai15.atom)((get) => { const id = get(lockedNodeIdAtom); if (!id) return null; const nodes = get(uiNodesAtom); return nodes.find((n) => n.id === id) || null; }); var lockedNodePageIndexAtom = (0, import_jotai15.atom)(0); var lockedNodePageCountAtom = (0, import_jotai15.atom)(1); var lockNodeAtom = (0, import_jotai15.atom)(null, (_get, set, payload) => { set(lockedNodeIdAtom, payload.nodeId); set(lockedNodePageIndexAtom, 0); }); var unlockNodeAtom = (0, import_jotai15.atom)(null, (_get, set) => { set(lockedNodeIdAtom, null); }); var nextLockedPageAtom = (0, import_jotai15.atom)(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current + 1) % pageCount); }); var prevLockedPageAtom = (0, import_jotai15.atom)(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current - 1 + pageCount) % pageCount); }); var goToLockedPageAtom = (0, import_jotai15.atom)(null, (get, set, index) => { const pageCount = get(lockedNodePageCountAtom); if (index >= 0 && index < pageCount) { set(lockedNodePageIndexAtom, index); } }); var hasLockedNodeAtom = (0, import_jotai15.atom)((get) => get(lockedNodeIdAtom) !== null); // src/core/node-type-registry.tsx var import_compiler_runtime = require("react/compiler-runtime"); var import_react = __toESM(require("react")); var import_jsx_runtime = require("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 $ = (0, import_compiler_runtime.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__ */ (0, import_jsx_runtime.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__ */ (0, import_jsx_runtime.jsx)("div", { style: t4, children: t5 }); $[6] = t5; $[7] = t6; } else { t6 = $[7]; } let t7; if ($[8] !== t3 || $[9] !== t6) { t7 = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: t1, children: [t3, t6] }); $[8] = t3; $[9] = t6; $[10] = t7; } else { t7 = $[10]; } return t7; }; // src/core/toast-store.ts var import_jotai16 = require("jotai"); var canvasToastAtom = (0, import_jotai16.atom)(null); var showToastAtom = (0, import_jotai16.atom)(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 var import_jotai17 = require("jotai"); var snapEnabledAtom = (0, import_jotai17.atom)(false); var snapGridSizeAtom = (0, import_jotai17.atom)(20); var snapTemporaryDisableAtom = (0, import_jotai17.atom)(false); var isSnappingActiveAtom = (0, import_jotai17.atom)((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 = (0, import_jotai17.atom)(null, (get, set) => { set(snapEnabledAtom, !get(snapEnabledAtom)); }); var setGridSizeAtom = (0, import_jotai17.atom)(null, (_get, set, size) => { set(snapGridSizeAtom, Math.max(5, Math.min(200, size))); }); var snapAlignmentEnabledAtom = (0, import_jotai17.atom)(true); var toggleAlignmentGuidesAtom = (0, import_jotai17.atom)(null, (get, set) => { set(snapAlignmentEnabledAtom, !get(snapAlignmentEnabledAtom)); }); var alignmentGuidesAtom = (0, import_jotai17.atom)({ verticalGuides: [], horizontalGuides: [] }); var clearAlignmentGuidesAtom = (0, import_jotai17.atom)(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 var import_jotai18 = require("jotai"); var import_utils = require("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 = (0, import_utils.atomWithStorage)("@blinksgg/canvas/settings", DEFAULT_STATE); var eventMappingsAtom = (0, import_jotai18.atom)((get) => get(canvasSettingsAtom).mappings); var activePresetIdAtom = (0, import_jotai18.atom)((get) => get(canvasSettingsAtom).activePresetId); var allPresetsAtom = (0, import_jotai18.atom)((get) => { const state = get(canvasSettingsAtom); return [...BUILT_IN_PRESETS, ...state.customPresets]; }); var activePresetAtom = (0, import_jotai18.atom)((get) => { const presetId = get(activePresetIdAtom); if (!presetId) return null; const allPresets = get(allPresetsAtom); return allPresets.find((p) => p.id === presetId) || null; }); var isPanelOpenAtom = (0, import_jotai18.atom)((get) => get(canvasSettingsAtom).isPanelOpen); var virtualizationEnabledAtom = (0, import_jotai18.atom)((get) => get(canvasSettingsAtom).virtualizationEnabled ?? true); var hasUnsavedChangesAtom = (0, import_jotai18.atom)((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 = (0, import_jotai18.atom)(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 = (0, import_jotai18.atom)(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 = (0, import_jotai18.atom)(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 = (0, import_jotai18.atom)(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 = (0, import_jotai18.atom)(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 = (0, import_jotai18.atom)(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: DEFAULT_MAPPINGS, activePresetId: "default" }); }); var togglePanelAtom = (0, import_jotai18.atom)(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: !current.isPanelOpen }); }); var setPanelOpenAtom = (0, import_jotai18.atom)(null, (get, set, isOpen) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: isOpen }); }); var setVirtualizationEnabledAtom = (0, import_jotai18.atom)(null, (get, set, enabled) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: enabled }); }); var toggleVirtualizationAtom = (0, import_jotai18.atom)(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: !(current.virtualizationEnabled ?? true) }); }); // src/core/canvas-serializer.ts var import_graphology4 = __toESM(require("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 import_graphology4.default(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 var import_jotai19 = require("jotai"); var debug11 = createDebug("clipboard"); var PASTE_OFFSET = { x: 50, y: 50 }; var clipboardAtom = (0, import_jotai19.atom)(null); var hasClipboardContentAtom = (0, import_jotai19.atom)((get) => get(clipboardAtom) !== null); var clipboardNodeCountAtom = (0, import_jotai19.atom)((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 = (0, import_jotai19.atom)(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 = (0, import_jotai19.atom)(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 = (0, import_jotai19.atom)(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 = (0, import_jotai19.atom)(null, (get, set) => { set(copyToClipboardAtom); return set(pasteFromClipboardAtom); }); var clearClipboardAtom = (0, import_jotai19.atom)(null, (_get, set) => { set(clipboardAtom, null); debug11("Clipboard cleared"); }); // src/core/virtualization-store.ts var import_jotai20 = require("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 = (0, import_jotai20.atom)((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 = (0, import_jotai20.atom)((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 = (0, import_jotai20.atom)((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 = (0, import_jotai20.atom)((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 = (0, import_jotai20.atom)((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 var import_jotai21 = require("jotai"); var activePointersAtom = (0, import_jotai21.atom)(/* @__PURE__ */ new Map()); var primaryInputSourceAtom = (0, import_jotai21.atom)("mouse"); var inputCapabilitiesAtom = (0, import_jotai21.atom)(detectInputCapabilities()); var isStylusActiveAtom = (0, import_jotai21.atom)((get) => { const pointers = get(activePointersAtom); for (const [, pointer] of pointers) { if (pointer.source === "pencil") return true; } return false; }); var isMultiTouchAtom = (0, import_jotai21.atom)((get) => { const pointers = get(activePointersAtom); let fingerCount = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") fingerCount++; } return fingerCount > 1; }); var fingerCountAtom = (0, import_jotai21.atom)((get) => { const pointers = get(activePointersAtom); let count = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") count++; } return count; }); var isTouchDeviceAtom = (0, import_jotai21.atom)((get) => { const caps = get(inputCapabilitiesAtom); return caps.hasTouch; }); var pointerDownAtom = (0, import_jotai21.atom)(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 = (0, import_jotai21.atom)(null, (get, set, pointerId) => { const pointers = new Map(get(activePointersAtom)); pointers.delete(pointerId); set(activePointersAtom, pointers); }); var clearPointersAtom = (0, import_jotai21.atom)(null, (_get, set) => { set(activePointersAtom, /* @__PURE__ */ new Map()); }); // src/core/selection-path-store.ts var import_jotai22 = require("jotai"); var selectionPathAtom = (0, import_jotai22.atom)(null); var isSelectingAtom = (0, import_jotai22.atom)((get) => get(selectionPathAtom) !== null); var startSelectionAtom = (0, import_jotai22.atom)(null, (_get, set, { type, point }) => { set(selectionPathAtom, { type, points: [point] }); }); var updateSelectionAtom = (0, import_jotai22.atom)(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 = (0, import_jotai22.atom)(null, (_get, set) => { set(selectionPathAtom, null); }); var endSelectionAtom = (0, import_jotai22.atom)(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 = (0, import_jotai22.atom)((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 var import_jotai23 = require("jotai"); var searchQueryAtom = (0, import_jotai23.atom)(""); var setSearchQueryAtom = (0, import_jotai23.atom)(null, (_get, set, query) => { set(searchQueryAtom, query); set(highlightedSearchIndexAtom, 0); }); var clearSearchAtom = (0, import_jotai23.atom)(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 = (0, import_jotai23.atom)((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 = (0, import_jotai23.atom)((get) => { return Array.from(get(searchResultsAtom)); }); var searchResultCountAtom = (0, import_jotai23.atom)((get) => { return get(searchResultsAtom).size; }); var searchEdgeResultsAtom = (0, import_jotai23.atom)((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 = (0, import_jotai23.atom)((get) => { return get(searchEdgeResultsAtom).size; }); var isFilterActiveAtom = (0, import_jotai23.atom)((get) => { return get(searchQueryAtom).trim().length > 0; }); var searchTotalResultCountAtom = (0, import_jotai23.atom)((get) => { return get(searchResultCountAtom) + get(searchEdgeResultCountAtom); }); var highlightedSearchIndexAtom = (0, import_jotai23.atom)(0); var nextSearchResultAtom = (0, import_jotai23.atom)(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 = (0, import_jotai23.atom)(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 = (0, import_jotai23.atom)((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 var import_jotai24 = require("jotai"); var import_utils2 = require("jotai/utils"); var DEFAULT_RULE_STATE = { customRules: [], palmRejection: true }; var gestureRuleSettingsAtom = (0, import_utils2.atomWithStorage)("canvas-gesture-rules", DEFAULT_RULE_STATE); var consumerGestureRulesAtom = (0, import_jotai24.atom)([]); var gestureRulesAtom = (0, import_jotai24.atom)((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 = (0, import_jotai24.atom)((get) => { return buildRuleIndex(get(gestureRulesAtom)); }); var palmRejectionEnabledAtom = (0, import_jotai24.atom)((get) => get(gestureRuleSettingsAtom).palmRejection, (get, set, enabled) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, palmRejection: enabled }); }); var addGestureRuleAtom = (0, import_jotai24.atom)(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 = (0, import_jotai24.atom)(null, (get, set, ruleId) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: current.customRules.filter((r) => r.id !== ruleId) }); }); var updateGestureRuleAtom = (0, import_jotai24.atom)(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 = (0, import_jotai24.atom)(null, (get, set) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: [] }); }); // src/core/external-keyboard-store.ts var import_jotai25 = require("jotai"); var hasExternalKeyboardAtom = (0, import_jotai25.atom)(false); var watchExternalKeyboardAtom = (0, import_jotai25.atom)(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 }; } //# sourceMappingURL=index.js.map