canvas/dist/commands/index.mjs
2026-03-11 18:42:08 -07:00

7679 lines
No EOL
246 KiB
JavaScript

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