var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/commands/registry.ts function registerCommand(command) { commandRegistry.register(command); } var CommandRegistry, commandRegistry; var init_registry = __esm({ "src/commands/registry.ts"() { "use strict"; 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 })) })); } }; commandRegistry = new CommandRegistry(); } }); // src/core/graph-store.ts var graph_store_exports = {}; __export(graph_store_exports, { currentGraphIdAtom: () => currentGraphIdAtom, draggingNodeIdAtom: () => draggingNodeIdAtom, edgeCreationAtom: () => edgeCreationAtom, graphAtom: () => graphAtom, graphOptions: () => graphOptions, graphUpdateVersionAtom: () => graphUpdateVersionAtom, preDragNodeAttributesAtom: () => preDragNodeAttributesAtom }); import { atom as atom3 } from "jotai"; import Graph from "graphology"; var graphOptions, currentGraphIdAtom, graphAtom, graphUpdateVersionAtom, edgeCreationAtom, draggingNodeIdAtom, preDragNodeAttributesAtom; var init_graph_store = __esm({ "src/core/graph-store.ts"() { "use strict"; graphOptions = { type: "directed", multi: true, allowSelfLoops: true }; currentGraphIdAtom = atom3(null); graphAtom = atom3(new Graph(graphOptions)); graphUpdateVersionAtom = atom3(0); edgeCreationAtom = atom3({ isCreating: false, sourceNodeId: null, sourceNodePosition: null, targetPosition: null, hoveredTargetNodeId: null, sourceHandle: null, targetHandle: null, sourcePort: null, targetPort: null, snappedTargetPosition: null }); draggingNodeIdAtom = atom3(null); preDragNodeAttributesAtom = atom3(null); } }); // src/utils/debug.ts import debugFactory from "debug"; function createDebug(module) { const base = debugFactory(`${NAMESPACE}:${module}`); const warn = debugFactory(`${NAMESPACE}:${module}:warn`); const error = debugFactory(`${NAMESPACE}:${module}:error`); warn.enabled = true; error.enabled = true; warn.log = console.warn.bind(console); error.log = console.error.bind(console); const debugFn = Object.assign(base, { warn, error }); return debugFn; } var NAMESPACE, debug; var init_debug = __esm({ "src/utils/debug.ts"() { "use strict"; NAMESPACE = "canvas"; debug = { graph: { node: createDebug("graph:node"), edge: createDebug("graph:edge"), sync: createDebug("graph:sync") }, ui: { selection: createDebug("ui:selection"), drag: createDebug("ui:drag"), resize: createDebug("ui:resize") }, sync: { status: createDebug("sync:status"), mutations: createDebug("sync:mutations"), queue: createDebug("sync:queue") }, viewport: createDebug("viewport") }; } }); // src/core/selection-store.ts var selection_store_exports = {}; __export(selection_store_exports, { addNodesToSelectionAtom: () => addNodesToSelectionAtom, clearEdgeSelectionAtom: () => clearEdgeSelectionAtom, clearSelectionAtom: () => clearSelectionAtom, focusedNodeIdAtom: () => focusedNodeIdAtom, handleNodePointerDownSelectionAtom: () => handleNodePointerDownSelectionAtom, hasFocusedNodeAtom: () => hasFocusedNodeAtom, hasSelectionAtom: () => hasSelectionAtom, removeNodesFromSelectionAtom: () => removeNodesFromSelectionAtom, selectEdgeAtom: () => selectEdgeAtom, selectSingleNodeAtom: () => selectSingleNodeAtom, selectedEdgeIdAtom: () => selectedEdgeIdAtom, selectedNodeIdsAtom: () => selectedNodeIdsAtom, selectedNodesCountAtom: () => selectedNodesCountAtom, setFocusedNodeAtom: () => setFocusedNodeAtom, toggleNodeInSelectionAtom: () => toggleNodeInSelectionAtom }); import { atom as atom4 } from "jotai"; var debug2, selectedNodeIdsAtom, selectedEdgeIdAtom, handleNodePointerDownSelectionAtom, selectSingleNodeAtom, toggleNodeInSelectionAtom, clearSelectionAtom, addNodesToSelectionAtom, removeNodesFromSelectionAtom, selectEdgeAtom, clearEdgeSelectionAtom, focusedNodeIdAtom, setFocusedNodeAtom, hasFocusedNodeAtom, selectedNodesCountAtom, hasSelectionAtom; var init_selection_store = __esm({ "src/core/selection-store.ts"() { "use strict"; init_debug(); debug2 = createDebug("selection"); selectedNodeIdsAtom = atom4(/* @__PURE__ */ new Set()); selectedEdgeIdAtom = atom4(null); handleNodePointerDownSelectionAtom = atom4(null, (get, set, { nodeId, isShiftPressed }) => { const currentSelection = get(selectedNodeIdsAtom); debug2("handleNodePointerDownSelection: nodeId=%s, shift=%s, current=%o", nodeId, isShiftPressed, Array.from(currentSelection)); set(selectedEdgeIdAtom, null); if (isShiftPressed) { const newSelection = new Set(currentSelection); if (newSelection.has(nodeId)) { newSelection.delete(nodeId); } else { newSelection.add(nodeId); } debug2("Shift-click, setting selection to: %o", Array.from(newSelection)); set(selectedNodeIdsAtom, newSelection); } else { if (!currentSelection.has(nodeId)) { debug2("Node not in selection, selecting: %s", nodeId); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set([nodeId])); } else { debug2("Node already selected, preserving multi-select"); } } }); selectSingleNodeAtom = atom4(null, (get, set, nodeId) => { debug2("selectSingleNode: %s", nodeId); set(selectedEdgeIdAtom, null); if (nodeId === null || nodeId === void 0) { debug2("Clearing selection"); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); } else { const currentSelection = get(selectedNodeIdsAtom); if (currentSelection.has(nodeId) && currentSelection.size === 1) { return; } set(selectedNodeIdsAtom, /* @__PURE__ */ new Set([nodeId])); } }); toggleNodeInSelectionAtom = atom4(null, (get, set, nodeId) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); if (newSelection.has(nodeId)) { newSelection.delete(nodeId); } else { newSelection.add(nodeId); } set(selectedNodeIdsAtom, newSelection); }); clearSelectionAtom = atom4(null, (_get, set) => { debug2("clearSelection"); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); }); addNodesToSelectionAtom = atom4(null, (get, set, nodeIds) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); for (const nodeId of nodeIds) { newSelection.add(nodeId); } set(selectedNodeIdsAtom, newSelection); }); removeNodesFromSelectionAtom = atom4(null, (get, set, nodeIds) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); for (const nodeId of nodeIds) { newSelection.delete(nodeId); } set(selectedNodeIdsAtom, newSelection); }); selectEdgeAtom = atom4(null, (get, set, edgeId) => { set(selectedEdgeIdAtom, edgeId); if (edgeId !== null) { set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); } }); clearEdgeSelectionAtom = atom4(null, (_get, set) => { set(selectedEdgeIdAtom, null); }); focusedNodeIdAtom = atom4(null); setFocusedNodeAtom = atom4(null, (_get, set, nodeId) => { set(focusedNodeIdAtom, nodeId); }); hasFocusedNodeAtom = atom4((get) => get(focusedNodeIdAtom) !== null); selectedNodesCountAtom = atom4((get) => get(selectedNodeIdsAtom).size); hasSelectionAtom = atom4((get) => get(selectedNodeIdsAtom).size > 0); } }); // src/utils/mutation-queue.ts function clearAllPendingMutations() { pendingNodeMutations.clear(); } var pendingNodeMutations; var init_mutation_queue = __esm({ "src/utils/mutation-queue.ts"() { "use strict"; pendingNodeMutations = /* @__PURE__ */ new Map(); } }); // src/core/perf.ts import { atom as atom5 } from "jotai"; function setPerfEnabled(enabled) { _enabled = enabled; } 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(); } } var perfEnabledAtom, _enabled; var init_perf = __esm({ "src/core/perf.ts"() { "use strict"; perfEnabledAtom = atom5(false); _enabled = false; if (typeof window !== "undefined") { window.__canvasPerf = setPerfEnabled; } } }); // src/core/graph-position.ts import { atom as atom6 } from "jotai"; import { atomFamily } from "jotai-family"; import Graph2 from "graphology"; function getPositionCache(graph) { let cache = _positionCacheByGraph.get(graph); if (!cache) { cache = /* @__PURE__ */ new Map(); _positionCacheByGraph.set(graph, cache); } return cache; } var debug3, _positionCacheByGraph, nodePositionUpdateCounterAtom, nodePositionAtomFamily, updateNodePositionAtom, cleanupNodePositionAtom, cleanupAllNodePositionsAtom, clearGraphOnSwitchAtom; var init_graph_position = __esm({ "src/core/graph-position.ts"() { "use strict"; init_graph_store(); init_debug(); init_mutation_queue(); init_perf(); debug3 = createDebug("graph:position"); _positionCacheByGraph = /* @__PURE__ */ new WeakMap(); nodePositionUpdateCounterAtom = atom6(0); nodePositionAtomFamily = atomFamily((nodeId) => atom6((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; })); updateNodePositionAtom = atom6(null, (get, set, { nodeId, position }) => { const end = canvasMark("drag-frame"); const graph = get(graphAtom); if (graph.hasNode(nodeId)) { debug3("Updating node %s position to %o", nodeId, position); graph.setNodeAttribute(nodeId, "x", position.x); graph.setNodeAttribute(nodeId, "y", position.y); set(nodePositionUpdateCounterAtom, (c) => c + 1); } end(); }); cleanupNodePositionAtom = atom6(null, (get, _set, nodeId) => { nodePositionAtomFamily.remove(nodeId); const graph = get(graphAtom); getPositionCache(graph).delete(nodeId); debug3("Removed position atom for node: %s", nodeId); }); cleanupAllNodePositionsAtom = atom6(null, (get, _set) => { const graph = get(graphAtom); const nodeIds = graph.nodes(); nodeIds.forEach((nodeId) => { nodePositionAtomFamily.remove(nodeId); }); _positionCacheByGraph.delete(graph); debug3("Removed %d position atoms", nodeIds.length); }); clearGraphOnSwitchAtom = atom6(null, (get, set) => { debug3("Clearing graph for switch"); set(cleanupAllNodePositionsAtom); clearAllPendingMutations(); const emptyGraph = new Graph2(graphOptions); set(graphAtom, emptyGraph); set(graphUpdateVersionAtom, (v) => v + 1); }); } }); // src/core/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 }; } var init_history_actions = __esm({ "src/core/history-actions.ts"() { "use strict"; } }); // src/core/history-store.ts var history_store_exports = {}; __export(history_store_exports, { applyDelta: () => applyDelta, canRedoAtom: () => canRedoAtom, canUndoAtom: () => canUndoAtom, clearHistoryAtom: () => clearHistoryAtom, createSnapshot: () => createSnapshot, historyLabelsAtom: () => historyLabelsAtom, historyStateAtom: () => historyStateAtom, invertDelta: () => invertDelta, pushDeltaAtom: () => pushDeltaAtom, pushHistoryAtom: () => pushHistoryAtom, redoAtom: () => redoAtom, redoCountAtom: () => redoCountAtom, undoAtom: () => undoAtom, undoCountAtom: () => undoCountAtom }); import { atom as atom7 } from "jotai"; var debug4, MAX_HISTORY_SIZE, historyStateAtom, canUndoAtom, canRedoAtom, undoCountAtom, redoCountAtom, pushDeltaAtom, pushHistoryAtom, undoAtom, redoAtom, clearHistoryAtom, historyLabelsAtom; var init_history_store = __esm({ "src/core/history-store.ts"() { "use strict"; init_graph_store(); init_graph_position(); init_debug(); init_history_actions(); init_history_actions(); debug4 = createDebug("history"); MAX_HISTORY_SIZE = 50; historyStateAtom = atom7({ past: [], future: [], isApplying: false }); canUndoAtom = atom7((get) => { const history = get(historyStateAtom); return history.past.length > 0 && !history.isApplying; }); canRedoAtom = atom7((get) => { const history = get(historyStateAtom); return history.future.length > 0 && !history.isApplying; }); undoCountAtom = atom7((get) => get(historyStateAtom).past.length); redoCountAtom = atom7((get) => get(historyStateAtom).future.length); pushDeltaAtom = atom7(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); }); pushHistoryAtom = atom7(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); }); undoAtom = atom7(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; } }); redoAtom = atom7(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; } }); clearHistoryAtom = atom7(null, (_get, set) => { set(historyStateAtom, { past: [], future: [], isApplying: false }); debug4("History cleared"); }); historyLabelsAtom = atom7((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 group_store_exports = {}; __export(group_store_exports, { autoResizeGroupAtom: () => autoResizeGroupAtom, collapseGroupAtom: () => collapseGroupAtom, collapsedEdgeRemapAtom: () => collapsedEdgeRemapAtom, collapsedGroupsAtom: () => collapsedGroupsAtom, expandGroupAtom: () => expandGroupAtom, getNodeDescendants: () => getNodeDescendants, groupChildCountAtom: () => groupChildCountAtom, groupSelectedNodesAtom: () => groupSelectedNodesAtom, isGroupNodeAtom: () => isGroupNodeAtom, isNodeCollapsed: () => isNodeCollapsed, moveNodesToGroupAtom: () => moveNodesToGroupAtom, nestNodesOnDropAtom: () => nestNodesOnDropAtom, nodeChildrenAtom: () => nodeChildrenAtom, nodeParentAtom: () => nodeParentAtom, removeFromGroupAtom: () => removeFromGroupAtom, setNodeParentAtom: () => setNodeParentAtom, toggleGroupCollapseAtom: () => toggleGroupCollapseAtom, ungroupNodesAtom: () => ungroupNodesAtom }); import { atom as atom8 } from "jotai"; 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; } 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; } } var collapsedGroupsAtom, toggleGroupCollapseAtom, collapseGroupAtom, expandGroupAtom, nodeChildrenAtom, nodeParentAtom, isGroupNodeAtom, groupChildCountAtom, setNodeParentAtom, moveNodesToGroupAtom, removeFromGroupAtom, groupSelectedNodesAtom, ungroupNodesAtom, nestNodesOnDropAtom, collapsedEdgeRemapAtom, autoResizeGroupAtom; var init_group_store = __esm({ "src/core/group-store.ts"() { "use strict"; init_graph_store(); init_graph_position(); init_history_store(); collapsedGroupsAtom = atom8(/* @__PURE__ */ new Set()); toggleGroupCollapseAtom = atom8(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); }); collapseGroupAtom = atom8(null, (get, set, groupId) => { const current = get(collapsedGroupsAtom); if (!current.has(groupId)) { const next = new Set(current); next.add(groupId); set(collapsedGroupsAtom, next); } }); expandGroupAtom = atom8(null, (get, set, groupId) => { const current = get(collapsedGroupsAtom); if (current.has(groupId)) { const next = new Set(current); next.delete(groupId); set(collapsedGroupsAtom, next); } }); nodeChildrenAtom = atom8((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return (parentId) => { const children = []; graph.forEachNode((nodeId, attrs) => { if (attrs.parentId === parentId) { children.push(nodeId); } }); return children; }; }); nodeParentAtom = atom8((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return (nodeId) => { if (!graph.hasNode(nodeId)) return void 0; return graph.getNodeAttribute(nodeId, "parentId"); }; }); isGroupNodeAtom = atom8((get) => { const getChildren = get(nodeChildrenAtom); return (nodeId) => getChildren(nodeId).length > 0; }); groupChildCountAtom = atom8((get) => { const getChildren = get(nodeChildrenAtom); return (groupId) => getChildren(groupId).length; }); setNodeParentAtom = atom8(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); }); moveNodesToGroupAtom = atom8(null, (get, set, { nodeIds, groupId }) => { for (const nodeId of nodeIds) { set(setNodeParentAtom, { nodeId, parentId: groupId }); } }); removeFromGroupAtom = atom8(null, (get, set, nodeId) => { set(setNodeParentAtom, { nodeId, parentId: void 0 }); }); groupSelectedNodesAtom = atom8(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); }); ungroupNodesAtom = atom8(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); }); nestNodesOnDropAtom = atom8(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); }); collapsedEdgeRemapAtom = atom8((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; }); autoResizeGroupAtom = atom8(null, (get, set, groupId) => { const graph = get(graphAtom); if (!graph.hasNode(groupId)) return; const children = []; graph.forEachNode((nodeId, attrs) => { if (attrs.parentId === groupId) { children.push(nodeId); } }); if (children.length === 0) return; const padding = 20; const headerHeight = 30; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const childId of children) { const attrs = graph.getNodeAttributes(childId); minX = Math.min(minX, attrs.x); minY = Math.min(minY, attrs.y); maxX = Math.max(maxX, attrs.x + (attrs.width || 200)); maxY = Math.max(maxY, attrs.y + (attrs.height || 100)); } graph.setNodeAttribute(groupId, "x", minX - padding); graph.setNodeAttribute(groupId, "y", minY - padding - headerHeight); graph.setNodeAttribute(groupId, "width", maxX - minX + 2 * padding); graph.setNodeAttribute(groupId, "height", maxY - minY + 2 * padding + headerHeight); set(nodePositionUpdateCounterAtom, (c) => c + 1); }); } }); // src/core/graph-derived.ts import { atom as atom9 } from "jotai"; import { atomFamily as atomFamily2 } from "jotai-family"; function getEdgeCache(graph) { let cache = _edgeCacheByGraph.get(graph); if (!cache) { cache = /* @__PURE__ */ new Map(); _edgeCacheByGraph.set(graph, cache); } return cache; } var highestZIndexAtom, _prevUiNodesByGraph, uiNodesAtom, nodeKeysAtom, nodeFamilyAtom, edgeKeysAtom, edgeKeysWithTempEdgeAtom, _edgeCacheByGraph, edgeFamilyAtom; var init_graph_derived = __esm({ "src/core/graph-derived.ts"() { "use strict"; init_graph_store(); init_graph_position(); init_viewport_store(); init_group_store(); highestZIndexAtom = atom9((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); let maxZ = 0; graph.forEachNode((_node, attributes) => { if (attributes.zIndex > maxZ) { maxZ = attributes.zIndex; } }); return maxZ; }); _prevUiNodesByGraph = /* @__PURE__ */ new WeakMap(); uiNodesAtom = atom9((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; }); nodeKeysAtom = atom9((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return graph.nodes(); }); nodeFamilyAtom = atomFamily2((nodeId) => atom9((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); edgeKeysAtom = atom9((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return graph.edges(); }); edgeKeysWithTempEdgeAtom = atom9((get) => { const keys = get(edgeKeysAtom); const edgeCreation = get(edgeCreationAtom); if (edgeCreation.isCreating) { return [...keys, "temp-creating-edge"]; } return keys; }); _edgeCacheByGraph = /* @__PURE__ */ new WeakMap(); edgeFamilyAtom = atomFamily2((key) => atom9((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/utils/layout.ts function getNodeCenter(node) { return { x: node.x + node.width / 2, y: node.y + node.height / 2 }; } function checkNodesOverlap(node1, node2) { const center1 = getNodeCenter(node1); const center2 = getNodeCenter(node2); const dx = Math.abs(center1.x - center2.x); const dy = Math.abs(center1.y - center2.y); const minDistanceX = (node1.width + node2.width) / 2; const minDistanceY = (node1.height + node2.height) / 2; return dx < minDistanceX && dy < minDistanceY; } var FitToBoundsMode, calculateBounds; var init_layout = __esm({ "src/utils/layout.ts"() { "use strict"; FitToBoundsMode = /* @__PURE__ */ (function(FitToBoundsMode2) { FitToBoundsMode2["Graph"] = "graph"; FitToBoundsMode2["Selection"] = "selection"; return FitToBoundsMode2; })({}); 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 viewport_store_exports = {}; __export(viewport_store_exports, { ZOOM_EXIT_THRESHOLD: () => ZOOM_EXIT_THRESHOLD, ZOOM_TRANSITION_THRESHOLD: () => ZOOM_TRANSITION_THRESHOLD, animateFitToBoundsAtom: () => animateFitToBoundsAtom, animateZoomToNodeAtom: () => animateZoomToNodeAtom, centerOnNodeAtom: () => centerOnNodeAtom, fitToBoundsAtom: () => fitToBoundsAtom, isZoomTransitioningAtom: () => isZoomTransitioningAtom, panAtom: () => panAtom, resetViewportAtom: () => resetViewportAtom, screenToWorldAtom: () => screenToWorldAtom, setZoomAtom: () => setZoomAtom, viewportRectAtom: () => viewportRectAtom, worldToScreenAtom: () => worldToScreenAtom, zoomAnimationTargetAtom: () => zoomAnimationTargetAtom, zoomAtom: () => zoomAtom, zoomFocusNodeIdAtom: () => zoomFocusNodeIdAtom, zoomTransitionProgressAtom: () => zoomTransitionProgressAtom }); import { atom as atom10 } from "jotai"; var zoomAtom, panAtom, viewportRectAtom, screenToWorldAtom, worldToScreenAtom, setZoomAtom, resetViewportAtom, fitToBoundsAtom, centerOnNodeAtom, ZOOM_TRANSITION_THRESHOLD, ZOOM_EXIT_THRESHOLD, zoomFocusNodeIdAtom, zoomTransitionProgressAtom, isZoomTransitioningAtom, zoomAnimationTargetAtom, animateZoomToNodeAtom, animateFitToBoundsAtom; var init_viewport_store = __esm({ "src/core/viewport-store.ts"() { "use strict"; init_graph_store(); init_graph_position(); init_graph_derived(); init_selection_store(); init_layout(); zoomAtom = atom10(1); panAtom = atom10({ x: 0, y: 0 }); viewportRectAtom = atom10(null); screenToWorldAtom = atom10((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 }; }; }); worldToScreenAtom = atom10((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 }; }; }); setZoomAtom = atom10(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); }); resetViewportAtom = atom10(null, (_get, set) => { set(zoomAtom, 1); set(panAtom, { x: 0, y: 0 }); }); fitToBoundsAtom = atom10(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 }); }); centerOnNodeAtom = atom10(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 }); }); ZOOM_TRANSITION_THRESHOLD = 3.5; ZOOM_EXIT_THRESHOLD = 2; zoomFocusNodeIdAtom = atom10(null); zoomTransitionProgressAtom = atom10(0); isZoomTransitioningAtom = atom10((get) => { const progress = get(zoomTransitionProgressAtom); return progress > 0 && progress < 1; }); zoomAnimationTargetAtom = atom10(null); animateZoomToNodeAtom = atom10(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() }); }); animateFitToBoundsAtom = atom10(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/reduced-motion-store.ts import { atom as atom11 } from "jotai"; var prefersReducedMotionAtom, watchReducedMotionAtom; var init_reduced_motion_store = __esm({ "src/core/reduced-motion-store.ts"() { "use strict"; prefersReducedMotionAtom = atom11(typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false); watchReducedMotionAtom = atom11(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/types.ts var init_types = __esm({ "src/core/types.ts"() { "use strict"; } }); // src/core/graph-mutations-edges.ts import { atom as atom12 } from "jotai"; var debug7, addEdgeToLocalGraphAtom, removeEdgeFromLocalGraphAtom, swapEdgeAtomicAtom, departingEdgesAtom, EDGE_ANIMATION_DURATION, removeEdgeWithAnimationAtom, editingEdgeLabelAtom, updateEdgeLabelAtom; var init_graph_mutations_edges = __esm({ "src/core/graph-mutations-edges.ts"() { "use strict"; init_graph_store(); init_graph_position(); init_graph_derived(); init_debug(); init_reduced_motion_store(); debug7 = createDebug("graph:mutations:edges"); addEdgeToLocalGraphAtom = atom12(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 { debug7("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) { debug7("Failed to add edge %s: %o", newEdge.id, e); } } } }); removeEdgeFromLocalGraphAtom = atom12(null, (get, set, edgeId) => { const graph = get(graphAtom); if (graph.hasEdge(edgeId)) { graph.dropEdge(edgeId); set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); } }); swapEdgeAtomicAtom = atom12(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 { debug7("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) { debug7("Failed to add edge %s: %o", newEdge.id, e); } } } set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); }); departingEdgesAtom = atom12(/* @__PURE__ */ new Map()); EDGE_ANIMATION_DURATION = 300; removeEdgeWithAnimationAtom = atom12(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); } }); editingEdgeLabelAtom = atom12(null); updateEdgeLabelAtom = atom12(null, (get, set, { edgeKey, label }) => { const graph = get(graphAtom); if (graph.hasEdge(edgeKey)) { graph.setEdgeAttribute(edgeKey, "label", label || void 0); set(graphUpdateVersionAtom, (v) => v + 1); set(nodePositionUpdateCounterAtom, (c) => c + 1); } }); } }); // src/core/graph-mutations-advanced.ts import { atom as atom13 } from "jotai"; var debug8, dropTargetNodeIdAtom, splitNodeAtom, mergeNodesAtom; var init_graph_mutations_advanced = __esm({ "src/core/graph-mutations-advanced.ts"() { "use strict"; init_graph_store(); init_graph_position(); init_history_store(); init_debug(); init_graph_mutations_edges(); init_graph_mutations(); debug8 = createDebug("graph:mutations:advanced"); dropTargetNodeIdAtom = atom13(null); splitNodeAtom = atom13(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); debug8("Split node %s \u2192 clone %s", nodeId, cloneId); }); mergeNodesAtom = atom13(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); debug8("Merged nodes %o \u2192 survivor %s", nodeIds, survivorId); }); } }); // src/core/graph-mutations.ts var graph_mutations_exports = {}; __export(graph_mutations_exports, { EDGE_ANIMATION_DURATION: () => EDGE_ANIMATION_DURATION, addNodeToLocalGraphAtom: () => addNodeToLocalGraphAtom, departingEdgesAtom: () => departingEdgesAtom, dropTargetNodeIdAtom: () => dropTargetNodeIdAtom, editingEdgeLabelAtom: () => editingEdgeLabelAtom, endNodeDragAtom: () => endNodeDragAtom, loadGraphFromDbAtom: () => loadGraphFromDbAtom, mergeNodesAtom: () => mergeNodesAtom, optimisticDeleteEdgeAtom: () => optimisticDeleteEdgeAtom, optimisticDeleteNodeAtom: () => optimisticDeleteNodeAtom, removeEdgeWithAnimationAtom: () => removeEdgeWithAnimationAtom, splitNodeAtom: () => splitNodeAtom, startNodeDragAtom: () => startNodeDragAtom, swapEdgeAtomicAtom: () => swapEdgeAtomicAtom, updateEdgeLabelAtom: () => updateEdgeLabelAtom }); import { atom as atom14 } from "jotai"; import Graph3 from "graphology"; var debug9, startNodeDragAtom, endNodeDragAtom, optimisticDeleteNodeAtom, optimisticDeleteEdgeAtom, addNodeToLocalGraphAtom, loadGraphFromDbAtom; var init_graph_mutations = __esm({ "src/core/graph-mutations.ts"() { "use strict"; init_graph_store(); init_graph_position(); init_graph_derived(); init_group_store(); init_debug(); init_graph_mutations_edges(); init_graph_mutations_advanced(); debug9 = createDebug("graph:mutations"); startNodeDragAtom = atom14(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); }); endNodeDragAtom = atom14(null, (get, set, _payload) => { const currentDraggingId = get(draggingNodeIdAtom); if (currentDraggingId) { debug9("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); }); optimisticDeleteNodeAtom = atom14(null, (get, set, { nodeId }) => { const graph = get(graphAtom); if (graph.hasNode(nodeId)) { graph.dropNode(nodeId); set(cleanupNodePositionAtom, nodeId); set(graphAtom, graph.copy()); debug9("Optimistically deleted node %s", nodeId); } }); optimisticDeleteEdgeAtom = atom14(null, (get, set, { edgeKey }) => { const graph = get(graphAtom); if (graph.hasEdge(edgeKey)) { graph.dropEdge(edgeKey); set(graphAtom, graph.copy()); debug9("Optimistically deleted edge %s", edgeKey); } }); addNodeToLocalGraphAtom = atom14(null, (get, set, newNode) => { const graph = get(graphAtom); if (graph.hasNode(newNode.id)) { debug9("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 }; debug9("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); }); loadGraphFromDbAtom = atom14(null, (get, set, fetchedNodes, fetchedEdges) => { debug9("========== START SYNC =========="); debug9("Fetched nodes: %d, edges: %d", fetchedNodes.length, fetchedEdges.length); const currentGraphId = get(currentGraphIdAtom); if (fetchedNodes.length > 0 && fetchedNodes[0].graph_id !== currentGraphId) { debug9("Skipping sync - data belongs to different graph"); return; } const existingGraph = get(graphAtom); const isDragging = get(draggingNodeIdAtom) !== null; if (isDragging) { debug9("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) { debug9("Merging DB data into existing graph"); graph = existingGraph.copy(); } else { debug9("Creating fresh graph (graph switch detected)"); graph = new Graph3(graphOptions); } const fetchedEdgeIds = new Set(fetchedEdges.map((e) => e.id)); if (hasAnyCommonNodes && existingNodeIds.size > 0) { graph.forEachNode((nodeId) => { if (!fetchedNodeIds.has(nodeId)) { debug9("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)) { debug9("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) { debug9("Failed to add edge %s: %o", edge.id, e); } } } }); set(graphAtom, graph); set(graphUpdateVersionAtom, (v) => v + 1); debug9("========== SYNC COMPLETE =========="); debug9("Final graph: %d nodes, %d edges", graph.order, graph.size); }); } }); // src/core/sync-store.ts import { atom as atom15 } from "jotai"; var debug10, syncStatusAtom, pendingMutationsCountAtom, isOnlineAtom, lastSyncErrorAtom, lastSyncTimeAtom, mutationQueueAtom, syncStateAtom, startMutationAtom, completeMutationAtom, trackMutationErrorAtom, setOnlineStatusAtom, queueMutationAtom, dequeueMutationAtom, incrementRetryCountAtom, getNextQueuedMutationAtom, clearMutationQueueAtom; var init_sync_store = __esm({ "src/core/sync-store.ts"() { "use strict"; init_debug(); debug10 = createDebug("sync"); syncStatusAtom = atom15("synced"); pendingMutationsCountAtom = atom15(0); isOnlineAtom = atom15(typeof navigator !== "undefined" ? navigator.onLine : true); lastSyncErrorAtom = atom15(null); lastSyncTimeAtom = atom15(Date.now()); mutationQueueAtom = atom15([]); syncStateAtom = atom15((get) => ({ status: get(syncStatusAtom), pendingMutations: get(pendingMutationsCountAtom), lastError: get(lastSyncErrorAtom), lastSyncTime: get(lastSyncTimeAtom), isOnline: get(isOnlineAtom), queuedMutations: get(mutationQueueAtom).length })); startMutationAtom = atom15(null, (get, set) => { const currentCount = get(pendingMutationsCountAtom); const newCount = currentCount + 1; set(pendingMutationsCountAtom, newCount); debug10("Mutation started. Pending count: %d -> %d", currentCount, newCount); if (newCount > 0 && get(syncStatusAtom) !== "syncing") { set(syncStatusAtom, "syncing"); debug10("Status -> syncing"); } }); completeMutationAtom = atom15(null, (get, set, success = true) => { const currentCount = get(pendingMutationsCountAtom); const newCount = Math.max(0, currentCount - 1); set(pendingMutationsCountAtom, newCount); debug10("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"); debug10("Status -> error"); } else if (!isOnline) { set(syncStatusAtom, "offline"); debug10("Status -> offline"); } else { set(syncStatusAtom, "synced"); debug10("Status -> synced"); } } }); trackMutationErrorAtom = atom15(null, (_get, set, error) => { set(lastSyncErrorAtom, error); debug10("Mutation failed: %s", error); }); setOnlineStatusAtom = atom15(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"); } } }); queueMutationAtom = atom15(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); debug10("Queued mutation: %s. Queue size: %d", mutation.type, newQueue.length); if (get(pendingMutationsCountAtom) === 0) { set(syncStatusAtom, "error"); } return newMutation.id; }); dequeueMutationAtom = atom15(null, (get, set, mutationId) => { const queue = get(mutationQueueAtom); const newQueue = queue.filter((m) => m.id !== mutationId); set(mutationQueueAtom, newQueue); debug10("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"); } }); incrementRetryCountAtom = atom15(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); }); getNextQueuedMutationAtom = atom15((get) => { const queue = get(mutationQueueAtom); return queue.find((m) => m.retryCount < m.maxRetries) ?? null; }); clearMutationQueueAtom = atom15(null, (get, set) => { set(mutationQueueAtom, []); debug10("Cleared mutation queue"); if (get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) { set(syncStatusAtom, get(isOnlineAtom) ? "synced" : "offline"); } }); } }); // src/core/interaction-store.ts import { atom as atom16 } from "jotai"; var inputModeAtom2, keyboardInteractionModeAtom, interactionFeedbackAtom, pendingInputResolverAtom2, resetInputModeAtom, resetKeyboardInteractionModeAtom, setKeyboardInteractionModeAtom, startPickNodeAtom, startPickNodesAtom, startPickPointAtom, provideInputAtom2, updateInteractionFeedbackAtom, isPickingModeAtom, isPickNodeModeAtom; var init_interaction_store = __esm({ "src/core/interaction-store.ts"() { "use strict"; inputModeAtom2 = atom16({ type: "normal" }); keyboardInteractionModeAtom = atom16("navigate"); interactionFeedbackAtom = atom16(null); pendingInputResolverAtom2 = atom16(null); resetInputModeAtom = atom16(null, (_get, set) => { set(inputModeAtom2, { type: "normal" }); set(interactionFeedbackAtom, null); set(pendingInputResolverAtom2, null); }); resetKeyboardInteractionModeAtom = atom16(null, (_get, set) => { set(keyboardInteractionModeAtom, "navigate"); }); setKeyboardInteractionModeAtom = atom16(null, (_get, set, mode) => { set(keyboardInteractionModeAtom, mode); }); startPickNodeAtom = atom16(null, (_get, set, options) => { set(inputModeAtom2, { type: "pickNode", ...options }); }); startPickNodesAtom = atom16(null, (_get, set, options) => { set(inputModeAtom2, { type: "pickNodes", ...options }); }); startPickPointAtom = atom16(null, (_get, set, options) => { set(inputModeAtom2, { type: "pickPoint", ...options }); }); provideInputAtom2 = atom16(null, (get, set, value) => { set(pendingInputResolverAtom2, value); }); updateInteractionFeedbackAtom = atom16(null, (get, set, feedback) => { const current = get(interactionFeedbackAtom); set(interactionFeedbackAtom, { ...current, ...feedback }); }); isPickingModeAtom = atom16((get) => { const mode = get(inputModeAtom2); return mode.type !== "normal"; }); isPickNodeModeAtom = atom16((get) => { const mode = get(inputModeAtom2); return mode.type === "pickNode" || mode.type === "pickNodes"; }); } }); // src/core/locked-node-store.ts import { atom as atom17 } from "jotai"; var lockedNodeIdAtom, lockedNodeDataAtom, lockedNodePageIndexAtom, lockedNodePageCountAtom, lockNodeAtom, unlockNodeAtom, nextLockedPageAtom, prevLockedPageAtom, goToLockedPageAtom, hasLockedNodeAtom; var init_locked_node_store = __esm({ "src/core/locked-node-store.ts"() { "use strict"; init_graph_derived(); lockedNodeIdAtom = atom17(null); lockedNodeDataAtom = atom17((get) => { const id = get(lockedNodeIdAtom); if (!id) return null; const nodes = get(uiNodesAtom); return nodes.find((n) => n.id === id) || null; }); lockedNodePageIndexAtom = atom17(0); lockedNodePageCountAtom = atom17(1); lockNodeAtom = atom17(null, (_get, set, payload) => { set(lockedNodeIdAtom, payload.nodeId); set(lockedNodePageIndexAtom, 0); }); unlockNodeAtom = atom17(null, (_get, set) => { set(lockedNodeIdAtom, null); }); nextLockedPageAtom = atom17(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current + 1) % pageCount); }); prevLockedPageAtom = atom17(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current - 1 + pageCount) % pageCount); }); goToLockedPageAtom = atom17(null, (get, set, index) => { const pageCount = get(lockedNodePageCountAtom); if (index >= 0 && index < pageCount) { set(lockedNodePageIndexAtom, index); } }); hasLockedNodeAtom = atom17((get) => get(lockedNodeIdAtom) !== null); } }); // src/core/node-type-registry.tsx import { c as _c3 } from "react/compiler-runtime"; import React2 from "react"; import { jsxs as _jsxs, jsx as _jsx2 } from "react/jsx-runtime"; 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 nodeTypeRegistry, FallbackNodeTypeComponent; var init_node_type_registry = __esm({ "src/core/node-type-registry.tsx"() { "use strict"; nodeTypeRegistry = /* @__PURE__ */ new Map(); FallbackNodeTypeComponent = (t0) => { const $ = _c3(11); const { nodeData } = t0; let t1; if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t1 = { padding: "12px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", color: "#666", fontSize: "12px" }; $[0] = t1; } else { t1 = $[0]; } const t2 = nodeData.dbData.node_type || "none"; let t3; if ($[1] !== t2) { t3 = /* @__PURE__ */ _jsxs("div", { children: ["Unknown type: ", t2] }); $[1] = t2; $[2] = t3; } else { t3 = $[2]; } let t4; if ($[3] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t4 = { marginTop: "4px", opacity: 0.7 }; $[3] = t4; } else { t4 = $[3]; } let t5; if ($[4] !== nodeData.id) { t5 = nodeData.id.substring(0, 8); $[4] = nodeData.id; $[5] = t5; } else { t5 = $[5]; } let t6; if ($[6] !== t5) { t6 = /* @__PURE__ */ _jsx2("div", { style: t4, children: t5 }); $[6] = t5; $[7] = t6; } else { t6 = $[7]; } let t7; if ($[8] !== t3 || $[9] !== t6) { t7 = /* @__PURE__ */ _jsxs("div", { style: t1, children: [t3, t6] }); $[8] = t3; $[9] = t6; $[10] = t7; } else { t7 = $[10]; } return t7; }; } }); // src/core/toast-store.ts var toast_store_exports = {}; __export(toast_store_exports, { canvasToastAtom: () => canvasToastAtom, showToastAtom: () => showToastAtom }); import { atom as atom18 } from "jotai"; var canvasToastAtom, showToastAtom; var init_toast_store = __esm({ "src/core/toast-store.ts"() { "use strict"; canvasToastAtom = atom18(null); showToastAtom = atom18(null, (_get, set, message) => { const id = `toast-${Date.now()}`; set(canvasToastAtom, { id, message, timestamp: Date.now() }); setTimeout(() => { set(canvasToastAtom, (current) => current?.id === id ? null : current); }, 2e3); }); } }); // src/core/snap-store.ts import { atom as atom19 } from "jotai"; 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 }; } 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) }; } var snapEnabledAtom, snapGridSizeAtom, snapTemporaryDisableAtom, isSnappingActiveAtom, toggleSnapAtom, setGridSizeAtom, snapAlignmentEnabledAtom, toggleAlignmentGuidesAtom, alignmentGuidesAtom, clearAlignmentGuidesAtom; var init_snap_store = __esm({ "src/core/snap-store.ts"() { "use strict"; snapEnabledAtom = atom19(false); snapGridSizeAtom = atom19(20); snapTemporaryDisableAtom = atom19(false); isSnappingActiveAtom = atom19((get) => { return get(snapEnabledAtom) && !get(snapTemporaryDisableAtom); }); toggleSnapAtom = atom19(null, (get, set) => { set(snapEnabledAtom, !get(snapEnabledAtom)); }); setGridSizeAtom = atom19(null, (_get, set, size) => { set(snapGridSizeAtom, Math.max(5, Math.min(200, size))); }); snapAlignmentEnabledAtom = atom19(true); toggleAlignmentGuidesAtom = atom19(null, (get, set) => { set(snapAlignmentEnabledAtom, !get(snapAlignmentEnabledAtom)); }); alignmentGuidesAtom = atom19({ verticalGuides: [], horizontalGuides: [] }); clearAlignmentGuidesAtom = atom19(null, (_get, set) => { set(alignmentGuidesAtom, { verticalGuides: [], horizontalGuides: [] }); }); } }); // src/core/event-types.ts var CanvasEventType, EVENT_TYPE_INFO; var init_event_types = __esm({ "src/core/event-types.ts"() { "use strict"; 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; })({}); 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, BuiltInActionId; var init_action_types = __esm({ "src/core/action-types.ts"() { "use strict"; 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; })({}); 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; var init_settings_state_types = __esm({ "src/core/settings-state-types.ts"() { "use strict"; init_event_types(); init_action_types(); DEFAULT_MAPPINGS = { [CanvasEventType.NodeClick]: BuiltInActionId.None, [CanvasEventType.NodeDoubleClick]: BuiltInActionId.FitToView, [CanvasEventType.NodeTripleClick]: BuiltInActionId.ToggleLock, [CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.NodeLongPress]: BuiltInActionId.OpenContextMenu, [CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge, [CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None, [CanvasEventType.EdgeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection, [CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.FitAllToView, [CanvasEventType.BackgroundRightClick]: BuiltInActionId.None, [CanvasEventType.BackgroundLongPress]: BuiltInActionId.CreateNode }; } }); // src/core/settings-types.ts var init_settings_types = __esm({ "src/core/settings-types.ts"() { "use strict"; init_event_types(); init_action_types(); init_settings_state_types(); } }); // 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()); } } }); } var init_actions_node = __esm({ "src/core/actions-node.ts"() { "use strict"; init_settings_types(); init_action_registry(); } }); // 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(); } }); } var init_actions_viewport = __esm({ "src/core/actions-viewport.ts"() { "use strict"; init_settings_types(); init_action_registry(); } }); // 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(); } var init_built_in_actions = __esm({ "src/core/built-in-actions.ts"() { "use strict"; init_settings_types(); init_action_registry(); init_actions_node(); init_actions_viewport(); } }); // src/core/action-registry.ts 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(); } 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); } var actionRegistry; var init_action_registry = __esm({ "src/core/action-registry.ts"() { "use strict"; init_settings_types(); init_built_in_actions(); actionRegistry = /* @__PURE__ */ new Map(); registerBuiltInActions(); } }); // src/core/action-executor.ts async function executeAction(actionId, context, helpers) { if (actionId === BuiltInActionId.None) { return { success: true, actionId }; } const action = getAction(actionId); if (!action) { debug11.warn("Action not found: %s", actionId); return { success: false, actionId, error: new Error(`Action not found: ${actionId}`) }; } if (action.requiresNode && !context.nodeId) { debug11.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) { debug11.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 { debug11.warn("deleteNode called but onDeleteNode callback not provided"); } }, isNodeLocked: (nodeId) => store.get(lockedNodeIdAtom) === nodeId, applyForceLayout: async () => { if (options.onApplyForceLayout) { await options.onApplyForceLayout(); } else { debug11.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 }; } var debug11; var init_action_executor = __esm({ "src/core/action-executor.ts"() { "use strict"; init_action_registry(); init_settings_types(); init_selection_store(); init_viewport_store(); init_locked_node_store(); init_history_store(); init_layout(); init_debug(); debug11 = createDebug("actions"); } }); // src/core/settings-presets.ts function getActionForEvent(mappings, event) { return mappings[event] || BuiltInActionId.None; } var BUILT_IN_PRESETS; var init_settings_presets = __esm({ "src/core/settings-presets.ts"() { "use strict"; init_settings_types(); 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 } }]; } }); // src/core/settings-store.ts import { atom as atom20 } from "jotai"; import { atomWithStorage as atomWithStorage2 } from "jotai/utils"; var debug12, DEFAULT_STATE, canvasSettingsAtom, eventMappingsAtom, activePresetIdAtom, allPresetsAtom, activePresetAtom, isPanelOpenAtom, virtualizationEnabledAtom, hasUnsavedChangesAtom, setEventMappingAtom, applyPresetAtom, saveAsPresetAtom, updatePresetAtom, deletePresetAtom, resetSettingsAtom, togglePanelAtom, setPanelOpenAtom, setVirtualizationEnabledAtom, toggleVirtualizationAtom; var init_settings_store = __esm({ "src/core/settings-store.ts"() { "use strict"; init_settings_types(); init_debug(); init_settings_presets(); init_settings_presets(); debug12 = createDebug("settings"); DEFAULT_STATE = { mappings: DEFAULT_MAPPINGS, activePresetId: "default", customPresets: [], isPanelOpen: false, virtualizationEnabled: true }; canvasSettingsAtom = atomWithStorage2("@blinksgg/canvas/settings", DEFAULT_STATE); eventMappingsAtom = atom20((get) => get(canvasSettingsAtom).mappings); activePresetIdAtom = atom20((get) => get(canvasSettingsAtom).activePresetId); allPresetsAtom = atom20((get) => { const state = get(canvasSettingsAtom); return [...BUILT_IN_PRESETS, ...state.customPresets]; }); activePresetAtom = atom20((get) => { const presetId = get(activePresetIdAtom); if (!presetId) return null; const allPresets = get(allPresetsAtom); return allPresets.find((p) => p.id === presetId) || null; }); isPanelOpenAtom = atom20((get) => get(canvasSettingsAtom).isPanelOpen); virtualizationEnabledAtom = atom20((get) => get(canvasSettingsAtom).virtualizationEnabled ?? true); hasUnsavedChangesAtom = atom20((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]); }); setEventMappingAtom = atom20(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 }); }); applyPresetAtom = atom20(null, (get, set, presetId) => { const allPresets = get(allPresetsAtom); const preset = allPresets.find((p) => p.id === presetId); if (!preset) { debug12.warn("Preset not found: %s", presetId); return; } const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: { ...preset.mappings }, activePresetId: presetId }); }); saveAsPresetAtom = atom20(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; }); updatePresetAtom = atom20(null, (get, set, presetId) => { const current = get(canvasSettingsAtom); const presetIndex = current.customPresets.findIndex((p) => p.id === presetId); if (presetIndex === -1) { debug12.warn("Cannot update preset: %s (not found or built-in)", presetId); return; } const updatedPresets = [...current.customPresets]; updatedPresets[presetIndex] = { ...updatedPresets[presetIndex], mappings: { ...current.mappings } }; set(canvasSettingsAtom, { ...current, customPresets: updatedPresets, activePresetId: presetId }); }); deletePresetAtom = atom20(null, (get, set, presetId) => { const current = get(canvasSettingsAtom); const newCustomPresets = current.customPresets.filter((p) => p.id !== presetId); if (newCustomPresets.length === current.customPresets.length) { debug12.warn("Cannot delete preset: %s (not found or built-in)", presetId); return; } const newActiveId = current.activePresetId === presetId ? "default" : current.activePresetId; const newMappings = newActiveId === "default" ? DEFAULT_MAPPINGS : current.mappings; set(canvasSettingsAtom, { ...current, customPresets: newCustomPresets, activePresetId: newActiveId, mappings: newMappings }); }); resetSettingsAtom = atom20(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: DEFAULT_MAPPINGS, activePresetId: "default" }); }); togglePanelAtom = atom20(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: !current.isPanelOpen }); }); setPanelOpenAtom = atom20(null, (get, set, isOpen) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: isOpen }); }); setVirtualizationEnabledAtom = atom20(null, (get, set, enabled) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: enabled }); }); toggleVirtualizationAtom = atom20(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: !(current.virtualizationEnabled ?? true) }); }); } }); // src/core/canvas-serializer.ts var canvas_serializer_exports = {}; __export(canvas_serializer_exports, { SNAPSHOT_VERSION: () => SNAPSHOT_VERSION, exportGraph: () => exportGraph, importGraph: () => importGraph, validateSnapshot: () => validateSnapshot }); import Graph4 from "graphology"; function exportGraph(store, metadata) { const graph = store.get(graphAtom); const zoom = store.get(zoomAtom); const pan = store.get(panAtom); const collapsed = store.get(collapsedGroupsAtom); const nodes = []; const groups = []; const seenGroupParents = /* @__PURE__ */ new Set(); graph.forEachNode((nodeId, attrs) => { const a = attrs; nodes.push({ id: nodeId, position: { x: a.x, y: a.y }, dimensions: { width: a.width, height: a.height }, size: a.size, color: a.color, zIndex: a.zIndex, label: a.label, parentId: a.parentId, dbData: a.dbData }); if (a.parentId) { const key = `${nodeId}:${a.parentId}`; if (!seenGroupParents.has(key)) { seenGroupParents.add(key); groups.push({ nodeId, parentId: a.parentId, isCollapsed: collapsed.has(a.parentId) }); } } }); const edges = []; graph.forEachEdge((key, attrs, source, target) => { const a = attrs; edges.push({ key, sourceId: source, targetId: target, attributes: { weight: a.weight, type: a.type, color: a.color, label: a.label }, dbData: a.dbData }); }); return { version: SNAPSHOT_VERSION, exportedAt: (/* @__PURE__ */ new Date()).toISOString(), nodes, edges, groups, viewport: { zoom, pan: { ...pan } }, metadata }; } function importGraph(store, snapshot, options = {}) { const { clearExisting = true, offsetPosition, remapIds = false } = options; const idMap = /* @__PURE__ */ new Map(); if (remapIds) { for (const node of snapshot.nodes) { idMap.set(node.id, crypto.randomUUID()); } for (const edge of snapshot.edges) { idMap.set(edge.key, crypto.randomUUID()); } } const remap = (id) => idMap.get(id) ?? id; let graph; if (clearExisting) { graph = new Graph4(graphOptions); } else { graph = store.get(graphAtom); } const ox = offsetPosition?.x ?? 0; const oy = offsetPosition?.y ?? 0; for (const node of snapshot.nodes) { const nodeId = remap(node.id); const parentId = node.parentId ? remap(node.parentId) : void 0; const dbData = remapIds ? { ...node.dbData, id: nodeId } : node.dbData; const attrs = { x: node.position.x + ox, y: node.position.y + oy, width: node.dimensions.width, height: node.dimensions.height, size: node.size, color: node.color, zIndex: node.zIndex, label: node.label, parentId, dbData }; graph.addNode(nodeId, attrs); } for (const edge of snapshot.edges) { const edgeKey = remap(edge.key); const sourceId = remap(edge.sourceId); const targetId = remap(edge.targetId); if (!graph.hasNode(sourceId) || !graph.hasNode(targetId)) continue; const dbData = remapIds ? { ...edge.dbData, id: edgeKey, source_node_id: sourceId, target_node_id: targetId } : edge.dbData; const attrs = { weight: edge.attributes.weight, type: edge.attributes.type, color: edge.attributes.color, label: edge.attributes.label, dbData }; graph.addEdgeWithKey(edgeKey, sourceId, targetId, attrs); } store.set(graphAtom, graph); store.set(graphUpdateVersionAtom, (v) => v + 1); store.set(nodePositionUpdateCounterAtom, (c) => c + 1); const collapsedSet = /* @__PURE__ */ new Set(); for (const group of snapshot.groups) { if (group.isCollapsed) { collapsedSet.add(remap(group.parentId)); } } store.set(collapsedGroupsAtom, collapsedSet); store.set(zoomAtom, snapshot.viewport.zoom); store.set(panAtom, { ...snapshot.viewport.pan }); } function validateSnapshot(data) { const errors = []; if (!data || typeof data !== "object") { return { valid: false, errors: ["Snapshot must be a non-null object"] }; } const obj = data; if (obj.version !== SNAPSHOT_VERSION) { errors.push(`Expected version ${SNAPSHOT_VERSION}, got ${String(obj.version)}`); } if (typeof obj.exportedAt !== "string") { errors.push('Missing or invalid "exportedAt" (expected ISO string)'); } if (!Array.isArray(obj.nodes)) { errors.push('Missing or invalid "nodes" (expected array)'); } else { for (let i = 0; i < obj.nodes.length; i++) { const node = obj.nodes[i]; if (!node || typeof node !== "object") { errors.push(`nodes[${i}]: expected object`); continue; } if (typeof node.id !== "string") errors.push(`nodes[${i}]: missing "id"`); if (!node.position || typeof node.position !== "object") errors.push(`nodes[${i}]: missing "position"`); if (!node.dimensions || typeof node.dimensions !== "object") errors.push(`nodes[${i}]: missing "dimensions"`); if (!node.dbData || typeof node.dbData !== "object") errors.push(`nodes[${i}]: missing "dbData"`); } } if (!Array.isArray(obj.edges)) { errors.push('Missing or invalid "edges" (expected array)'); } else { for (let i = 0; i < obj.edges.length; i++) { const edge = obj.edges[i]; if (!edge || typeof edge !== "object") { errors.push(`edges[${i}]: expected object`); continue; } if (typeof edge.key !== "string") errors.push(`edges[${i}]: missing "key"`); if (typeof edge.sourceId !== "string") errors.push(`edges[${i}]: missing "sourceId"`); if (typeof edge.targetId !== "string") errors.push(`edges[${i}]: missing "targetId"`); if (!edge.dbData || typeof edge.dbData !== "object") errors.push(`edges[${i}]: missing "dbData"`); } } if (!Array.isArray(obj.groups)) { errors.push('Missing or invalid "groups" (expected array)'); } if (!obj.viewport || typeof obj.viewport !== "object") { errors.push('Missing or invalid "viewport" (expected object)'); } else { const vp = obj.viewport; if (typeof vp.zoom !== "number") errors.push('viewport: missing "zoom"'); if (!vp.pan || typeof vp.pan !== "object") errors.push('viewport: missing "pan"'); } return { valid: errors.length === 0, errors }; } var SNAPSHOT_VERSION; var init_canvas_serializer = __esm({ "src/core/canvas-serializer.ts"() { "use strict"; init_graph_store(); init_graph_position(); init_viewport_store(); init_group_store(); SNAPSHOT_VERSION = 1; } }); // src/core/clipboard-store.ts var clipboard_store_exports = {}; __export(clipboard_store_exports, { PASTE_OFFSET: () => PASTE_OFFSET, clearClipboardAtom: () => clearClipboardAtom, clipboardAtom: () => clipboardAtom, clipboardNodeCountAtom: () => clipboardNodeCountAtom, copyToClipboardAtom: () => copyToClipboardAtom, cutToClipboardAtom: () => cutToClipboardAtom, duplicateSelectionAtom: () => duplicateSelectionAtom, hasClipboardContentAtom: () => hasClipboardContentAtom, pasteFromClipboardAtom: () => pasteFromClipboardAtom }); import { atom as atom21 } from "jotai"; 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 debug13, PASTE_OFFSET, clipboardAtom, hasClipboardContentAtom, clipboardNodeCountAtom, copyToClipboardAtom, cutToClipboardAtom, pasteFromClipboardAtom, duplicateSelectionAtom, clearClipboardAtom; var init_clipboard_store = __esm({ "src/core/clipboard-store.ts"() { "use strict"; init_graph_store(); init_graph_mutations(); init_graph_mutations_edges(); init_selection_store(); init_history_store(); init_debug(); debug13 = createDebug("clipboard"); PASTE_OFFSET = { x: 50, y: 50 }; clipboardAtom = atom21(null); hasClipboardContentAtom = atom21((get) => get(clipboardAtom) !== null); clipboardNodeCountAtom = atom21((get) => { const clipboard = get(clipboardAtom); return clipboard?.nodes.length ?? 0; }); copyToClipboardAtom = atom21(null, (get, set, nodeIds) => { const selectedIds = nodeIds ?? Array.from(get(selectedNodeIdsAtom)); if (selectedIds.length === 0) { debug13("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)) { debug13("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); debug13("Copied %d nodes and %d edges to clipboard", nodes.length, edges.length); }); cutToClipboardAtom = atom21(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); debug13("Cut %d nodes \u2014 copied to clipboard and deleted from graph", selectedIds.length); }); pasteFromClipboardAtom = atom21(null, (get, set, offset) => { const clipboard = get(clipboardAtom); if (!clipboard || clipboard.nodes.length === 0) { debug13("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 } }; debug13("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) { debug13("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() }; debug13("Pasting edge %s -> %s (from %s to %s)", edgeData.dbData.id, newEdgeId, newSourceId, newTargetId); set(addEdgeToLocalGraphAtom, newDbEdge); } set(clearSelectionAtom); set(addNodesToSelectionAtom, newNodeIds); debug13("Pasted %d nodes and %d edges", newNodeIds.length, clipboard.edges.length); return newNodeIds; }); duplicateSelectionAtom = atom21(null, (get, set) => { set(copyToClipboardAtom); return set(pasteFromClipboardAtom); }); clearClipboardAtom = atom21(null, (_get, set) => { set(clipboardAtom, null); debug13("Clipboard cleared"); }); } }); // src/core/spatial-index.ts var SpatialGrid; var init_spatial_index = __esm({ "src/core/spatial-index.ts"() { "use strict"; 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 import { atom as atom22 } from "jotai"; var VIRTUALIZATION_BUFFER, spatialIndexAtom, visibleBoundsAtom, visibleNodeKeysAtom, visibleEdgeKeysAtom, virtualizationMetricsAtom; var init_virtualization_store = __esm({ "src/core/virtualization-store.ts"() { "use strict"; init_graph_store(); init_graph_position(); init_graph_derived(); init_viewport_store(); init_settings_store(); init_group_store(); init_spatial_index(); init_perf(); init_settings_store(); VIRTUALIZATION_BUFFER = 200; spatialIndexAtom = atom22((get) => { get(graphUpdateVersionAtom); get(nodePositionUpdateCounterAtom); const graph = get(graphAtom); const grid = new SpatialGrid(500); graph.forEachNode((nodeId, attrs) => { const a = attrs; grid.insert(nodeId, a.x, a.y, a.width || 200, a.height || 100); }); return grid; }); visibleBoundsAtom = atom22((get) => { const viewport = get(viewportRectAtom); const pan = get(panAtom); const zoom = get(zoomAtom); if (!viewport || zoom === 0) { return null; } const buffer = VIRTUALIZATION_BUFFER; return { minX: (-buffer - pan.x) / zoom, minY: (-buffer - pan.y) / zoom, maxX: (viewport.width + buffer - pan.x) / zoom, maxY: (viewport.height + buffer - pan.y) / zoom }; }); visibleNodeKeysAtom = atom22((get) => { const end = canvasMark("virtualization-cull"); const enabled = get(virtualizationEnabledAtom); const allKeys = get(nodeKeysAtom); if (!enabled) { end(); return allKeys; } const bounds = get(visibleBoundsAtom); if (!bounds) { end(); return allKeys; } const grid = get(spatialIndexAtom); const visibleSet = grid.query(bounds); const result = allKeys.filter((k) => visibleSet.has(k)); end(); return result; }); visibleEdgeKeysAtom = atom22((get) => { const enabled = get(virtualizationEnabledAtom); const allEdgeKeys = get(edgeKeysAtom); const edgeCreation = get(edgeCreationAtom); const remap = get(collapsedEdgeRemapAtom); const tempEdgeKey = edgeCreation.isCreating ? "temp-creating-edge" : null; get(graphUpdateVersionAtom); const graph = get(graphAtom); const filteredEdges = allEdgeKeys.filter((edgeKey) => { const source = graph.source(edgeKey); const target = graph.target(edgeKey); const effectiveSource = remap.get(source) ?? source; const effectiveTarget = remap.get(target) ?? target; if (effectiveSource === effectiveTarget) return false; return true; }); if (!enabled) { return tempEdgeKey ? [...filteredEdges, tempEdgeKey] : filteredEdges; } const visibleNodeKeys = get(visibleNodeKeysAtom); const visibleNodeSet = new Set(visibleNodeKeys); const visibleEdges = filteredEdges.filter((edgeKey) => { const source = graph.source(edgeKey); const target = graph.target(edgeKey); const effectiveSource = remap.get(source) ?? source; const effectiveTarget = remap.get(target) ?? target; return visibleNodeSet.has(effectiveSource) && visibleNodeSet.has(effectiveTarget); }); return tempEdgeKey ? [...visibleEdges, tempEdgeKey] : visibleEdges; }); virtualizationMetricsAtom = atom22((get) => { const enabled = get(virtualizationEnabledAtom); const totalNodes = get(nodeKeysAtom).length; const totalEdges = get(edgeKeysAtom).length; const visibleNodes = get(visibleNodeKeysAtom).length; const visibleEdges = get(visibleEdgeKeysAtom).length; const bounds = get(visibleBoundsAtom); return { enabled, totalNodes, totalEdges, visibleNodes, visibleEdges, culledNodes: totalNodes - visibleNodes, culledEdges: totalEdges - visibleEdges, bounds }; }); } }); // src/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; } var init_canvas_api = __esm({ "src/core/canvas-api.ts"() { "use strict"; init_action_executor(); init_canvas_serializer(); init_settings_store(); init_selection_store(); init_viewport_store(); init_graph_store(); init_graph_derived(); init_graph_mutations(); init_graph_mutations_edges(); init_history_store(); init_clipboard_store(); init_snap_store(); init_virtualization_store(); init_layout(); } }); // 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 }; } } 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; } var DEFAULT_PORT; var init_port_types = __esm({ "src/core/port-types.ts"() { "use strict"; DEFAULT_PORT = { id: "default", type: "bidirectional", side: "right", position: 0.5 }; } }); // 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 }; } } function getHitTargetSize(source) { return HIT_TARGET_SIZES[source]; } var HIT_TARGET_SIZES; var init_input_classifier = __esm({ "src/core/input-classifier.ts"() { "use strict"; 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 }; } }); // src/core/input-store.ts import { atom as atom23 } from "jotai"; var activePointersAtom, primaryInputSourceAtom, inputCapabilitiesAtom, isStylusActiveAtom, isMultiTouchAtom, fingerCountAtom, isTouchDeviceAtom, pointerDownAtom, pointerUpAtom, clearPointersAtom; var init_input_store = __esm({ "src/core/input-store.ts"() { "use strict"; init_input_classifier(); activePointersAtom = atom23(/* @__PURE__ */ new Map()); primaryInputSourceAtom = atom23("mouse"); inputCapabilitiesAtom = atom23(detectInputCapabilities()); isStylusActiveAtom = atom23((get) => { const pointers = get(activePointersAtom); for (const [, pointer] of pointers) { if (pointer.source === "pencil") return true; } return false; }); isMultiTouchAtom = atom23((get) => { const pointers = get(activePointersAtom); let fingerCount = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") fingerCount++; } return fingerCount > 1; }); fingerCountAtom = atom23((get) => { const pointers = get(activePointersAtom); let count = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") count++; } return count; }); isTouchDeviceAtom = atom23((get) => { const caps = get(inputCapabilitiesAtom); return caps.hasTouch; }); pointerDownAtom = atom23(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 }); } } }); pointerUpAtom = atom23(null, (get, set, pointerId) => { const pointers = new Map(get(activePointersAtom)); pointers.delete(pointerId); set(activePointersAtom, pointers); }); clearPointersAtom = atom23(null, (_get, set) => { set(activePointersAtom, /* @__PURE__ */ new Map()); }); } }); // src/core/selection-path-store.ts import { atom as atom24 } from "jotai"; 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; } var selectionPathAtom, isSelectingAtom, startSelectionAtom, updateSelectionAtom, cancelSelectionAtom, endSelectionAtom, selectionRectAtom; var init_selection_path_store = __esm({ "src/core/selection-path-store.ts"() { "use strict"; init_graph_derived(); init_selection_store(); selectionPathAtom = atom24(null); isSelectingAtom = atom24((get) => get(selectionPathAtom) !== null); startSelectionAtom = atom24(null, (_get, set, { type, point }) => { set(selectionPathAtom, { type, points: [point] }); }); updateSelectionAtom = atom24(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] }); } }); cancelSelectionAtom = atom24(null, (_get, set) => { set(selectionPathAtom, null); }); endSelectionAtom = atom24(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); }); selectionRectAtom = atom24((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) }; }); } }); // src/core/search-store.ts var search_store_exports = {}; __export(search_store_exports, { clearSearchAtom: () => clearSearchAtom, fuzzyMatch: () => fuzzyMatch, highlightedSearchIndexAtom: () => highlightedSearchIndexAtom, highlightedSearchNodeIdAtom: () => highlightedSearchNodeIdAtom, isFilterActiveAtom: () => isFilterActiveAtom, nextSearchResultAtom: () => nextSearchResultAtom, prevSearchResultAtom: () => prevSearchResultAtom, searchEdgeResultCountAtom: () => searchEdgeResultCountAtom, searchEdgeResultsAtom: () => searchEdgeResultsAtom, searchQueryAtom: () => searchQueryAtom, searchResultCountAtom: () => searchResultCountAtom, searchResultsArrayAtom: () => searchResultsArrayAtom, searchResultsAtom: () => searchResultsAtom, searchTotalResultCountAtom: () => searchTotalResultCountAtom, setSearchQueryAtom: () => setSearchQueryAtom }); import { atom as atom25 } from "jotai"; 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 searchQueryAtom, setSearchQueryAtom, clearSearchAtom, searchResultsAtom, searchResultsArrayAtom, searchResultCountAtom, searchEdgeResultsAtom, searchEdgeResultCountAtom, isFilterActiveAtom, searchTotalResultCountAtom, highlightedSearchIndexAtom, nextSearchResultAtom, prevSearchResultAtom, highlightedSearchNodeIdAtom; var init_search_store = __esm({ "src/core/search-store.ts"() { "use strict"; init_graph_derived(); init_graph_store(); init_viewport_store(); init_selection_store(); searchQueryAtom = atom25(""); setSearchQueryAtom = atom25(null, (_get, set, query) => { set(searchQueryAtom, query); set(highlightedSearchIndexAtom, 0); }); clearSearchAtom = atom25(null, (_get, set) => { set(searchQueryAtom, ""); set(highlightedSearchIndexAtom, 0); }); searchResultsAtom = atom25((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; }); searchResultsArrayAtom = atom25((get) => { return Array.from(get(searchResultsAtom)); }); searchResultCountAtom = atom25((get) => { return get(searchResultsAtom).size; }); searchEdgeResultsAtom = atom25((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; }); searchEdgeResultCountAtom = atom25((get) => { return get(searchEdgeResultsAtom).size; }); isFilterActiveAtom = atom25((get) => { return get(searchQueryAtom).trim().length > 0; }); searchTotalResultCountAtom = atom25((get) => { return get(searchResultCountAtom) + get(searchEdgeResultCountAtom); }); highlightedSearchIndexAtom = atom25(0); nextSearchResultAtom = atom25(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); }); prevSearchResultAtom = atom25(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); }); highlightedSearchNodeIdAtom = atom25((get) => { const results = get(searchResultsArrayAtom); if (results.length === 0) return null; const index = get(highlightedSearchIndexAtom); return results[index] ?? null; }); } }); // src/core/gesture-resolver.ts var init_gesture_resolver = __esm({ "src/core/gesture-resolver.ts"() { "use strict"; } }); // src/core/gesture-rules-defaults.ts 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 MODIFIER_KEYS, SOURCE_LABELS, GESTURE_LABELS, TARGET_LABELS, BUTTON_LABELS, DEFAULT_GESTURE_RULES; var init_gesture_rules_defaults = __esm({ "src/core/gesture-rules-defaults.ts"() { "use strict"; MODIFIER_KEYS = ["shift", "ctrl", "alt", "meta"]; SOURCE_LABELS = { mouse: "Mouse", pencil: "Pencil", finger: "Touch" }; 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" }; TARGET_LABELS = { node: "node", edge: "edge", port: "port", "resize-handle": "resize handle", background: "background" }; BUTTON_LABELS = { 0: "Left", 1: "Middle", 2: "Right" }; 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 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; } 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); } var MODIFIER_KEYS2, PALM_REJECTION_RULE; var init_gesture_rules = __esm({ "src/core/gesture-rules.ts"() { "use strict"; init_gesture_rules_defaults(); MODIFIER_KEYS2 = ["shift", "ctrl", "alt", "meta"]; PALM_REJECTION_RULE = { id: "__palm-rejection__", pattern: {}, actionId: "none", label: "Palm rejection" }; } }); // src/core/gesture-rule-store.ts import { atom as atom26 } from "jotai"; import { atomWithStorage as atomWithStorage3 } from "jotai/utils"; var DEFAULT_RULE_STATE, gestureRuleSettingsAtom, consumerGestureRulesAtom, gestureRulesAtom, gestureRuleIndexAtom, palmRejectionEnabledAtom, addGestureRuleAtom, removeGestureRuleAtom, updateGestureRuleAtom, resetGestureRulesAtom; var init_gesture_rule_store = __esm({ "src/core/gesture-rule-store.ts"() { "use strict"; init_gesture_rules(); DEFAULT_RULE_STATE = { customRules: [], palmRejection: true }; gestureRuleSettingsAtom = atomWithStorage3("canvas-gesture-rules", DEFAULT_RULE_STATE); consumerGestureRulesAtom = atom26([]); gestureRulesAtom = atom26((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; }); gestureRuleIndexAtom = atom26((get) => { return buildRuleIndex(get(gestureRulesAtom)); }); palmRejectionEnabledAtom = atom26((get) => get(gestureRuleSettingsAtom).palmRejection, (get, set, enabled) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, palmRejection: enabled }); }); addGestureRuleAtom = atom26(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 }); }); removeGestureRuleAtom = atom26(null, (get, set, ruleId) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: current.customRules.filter((r) => r.id !== ruleId) }); }); updateGestureRuleAtom = atom26(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 }); }); resetGestureRulesAtom = atom26(null, (get, set) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: [] }); }); } }); // src/core/external-keyboard-store.ts import { atom as atom27 } from "jotai"; var hasExternalKeyboardAtom, watchExternalKeyboardAtom; var init_external_keyboard_store = __esm({ "src/core/external-keyboard-store.ts"() { "use strict"; hasExternalKeyboardAtom = atom27(false); watchExternalKeyboardAtom = atom27(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; var init_plugin_types = __esm({ "src/core/plugin-types.ts"() { "use strict"; 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, NO_HELD_KEYS; var init_types2 = __esm({ "src/gestures/types.ts"() { "use strict"; NO_MODIFIERS = Object.freeze({ shift: false, ctrl: false, alt: false, meta: false }); NO_HELD_KEYS = Object.freeze({ byKey: Object.freeze({}), byCode: Object.freeze({}) }); } }); // src/gestures/dispatcher.ts function registerAction2(actionId, handler) { handlers.set(actionId, handler); } function unregisterAction2(actionId) { handlers.delete(actionId); } var handlers; var init_dispatcher = __esm({ "src/gestures/dispatcher.ts"() { "use strict"; init_types2(); handlers = /* @__PURE__ */ new Map(); } }); // src/utils/edge-path-calculators.ts var init_edge_path_calculators = __esm({ "src/utils/edge-path-calculators.ts"() { "use strict"; } }); // src/utils/edge-path-registry.ts function registerEdgePathCalculator(name, calculator) { customCalculators.set(name, calculator); } function unregisterEdgePathCalculator(name) { return customCalculators.delete(name); } var customCalculators; var init_edge_path_registry = __esm({ "src/utils/edge-path-registry.ts"() { "use strict"; init_edge_path_calculators(); customCalculators = /* @__PURE__ */ new Map(); } }); // src/core/plugin-registry.ts function registerPlugin(plugin) { debug14("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() }); debug14("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); debug14("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); } debug14("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 }; } var debug14, plugins; var init_plugin_registry = __esm({ "src/core/plugin-registry.ts"() { "use strict"; init_plugin_types(); init_node_type_registry(); init_action_registry(); init_dispatcher(); init_registry(); init_edge_path_registry(); init_debug(); debug14 = createDebug("plugins"); plugins = /* @__PURE__ */ new Map(); } }); // src/core/index.ts var core_exports = {}; __export(core_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: () => inputModeAtom2, 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: () => pendingInputResolverAtom2, pendingMutationsCountAtom: () => pendingMutationsCountAtom, perfEnabledAtom: () => perfEnabledAtom, pointInPolygon: () => pointInPolygon, pointerDownAtom: () => pointerDownAtom, pointerUpAtom: () => pointerUpAtom, preDragNodeAttributesAtom: () => preDragNodeAttributesAtom, prefersReducedMotionAtom: () => prefersReducedMotionAtom, prevLockedPageAtom: () => prevLockedPageAtom, prevSearchResultAtom: () => prevSearchResultAtom, primaryInputSourceAtom: () => primaryInputSourceAtom, provideInputAtom: () => provideInputAtom2, 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 }); var init_core = __esm({ "src/core/index.ts"() { "use strict"; init_types(); init_graph_store(); init_graph_position(); init_graph_derived(); init_graph_mutations(); init_viewport_store(); init_selection_store(); init_sync_store(); init_interaction_store(); init_locked_node_store(); init_node_type_registry(); init_history_store(); init_toast_store(); init_snap_store(); init_settings_types(); init_action_registry(); init_action_executor(); init_settings_store(); init_canvas_api(); init_virtualization_store(); init_port_types(); init_clipboard_store(); init_input_classifier(); init_input_store(); init_selection_path_store(); init_group_store(); init_search_store(); init_gesture_resolver(); init_gesture_rules(); init_gesture_rule_store(); init_reduced_motion_store(); init_external_keyboard_store(); init_perf(); init_spatial_index(); init_plugin_types(); init_plugin_registry(); init_canvas_serializer(); } }); // src/commands/index.ts init_registry(); // src/commands/store.ts init_registry(); import { atom as atom2 } from "jotai"; // src/commands/store-atoms.ts import { atom } from "jotai"; import { atomWithStorage } from "jotai/utils"; var inputModeAtom = atom({ type: "normal" }); var commandLineVisibleAtom = atom(false); var commandLineStateAtom = atom({ phase: "idle" }); var commandFeedbackAtom = atom(null); var commandHistoryAtom = atomWithStorage("canvas-command-history", []); var selectedSuggestionIndexAtom = atom(0); var pendingInputResolverAtom = atom(null); var isCommandActiveAtom = atom((get) => { const state = get(commandLineStateAtom); return state.phase === "collecting" || state.phase === "executing"; }); var currentInputAtom = atom((get) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return null; return state.command.inputs[state.inputIndex]; }); var commandProgressAtom = atom((get) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return null; return { current: state.inputIndex + 1, total: state.command.inputs.length }; }); // src/commands/store.ts var openCommandLineAtom = atom2(null, (get, set) => { set(commandLineVisibleAtom, true); set(commandLineStateAtom, { phase: "searching", query: "", suggestions: commandRegistry.all() }); set(selectedSuggestionIndexAtom, 0); }); var closeCommandLineAtom = atom2(null, (get, set) => { set(commandLineVisibleAtom, false); set(commandLineStateAtom, { phase: "idle" }); set(inputModeAtom, { type: "normal" }); set(commandFeedbackAtom, null); set(pendingInputResolverAtom, null); }); var updateSearchQueryAtom = atom2(null, (get, set, query) => { const suggestions = commandRegistry.search(query); set(commandLineStateAtom, { phase: "searching", query, suggestions }); set(selectedSuggestionIndexAtom, 0); }); var selectCommandAtom = atom2(null, (get, set, command) => { const history = get(commandHistoryAtom); const newHistory = [command.name, ...history.filter((h) => h !== command.name)].slice(0, 50); set(commandHistoryAtom, newHistory); if (command.inputs.length === 0) { set(commandLineStateAtom, { phase: "executing", command }); return; } set(commandLineStateAtom, { phase: "collecting", command, inputIndex: 0, collected: {} }); const firstInput = command.inputs[0]; set(inputModeAtom, inputDefToMode(firstInput)); }); var provideInputAtom = atom2(null, (get, set, value) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return; const { command, inputIndex, collected } = state; const currentInput = command.inputs[inputIndex]; if (currentInput.validate) { const result = currentInput.validate(value, collected); if (result !== true) { set(commandLineStateAtom, { phase: "error", message: typeof result === "string" ? result : `Invalid value for ${currentInput.name}` }); return; } } const newCollected = { ...collected, [currentInput.name]: value }; if (inputIndex < command.inputs.length - 1) { const nextInputIndex = inputIndex + 1; const nextInput = command.inputs[nextInputIndex]; set(commandLineStateAtom, { phase: "collecting", command, inputIndex: nextInputIndex, collected: newCollected }); set(inputModeAtom, inputDefToMode(nextInput, newCollected)); if (command.feedback) { const feedback = command.feedback(newCollected, nextInput); if (feedback) { const feedbackState = { hoveredNodeId: feedback.highlightNodeId, ghostNode: feedback.ghostNode, crosshair: feedback.crosshair, // Handle previewEdge conversion - toCursor variant needs cursorWorldPos previewEdge: feedback.previewEdge && "to" in feedback.previewEdge ? { from: feedback.previewEdge.from, to: feedback.previewEdge.to } : void 0 }; set(commandFeedbackAtom, feedbackState); } else { set(commandFeedbackAtom, null); } } } else { set(commandLineStateAtom, { phase: "collecting", command, inputIndex, collected: newCollected }); set(inputModeAtom, { type: "normal" }); } }); var skipInputAtom = atom2(null, (get, set) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return; const { command, inputIndex } = state; const currentInput = command.inputs[inputIndex]; if (currentInput.required !== false) { return; } const value = currentInput.default; set(provideInputAtom, value); }); var goBackInputAtom = atom2(null, (get, set) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return; const { command, inputIndex, collected } = state; if (inputIndex === 0) { set(commandLineStateAtom, { phase: "searching", query: command.name, suggestions: [command] }); set(inputModeAtom, { type: "normal" }); return; } const prevInputIndex = inputIndex - 1; const prevInput = command.inputs[prevInputIndex]; const newCollected = { ...collected }; delete newCollected[prevInput.name]; set(commandLineStateAtom, { phase: "collecting", command, inputIndex: prevInputIndex, collected: newCollected }); set(inputModeAtom, inputDefToMode(prevInput, newCollected)); }); var setCommandErrorAtom = atom2(null, (get, set, message) => { set(commandLineStateAtom, { phase: "error", message }); set(inputModeAtom, { type: "normal" }); }); var clearCommandErrorAtom = atom2(null, (get, set) => { set(commandLineStateAtom, { phase: "idle" }); }); function inputDefToMode(input, collected) { switch (input.type) { case "point": return { type: "pickPoint", prompt: input.prompt, snapToGrid: input.snapToGrid }; case "node": return { type: "pickNode", prompt: input.prompt, filter: input.filter ? (node) => input.filter(node, collected || {}) : void 0 }; case "nodes": return { type: "pickNodes", prompt: input.prompt, filter: input.filter ? (node) => input.filter(node, collected || {}) : void 0 }; case "select": return { type: "select", prompt: input.prompt, options: input.options || [] }; case "text": case "number": case "color": case "boolean": default: return { type: "text", prompt: input.prompt }; } } // src/commands/keyboard.ts var DEFAULT_SHORTCUTS = { openCommandLine: "/", closeCommandLine: "Escape", clearSelection: "Escape", copy: "ctrl+c", cut: "ctrl+x", paste: "ctrl+v", duplicate: "ctrl+d", selectAll: "ctrl+a", delete: "Delete", search: "ctrl+f", nextSearchResult: "Enter", prevSearchResult: "shift+Enter", nextSearchResultAlt: "ctrl+g", prevSearchResultAlt: "ctrl+shift+g", mergeNodes: "ctrl+m" }; function useGlobalKeyboard(_options) { } function useKeyState(_key) { return false; } // src/commands/executor.ts function collectInput(_get, _set, _inputDef, _collected) { return Promise.reject(new Error("Interactive input collection is not yet implemented. Pre-fill all inputs via initialInputs.")); } async function executeCommandInteractive(get, set, command, initialInputs) { const collected = { ...initialInputs }; for (let i = 0; i < command.inputs.length; i++) { const inputDef = command.inputs[i]; if (collected[inputDef.name] !== void 0) { continue; } if (inputDef.required === false && inputDef.default !== void 0) { collected[inputDef.name] = inputDef.default; continue; } set(commandLineStateAtom, { phase: "collecting", command, inputIndex: i, collected }); const value = await collectInput(get, set, inputDef, collected); collected[inputDef.name] = value; } set(commandLineStateAtom, { phase: "executing", command }); } function handlePickedPoint(set, point) { set(provideInputAtom, point); } function handlePickedNode(set, node) { set(provideInputAtom, node); } function cancelCommand(set) { set(closeCommandLineAtom); } // src/commands/CommandProvider.tsx init_graph_store(); init_selection_store(); init_viewport_store(); init_history_store(); import { c as _c2 } from "react/compiler-runtime"; import React, { createContext, useContext, useEffect, useRef as useRef5 } from "react"; import { useAtomValue as useAtomValue6, useSetAtom as useSetAtom4, useAtom } from "jotai"; import { useStore } from "jotai"; init_registry(); // src/hooks/useLayout.ts init_graph_store(); init_graph_position(); init_graph_derived(); init_viewport_store(); init_selection_store(); init_layout(); init_layout(); import { c as _c } from "react/compiler-runtime"; import { useAtomValue, useSetAtom } from "jotai"; var useFitToBounds = () => { const $ = _c(2); const setFitToBounds = useSetAtom(fitToBoundsAtom); let t0; if ($[0] !== setFitToBounds) { const fitToBounds = (mode, t1) => { const padding = t1 === void 0 ? 20 : t1; setFitToBounds({ mode, padding }); }; t0 = { fitToBounds }; $[0] = setFitToBounds; $[1] = t0; } else { t0 = $[1]; } return t0; }; // src/hooks/useForceLayout.ts init_graph_store(); init_graph_position(); init_graph_derived(); init_layout(); init_debug(); import * as d3 from "d3-force"; import { useAtomValue as useAtomValue2, useSetAtom as useSetAtom2 } from "jotai"; import { useRef } from "react"; var debug5 = createDebug("force-layout"); var useForceLayout = (options = {}) => { const { onPositionsChanged, maxIterations = 1e3, chargeStrength = -6e3, linkStrength = 0.03 } = options; const nodes = useAtomValue2(uiNodesAtom); const graph = useAtomValue2(graphAtom); const updateNodePosition = useSetAtom2(updateNodePositionAtom); const isRunningRef = useRef(false); const createVirtualLinks = () => { const links = []; for (let i = 0; i < nodes.length; i++) { for (let j = 1; j <= 3; j++) { const targetIndex = (i + j) % nodes.length; links.push({ source: nodes[i].id, target: nodes[targetIndex].id, strength: 0.05 // Very weak connection }); } } return links; }; const applyForceLayout = async () => { if (isRunningRef.current) { debug5.warn("Layout already running, ignoring request."); return; } if (nodes.length === 0) { debug5.warn("No nodes to layout."); return; } isRunningRef.current = true; const simulationNodes = nodes.map((node) => { const width = node.width || 80; const height = node.height || 80; return { id: node.id, x: node.position?.x || 0, y: node.position?.y || 0, width, height, radius: Math.max(width, height) + 80 }; }); const simulation = d3.forceSimulation(simulationNodes).force("charge", d3.forceManyBody().strength(chargeStrength).distanceMax(900)).force("collide", d3.forceCollide().radius((d) => d.radius).strength(2).iterations(8)).force("link", d3.forceLink(createVirtualLinks()).id((d_0) => d_0.id).strength(linkStrength)).force("center", d3.forceCenter(0, 0)).stop(); debug5("Starting simulation..."); return new Promise((resolve) => { let iterations = 0; function runSimulationStep() { if (iterations >= maxIterations) { debug5("Reached max iterations (%d), finalizing.", maxIterations); finalizeLayout(); return; } simulation.tick(); iterations++; let hasOverlaps = false; for (let i_0 = 0; i_0 < simulationNodes.length; i_0++) { for (let j_0 = i_0 + 1; j_0 < simulationNodes.length; j_0++) { if (checkNodesOverlap(simulationNodes[i_0], simulationNodes[j_0])) { hasOverlaps = true; break; } } if (hasOverlaps) break; } if (!hasOverlaps) { debug5("No overlaps after %d iterations.", iterations); finalizeLayout(); return; } requestAnimationFrame(runSimulationStep); } function finalizeLayout() { const positionUpdates = []; simulationNodes.forEach((simNode) => { if (graph.hasNode(simNode.id)) { const newPosition = { x: Math.round(simNode.x), y: Math.round(simNode.y) }; updateNodePosition({ nodeId: simNode.id, position: newPosition }); positionUpdates.push({ nodeId: simNode.id, position: newPosition }); } }); if (onPositionsChanged && positionUpdates.length > 0) { debug5("Saving %d positions via callback...", positionUpdates.length); Promise.resolve(onPositionsChanged(positionUpdates)).then(() => debug5("Positions saved successfully.")).catch((err) => debug5.error("Error saving positions: %O", err)); } debug5("Layout complete."); isRunningRef.current = false; resolve(); } requestAnimationFrame(runSimulationStep); }); }; return { applyForceLayout, isRunning: isRunningRef.current }; }; // src/hooks/useTreeLayout.ts init_graph_store(); init_graph_derived(); import { useAtomValue as useAtomValue4 } from "jotai"; import { useRef as useRef3 } from "react"; // src/hooks/useAnimatedLayout.ts init_graph_store(); init_graph_position(); init_history_store(); init_debug(); init_reduced_motion_store(); import { useAtomValue as useAtomValue3, useSetAtom as useSetAtom3 } from "jotai"; import { useRef as useRef2 } from "react"; var debug6 = createDebug("animated-layout"); function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } function useAnimatedLayout(options = {}) { const { onPositionsChanged, duration = 400 } = options; const graph = useAtomValue3(graphAtom); const updateNodePosition = useSetAtom3(updateNodePositionAtom); const pushHistory = useSetAtom3(pushHistoryAtom); const setPositionCounter = useSetAtom3(nodePositionUpdateCounterAtom); const reducedMotion = useAtomValue3(prefersReducedMotionAtom); const isAnimatingRef = useRef2(false); const animate = async (targets, label) => { if (isAnimatingRef.current) return; if (targets.size === 0) return; if (label) pushHistory(label); isAnimatingRef.current = true; if (reducedMotion) { for (const [nodeId, target] of targets) { updateNodePosition({ nodeId, position: target }); } isAnimatingRef.current = false; setPositionCounter((c) => c + 1); if (onPositionsChanged) { const updates = []; for (const [nodeId_0, target_0] of targets) { updates.push({ nodeId: nodeId_0, position: target_0 }); } Promise.resolve(onPositionsChanged(updates)).catch((err) => debug6.error("Position change callback failed: %O", err)); } return; } const starts = /* @__PURE__ */ new Map(); for (const [nodeId_1] of targets) { if (graph.hasNode(nodeId_1)) { const attrs = graph.getNodeAttributes(nodeId_1); starts.set(nodeId_1, { x: attrs.x, y: attrs.y }); } } return new Promise((resolve) => { const startTime = performance.now(); function tick() { const elapsed = performance.now() - startTime; const t = Math.min(elapsed / duration, 1); const eased = easeInOutCubic(t); for (const [nodeId_2, target_1] of targets) { const start = starts.get(nodeId_2); if (!start) continue; const x = Math.round(start.x + (target_1.x - start.x) * eased); const y = Math.round(start.y + (target_1.y - start.y) * eased); updateNodePosition({ nodeId: nodeId_2, position: { x, y } }); } if (t < 1) { requestAnimationFrame(tick); } else { isAnimatingRef.current = false; setPositionCounter((c_0) => c_0 + 1); if (onPositionsChanged) { const updates_0 = []; for (const [nodeId_3, target_2] of targets) { updates_0.push({ nodeId: nodeId_3, position: target_2 }); } Promise.resolve(onPositionsChanged(updates_0)).catch((err_0) => debug6.error("Position change callback failed: %O", err_0)); } resolve(); } } requestAnimationFrame(tick); }); }; return { animate, isAnimating: isAnimatingRef.current }; } // src/hooks/useTreeLayout.ts function useTreeLayout(options = {}) { const { direction = "top-down", levelGap = 200, nodeGap = 100, ...animateOptions } = options; const graph = useAtomValue4(graphAtom); const nodes = useAtomValue4(uiNodesAtom); const { animate, isAnimating } = useAnimatedLayout(animateOptions); const isRunningRef = useRef3(false); const applyLayout = async () => { if (isRunningRef.current || isAnimating) return; if (nodes.length === 0) return; isRunningRef.current = true; const nodeIds = new Set(nodes.map((n) => n.id)); const children = /* @__PURE__ */ new Map(); const hasIncoming = /* @__PURE__ */ new Set(); for (const nodeId of nodeIds) { children.set(nodeId, []); } graph.forEachEdge((_key, _attrs, source, target) => { if (nodeIds.has(source) && nodeIds.has(target) && source !== target) { children.get(source)?.push(target); hasIncoming.add(target); } }); const roots = [...nodeIds].filter((id) => !hasIncoming.has(id)); if (roots.length === 0) { roots.push(nodes[0].id); } const levels = /* @__PURE__ */ new Map(); const queue = [...roots]; for (const r of roots) levels.set(r, 0); while (queue.length > 0) { const current = queue.shift(); const level = levels.get(current); for (const child of children.get(current) || []) { if (!levels.has(child)) { levels.set(child, level + 1); queue.push(child); } } } for (const nodeId_0 of nodeIds) { if (!levels.has(nodeId_0)) levels.set(nodeId_0, 0); } const byLevel = /* @__PURE__ */ new Map(); for (const [nodeId_1, level_0] of levels) { if (!byLevel.has(level_0)) byLevel.set(level_0, []); byLevel.get(level_0).push(nodeId_1); } const targets = /* @__PURE__ */ new Map(); const maxLevel = Math.max(...byLevel.keys()); for (const [level_1, nodeIdsAtLevel] of byLevel) { const count = nodeIdsAtLevel.length; let maxNodeSize = 200; for (const nid of nodeIdsAtLevel) { if (graph.hasNode(nid)) { const attrs = graph.getNodeAttributes(nid); maxNodeSize = Math.max(maxNodeSize, attrs.width || 200); } } const totalWidth = (count - 1) * (maxNodeSize + nodeGap); const startX = -totalWidth / 2; for (let i = 0; i < count; i++) { const primary = level_1 * levelGap; const secondary = startX + i * (maxNodeSize + nodeGap); if (direction === "top-down") { targets.set(nodeIdsAtLevel[i], { x: secondary, y: primary }); } else { targets.set(nodeIdsAtLevel[i], { x: primary, y: secondary }); } } } await animate(targets, direction === "top-down" ? "Tree layout" : "Horizontal layout"); isRunningRef.current = false; }; return { applyLayout, isRunning: isRunningRef.current || isAnimating }; } // src/hooks/useGridLayout.ts init_graph_store(); init_graph_derived(); import { useAtomValue as useAtomValue5 } from "jotai"; import { useRef as useRef4 } from "react"; function useGridLayout(options = {}) { const { columns, gap = 80, ...animateOptions } = options; const graph = useAtomValue5(graphAtom); const nodes = useAtomValue5(uiNodesAtom); const { animate, isAnimating } = useAnimatedLayout(animateOptions); const isRunningRef = useRef4(false); const applyLayout = async () => { if (isRunningRef.current || isAnimating) return; if (nodes.length === 0) return; isRunningRef.current = true; const sorted = [...nodes].sort((a, b) => { const ay = a.position?.y ?? 0; const by = b.position?.y ?? 0; if (Math.abs(ay - by) > 50) return ay - by; return (a.position?.x ?? 0) - (b.position?.x ?? 0); }); const cols = columns ?? Math.ceil(Math.sqrt(sorted.length)); let maxW = 200; let maxH = 100; for (const node of sorted) { if (graph.hasNode(node.id)) { const attrs = graph.getNodeAttributes(node.id); maxW = Math.max(maxW, attrs.width || 200); maxH = Math.max(maxH, attrs.height || 100); } } const cellW = maxW + gap; const cellH = maxH + gap; const rows = Math.ceil(sorted.length / cols); const totalW = (cols - 1) * cellW; const totalH = (rows - 1) * cellH; const offsetX = -totalW / 2; const offsetY = -totalH / 2; const targets = /* @__PURE__ */ new Map(); for (let i = 0; i < sorted.length; i++) { const col = i % cols; const row = Math.floor(i / cols); targets.set(sorted[i].id, { x: Math.round(offsetX + col * cellW), y: Math.round(offsetY + row * cellH) }); } await animate(targets, "Grid layout"); isRunningRef.current = false; }; return { applyLayout, isRunning: isRunningRef.current || isAnimating }; } // src/commands/CommandProvider.tsx import { jsx as _jsx } from "react/jsx-runtime"; var CommandContextContext = /* @__PURE__ */ createContext(null); function CommandProvider(t0) { const $ = _c2(52); const { children, onCreateNode, onUpdateNode, onDeleteNode, onCreateEdge, onDeleteEdge, onForceLayoutPersist } = t0; const store = useStore(); const currentGraphId = useAtomValue6(currentGraphIdAtom); const selectedNodeIds = useAtomValue6(selectedNodeIdsAtom); const zoom = useAtomValue6(zoomAtom); const pan = useAtomValue6(panAtom); const undo = useSetAtom4(undoAtom); const redo = useSetAtom4(redoAtom); const { fitToBounds } = useFitToBounds(); let t1; if ($[0] !== onForceLayoutPersist) { t1 = onForceLayoutPersist ? async (updates) => { await onForceLayoutPersist(updates.map(_temp)); } : void 0; $[0] = onForceLayoutPersist; $[1] = t1; } else { t1 = $[1]; } const persistCallback = t1; let t2; if ($[2] !== persistCallback) { t2 = { onPositionsChanged: persistCallback }; $[2] = persistCallback; $[3] = t2; } else { t2 = $[3]; } const { applyForceLayout } = useForceLayout(t2); let t3; if ($[4] !== persistCallback) { t3 = { onPositionsChanged: persistCallback }; $[4] = persistCallback; $[5] = t3; } else { t3 = $[5]; } const { applyLayout: applyTreeLayoutTopDown } = useTreeLayout(t3); let t4; if ($[6] !== persistCallback) { t4 = { direction: "left-right", onPositionsChanged: persistCallback }; $[6] = persistCallback; $[7] = t4; } else { t4 = $[7]; } const { applyLayout: applyTreeLayoutLeftRight } = useTreeLayout(t4); let t5; if ($[8] !== persistCallback) { t5 = { onPositionsChanged: persistCallback }; $[8] = persistCallback; $[9] = t5; } else { t5 = $[9]; } const { applyLayout: applyGridLayoutDefault } = useGridLayout(t5); const closeCommandLine = useSetAtom4(closeCommandLineAtom); const setCommandError = useSetAtom4(setCommandErrorAtom); let t6; if ($[10] !== applyForceLayout || $[11] !== applyGridLayoutDefault || $[12] !== applyTreeLayoutLeftRight || $[13] !== applyTreeLayoutTopDown || $[14] !== currentGraphId || $[15] !== fitToBounds || $[16] !== onCreateEdge || $[17] !== onCreateNode || $[18] !== onDeleteEdge || $[19] !== onDeleteNode || $[20] !== onUpdateNode || $[21] !== pan || $[22] !== redo || $[23] !== selectedNodeIds || $[24] !== store.get || $[25] !== store.set || $[26] !== undo || $[27] !== zoom) { t6 = () => ({ get: store.get, set: store.set, currentGraphId, selectedNodeIds, viewport: { zoom, pan }, mutations: { createNode: async (payload) => { if (!onCreateNode) { throw new Error("onCreateNode callback not provided to CommandProvider"); } return onCreateNode(payload); }, updateNode: async (nodeId, updates_0) => { if (!onUpdateNode) { throw new Error("onUpdateNode callback not provided to CommandProvider"); } return onUpdateNode(nodeId, updates_0); }, deleteNode: async (nodeId_0) => { if (!onDeleteNode) { throw new Error("onDeleteNode callback not provided to CommandProvider"); } return onDeleteNode(nodeId_0); }, createEdge: async (payload_0) => { if (!onCreateEdge) { throw new Error("onCreateEdge callback not provided to CommandProvider"); } return onCreateEdge(payload_0); }, deleteEdge: async (edgeId) => { if (!onDeleteEdge) { throw new Error("onDeleteEdge callback not provided to CommandProvider"); } return onDeleteEdge(edgeId); } }, layout: { fitToBounds: (mode, padding) => { const fitMode = mode === "graph" ? FitToBoundsMode.Graph : FitToBoundsMode.Selection; fitToBounds(fitMode, padding); }, applyForceLayout, applyTreeLayout: async (opts) => { if (opts?.direction === "left-right") { await applyTreeLayoutLeftRight(); } else { await applyTreeLayoutTopDown(); } }, applyGridLayout: async () => { await applyGridLayoutDefault(); } }, history: { undo, redo } }); $[10] = applyForceLayout; $[11] = applyGridLayoutDefault; $[12] = applyTreeLayoutLeftRight; $[13] = applyTreeLayoutTopDown; $[14] = currentGraphId; $[15] = fitToBounds; $[16] = onCreateEdge; $[17] = onCreateNode; $[18] = onDeleteEdge; $[19] = onDeleteNode; $[20] = onUpdateNode; $[21] = pan; $[22] = redo; $[23] = selectedNodeIds; $[24] = store.get; $[25] = store.set; $[26] = undo; $[27] = zoom; $[28] = t6; } else { t6 = $[28]; } const getContext = t6; let t7; if ($[29] !== closeCommandLine || $[30] !== getContext || $[31] !== setCommandError) { t7 = async (commandName, inputs) => { const command = commandRegistry.get(commandName); if (!command) { throw new Error(`Unknown command: ${commandName}`); } for (const inputDef of command.inputs) { if (inputDef.required !== false && inputs[inputDef.name] === void 0) { throw new Error(`Missing required input: ${inputDef.name}`); } } const ctx = getContext(); ; try { await command.execute(inputs, ctx); closeCommandLine(); } catch (t82) { const error = t82; const message = error instanceof Error ? error.message : "Command execution failed"; setCommandError(message); throw error; } }; $[29] = closeCommandLine; $[30] = getContext; $[31] = setCommandError; $[32] = t7; } else { t7 = $[32]; } const executeCommand = t7; const hasCommand = _temp2; const [commandState, setCommandState] = useAtom(commandLineStateAtom); const isExecutingRef = useRef5(false); let t8; if ($[33] !== commandState || $[34] !== getContext || $[35] !== setCommandError || $[36] !== setCommandState || $[37] !== store) { t8 = () => { if (commandState.phase === "executing" && !isExecutingRef.current) { const { command: command_0 } = commandState; if (command_0.inputs.length === 0) { isExecutingRef.current = true; const ctx_0 = getContext(); command_0.execute({}, ctx_0).then(() => { setCommandState({ phase: "searching", query: "", suggestions: commandRegistry.all() }); }).catch((error_0) => { const message_0 = error_0 instanceof Error ? error_0.message : "Command execution failed"; setCommandError(message_0); }).finally(() => { isExecutingRef.current = false; }); } return; } if (commandState.phase !== "collecting") { isExecutingRef.current = false; return; } const { command: command_1, inputIndex, collected } = commandState; const lastInputIndex = command_1.inputs.length - 1; const lastInput = command_1.inputs[lastInputIndex]; if (inputIndex === lastInputIndex && collected[lastInput.name] !== void 0 && !isExecutingRef.current) { isExecutingRef.current = true; setCommandState({ phase: "executing", command: command_1 }); const ctx_1 = getContext(); command_1.execute(collected, ctx_1).then(() => { setCommandState({ phase: "searching", query: "", suggestions: commandRegistry.all() }); store.set(inputModeAtom, { type: "normal" }); store.set(commandFeedbackAtom, null); }).catch((error_1) => { const message_1 = error_1 instanceof Error ? error_1.message : "Command execution failed"; setCommandError(message_1); }).finally(() => { isExecutingRef.current = false; }); } }; $[33] = commandState; $[34] = getContext; $[35] = setCommandError; $[36] = setCommandState; $[37] = store; $[38] = t8; } else { t8 = $[38]; } let t9; if ($[39] !== closeCommandLine || $[40] !== commandState || $[41] !== getContext || $[42] !== setCommandError || $[43] !== setCommandState || $[44] !== store) { t9 = [commandState, getContext, closeCommandLine, setCommandError, setCommandState, store]; $[39] = closeCommandLine; $[40] = commandState; $[41] = getContext; $[42] = setCommandError; $[43] = setCommandState; $[44] = store; $[45] = t9; } else { t9 = $[45]; } useEffect(t8, t9); let t10; if ($[46] !== executeCommand || $[47] !== getContext) { t10 = { executeCommand, getContext, hasCommand }; $[46] = executeCommand; $[47] = getContext; $[48] = t10; } else { t10 = $[48]; } const value = t10; let t11; if ($[49] !== children || $[50] !== value) { t11 = /* @__PURE__ */ _jsx(CommandContextContext, { value, children }); $[49] = children; $[50] = value; $[51] = t11; } else { t11 = $[51]; } return t11; } function _temp2(name) { return commandRegistry.has(name); } function _temp(u) { return { nodeId: u.nodeId, x: u.position.x, y: u.position.y }; } function useCommandContext() { const context = useContext(CommandContextContext); if (!context) { throw new Error("useCommandContext must be used within a CommandProvider"); } return context; } function useExecuteCommand() { const { executeCommand } = useCommandContext(); return executeCommand; } // src/commands/builtins/viewport-commands.ts init_registry(); var fitToViewCommand = { name: "fitToView", description: "Fit all nodes in the viewport", aliases: ["fit", "fitAll"], category: "viewport", inputs: [], execute: async (_inputs, ctx) => { ctx.layout.fitToBounds("graph"); } }; var fitSelectionCommand = { name: "fitSelection", description: "Fit selected nodes in the viewport", aliases: ["fitSel"], category: "viewport", inputs: [], execute: async (_inputs, ctx) => { if (ctx.selectedNodeIds.size === 0) { throw new Error("No nodes selected"); } ctx.layout.fitToBounds("selection"); } }; var resetViewportCommand = { name: "resetViewport", description: "Reset zoom and pan to default", aliases: ["reset", "home"], category: "viewport", inputs: [], execute: async (_inputs, ctx) => { const { resetViewportAtom: resetViewportAtom2 } = await Promise.resolve().then(() => (init_viewport_store(), viewport_store_exports)); ctx.set(resetViewportAtom2); } }; var zoomInCommand = { name: "zoomIn", description: "Zoom in on the canvas", aliases: ["+", "in"], category: "viewport", inputs: [], execute: async (_inputs, ctx) => { const { zoomAtom: zoomAtom2, setZoomAtom: setZoomAtom2 } = await Promise.resolve().then(() => (init_viewport_store(), viewport_store_exports)); const currentZoom = ctx.get(zoomAtom2); ctx.set(setZoomAtom2, { zoom: Math.min(5, currentZoom * 1.25) }); } }; var zoomOutCommand = { name: "zoomOut", description: "Zoom out on the canvas", aliases: ["-", "out"], category: "viewport", inputs: [], execute: async (_inputs, ctx) => { const { zoomAtom: zoomAtom2, setZoomAtom: setZoomAtom2 } = await Promise.resolve().then(() => (init_viewport_store(), viewport_store_exports)); const currentZoom = ctx.get(zoomAtom2); ctx.set(setZoomAtom2, { zoom: Math.max(0.1, currentZoom / 1.25) }); } }; function registerViewportCommands() { registerCommand(fitToViewCommand); registerCommand(fitSelectionCommand); registerCommand(resetViewportCommand); registerCommand(zoomInCommand); registerCommand(zoomOutCommand); } // src/commands/builtins/selection-commands.ts init_registry(); var selectAllCommand = { name: "selectAll", description: "Select all nodes in the graph", aliases: ["all"], category: "selection", inputs: [], execute: async (_inputs, ctx) => { const { nodeKeysAtom: nodeKeysAtom2, selectedNodeIdsAtom: selectedNodeIdsAtom2 } = await Promise.resolve().then(() => (init_core(), core_exports)); const nodeKeys = ctx.get(nodeKeysAtom2); ctx.set(selectedNodeIdsAtom2, new Set(nodeKeys)); } }; var clearSelectionCommand = { name: "clearSelection", description: "Clear all selection", aliases: ["deselect", "clear"], category: "selection", inputs: [], execute: async (_inputs, ctx) => { const { clearSelectionAtom: clearSelectionAtom2 } = await Promise.resolve().then(() => (init_core(), core_exports)); ctx.set(clearSelectionAtom2); } }; var invertSelectionCommand = { name: "invertSelection", description: "Invert the current selection", aliases: ["invert"], category: "selection", inputs: [], execute: async (_inputs, ctx) => { const { nodeKeysAtom: nodeKeysAtom2, selectedNodeIdsAtom: selectedNodeIdsAtom2 } = await Promise.resolve().then(() => (init_core(), core_exports)); const allNodeKeys = ctx.get(nodeKeysAtom2); const currentSelection = ctx.selectedNodeIds; const invertedSelection = allNodeKeys.filter((id) => !currentSelection.has(id)); ctx.set(selectedNodeIdsAtom2, new Set(invertedSelection)); } }; function registerSelectionCommands() { registerCommand(selectAllCommand); registerCommand(clearSelectionCommand); registerCommand(invertSelectionCommand); } // src/commands/builtins/history-commands.ts init_registry(); var undoCommand = { name: "undo", description: "Undo the last action", aliases: ["z"], category: "history", inputs: [], execute: async (_inputs, ctx) => { ctx.history.undo(); } }; var redoCommand = { name: "redo", description: "Redo the last undone action", aliases: ["y"], category: "history", inputs: [], execute: async (_inputs, ctx) => { ctx.history.redo(); } }; function registerHistoryCommands() { registerCommand(undoCommand); registerCommand(redoCommand); } // src/commands/builtins/layout-commands.ts init_registry(); var forceLayoutCommand = { name: "forceLayout", description: "Apply force-directed layout to all nodes", aliases: ["force", "autoLayout"], category: "layout", inputs: [], execute: async (_inputs, ctx) => { await ctx.layout.applyForceLayout(); } }; var treeLayoutCommand = { name: "treeLayout", description: "Arrange nodes in a hierarchical tree (top-down)", aliases: ["tree"], category: "layout", inputs: [], execute: async (_inputs, ctx) => { await ctx.layout.applyTreeLayout(); } }; var gridLayoutCommand = { name: "gridLayout", description: "Arrange nodes in a uniform grid", aliases: ["grid"], category: "layout", inputs: [], execute: async (_inputs, ctx) => { await ctx.layout.applyGridLayout(); } }; var horizontalLayoutCommand = { name: "horizontalLayout", description: "Arrange nodes in a horizontal tree (left-to-right)", aliases: ["horizontal", "hLayout"], category: "layout", inputs: [], execute: async (_inputs, ctx) => { await ctx.layout.applyTreeLayout({ direction: "left-right" }); } }; function registerLayoutCommands() { registerCommand(forceLayoutCommand); registerCommand(treeLayoutCommand); registerCommand(gridLayoutCommand); registerCommand(horizontalLayoutCommand); } // src/commands/builtins/clipboard-commands.ts init_registry(); var copyCommand = { name: "copy", description: "Copy selected nodes to clipboard", aliases: ["cp"], category: "selection", inputs: [], execute: async (_inputs, ctx) => { const { copyToClipboardAtom: copyToClipboardAtom2, hasClipboardContentAtom: hasClipboardContentAtom2 } = await Promise.resolve().then(() => (init_clipboard_store(), clipboard_store_exports)); if (ctx.selectedNodeIds.size === 0) { throw new Error("No nodes selected to copy"); } ctx.set(copyToClipboardAtom2, Array.from(ctx.selectedNodeIds)); const hasContent = ctx.get(hasClipboardContentAtom2); if (!hasContent) { throw new Error("Failed to copy nodes"); } } }; var cutCommand = { name: "cut", description: "Cut selected nodes (copy to clipboard)", aliases: ["x"], category: "selection", inputs: [], execute: async (_inputs, ctx) => { const { copyToClipboardAtom: copyToClipboardAtom2, hasClipboardContentAtom: hasClipboardContentAtom2 } = await Promise.resolve().then(() => (init_clipboard_store(), clipboard_store_exports)); if (ctx.selectedNodeIds.size === 0) { throw new Error("No nodes selected to cut"); } ctx.set(copyToClipboardAtom2, Array.from(ctx.selectedNodeIds)); const hasContent = ctx.get(hasClipboardContentAtom2); if (!hasContent) { throw new Error("Failed to cut nodes"); } for (const nodeId of ctx.selectedNodeIds) { await ctx.mutations.deleteNode(nodeId); } } }; var pasteCommand = { name: "paste", description: "Paste nodes from clipboard", aliases: ["v"], category: "selection", inputs: [{ name: "position", type: "point", prompt: "Click to paste at position (or Enter for default offset)", required: false }], execute: async (inputs, ctx) => { const { pasteFromClipboardAtom: pasteFromClipboardAtom2, clipboardAtom: clipboardAtom2, PASTE_OFFSET: PASTE_OFFSET2 } = await Promise.resolve().then(() => (init_clipboard_store(), clipboard_store_exports)); const clipboard = ctx.get(clipboardAtom2); if (!clipboard || clipboard.nodes.length === 0) { throw new Error("Clipboard is empty"); } let offset = PASTE_OFFSET2; if (inputs.position) { const pos = inputs.position; offset = { x: pos.x - clipboard.bounds.minX, y: pos.y - clipboard.bounds.minY }; } const newNodeIds = ctx.set(pasteFromClipboardAtom2, offset); if (!newNodeIds || newNodeIds.length === 0) { throw new Error("Failed to paste nodes"); } }, feedback: (collected, currentInput) => { if (currentInput.name === "position") { return { crosshair: true }; } return null; } }; var duplicateCommand = { name: "duplicate", description: "Duplicate selected nodes", aliases: ["d", "dup"], category: "selection", inputs: [], execute: async (_inputs, ctx) => { const { duplicateSelectionAtom: duplicateSelectionAtom2 } = await Promise.resolve().then(() => (init_clipboard_store(), clipboard_store_exports)); if (ctx.selectedNodeIds.size === 0) { throw new Error("No nodes selected to duplicate"); } const newNodeIds = ctx.set(duplicateSelectionAtom2); if (!newNodeIds || newNodeIds.length === 0) { throw new Error("Failed to duplicate nodes"); } } }; var deleteSelectedCommand = { name: "deleteSelected", description: "Delete selected nodes", aliases: ["del", "delete", "remove"], category: "selection", inputs: [], execute: async (_inputs, ctx) => { if (ctx.selectedNodeIds.size === 0) { throw new Error("No nodes selected to delete"); } const { pushHistoryAtom: pushHistoryAtom2 } = await Promise.resolve().then(() => (init_core(), core_exports)); ctx.set(pushHistoryAtom2, "Delete nodes"); for (const nodeId of ctx.selectedNodeIds) { await ctx.mutations.deleteNode(nodeId); } } }; function registerClipboardCommands() { registerCommand(copyCommand); registerCommand(cutCommand); registerCommand(pasteCommand); registerCommand(duplicateCommand); registerCommand(deleteSelectedCommand); } // src/commands/builtins/group-commands.ts init_registry(); var groupNodesCommand = { name: "groupNodes", description: "Group selected nodes into a container", aliases: ["group"], category: "nodes", inputs: [{ name: "label", type: "text", prompt: "Group label:", required: false, default: "Group" }], execute: async (inputs, ctx) => { if (ctx.selectedNodeIds.size < 2) return; const { addNodeToLocalGraphAtom: addNodeToLocalGraphAtom2 } = await Promise.resolve().then(() => (init_graph_mutations(), graph_mutations_exports)); const { groupSelectedNodesAtom: groupSelectedNodesAtom2 } = await Promise.resolve().then(() => (init_group_store(), group_store_exports)); const label = inputs.label || "Group"; const groupNodeId = crypto.randomUUID(); ctx.set(addNodeToLocalGraphAtom2, { id: groupNodeId, graph_id: ctx.currentGraphId || "", label, node_type: "group", configuration: null, ui_properties: { x: 0, y: 0, width: 500, height: 500, size: 15, zIndex: 0 }, data: null, created_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() }); ctx.set(groupSelectedNodesAtom2, { nodeIds: Array.from(ctx.selectedNodeIds), groupNodeId }); } }; var ungroupNodesCommand = { name: "ungroupNodes", description: "Remove grouping from a group node", aliases: ["ungroup"], category: "nodes", inputs: [{ name: "groupNode", type: "node", prompt: "Pick a group node to ungroup:" }], execute: async (inputs, ctx) => { const groupId = inputs.groupNode; if (!groupId) return; const { ungroupNodesAtom: ungroupNodesAtom2 } = await Promise.resolve().then(() => (init_group_store(), group_store_exports)); ctx.set(ungroupNodesAtom2, groupId); } }; var collapseGroupCommand = { name: "collapseGroup", description: "Collapse a group node to hide its children", aliases: ["collapse"], category: "nodes", inputs: [{ name: "groupNode", type: "node", prompt: "Pick a group node to collapse:" }], execute: async (inputs, ctx) => { const groupId = inputs.groupNode; if (!groupId) return; const { collapseGroupAtom: collapseGroupAtom2 } = await Promise.resolve().then(() => (init_group_store(), group_store_exports)); ctx.set(collapseGroupAtom2, groupId); } }; var expandGroupCommand = { name: "expandGroup", description: "Expand a collapsed group node", aliases: ["expand"], category: "nodes", inputs: [{ name: "groupNode", type: "node", prompt: "Pick a group node to expand:" }], execute: async (inputs, ctx) => { const groupId = inputs.groupNode; if (!groupId) return; const { expandGroupAtom: expandGroupAtom2 } = await Promise.resolve().then(() => (init_group_store(), group_store_exports)); ctx.set(expandGroupAtom2, groupId); } }; function registerGroupCommands() { registerCommand(groupNodesCommand); registerCommand(ungroupNodesCommand); registerCommand(collapseGroupCommand); registerCommand(expandGroupCommand); } // src/commands/builtins/search-commands.ts init_registry(); var searchNodesCommand = { name: "searchNodes", description: "Search nodes by label, type, or ID", aliases: ["find", "search"], category: "selection", inputs: [{ name: "query", type: "text", prompt: "Search:", required: true }], execute: async (inputs, ctx) => { const query = inputs.query; if (!query) return; const { setSearchQueryAtom: setSearchQueryAtom2 } = await Promise.resolve().then(() => (init_search_store(), search_store_exports)); ctx.set(setSearchQueryAtom2, query); } }; var clearSearchCommand = { name: "clearSearch", description: "Clear the active search filter", aliases: ["clearsearch"], category: "selection", inputs: [], execute: async (_inputs, ctx) => { const { clearSearchAtom: clearSearchAtom2 } = await Promise.resolve().then(() => (init_search_store(), search_store_exports)); ctx.set(clearSearchAtom2); } }; function registerSearchCommands() { registerCommand(searchNodesCommand); registerCommand(clearSearchCommand); } // src/commands/builtins/merge-commands.ts init_registry(); var mergeNodesCommand = { name: "mergeNodes", description: "Merge selected nodes into one (first selected survives)", aliases: ["merge"], category: "nodes", inputs: [], execute: async (_inputs, ctx) => { if (ctx.selectedNodeIds.size < 2) return; const { mergeNodesAtom: mergeNodesAtom2 } = await Promise.resolve().then(() => (init_graph_mutations(), graph_mutations_exports)); const { clearSelectionAtom: clearSelectionAtom2, addNodesToSelectionAtom: addNodesToSelectionAtom2 } = await Promise.resolve().then(() => (init_selection_store(), selection_store_exports)); const nodeIds = Array.from(ctx.selectedNodeIds); ctx.set(mergeNodesAtom2, { nodeIds }); ctx.set(clearSelectionAtom2); ctx.set(addNodesToSelectionAtom2, [nodeIds[0]]); } }; function registerMergeCommands() { registerCommand(mergeNodesCommand); } // src/commands/builtins/serialization-commands.ts init_registry(); var exportCanvasCommand = { name: "exportCanvas", description: "Export the current canvas to a JSON snapshot (copies to clipboard)", aliases: ["export"], category: "custom", inputs: [], execute: async (_inputs, ctx) => { const { exportGraph: exportGraph2 } = await Promise.resolve().then(() => (init_canvas_serializer(), canvas_serializer_exports)); const { showToastAtom: showToastAtom2 } = await Promise.resolve().then(() => (init_toast_store(), toast_store_exports)); const { graphAtom: graphAtom2 } = await Promise.resolve().then(() => (init_graph_store(), graph_store_exports)); const graph = ctx.get(graphAtom2); const nodeCount = graph.order; const edgeCount = graph.size; if (nodeCount === 0) { throw new Error("Canvas is empty \u2014 nothing to export"); } const store = { get: ctx.get, set: ctx.set }; const snapshot = exportGraph2(store); const json = JSON.stringify(snapshot, null, 2); await navigator.clipboard.writeText(json); ctx.set(showToastAtom2, `Exported ${nodeCount} nodes, ${edgeCount} edges to clipboard`); } }; var importCanvasCommand = { name: "importCanvas", description: "Import a canvas from a JSON snapshot (reads from clipboard)", aliases: ["import"], category: "custom", inputs: [], execute: async (_inputs, ctx) => { const { importGraph: importGraph2, validateSnapshot: validateSnapshot2 } = await Promise.resolve().then(() => (init_canvas_serializer(), canvas_serializer_exports)); const { showToastAtom: showToastAtom2 } = await Promise.resolve().then(() => (init_toast_store(), toast_store_exports)); const { pushHistoryAtom: pushHistoryAtom2 } = await Promise.resolve().then(() => (init_history_store(), history_store_exports)); let json; try { json = await navigator.clipboard.readText(); } catch { throw new Error("Could not read clipboard \u2014 paste the snapshot JSON manually"); } let data; try { data = JSON.parse(json); } catch { throw new Error("Clipboard does not contain valid JSON"); } const result = validateSnapshot2(data); if (!result.valid) { throw new Error(`Invalid snapshot: ${result.errors[0]}`); } ctx.set(pushHistoryAtom2, "Import canvas"); const store = { get: ctx.get, set: ctx.set }; const snapshot = data; importGraph2(store, snapshot); ctx.set(showToastAtom2, `Imported ${snapshot.nodes.length} nodes, ${snapshot.edges.length} edges`); } }; function registerSerializationCommands() { registerCommand(exportCanvasCommand); registerCommand(importCanvasCommand); } // src/commands/builtins/index.ts function registerBuiltinCommands() { registerViewportCommands(); registerSelectionCommands(); registerHistoryCommands(); registerLayoutCommands(); registerClipboardCommands(); registerGroupCommands(); registerSearchCommands(); registerMergeCommands(); registerSerializationCommands(); } export { CommandProvider, DEFAULT_SHORTCUTS, cancelCommand, clearCommandErrorAtom, clearSelectionCommand, closeCommandLineAtom, collectInput, commandFeedbackAtom, commandHistoryAtom, commandLineStateAtom, commandLineVisibleAtom, commandProgressAtom, commandRegistry, copyCommand, currentInputAtom, cutCommand, deleteSelectedCommand, duplicateCommand, executeCommandInteractive, fitSelectionCommand, fitToViewCommand, forceLayoutCommand, goBackInputAtom, handlePickedNode, handlePickedPoint, invertSelectionCommand, isCommandActiveAtom, openCommandLineAtom, pasteCommand, redoCommand, registerBuiltinCommands, registerClipboardCommands, registerCommand, registerHistoryCommands, registerLayoutCommands, registerSelectionCommands, registerViewportCommands, resetViewportCommand, selectAllCommand, selectCommandAtom, selectedSuggestionIndexAtom, setCommandErrorAtom, skipInputAtom, undoCommand, updateSearchQueryAtom, useCommandContext, useExecuteCommand, useGlobalKeyboard, useKeyState, zoomInCommand, zoomOutCommand }; //# sourceMappingURL=index.mjs.map