"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/gestures/index.ts var index_exports = {}; __export(index_exports, { ACTIVE_INTERACTION_CONTEXT: () => ACTIVE_INTERACTION_CONTEXT, DEFAULT_CONTEXT: () => DEFAULT_CONTEXT, GestureProvider: () => GestureProvider, IDLE: () => IDLE, INPUT_MODE_CONTEXTS: () => INPUT_MODE_CONTEXTS, InputProvider: () => InputProvider, KEYBOARD_MANIPULATE_CONTEXT: () => KEYBOARD_MANIPULATE_CONTEXT, KEYBOARD_NAVIGATE_CONTEXT: () => KEYBOARD_NAVIGATE_CONTEXT, LONG_PRESS_TIMER: () => LONG_PRESS_TIMER, NO_HELD_KEYS: () => NO_HELD_KEYS, NO_MODIFIERS: () => NO_MODIFIERS, PALM_REJECTION_CONTEXT: () => PALM_REJECTION_CONTEXT, PICK_NODES_CONTEXT: () => PICK_NODES_CONTEXT, PICK_NODE_CONTEXT: () => PICK_NODE_CONTEXT, PICK_POINT_CONTEXT: () => PICK_POINT_CONTEXT, PanInertia: () => PanInertia, SEARCH_CONTEXT: () => SEARCH_CONTEXT, SETTLE_TIMER: () => SETTLE_TIMER, TimedStateRunner: () => TimedStateRunner, VelocitySampler: () => VelocitySampler, ZoomInertia: () => ZoomInertia, activateFocusedNode: () => activateFocusedNode, buildMappingIndex: () => buildMappingIndex, cancelActiveInteraction: () => cancelActiveInteraction, clearHandlers: () => clearHandlers, createPinchHandlers: () => createPinchHandlers, createWheelHandler: () => createWheelHandler, cutSelection: () => cutSelection, cycleFocus: () => cycleFocus, deleteSelection: () => deleteSelection, dispatch: () => dispatch, escapeInput: () => escapeInput, extractModifiers: () => extractModifiers, findNearestNode: () => findNearestNode, getCurrentSubject: () => getCurrentSubject, getHandler: () => getHandler, indexContext: () => indexContext, isKeyInputEvent: () => isKeyInputEvent, isPointerGestureEvent: () => isPointerGestureEvent, navigateFocus: () => navigateFocus, normalizePointer: () => normalizePointer, nudgeSelection: () => nudgeSelection, registerAction: () => registerAction, resolve: () => resolve, snapZoom: () => snapZoom, specificity: () => specificity, transition: () => transition, unregisterAction: () => unregisterAction, useCanvasGestures: () => useCanvasGestures, useGestureContext: () => useGestureContext, useGestureSystem: () => useGestureSystem, useGuardContext: () => useGuardContext, useInertia: () => useInertia, useInputContext: () => useInputContext, useInputModeGestureContext: () => useInputModeGestureContext, useInputSystem: () => useInputSystem, useNodeGestures: () => useNodeGestures, useRegisterInputActions: () => useRegisterInputActions }); module.exports = __toCommonJS(index_exports); // src/gestures/types.ts var NO_MODIFIERS = Object.freeze({ shift: false, ctrl: false, alt: false, meta: false }); var NO_HELD_KEYS = Object.freeze({ byKey: Object.freeze({}), byCode: Object.freeze({}) }); function isKeyInputEvent(event) { return event.kind === "key"; } function isPointerGestureEvent(event) { return event.kind !== "key"; } // 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 }; } // src/gestures/normalize.ts function extractModifiers(e) { if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { return NO_MODIFIERS; } return { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey }; } function clampButton(raw) { if (raw === 1) return 1; if (raw === 2) return 2; return 0; } function lifecycleFromEventType(type) { switch (type) { case "pointerdown": return "down"; case "pointerup": return "up"; case "pointercancel": return "cancel"; default: return "move"; } } function normalizePointer(e) { const classified = classifyPointer(e); return { pointerId: e.pointerId, lifecycle: lifecycleFromEventType(e.type), source: classified.source, button: clampButton(e.button), modifiers: extractModifiers(e), screenX: e.clientX, screenY: e.clientY, pressure: e.pressure, timestamp: e.timeStamp }; } // src/gestures/timed-state.ts var LONG_PRESS_TIMER = "long-press"; var SETTLE_TIMER = "settle"; var DEFAULT_LONG_PRESS_MS = 600; var DEFAULT_MULTI_TAP_WINDOW_MS = 300; var IDLE = { tag: "idle", tapCount: 0 }; var DEFAULT_CONFIG = { longPressMs: DEFAULT_LONG_PRESS_MS, multiTapWindowMs: DEFAULT_MULTI_TAP_WINDOW_MS }; function transition(state, event, config = DEFAULT_CONFIG) { switch (event) { case "down": return onDown(state, config); case "up": return onUp(state, config); case "move-beyond-threshold": return onMoveBeyond(state); case "cancel": return onCancel(); case "timer:long-press": return onLongPressTimer(state); case "timer:settle": return onSettleTimer(); default: return { state }; } } function onDown(state, config) { switch (state.tag) { case "idle": return { state: { tag: "pressed", tapCount: 0 }, cancelTimer: SETTLE_TIMER, scheduleTimer: { id: LONG_PRESS_TIMER, delayMs: config.longPressMs } }; case "released": return { state: { tag: "pressed", tapCount: state.tapCount }, cancelTimer: SETTLE_TIMER, scheduleTimer: { id: LONG_PRESS_TIMER, delayMs: config.longPressMs } }; default: return { state }; } } function onUp(state, config) { if (state.tag !== "pressed") { if (state.tag === "long-pressed") { return { state: IDLE, cancelTimer: LONG_PRESS_TIMER }; } return { state }; } const newCount = state.tapCount + 1; const emit = tapTypeForCount(newCount); return { state: { tag: "released", tapCount: newCount }, emit, cancelTimer: LONG_PRESS_TIMER, scheduleTimer: { id: SETTLE_TIMER, delayMs: config.multiTapWindowMs } }; } function onMoveBeyond(state) { if (state.tag === "pressed") { return { state: IDLE, cancelTimer: LONG_PRESS_TIMER }; } return { state }; } function onCancel() { return { state: IDLE, cancelTimer: LONG_PRESS_TIMER }; } function onLongPressTimer(state) { if (state.tag === "pressed") { return { state: { tag: "long-pressed", tapCount: 0 }, emit: "long-press" }; } return { state }; } function onSettleTimer() { return { state: IDLE }; } function tapTypeForCount(count) { switch (count) { case 2: return "double-tap"; case 3: return "triple-tap"; default: return "tap"; } } // src/gestures/timed-state-runner.ts var TimedStateRunner = class { constructor(config) { __publicField(this, "state", IDLE); __publicField(this, "timers", /* @__PURE__ */ new Map()); /** Called when the state machine emits a gesture (tap, double-tap, long-press, etc.) */ __publicField(this, "onEmit", null); this.config = { longPressMs: config?.longPressMs ?? DEFAULT_LONG_PRESS_MS, multiTapWindowMs: config?.multiTapWindowMs ?? DEFAULT_MULTI_TAP_WINDOW_MS }; } /** * Feed a lifecycle event into the state machine. * Returns the emitted gesture type if any (synchronous emit only). * Timer-based emits are delivered asynchronously via `onEmit`. */ feed(event) { return this.apply(event); } /** Current state tag (for debugging/testing). */ get tag() { return this.state.tag; } /** Destroy: cancel all pending timers. */ destroy() { for (const timer of this.timers.values()) { clearTimeout(timer); } this.timers.clear(); this.state = IDLE; this.onEmit = null; } apply(event) { const result = transition(this.state, event, this.config); this.state = result.state; if (result.cancelTimer) { const timer = this.timers.get(result.cancelTimer); if (timer !== void 0) { clearTimeout(timer); this.timers.delete(result.cancelTimer); } } if (result.scheduleTimer) { const { id, delayMs } = result.scheduleTimer; const existing = this.timers.get(id); if (existing !== void 0) { clearTimeout(existing); } this.timers.set(id, setTimeout(() => { this.timers.delete(id); const timerEvent = `timer:${id}`; const timerResult = transition(this.state, timerEvent, this.config); this.state = timerResult.state; if (timerResult.emit) { this.onEmit?.(timerResult.emit); } }, delayMs)); } return result.emit; } }; // src/gestures/specificity.ts var SCORE_TYPE = 128; var SCORE_KEY = 64; var SCORE_CODE = 64; var SCORE_SUBJECT = 32; var SCORE_MODIFIER_POSITIVE = 16; var SCORE_MODIFIER_NEGATIVE = 8; var SCORE_HELD_KEY_POSITIVE = 16; var SCORE_HELD_KEY_NEGATIVE = 8; var SCORE_SOURCE = 4; var SCORE_BUTTON = 2; function specificity(pattern, event) { let score = 0; if (pattern.kind !== void 0) { const actualKind = isKeyInputEvent(event) ? "key" : "pointer"; if (pattern.kind !== actualKind) return -1; } if (pattern.type !== void 0) { if (!isPointerGestureEvent(event) || pattern.type !== event.type) return -1; score += SCORE_TYPE; } if (pattern.subjectKind !== void 0) { if (event.subject === void 0 || pattern.subjectKind !== event.subject.kind) return -1; score += SCORE_SUBJECT; } if (pattern.phase !== void 0 && pattern.phase !== event.phase) { return -1; } if (pattern.source !== void 0) { if (!isPointerGestureEvent(event)) return -1; if (pattern.source !== event.source) return -1; score += SCORE_SOURCE; } if (pattern.button !== void 0) { if (!isPointerGestureEvent(event)) return -1; if (pattern.button !== event.button) return -1; score += SCORE_BUTTON; } if (pattern.key !== void 0) { if (!isKeyInputEvent(event) || pattern.key !== event.key) return -1; score += SCORE_KEY; } if (pattern.code !== void 0) { if (!isKeyInputEvent(event) || pattern.code !== event.code) return -1; score += SCORE_CODE; } if (pattern.modifiers !== void 0) { const ms = modifierScore(pattern.modifiers, event.modifiers); if (ms === -1) return -1; score += ms; } if (pattern.heldKeys !== void 0) { const hs = heldKeyScore(pattern.heldKeys, event.heldKeys); if (hs === -1) return -1; score += hs; } return score; } function modifierScore(pattern, actual) { let score = 0; const keys = ["shift", "ctrl", "alt", "meta"]; for (const key of keys) { const required = pattern[key]; if (required === void 0) continue; if (required !== actual[key]) return -1; score += required ? SCORE_MODIFIER_POSITIVE : SCORE_MODIFIER_NEGATIVE; } if (pattern.custom) { const actualCustom = actual.custom ?? {}; for (const [key, required] of Object.entries(pattern.custom)) { const actualVal = actualCustom[key] ?? false; if (required !== actualVal) return -1; score += required ? SCORE_MODIFIER_POSITIVE : SCORE_MODIFIER_NEGATIVE; } } return score; } function heldKeyScore(pattern, actual) { let score = 0; for (const [key, required] of Object.entries(pattern.byKey ?? {})) { const actualVal = actual.byKey[key] ?? false; if (required !== actualVal) return -1; score += required ? SCORE_HELD_KEY_POSITIVE : SCORE_HELD_KEY_NEGATIVE; } for (const [code, required] of Object.entries(pattern.byCode ?? {})) { const actualVal = actual.byCode[code] ?? false; if (required !== actualVal) return -1; score += required ? SCORE_HELD_KEY_POSITIVE : SCORE_HELD_KEY_NEGATIVE; } return score; } // src/gestures/mapper.ts var WILDCARD = "__wildcard__"; function getPatternBucketKey(binding) { const { pattern } = binding; if (pattern.type !== void 0) { return `pointer:${pattern.type}`; } if (pattern.kind === "key" || pattern.key !== void 0 || pattern.code !== void 0) { return pattern.phase === "down" || pattern.phase === "up" ? `key:${pattern.phase}` : null; } return null; } function getEventBucketKey(event) { return isKeyInputEvent(event) ? `key:${event.phase}` : `pointer:${event.type}`; } function indexContext(ctx) { const typed = /* @__PURE__ */ new Map(); const wildcards = []; for (const binding of ctx.bindings) { const key = getPatternBucketKey(binding); if (key === null) { wildcards.push(binding); } else { let bucket = typed.get(key); if (!bucket) { bucket = []; typed.set(key, bucket); } bucket.push(binding); } } const buckets = /* @__PURE__ */ new Map(); for (const [key, bucket] of typed) { buckets.set(key, bucket); } if (wildcards.length > 0) { buckets.set(WILDCARD, wildcards); } return { contextId: ctx.id, priority: ctx.priority, enabled: ctx.enabled, buckets }; } function buildMappingIndex(contexts) { return contexts.map(indexContext).sort((a, b) => a.priority - b.priority); } function resolve(event, index, guard) { let winner = null; for (const ctx of index) { if (!ctx.enabled) continue; const bucket = ctx.buckets.get(getEventBucketKey(event)); const wildcardBucket = ctx.buckets.get(WILDCARD); if (!bucket && !wildcardBucket) continue; let bestScore = -1; let bestBinding = null; for (const binding of [...bucket ?? [], ...wildcardBucket ?? []]) { const score = specificity(binding.pattern, event); if (score === -1) continue; if (binding.when && !binding.when(guard)) continue; if (score > bestScore) { bestScore = score; bestBinding = binding; } } if (bestBinding !== null) { const action = { actionId: bestBinding.actionId, binding: bestBinding, contextId: ctx.contextId, score: bestScore, consumed: bestBinding.consumeInput === true }; if (action.consumed) { return action; } if (winner === null) { winner = action; } } } return winner; } // src/gestures/keyboard-contexts.ts var SEARCH_CONTEXT = { id: "search-active", priority: 25, enabled: true, bindings: [{ id: "search-next-enter", pattern: { kind: "key", phase: "down", key: "Enter", modifiers: { shift: false } }, actionId: "search-next-result", when: (ctx) => ctx.isSearchActive && !ctx.commandLineVisible, consumeInput: true }, { id: "search-prev-enter", pattern: { kind: "key", phase: "down", key: "Enter", modifiers: { shift: true } }, actionId: "search-prev-result", when: (ctx) => ctx.isSearchActive && !ctx.commandLineVisible, consumeInput: true }, { id: "search-next-ctrl-g", pattern: { kind: "key", phase: "down", key: "g", modifiers: { ctrl: true, shift: false } }, actionId: "search-next-result", when: (ctx) => ctx.isSearchActive, consumeInput: true }, { id: "search-prev-ctrl-g", pattern: { kind: "key", phase: "down", key: "g", modifiers: { ctrl: true, shift: true } }, actionId: "search-prev-result", when: (ctx) => ctx.isSearchActive, consumeInput: true }, { id: "search-next-meta-g", pattern: { kind: "key", phase: "down", key: "g", modifiers: { meta: true, shift: false } }, actionId: "search-next-result", when: (ctx) => ctx.isSearchActive, consumeInput: true }, { id: "search-prev-meta-g", pattern: { kind: "key", phase: "down", key: "g", modifiers: { meta: true, shift: true } }, actionId: "search-prev-result", when: (ctx) => ctx.isSearchActive, consumeInput: true }] }; var KEYBOARD_MANIPULATE_CONTEXT = { id: "keyboard:manipulate", priority: 30, enabled: true, bindings: [{ id: "manipulate-arrow-up", pattern: { kind: "key", phase: "down", key: "ArrowUp", modifiers: { shift: false } }, actionId: "nudge-selection-up", when: (ctx) => ctx.keyboardInteractionMode === "manipulate", consumeInput: true }, { id: "manipulate-arrow-down", pattern: { kind: "key", phase: "down", key: "ArrowDown", modifiers: { shift: false } }, actionId: "nudge-selection-down", when: (ctx) => ctx.keyboardInteractionMode === "manipulate", consumeInput: true }, { id: "manipulate-arrow-left", pattern: { kind: "key", phase: "down", key: "ArrowLeft", modifiers: { shift: false } }, actionId: "nudge-selection-left", when: (ctx) => ctx.keyboardInteractionMode === "manipulate", consumeInput: true }, { id: "manipulate-arrow-right", pattern: { kind: "key", phase: "down", key: "ArrowRight", modifiers: { shift: false } }, actionId: "nudge-selection-right", when: (ctx) => ctx.keyboardInteractionMode === "manipulate", consumeInput: true }, { id: "manipulate-shift-arrow-up", pattern: { kind: "key", phase: "down", key: "ArrowUp", modifiers: { shift: true } }, actionId: "nudge-selection-up-large", when: (ctx) => ctx.keyboardInteractionMode === "manipulate", consumeInput: true }, { id: "manipulate-shift-arrow-down", pattern: { kind: "key", phase: "down", key: "ArrowDown", modifiers: { shift: true } }, actionId: "nudge-selection-down-large", when: (ctx) => ctx.keyboardInteractionMode === "manipulate", consumeInput: true }, { id: "manipulate-shift-arrow-left", pattern: { kind: "key", phase: "down", key: "ArrowLeft", modifiers: { shift: true } }, actionId: "nudge-selection-left-large", when: (ctx) => ctx.keyboardInteractionMode === "manipulate", consumeInput: true }, { id: "manipulate-shift-arrow-right", pattern: { kind: "key", phase: "down", key: "ArrowRight", modifiers: { shift: true } }, actionId: "nudge-selection-right-large", when: (ctx) => ctx.keyboardInteractionMode === "manipulate", consumeInput: true }, { id: "manipulate-escape", pattern: { kind: "key", phase: "down", key: "Escape" }, actionId: "exit-keyboard-manipulate-mode", when: (ctx) => ctx.keyboardInteractionMode === "manipulate", consumeInput: true }] }; var KEYBOARD_NAVIGATE_CONTEXT = { id: "keyboard:navigate", priority: 40, enabled: true, bindings: [{ id: "navigate-arrow-up", pattern: { kind: "key", phase: "down", key: "ArrowUp" }, actionId: "navigate-focus-up", when: (ctx) => ctx.keyboardInteractionMode === "navigate", consumeInput: true }, { id: "navigate-arrow-down", pattern: { kind: "key", phase: "down", key: "ArrowDown" }, actionId: "navigate-focus-down", when: (ctx) => ctx.keyboardInteractionMode === "navigate", consumeInput: true }, { id: "navigate-arrow-left", pattern: { kind: "key", phase: "down", key: "ArrowLeft" }, actionId: "navigate-focus-left", when: (ctx) => ctx.keyboardInteractionMode === "navigate", consumeInput: true }, { id: "navigate-arrow-right", pattern: { kind: "key", phase: "down", key: "ArrowRight" }, actionId: "navigate-focus-right", when: (ctx) => ctx.keyboardInteractionMode === "navigate", consumeInput: true }, { id: "navigate-enter", pattern: { kind: "key", phase: "down", key: "Enter" }, actionId: "enter-keyboard-manipulate-mode", when: (ctx) => ctx.keyboardInteractionMode === "navigate", consumeInput: true }, { id: "navigate-space", pattern: { kind: "key", phase: "down", code: "Space" }, actionId: "activate-focused-node", when: (ctx) => ctx.keyboardInteractionMode === "navigate", consumeInput: true }, { id: "navigate-tab", pattern: { kind: "key", phase: "down", key: "Tab", modifiers: { shift: false } }, actionId: "cycle-focus-forward", when: (ctx) => ctx.keyboardInteractionMode === "navigate", consumeInput: true }, { id: "navigate-shift-tab", pattern: { kind: "key", phase: "down", key: "Tab", modifiers: { shift: true } }, actionId: "cycle-focus-backward", when: (ctx) => ctx.keyboardInteractionMode === "navigate", consumeInput: true }] }; // src/gestures/pointer-bindings.ts var POINTER_BINDINGS = [ // --- Pointer taps --- { id: "tap-node", pattern: { type: "tap", subjectKind: "node" }, actionId: "select-node" }, { id: "tap-edge", pattern: { type: "tap", subjectKind: "edge" }, actionId: "select-edge" }, { id: "tap-bg", pattern: { type: "tap", subjectKind: "background" }, actionId: "clear-selection" }, { id: "shift-tap-node", pattern: { type: "tap", subjectKind: "node", modifiers: { shift: true } }, actionId: "toggle-selection" }, // --- Right-click --- { id: "rc-node", pattern: { type: "tap", subjectKind: "node", button: 2 }, actionId: "context-menu", consumeInput: true }, { id: "rc-bg", pattern: { type: "tap", subjectKind: "background", button: 2 }, actionId: "context-menu", consumeInput: true }, // --- Double/triple tap --- { id: "dtap-node", pattern: { type: "double-tap", subjectKind: "node" }, actionId: "fit-to-view" }, { id: "ttap-node", pattern: { type: "triple-tap", subjectKind: "node" }, actionId: "toggle-lock" }, // --- Drags --- { id: "drag-node", pattern: { type: "drag", subjectKind: "node" }, actionId: "move-node" }, { id: "drag-bg-finger", pattern: { type: "drag", subjectKind: "background", source: "finger" }, actionId: "pan" }, { id: "drag-bg-mouse", pattern: { type: "drag", subjectKind: "background", source: "mouse" }, actionId: "pan" }, { id: "drag-bg-pencil", pattern: { type: "drag", subjectKind: "background", source: "pencil" }, actionId: "lasso-select" }, { id: "space-drag-pan", pattern: { type: "drag", heldKeys: { byCode: { Space: true } } }, actionId: "pan" }, { id: "shift-drag-bg", pattern: { type: "drag", subjectKind: "background", modifiers: { shift: true } }, actionId: "rect-select" }, // Right/middle drag — no-ops by default { id: "rdrag-node", pattern: { type: "drag", subjectKind: "node", button: 2 }, actionId: "none" }, { id: "rdrag-bg", pattern: { type: "drag", subjectKind: "background", button: 2 }, actionId: "none" }, // --- Long-press --- { id: "lp-node", pattern: { type: "long-press", subjectKind: "node" }, actionId: "context-menu" }, { id: "lp-bg-finger", pattern: { type: "long-press", subjectKind: "background", source: "finger" }, actionId: "create-node" }, // --- Pinch / scroll --- { id: "pinch-bg", pattern: { type: "pinch", subjectKind: "background" }, actionId: "zoom" }, { id: "scroll-any", pattern: { type: "scroll" }, actionId: "zoom" }, { id: "pinch-node", pattern: { type: "pinch", subjectKind: "node" }, actionId: "split-node" } ]; // src/gestures/keyboard-bindings.ts var isCommandShortcut = (ctx) => !ctx.commandLineVisible; var KEYBOARD_BINDINGS = [ { id: "slash-open-command-line", pattern: { kind: "key", phase: "down", code: "Slash", modifiers: { ctrl: false, meta: false, shift: false } }, actionId: "open-command-line", when: isCommandShortcut, consumeInput: true }, { id: "escape-fallback", pattern: { kind: "key", phase: "down", key: "Escape" }, actionId: "escape-input", when: isCommandShortcut, consumeInput: true }, { id: "delete-selection", pattern: { kind: "key", phase: "down", key: "Delete" }, actionId: "delete-selection", when: isCommandShortcut, consumeInput: true }, { id: "backspace-delete-selection", pattern: { kind: "key", phase: "down", key: "Backspace", modifiers: { ctrl: false, meta: false } }, actionId: "delete-selection", when: isCommandShortcut, consumeInput: true }, // Search { id: "open-search-ctrl-f", pattern: { kind: "key", phase: "down", key: "f", modifiers: { ctrl: true, shift: false } }, actionId: "open-search", consumeInput: true }, { id: "open-search-meta-f", pattern: { kind: "key", phase: "down", key: "f", modifiers: { meta: true, shift: false } }, actionId: "open-search", consumeInput: true }, // Copy / Cut / Paste { id: "copy-selection-ctrl-c", pattern: { kind: "key", phase: "down", key: "c", modifiers: { ctrl: true, shift: false } }, actionId: "copy-selection", when: isCommandShortcut, consumeInput: true }, { id: "copy-selection-meta-c", pattern: { kind: "key", phase: "down", key: "c", modifiers: { meta: true, shift: false } }, actionId: "copy-selection", when: isCommandShortcut, consumeInput: true }, { id: "cut-selection-ctrl-x", pattern: { kind: "key", phase: "down", key: "x", modifiers: { ctrl: true, shift: false } }, actionId: "cut-selection", when: isCommandShortcut, consumeInput: true }, { id: "cut-selection-meta-x", pattern: { kind: "key", phase: "down", key: "x", modifiers: { meta: true, shift: false } }, actionId: "cut-selection", when: isCommandShortcut, consumeInput: true }, { id: "paste-selection-ctrl-v", pattern: { kind: "key", phase: "down", key: "v", modifiers: { ctrl: true, shift: false } }, actionId: "paste-selection", when: isCommandShortcut, consumeInput: true }, { id: "paste-selection-meta-v", pattern: { kind: "key", phase: "down", key: "v", modifiers: { meta: true, shift: false } }, actionId: "paste-selection", when: isCommandShortcut, consumeInput: true }, // Duplicate { id: "duplicate-selection-ctrl-d", pattern: { kind: "key", phase: "down", key: "d", modifiers: { ctrl: true, shift: false } }, actionId: "duplicate-selection", when: isCommandShortcut, consumeInput: true }, { id: "duplicate-selection-meta-d", pattern: { kind: "key", phase: "down", key: "d", modifiers: { meta: true, shift: false } }, actionId: "duplicate-selection", when: isCommandShortcut, consumeInput: true }, // Select All { id: "select-all-ctrl-a", pattern: { kind: "key", phase: "down", key: "a", modifiers: { ctrl: true, shift: false } }, actionId: "select-all", when: isCommandShortcut, consumeInput: true }, { id: "select-all-meta-a", pattern: { kind: "key", phase: "down", key: "a", modifiers: { meta: true, shift: false } }, actionId: "select-all", when: isCommandShortcut, consumeInput: true }, // Merge { id: "merge-selection-ctrl-m", pattern: { kind: "key", phase: "down", key: "m", modifiers: { ctrl: true, shift: false } }, actionId: "merge-selection", when: isCommandShortcut, consumeInput: true }, { id: "merge-selection-meta-m", pattern: { kind: "key", phase: "down", key: "m", modifiers: { meta: true, shift: false } }, actionId: "merge-selection", when: isCommandShortcut, consumeInput: true }, // Undo / Redo { id: "undo-ctrl-z", pattern: { kind: "key", phase: "down", key: "z", modifiers: { ctrl: true, shift: false } }, actionId: "undo", when: isCommandShortcut, consumeInput: true }, { id: "undo-meta-z", pattern: { kind: "key", phase: "down", key: "z", modifiers: { meta: true, shift: false } }, actionId: "undo", when: isCommandShortcut, consumeInput: true }, { id: "redo-ctrl-shift-z", pattern: { kind: "key", phase: "down", key: "z", modifiers: { ctrl: true, shift: true } }, actionId: "redo", when: isCommandShortcut, consumeInput: true }, { id: "redo-meta-shift-z", pattern: { kind: "key", phase: "down", key: "z", modifiers: { meta: true, shift: true } }, actionId: "redo", when: isCommandShortcut, consumeInput: true }, { id: "redo-ctrl-y", pattern: { kind: "key", phase: "down", key: "y", modifiers: { ctrl: true } }, actionId: "redo", when: isCommandShortcut, consumeInput: true }, { id: "redo-meta-y", pattern: { kind: "key", phase: "down", key: "y", modifiers: { meta: true } }, actionId: "redo", when: isCommandShortcut, consumeInput: true } ]; // src/gestures/pointer-contexts.ts var DEFAULT_CONTEXT = { id: "default", priority: 100, enabled: true, bindings: [...POINTER_BINDINGS, ...KEYBOARD_BINDINGS] }; var PICK_NODE_CONTEXT = { id: "input-mode:pickNode", priority: 5, enabled: true, bindings: [{ id: "pick-tap-node", pattern: { type: "tap", subjectKind: "node" }, actionId: "resolve-pick-node", consumeInput: true }, { id: "pick-cancel-bg", pattern: { type: "tap", subjectKind: "background" }, actionId: "cancel-pick", consumeInput: true }, { id: "pick-cancel-key", pattern: { kind: "key", phase: "down", key: "Escape" }, actionId: "cancel-pick", consumeInput: true }] }; var PICK_NODES_CONTEXT = { id: "input-mode:pickNodes", priority: 5, enabled: true, bindings: [{ id: "pick-tap-node", pattern: { type: "tap", subjectKind: "node" }, actionId: "resolve-pick-node", consumeInput: true }, { id: "pick-done", pattern: { type: "double-tap", subjectKind: "background" }, actionId: "finish-pick-nodes", consumeInput: true }, { id: "pick-cancel-bg", pattern: { type: "tap", subjectKind: "background", button: 2 }, actionId: "cancel-pick", consumeInput: true }, { id: "pick-cancel-key", pattern: { kind: "key", phase: "down", key: "Escape" }, actionId: "cancel-pick", consumeInput: true }] }; var PICK_POINT_CONTEXT = { id: "input-mode:pickPoint", priority: 5, enabled: true, bindings: [{ id: "pick-tap", pattern: { type: "tap" }, actionId: "resolve-pick-point", consumeInput: true }, { id: "pick-cancel-key", pattern: { kind: "key", phase: "down", key: "Escape" }, actionId: "cancel-pick", consumeInput: true }] }; var INPUT_MODE_CONTEXTS = { normal: null, pickNode: PICK_NODE_CONTEXT, pickNodes: PICK_NODES_CONTEXT, pickPoint: PICK_POINT_CONTEXT, text: null, select: null }; // src/gestures/contexts.ts var PALM_REJECTION_CONTEXT = { id: "palm-rejection", priority: 0, enabled: true, bindings: [{ id: "pr-finger-tap", pattern: { type: "tap", source: "finger" }, actionId: "none", when: (ctx) => ctx.isStylusActive, consumeInput: true }, { id: "pr-finger-dtap", pattern: { type: "double-tap", source: "finger" }, actionId: "none", when: (ctx) => ctx.isStylusActive, consumeInput: true }, { id: "pr-finger-ttap", pattern: { type: "triple-tap", source: "finger" }, actionId: "none", when: (ctx) => ctx.isStylusActive, consumeInput: true }, { id: "pr-finger-lp", pattern: { type: "long-press", source: "finger" }, actionId: "none", when: (ctx) => ctx.isStylusActive, consumeInput: true }, { id: "pr-finger-drag-node", pattern: { type: "drag", subjectKind: "node", source: "finger" }, actionId: "pan", when: (ctx) => ctx.isStylusActive, consumeInput: true }] }; var ACTIVE_INTERACTION_CONTEXT = { id: "active-interaction", priority: 15, enabled: true, bindings: [{ id: "escape-cancel-active-interaction", pattern: { kind: "key", phase: "down", key: "Escape" }, actionId: "cancel-active-input", when: (ctx) => ctx.isDragging || ctx.isSplitting || Boolean(ctx.custom.isSelecting) || Boolean(ctx.custom.isCreatingEdge), consumeInput: true }] }; // src/gestures/dispatcher.ts var handlers = /* @__PURE__ */ new Map(); function registerAction(actionId, handler) { handlers.set(actionId, handler); } function unregisterAction(actionId) { handlers.delete(actionId); } function getHandler(actionId) { return handlers.get(actionId); } function clearHandlers() { handlers.clear(); } function dispatch(event, resolution) { if (resolution.actionId === "none") return true; const handler = handlers.get(resolution.actionId); if (!handler) return false; if (typeof handler === "function") { if (isKeyInputEvent(event) && event.phase === "down" || !isKeyInputEvent(event) && (event.phase === "start" || event.phase === "instant")) { handler(event); } return true; } routePhase(handler, event.phase, event); return true; } function routePhase(handler, phase, event) { if (isKeyInputEvent(event)) { routeKeyPhase(handler, phase, event); return; } switch (phase) { case "start": handler.onStart?.(event); break; case "move": handler.onMove?.(event); break; case "end": handler.onEnd?.(event); break; case "instant": handler.onInstant?.(event); break; case "cancel": handler.onCancel?.(event); break; } } function routeKeyPhase(handler, phase, event) { switch (phase) { case "down": handler.onDown?.(event); break; case "up": handler.onUp?.(event); break; } } // src/gestures/inertia.ts var PAN_FRICTION = 0.92; var ZOOM_FRICTION = 0.88; var MIN_VELOCITY = 0.5; var ZOOM_SNAP_THRESHOLD = 0.03; var VELOCITY_SAMPLE_COUNT = 5; var VelocitySampler = class { constructor(maxSamples = VELOCITY_SAMPLE_COUNT) { __publicField(this, "samples", []); this.maxSamples = maxSamples; } sample(vx, vy, t) { if (this.samples.length >= this.maxSamples) { this.samples.shift(); } this.samples.push({ vx, vy, t }); } average() { const n = this.samples.length; if (n === 0) return { x: 0, y: 0 }; let sx = 0; let sy = 0; for (const s of this.samples) { sx += s.vx; sy += s.vy; } return { x: sx / n, y: sy / n }; } reset() { this.samples.length = 0; } }; var PanInertia = class { constructor(velocity, friction = PAN_FRICTION, minVelocity = MIN_VELOCITY) { this.friction = friction; this.minVelocity = minVelocity; this.vx = velocity.x; this.vy = velocity.y; } /** * Advance one frame. Returns the velocity delta to apply, * or null if below threshold (animation complete). */ tick() { this.vx *= this.friction; this.vy *= this.friction; if (Math.abs(this.vx) < this.minVelocity && Math.abs(this.vy) < this.minVelocity) { return null; } return { x: this.vx, y: this.vy }; } }; var ZoomInertia = class { constructor(velocity, origin, friction = ZOOM_FRICTION, minVelocity = MIN_VELOCITY, snapThreshold = ZOOM_SNAP_THRESHOLD) { this.origin = origin; this.friction = friction; this.minVelocity = minVelocity; this.snapThreshold = snapThreshold; this.v = velocity; } /** * Advance one frame. Returns zoom delta + origin, * or null if below threshold. */ tick(currentZoom) { this.v *= this.friction; if (Math.abs(this.v) < this.minVelocity) { if (Math.abs(currentZoom - 1) < this.snapThreshold) { const snapDelta = 1 - currentZoom; return Math.abs(snapDelta) > 1e-3 ? { delta: snapDelta, origin: this.origin } : null; } return null; } return { delta: this.v, origin: this.origin }; } }; // src/core/graph-store.ts var import_jotai = require("jotai"); var import_graphology = __toESM(require("graphology")); var graphOptions = { type: "directed", multi: true, allowSelfLoops: true }; var currentGraphIdAtom = (0, import_jotai.atom)(null); var graphAtom = (0, import_jotai.atom)(new import_graphology.default(graphOptions)); var graphUpdateVersionAtom = (0, import_jotai.atom)(0); var edgeCreationAtom = (0, import_jotai.atom)({ isCreating: false, sourceNodeId: null, sourceNodePosition: null, targetPosition: null, hoveredTargetNodeId: null, sourceHandle: null, targetHandle: null, sourcePort: null, targetPort: null, snappedTargetPosition: null }); var draggingNodeIdAtom = (0, import_jotai.atom)(null); var preDragNodeAttributesAtom = (0, import_jotai.atom)(null); // src/core/graph-position.ts var import_jotai3 = require("jotai"); var import_jotai_family = require("jotai-family"); var import_graphology2 = __toESM(require("graphology")); // src/utils/debug.ts var import_debug = __toESM(require("debug")); var NAMESPACE = "canvas"; function createDebug(module2) { const base = (0, import_debug.default)(`${NAMESPACE}:${module2}`); const warn = (0, import_debug.default)(`${NAMESPACE}:${module2}:warn`); const error = (0, import_debug.default)(`${NAMESPACE}:${module2}:error`); warn.enabled = true; error.enabled = true; warn.log = console.warn.bind(console); error.log = console.error.bind(console); const debugFn = Object.assign(base, { warn, error }); return debugFn; } var debug = { graph: { node: createDebug("graph:node"), edge: createDebug("graph:edge"), sync: createDebug("graph:sync") }, ui: { selection: createDebug("ui:selection"), drag: createDebug("ui:drag"), resize: createDebug("ui:resize") }, sync: { status: createDebug("sync:status"), mutations: createDebug("sync:mutations"), queue: createDebug("sync:queue") }, viewport: createDebug("viewport") }; // src/utils/mutation-queue.ts var pendingNodeMutations = /* @__PURE__ */ new Map(); function clearAllPendingMutations() { pendingNodeMutations.clear(); } // src/core/perf.ts var import_jotai2 = require("jotai"); var perfEnabledAtom = (0, import_jotai2.atom)(false); var _enabled = false; function setPerfEnabled(enabled) { _enabled = enabled; } if (typeof window !== "undefined") { window.__canvasPerf = setPerfEnabled; } function canvasMark(name) { if (!_enabled) return _noop; const markName = `canvas:${name}`; try { performance.mark(markName); } catch { return _noop; } return () => { try { performance.measure(`canvas:${name}`, markName); } catch { } }; } function _noop() { } // src/core/graph-position.ts var debug2 = createDebug("graph:position"); var _positionCacheByGraph = /* @__PURE__ */ new WeakMap(); function getPositionCache(graph) { let cache = _positionCacheByGraph.get(graph); if (!cache) { cache = /* @__PURE__ */ new Map(); _positionCacheByGraph.set(graph, cache); } return cache; } var nodePositionUpdateCounterAtom = (0, import_jotai3.atom)(0); var nodePositionAtomFamily = (0, import_jotai_family.atomFamily)((nodeId) => (0, import_jotai3.atom)((get) => { get(nodePositionUpdateCounterAtom); const graph = get(graphAtom); if (!graph.hasNode(nodeId)) { return { x: 0, y: 0 }; } const x = graph.getNodeAttribute(nodeId, "x"); const y = graph.getNodeAttribute(nodeId, "y"); const cache = getPositionCache(graph); const prev = cache.get(nodeId); if (prev && prev.x === x && prev.y === y) { return prev; } const pos = { x, y }; cache.set(nodeId, pos); return pos; })); var updateNodePositionAtom = (0, import_jotai3.atom)(null, (get, set, { nodeId, position }) => { const end = canvasMark("drag-frame"); const graph = get(graphAtom); if (graph.hasNode(nodeId)) { debug2("Updating node %s position to %o", nodeId, position); graph.setNodeAttribute(nodeId, "x", position.x); graph.setNodeAttribute(nodeId, "y", position.y); set(nodePositionUpdateCounterAtom, (c) => c + 1); } end(); }); var cleanupNodePositionAtom = (0, import_jotai3.atom)(null, (get, _set, nodeId) => { nodePositionAtomFamily.remove(nodeId); const graph = get(graphAtom); getPositionCache(graph).delete(nodeId); debug2("Removed position atom for node: %s", nodeId); }); var cleanupAllNodePositionsAtom = (0, import_jotai3.atom)(null, (get, _set) => { const graph = get(graphAtom); const nodeIds = graph.nodes(); nodeIds.forEach((nodeId) => { nodePositionAtomFamily.remove(nodeId); }); _positionCacheByGraph.delete(graph); debug2("Removed %d position atoms", nodeIds.length); }); var clearGraphOnSwitchAtom = (0, import_jotai3.atom)(null, (get, set) => { debug2("Clearing graph for switch"); set(cleanupAllNodePositionsAtom); clearAllPendingMutations(); const emptyGraph = new import_graphology2.default(graphOptions); set(graphAtom, emptyGraph); set(graphUpdateVersionAtom, (v) => v + 1); }); // src/core/graph-derived.ts var import_jotai8 = require("jotai"); var import_jotai_family2 = require("jotai-family"); // src/core/viewport-store.ts var import_jotai5 = require("jotai"); // src/core/selection-store.ts var import_jotai4 = require("jotai"); var debug3 = createDebug("selection"); var selectedNodeIdsAtom = (0, import_jotai4.atom)(/* @__PURE__ */ new Set()); var selectedEdgeIdAtom = (0, import_jotai4.atom)(null); var handleNodePointerDownSelectionAtom = (0, import_jotai4.atom)(null, (get, set, { nodeId, isShiftPressed }) => { const currentSelection = get(selectedNodeIdsAtom); debug3("handleNodePointerDownSelection: nodeId=%s, shift=%s, current=%o", nodeId, isShiftPressed, Array.from(currentSelection)); set(selectedEdgeIdAtom, null); if (isShiftPressed) { const newSelection = new Set(currentSelection); if (newSelection.has(nodeId)) { newSelection.delete(nodeId); } else { newSelection.add(nodeId); } debug3("Shift-click, setting selection to: %o", Array.from(newSelection)); set(selectedNodeIdsAtom, newSelection); } else { if (!currentSelection.has(nodeId)) { debug3("Node not in selection, selecting: %s", nodeId); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set([nodeId])); } else { debug3("Node already selected, preserving multi-select"); } } }); var selectSingleNodeAtom = (0, import_jotai4.atom)(null, (get, set, nodeId) => { debug3("selectSingleNode: %s", nodeId); set(selectedEdgeIdAtom, null); if (nodeId === null || nodeId === void 0) { debug3("Clearing selection"); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); } else { const currentSelection = get(selectedNodeIdsAtom); if (currentSelection.has(nodeId) && currentSelection.size === 1) { return; } set(selectedNodeIdsAtom, /* @__PURE__ */ new Set([nodeId])); } }); var toggleNodeInSelectionAtom = (0, import_jotai4.atom)(null, (get, set, nodeId) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); if (newSelection.has(nodeId)) { newSelection.delete(nodeId); } else { newSelection.add(nodeId); } set(selectedNodeIdsAtom, newSelection); }); var clearSelectionAtom = (0, import_jotai4.atom)(null, (_get, set) => { debug3("clearSelection"); set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); }); var addNodesToSelectionAtom = (0, import_jotai4.atom)(null, (get, set, nodeIds) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); for (const nodeId of nodeIds) { newSelection.add(nodeId); } set(selectedNodeIdsAtom, newSelection); }); var removeNodesFromSelectionAtom = (0, import_jotai4.atom)(null, (get, set, nodeIds) => { const currentSelection = get(selectedNodeIdsAtom); const newSelection = new Set(currentSelection); for (const nodeId of nodeIds) { newSelection.delete(nodeId); } set(selectedNodeIdsAtom, newSelection); }); var selectEdgeAtom = (0, import_jotai4.atom)(null, (get, set, edgeId) => { set(selectedEdgeIdAtom, edgeId); if (edgeId !== null) { set(selectedNodeIdsAtom, /* @__PURE__ */ new Set()); } }); var clearEdgeSelectionAtom = (0, import_jotai4.atom)(null, (_get, set) => { set(selectedEdgeIdAtom, null); }); var focusedNodeIdAtom = (0, import_jotai4.atom)(null); var setFocusedNodeAtom = (0, import_jotai4.atom)(null, (_get, set, nodeId) => { set(focusedNodeIdAtom, nodeId); }); var hasFocusedNodeAtom = (0, import_jotai4.atom)((get) => get(focusedNodeIdAtom) !== null); var selectedNodesCountAtom = (0, import_jotai4.atom)((get) => get(selectedNodeIdsAtom).size); var hasSelectionAtom = (0, import_jotai4.atom)((get) => get(selectedNodeIdsAtom).size > 0); // src/utils/layout.ts var FitToBoundsMode = /* @__PURE__ */ (function(FitToBoundsMode2) { FitToBoundsMode2["Graph"] = "graph"; FitToBoundsMode2["Selection"] = "selection"; return FitToBoundsMode2; })({}); var calculateBounds = (nodes) => { if (nodes.length === 0) { return { x: 0, y: 0, width: 0, height: 0 }; } const minX = Math.min(...nodes.map((node) => node.x)); const minY = Math.min(...nodes.map((node) => node.y)); const maxX = Math.max(...nodes.map((node) => node.x + node.width)); const maxY = Math.max(...nodes.map((node) => node.y + node.height)); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }; // src/core/viewport-store.ts var zoomAtom = (0, import_jotai5.atom)(1); var panAtom = (0, import_jotai5.atom)({ x: 0, y: 0 }); var viewportRectAtom = (0, import_jotai5.atom)(null); var screenToWorldAtom = (0, import_jotai5.atom)((get) => { return (screenX, screenY) => { const pan = get(panAtom); const zoom = get(zoomAtom); const rect = get(viewportRectAtom); if (!rect) { return { x: screenX, y: screenY }; } const relativeX = screenX - rect.left; const relativeY = screenY - rect.top; return { x: (relativeX - pan.x) / zoom, y: (relativeY - pan.y) / zoom }; }; }); var worldToScreenAtom = (0, import_jotai5.atom)((get) => { return (worldX, worldY) => { const pan = get(panAtom); const zoom = get(zoomAtom); const rect = get(viewportRectAtom); if (!rect) { return { x: worldX, y: worldY }; } return { x: worldX * zoom + pan.x + rect.left, y: worldY * zoom + pan.y + rect.top }; }; }); var setZoomAtom = (0, import_jotai5.atom)(null, (get, set, { zoom, centerX, centerY }) => { const currentZoom = get(zoomAtom); const pan = get(panAtom); const rect = get(viewportRectAtom); const newZoom = Math.max(0.1, Math.min(5, zoom)); if (centerX !== void 0 && centerY !== void 0 && rect) { const relativeX = centerX - rect.left; const relativeY = centerY - rect.top; const worldX = (relativeX - pan.x) / currentZoom; const worldY = (relativeY - pan.y) / currentZoom; const newPanX = relativeX - worldX * newZoom; const newPanY = relativeY - worldY * newZoom; set(panAtom, { x: newPanX, y: newPanY }); } set(zoomAtom, newZoom); }); var resetViewportAtom = (0, import_jotai5.atom)(null, (_get, set) => { set(zoomAtom, 1); set(panAtom, { x: 0, y: 0 }); }); var fitToBoundsAtom = (0, import_jotai5.atom)(null, (get, set, { mode, padding = 20 }) => { const normalizedMode = typeof mode === "string" ? mode === "graph" ? FitToBoundsMode.Graph : FitToBoundsMode.Selection : mode; const viewportSize = get(viewportRectAtom); if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return; get(nodePositionUpdateCounterAtom); let bounds; if (normalizedMode === FitToBoundsMode.Graph) { const graph = get(graphAtom); const nodes = graph.nodes().map((node) => { const attrs = graph.getNodeAttributes(node); return { x: attrs.x, y: attrs.y, width: attrs.width || 500, height: attrs.height || 500 }; }); bounds = calculateBounds(nodes); } else { const selectedIds = get(selectedNodeIdsAtom); const allNodes = get(uiNodesAtom); const selectedNodes = allNodes.filter((n) => selectedIds.has(n.id)).map((n) => ({ x: n.position.x, y: n.position.y, width: n.width ?? 500, height: n.height ?? 500 })); bounds = calculateBounds(selectedNodes); } if (bounds.width <= 0 || bounds.height <= 0) return; const maxHPad = Math.max(0, viewportSize.width / 2 - 1); const maxVPad = Math.max(0, viewportSize.height / 2 - 1); const safePadding = Math.max(0, Math.min(padding, maxHPad, maxVPad)); const effW = Math.max(1, viewportSize.width - 2 * safePadding); const effH = Math.max(1, viewportSize.height - 2 * safePadding); const scale = Math.min(effW / bounds.width, effH / bounds.height); if (scale <= 0 || !isFinite(scale)) return; set(zoomAtom, scale); const scaledW = bounds.width * scale; const scaledH = bounds.height * scale; const startX = safePadding + (effW - scaledW) / 2; const startY = safePadding + (effH - scaledH) / 2; set(panAtom, { x: startX - bounds.x * scale, y: startY - bounds.y * scale }); }); var centerOnNodeAtom = (0, import_jotai5.atom)(null, (get, set, nodeId) => { const nodes = get(uiNodesAtom); const node = nodes.find((n) => n.id === nodeId); if (!node) return; const { x, y, width = 200, height = 100 } = node; const zoom = get(zoomAtom); const centerX = x + width / 2; const centerY = y + height / 2; const rect = get(viewportRectAtom); const halfWidth = rect ? rect.width / 2 : 400; const halfHeight = rect ? rect.height / 2 : 300; set(panAtom, { x: halfWidth - centerX * zoom, y: halfHeight - centerY * zoom }); }); var zoomFocusNodeIdAtom = (0, import_jotai5.atom)(null); var zoomTransitionProgressAtom = (0, import_jotai5.atom)(0); var isZoomTransitioningAtom = (0, import_jotai5.atom)((get) => { const progress = get(zoomTransitionProgressAtom); return progress > 0 && progress < 1; }); var zoomAnimationTargetAtom = (0, import_jotai5.atom)(null); var animateZoomToNodeAtom = (0, import_jotai5.atom)(null, (get, set, { nodeId, targetZoom, duration = 300 }) => { const nodes = get(uiNodesAtom); const node = nodes.find((n) => n.id === nodeId); if (!node) return; const { x, y, width = 200, height = 100 } = node; const centerX = x + width / 2; const centerY = y + height / 2; const rect = get(viewportRectAtom); const halfWidth = rect ? rect.width / 2 : 400; const halfHeight = rect ? rect.height / 2 : 300; const finalZoom = targetZoom ?? get(zoomAtom); const targetPan = { x: halfWidth - centerX * finalZoom, y: halfHeight - centerY * finalZoom }; set(zoomFocusNodeIdAtom, nodeId); set(zoomAnimationTargetAtom, { targetZoom: finalZoom, targetPan, startZoom: get(zoomAtom), startPan: { ...get(panAtom) }, duration, startTime: performance.now() }); }); var animateFitToBoundsAtom = (0, import_jotai5.atom)(null, (get, set, { mode, padding = 20, duration = 300 }) => { const viewportSize = get(viewportRectAtom); if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return; get(nodePositionUpdateCounterAtom); let bounds; if (mode === "graph") { const graph = get(graphAtom); const nodes = graph.nodes().map((node) => { const attrs = graph.getNodeAttributes(node); return { x: attrs.x, y: attrs.y, width: attrs.width || 500, height: attrs.height || 500 }; }); bounds = calculateBounds(nodes); } else { const selectedIds = get(selectedNodeIdsAtom); const allNodes = get(uiNodesAtom); const selectedNodes = allNodes.filter((n) => selectedIds.has(n.id)).map((n) => ({ x: n.position.x, y: n.position.y, width: n.width ?? 500, height: n.height ?? 500 })); bounds = calculateBounds(selectedNodes); } if (bounds.width <= 0 || bounds.height <= 0) return; const safePadding = Math.max(0, Math.min(padding, viewportSize.width / 2 - 1, viewportSize.height / 2 - 1)); const effW = Math.max(1, viewportSize.width - 2 * safePadding); const effH = Math.max(1, viewportSize.height - 2 * safePadding); const scale = Math.min(effW / bounds.width, effH / bounds.height); if (scale <= 0 || !isFinite(scale)) return; const scaledW = bounds.width * scale; const scaledH = bounds.height * scale; const startX = safePadding + (effW - scaledW) / 2; const startY = safePadding + (effH - scaledH) / 2; const targetPan = { x: startX - bounds.x * scale, y: startY - bounds.y * scale }; set(zoomAnimationTargetAtom, { targetZoom: scale, targetPan, startZoom: get(zoomAtom), startPan: { ...get(panAtom) }, duration, startTime: performance.now() }); }); // src/core/group-store.ts var import_jotai7 = require("jotai"); // src/core/history-store.ts var import_jotai6 = require("jotai"); // src/core/history-actions.ts function applyDelta(graph, delta) { switch (delta.type) { case "move-node": { if (!graph.hasNode(delta.nodeId)) return false; graph.setNodeAttribute(delta.nodeId, "x", delta.to.x); graph.setNodeAttribute(delta.nodeId, "y", delta.to.y); return false; } case "resize-node": { if (!graph.hasNode(delta.nodeId)) return false; graph.setNodeAttribute(delta.nodeId, "width", delta.to.width); graph.setNodeAttribute(delta.nodeId, "height", delta.to.height); return false; } case "add-node": { if (graph.hasNode(delta.nodeId)) return false; graph.addNode(delta.nodeId, delta.attributes); return true; } case "remove-node": { if (!graph.hasNode(delta.nodeId)) return false; graph.dropNode(delta.nodeId); return true; } case "add-edge": { if (graph.hasEdge(delta.edgeId)) return false; if (!graph.hasNode(delta.source) || !graph.hasNode(delta.target)) return false; graph.addEdgeWithKey(delta.edgeId, delta.source, delta.target, delta.attributes); return true; } case "remove-edge": { if (!graph.hasEdge(delta.edgeId)) return false; graph.dropEdge(delta.edgeId); return true; } case "update-node-attr": { if (!graph.hasNode(delta.nodeId)) return false; graph.setNodeAttribute(delta.nodeId, delta.key, delta.to); return false; } case "batch": { let structuralChange = false; for (const d of delta.deltas) { if (applyDelta(graph, d)) structuralChange = true; } return structuralChange; } case "full-snapshot": { graph.clear(); for (const node of delta.nodes) { graph.addNode(node.id, node.attributes); } for (const edge of delta.edges) { if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) { graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge.attributes); } } return true; } } } function invertDelta(delta) { switch (delta.type) { case "move-node": return { ...delta, from: delta.to, to: delta.from }; case "resize-node": return { ...delta, from: delta.to, to: delta.from }; case "add-node": return { type: "remove-node", nodeId: delta.nodeId, attributes: delta.attributes, connectedEdges: [] }; case "remove-node": { const batch = [{ type: "add-node", nodeId: delta.nodeId, attributes: delta.attributes }, ...delta.connectedEdges.map((e) => ({ type: "add-edge", edgeId: e.id, source: e.source, target: e.target, attributes: e.attributes }))]; return batch.length === 1 ? batch[0] : { type: "batch", deltas: batch }; } case "add-edge": return { type: "remove-edge", edgeId: delta.edgeId, source: delta.source, target: delta.target, attributes: delta.attributes }; case "remove-edge": return { type: "add-edge", edgeId: delta.edgeId, source: delta.source, target: delta.target, attributes: delta.attributes }; case "update-node-attr": return { ...delta, from: delta.to, to: delta.from }; case "batch": return { type: "batch", deltas: delta.deltas.map(invertDelta).reverse() }; case "full-snapshot": return delta; } } function createSnapshot(graph, label) { const nodes = []; const edges = []; graph.forEachNode((nodeId, attributes) => { nodes.push({ id: nodeId, attributes: { ...attributes } }); }); graph.forEachEdge((edgeId, attributes, source, target) => { edges.push({ id: edgeId, source, target, attributes: { ...attributes } }); }); return { timestamp: Date.now(), label, nodes, edges }; } // src/core/history-store.ts var debug4 = createDebug("history"); var MAX_HISTORY_SIZE = 50; var historyStateAtom = (0, import_jotai6.atom)({ past: [], future: [], isApplying: false }); var canUndoAtom = (0, import_jotai6.atom)((get) => { const history = get(historyStateAtom); return history.past.length > 0 && !history.isApplying; }); var canRedoAtom = (0, import_jotai6.atom)((get) => { const history = get(historyStateAtom); return history.future.length > 0 && !history.isApplying; }); var undoCountAtom = (0, import_jotai6.atom)((get) => get(historyStateAtom).past.length); var redoCountAtom = (0, import_jotai6.atom)((get) => get(historyStateAtom).future.length); var pushDeltaAtom = (0, import_jotai6.atom)(null, (get, set, delta) => { const history = get(historyStateAtom); if (history.isApplying) return; const { label, ...cleanDelta } = delta; const entry = { forward: cleanDelta, reverse: invertDelta(cleanDelta), timestamp: Date.now(), label }; const newPast = [...history.past, entry]; if (newPast.length > MAX_HISTORY_SIZE) newPast.shift(); set(historyStateAtom, { past: newPast, future: [], // Clear redo stack isApplying: false }); debug4("Pushed delta: %s (past: %d)", label || delta.type, newPast.length); }); var pushHistoryAtom = (0, import_jotai6.atom)(null, (get, set, label) => { const history = get(historyStateAtom); if (history.isApplying) return; const graph = get(graphAtom); const snapshot = createSnapshot(graph, label); const forward = { type: "full-snapshot", nodes: snapshot.nodes, edges: snapshot.edges }; const entry = { forward, reverse: forward, // For full snapshots, reverse IS the current state timestamp: Date.now(), label }; const newPast = [...history.past, entry]; if (newPast.length > MAX_HISTORY_SIZE) newPast.shift(); set(historyStateAtom, { past: newPast, future: [], isApplying: false }); debug4("Pushed snapshot: %s (past: %d)", label || "unnamed", newPast.length); }); var undoAtom = (0, import_jotai6.atom)(null, (get, set) => { const history = get(historyStateAtom); if (history.past.length === 0 || history.isApplying) return false; set(historyStateAtom, { ...history, isApplying: true }); try { const graph = get(graphAtom); const newPast = [...history.past]; const entry = newPast.pop(); let forwardForRedo = entry.forward; if (entry.reverse.type === "full-snapshot") { const currentSnapshot = createSnapshot(graph, "current"); forwardForRedo = { type: "full-snapshot", nodes: currentSnapshot.nodes, edges: currentSnapshot.edges }; } const structuralChange = applyDelta(graph, entry.reverse); if (structuralChange) { set(graphAtom, graph); set(graphUpdateVersionAtom, (v) => v + 1); } set(nodePositionUpdateCounterAtom, (c) => c + 1); const redoEntry = { forward: forwardForRedo, reverse: entry.reverse, timestamp: entry.timestamp, label: entry.label }; set(historyStateAtom, { past: newPast, future: [redoEntry, ...history.future], isApplying: false }); debug4("Undo: %s (past: %d, future: %d)", entry.label, newPast.length, history.future.length + 1); return true; } catch (error) { debug4.error("Undo failed: %O", error); set(historyStateAtom, { ...history, isApplying: false }); return false; } }); var redoAtom = (0, import_jotai6.atom)(null, (get, set) => { const history = get(historyStateAtom); if (history.future.length === 0 || history.isApplying) return false; set(historyStateAtom, { ...history, isApplying: true }); try { const graph = get(graphAtom); const newFuture = [...history.future]; const entry = newFuture.shift(); let reverseForUndo = entry.reverse; if (entry.forward.type === "full-snapshot") { const currentSnapshot = createSnapshot(graph, "current"); reverseForUndo = { type: "full-snapshot", nodes: currentSnapshot.nodes, edges: currentSnapshot.edges }; } const structuralChange = applyDelta(graph, entry.forward); if (structuralChange) { set(graphAtom, graph); set(graphUpdateVersionAtom, (v) => v + 1); } set(nodePositionUpdateCounterAtom, (c) => c + 1); const undoEntry = { forward: entry.forward, reverse: reverseForUndo, timestamp: entry.timestamp, label: entry.label }; set(historyStateAtom, { past: [...history.past, undoEntry], future: newFuture, isApplying: false }); debug4("Redo: %s (past: %d, future: %d)", entry.label, history.past.length + 1, newFuture.length); return true; } catch (error) { debug4.error("Redo failed: %O", error); set(historyStateAtom, { ...history, isApplying: false }); return false; } }); var clearHistoryAtom = (0, import_jotai6.atom)(null, (_get, set) => { set(historyStateAtom, { past: [], future: [], isApplying: false }); debug4("History cleared"); }); var historyLabelsAtom = (0, import_jotai6.atom)((get) => { const history = get(historyStateAtom); return { past: history.past.map((e) => e.label || "Unnamed"), future: history.future.map((e) => e.label || "Unnamed") }; }); // src/core/group-store.ts var collapsedGroupsAtom = (0, import_jotai7.atom)(/* @__PURE__ */ new Set()); var toggleGroupCollapseAtom = (0, import_jotai7.atom)(null, (get, set, groupId) => { const current = get(collapsedGroupsAtom); const next = new Set(current); if (next.has(groupId)) { next.delete(groupId); } else { next.add(groupId); } set(collapsedGroupsAtom, next); }); var collapseGroupAtom = (0, import_jotai7.atom)(null, (get, set, groupId) => { const current = get(collapsedGroupsAtom); if (!current.has(groupId)) { const next = new Set(current); next.add(groupId); set(collapsedGroupsAtom, next); } }); var expandGroupAtom = (0, import_jotai7.atom)(null, (get, set, groupId) => { const current = get(collapsedGroupsAtom); if (current.has(groupId)) { const next = new Set(current); next.delete(groupId); set(collapsedGroupsAtom, next); } }); var nodeChildrenAtom = (0, import_jotai7.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return (parentId) => { const children = []; graph.forEachNode((nodeId, attrs) => { if (attrs.parentId === parentId) { children.push(nodeId); } }); return children; }; }); var nodeParentAtom = (0, import_jotai7.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return (nodeId) => { if (!graph.hasNode(nodeId)) return void 0; return graph.getNodeAttribute(nodeId, "parentId"); }; }); var isGroupNodeAtom = (0, import_jotai7.atom)((get) => { const getChildren = get(nodeChildrenAtom); return (nodeId) => getChildren(nodeId).length > 0; }); var groupChildCountAtom = (0, import_jotai7.atom)((get) => { const getChildren = get(nodeChildrenAtom); return (groupId) => getChildren(groupId).length; }); var setNodeParentAtom = (0, import_jotai7.atom)(null, (get, set, { nodeId, parentId }) => { const graph = get(graphAtom); if (!graph.hasNode(nodeId)) return; if (parentId) { if (parentId === nodeId) return; let current = parentId; while (current) { if (current === nodeId) return; if (!graph.hasNode(current)) break; current = graph.getNodeAttribute(current, "parentId"); } } graph.setNodeAttribute(nodeId, "parentId", parentId); set(graphUpdateVersionAtom, (v) => v + 1); }); var moveNodesToGroupAtom = (0, import_jotai7.atom)(null, (get, set, { nodeIds, groupId }) => { for (const nodeId of nodeIds) { set(setNodeParentAtom, { nodeId, parentId: groupId }); } }); var removeFromGroupAtom = (0, import_jotai7.atom)(null, (get, set, nodeId) => { set(setNodeParentAtom, { nodeId, parentId: void 0 }); }); var groupSelectedNodesAtom = (0, import_jotai7.atom)(null, (get, set, { nodeIds, groupNodeId }) => { set(pushHistoryAtom, `Group ${nodeIds.length} nodes`); const graph = get(graphAtom); let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const nodeId of nodeIds) { if (!graph.hasNode(nodeId)) continue; const attrs = graph.getNodeAttributes(nodeId); minX = Math.min(minX, attrs.x); minY = Math.min(minY, attrs.y); maxX = Math.max(maxX, attrs.x + (attrs.width || 200)); maxY = Math.max(maxY, attrs.y + (attrs.height || 100)); } const padding = 20; if (graph.hasNode(groupNodeId)) { graph.setNodeAttribute(groupNodeId, "x", minX - padding); graph.setNodeAttribute(groupNodeId, "y", minY - padding - 30); graph.setNodeAttribute(groupNodeId, "width", maxX - minX + 2 * padding); graph.setNodeAttribute(groupNodeId, "height", maxY - minY + 2 * padding + 30); } for (const nodeId of nodeIds) { if (nodeId !== groupNodeId && graph.hasNode(nodeId)) { graph.setNodeAttribute(nodeId, "parentId", groupNodeId); } } set(graphUpdateVersionAtom, (v) => v + 1); set(nodePositionUpdateCounterAtom, (c) => c + 1); }); var ungroupNodesAtom = (0, import_jotai7.atom)(null, (get, set, groupId) => { set(pushHistoryAtom, "Ungroup nodes"); const graph = get(graphAtom); graph.forEachNode((nodeId, attrs) => { if (attrs.parentId === groupId) { graph.setNodeAttribute(nodeId, "parentId", void 0); } }); set(graphUpdateVersionAtom, (v) => v + 1); }); var nestNodesOnDropAtom = (0, import_jotai7.atom)(null, (get, set, { nodeIds, targetId }) => { set(pushHistoryAtom, "Nest nodes"); for (const nodeId of nodeIds) { if (nodeId === targetId) continue; set(setNodeParentAtom, { nodeId, parentId: targetId }); } set(autoResizeGroupAtom, targetId); }); var collapsedEdgeRemapAtom = (0, import_jotai7.atom)((get) => { const collapsed = get(collapsedGroupsAtom); if (collapsed.size === 0) return /* @__PURE__ */ new Map(); get(graphUpdateVersionAtom); const graph = get(graphAtom); const remap = /* @__PURE__ */ new Map(); for (const nodeId of graph.nodes()) { let current = nodeId; let outermost = null; while (true) { if (!graph.hasNode(current)) break; const parent = graph.getNodeAttribute(current, "parentId"); if (!parent) break; if (collapsed.has(parent)) outermost = parent; current = parent; } if (outermost) remap.set(nodeId, outermost); } return remap; }); var autoResizeGroupAtom = (0, import_jotai7.atom)(null, (get, set, groupId) => { const graph = get(graphAtom); if (!graph.hasNode(groupId)) return; const children = []; graph.forEachNode((nodeId, attrs) => { if (attrs.parentId === groupId) { children.push(nodeId); } }); if (children.length === 0) return; const padding = 20; const headerHeight = 30; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const childId of children) { const attrs = graph.getNodeAttributes(childId); minX = Math.min(minX, attrs.x); minY = Math.min(minY, attrs.y); maxX = Math.max(maxX, attrs.x + (attrs.width || 200)); maxY = Math.max(maxY, attrs.y + (attrs.height || 100)); } graph.setNodeAttribute(groupId, "x", minX - padding); graph.setNodeAttribute(groupId, "y", minY - padding - headerHeight); graph.setNodeAttribute(groupId, "width", maxX - minX + 2 * padding); graph.setNodeAttribute(groupId, "height", maxY - minY + 2 * padding + headerHeight); set(nodePositionUpdateCounterAtom, (c) => c + 1); }); // src/core/graph-derived.ts var highestZIndexAtom = (0, import_jotai8.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); let maxZ = 0; graph.forEachNode((_node, attributes) => { if (attributes.zIndex > maxZ) { maxZ = attributes.zIndex; } }); return maxZ; }); var _prevUiNodesByGraph = /* @__PURE__ */ new WeakMap(); var uiNodesAtom = (0, import_jotai8.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); const currentDraggingId = get(draggingNodeIdAtom); const collapsed = get(collapsedGroupsAtom); const nodes = []; graph.forEachNode((nodeId, attributes) => { if (collapsed.size > 0) { let current = nodeId; let hidden = false; while (true) { if (!graph.hasNode(current)) break; const pid = graph.getNodeAttributes(current).parentId; if (!pid) break; if (collapsed.has(pid)) { hidden = true; break; } current = pid; } if (hidden) return; } const position = get(nodePositionAtomFamily(nodeId)); nodes.push({ ...attributes, id: nodeId, position, isDragging: nodeId === currentDraggingId }); }); const prev = _prevUiNodesByGraph.get(graph) ?? []; if (nodes.length === prev.length && nodes.every((n, i) => n.id === prev[i].id && n.position === prev[i].position && n.isDragging === prev[i].isDragging)) { return prev; } _prevUiNodesByGraph.set(graph, nodes); return nodes; }); var nodeKeysAtom = (0, import_jotai8.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return graph.nodes(); }); var nodeFamilyAtom = (0, import_jotai_family2.atomFamily)((nodeId) => (0, import_jotai8.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); if (!graph.hasNode(nodeId)) { return null; } const attributes = graph.getNodeAttributes(nodeId); const position = get(nodePositionAtomFamily(nodeId)); const currentDraggingId = get(draggingNodeIdAtom); return { ...attributes, id: nodeId, position, isDragging: nodeId === currentDraggingId }; }), (a, b) => a === b); var edgeKeysAtom = (0, import_jotai8.atom)((get) => { get(graphUpdateVersionAtom); const graph = get(graphAtom); return graph.edges(); }); var edgeKeysWithTempEdgeAtom = (0, import_jotai8.atom)((get) => { const keys = get(edgeKeysAtom); const edgeCreation = get(edgeCreationAtom); if (edgeCreation.isCreating) { return [...keys, "temp-creating-edge"]; } return keys; }); var _edgeCacheByGraph = /* @__PURE__ */ new WeakMap(); function getEdgeCache(graph) { let cache = _edgeCacheByGraph.get(graph); if (!cache) { cache = /* @__PURE__ */ new Map(); _edgeCacheByGraph.set(graph, cache); } return cache; } var edgeFamilyAtom = (0, import_jotai_family2.atomFamily)((key) => (0, import_jotai8.atom)((get) => { get(graphUpdateVersionAtom); if (key === "temp-creating-edge") { const edgeCreationState = get(edgeCreationAtom); const graph2 = get(graphAtom); if (edgeCreationState.isCreating && edgeCreationState.sourceNodeId && edgeCreationState.targetPosition) { const sourceNodeAttrs = graph2.getNodeAttributes(edgeCreationState.sourceNodeId); const sourceNodePosition = get(nodePositionAtomFamily(edgeCreationState.sourceNodeId)); const pan = get(panAtom); const zoom = get(zoomAtom); const viewportRect = get(viewportRectAtom); if (sourceNodeAttrs && viewportRect) { const mouseX = edgeCreationState.targetPosition.x - viewportRect.left; const mouseY = edgeCreationState.targetPosition.y - viewportRect.top; const worldTargetX = (mouseX - pan.x) / zoom; const worldTargetY = (mouseY - pan.y) / zoom; const tempEdge = { key: "temp-creating-edge", sourceId: edgeCreationState.sourceNodeId, targetId: "temp-cursor", sourcePosition: sourceNodePosition, targetPosition: { x: worldTargetX, y: worldTargetY }, sourceNodeSize: sourceNodeAttrs.size, sourceNodeWidth: sourceNodeAttrs.width, sourceNodeHeight: sourceNodeAttrs.height, targetNodeSize: 0, targetNodeWidth: 0, targetNodeHeight: 0, type: "dashed", color: "#FF9800", weight: 2, label: void 0, dbData: { id: "temp-creating-edge", graph_id: get(currentGraphIdAtom) || "", source_node_id: edgeCreationState.sourceNodeId, target_node_id: "temp-cursor", edge_type: "temp", filter_condition: null, ui_properties: null, data: null, created_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() } }; return tempEdge; } } return null; } const graph = get(graphAtom); if (!graph.hasEdge(key)) { getEdgeCache(graph).delete(key); return null; } const sourceId = graph.source(key); const targetId = graph.target(key); const attributes = graph.getEdgeAttributes(key); const remap = get(collapsedEdgeRemapAtom); const effectiveSourceId = remap.get(sourceId) ?? sourceId; const effectiveTargetId = remap.get(targetId) ?? targetId; if (!graph.hasNode(effectiveSourceId) || !graph.hasNode(effectiveTargetId)) { getEdgeCache(graph).delete(key); return null; } const sourceAttributes = graph.getNodeAttributes(effectiveSourceId); const targetAttributes = graph.getNodeAttributes(effectiveTargetId); const sourcePosition = get(nodePositionAtomFamily(effectiveSourceId)); const targetPosition = get(nodePositionAtomFamily(effectiveTargetId)); if (sourceAttributes && targetAttributes) { const next = { ...attributes, key, sourceId: effectiveSourceId, targetId: effectiveTargetId, sourcePosition, targetPosition, sourceNodeSize: sourceAttributes.size, targetNodeSize: targetAttributes.size, sourceNodeWidth: sourceAttributes.width ?? sourceAttributes.size, sourceNodeHeight: sourceAttributes.height ?? sourceAttributes.size, targetNodeWidth: targetAttributes.width ?? targetAttributes.size, targetNodeHeight: targetAttributes.height ?? targetAttributes.size }; const edgeCache = getEdgeCache(graph); const prev = edgeCache.get(key); if (prev && prev.sourcePosition === next.sourcePosition && prev.targetPosition === next.targetPosition && prev.sourceId === next.sourceId && prev.targetId === next.targetId && prev.type === next.type && prev.color === next.color && prev.weight === next.weight && prev.label === next.label && prev.sourceNodeSize === next.sourceNodeSize && prev.targetNodeSize === next.targetNodeSize && prev.sourceNodeWidth === next.sourceNodeWidth && prev.sourceNodeHeight === next.sourceNodeHeight && prev.targetNodeWidth === next.targetNodeWidth && prev.targetNodeHeight === next.targetNodeHeight) { return prev; } edgeCache.set(key, next); return next; } getEdgeCache(graph).delete(key); return null; }), (a, b) => a === b); // src/core/graph-mutations.ts var import_jotai12 = require("jotai"); var import_graphology3 = __toESM(require("graphology")); // src/core/graph-mutations-edges.ts var import_jotai10 = require("jotai"); // src/core/reduced-motion-store.ts var import_jotai9 = require("jotai"); var prefersReducedMotionAtom = (0, import_jotai9.atom)(typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false); var watchReducedMotionAtom = (0, import_jotai9.atom)(null, (_get, set) => { if (typeof window === "undefined" || typeof window.matchMedia !== "function") return; const mql = window.matchMedia("(prefers-reduced-motion: reduce)"); const handler = (e) => { set(prefersReducedMotionAtom, e.matches); }; set(prefersReducedMotionAtom, mql.matches); mql.addEventListener("change", handler); return () => mql.removeEventListener("change", handler); }); // src/core/graph-mutations-edges.ts var debug5 = createDebug("graph:mutations:edges"); var addEdgeToLocalGraphAtom = (0, import_jotai10.atom)(null, (get, set, newEdge) => { const graph = get(graphAtom); if (graph.hasNode(newEdge.source_node_id) && graph.hasNode(newEdge.target_node_id)) { const uiProps = newEdge.ui_properties || {}; const attributes = { type: typeof uiProps.style === "string" ? uiProps.style : "solid", color: typeof uiProps.color === "string" ? uiProps.color : "#999", label: newEdge.edge_type ?? void 0, weight: typeof uiProps.weight === "number" ? uiProps.weight : 1, dbData: newEdge }; if (!graph.hasEdge(newEdge.id)) { try { debug5("Adding edge %s to local graph", newEdge.id); graph.addEdgeWithKey(newEdge.id, newEdge.source_node_id, newEdge.target_node_id, attributes); set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); } catch (e) { debug5("Failed to add edge %s: %o", newEdge.id, e); } } } }); var removeEdgeFromLocalGraphAtom = (0, import_jotai10.atom)(null, (get, set, edgeId) => { const graph = get(graphAtom); if (graph.hasEdge(edgeId)) { graph.dropEdge(edgeId); set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); } }); var swapEdgeAtomicAtom = (0, import_jotai10.atom)(null, (get, set, { tempEdgeId, newEdge }) => { const graph = get(graphAtom); if (graph.hasEdge(tempEdgeId)) { graph.dropEdge(tempEdgeId); } if (graph.hasNode(newEdge.source_node_id) && graph.hasNode(newEdge.target_node_id)) { const uiProps = newEdge.ui_properties || {}; const attributes = { type: typeof uiProps.style === "string" ? uiProps.style : "solid", color: typeof uiProps.color === "string" ? uiProps.color : "#999", label: newEdge.edge_type ?? void 0, weight: typeof uiProps.weight === "number" ? uiProps.weight : 1, dbData: newEdge }; if (!graph.hasEdge(newEdge.id)) { try { debug5("Atomically swapping temp edge %s with real edge %s", tempEdgeId, newEdge.id); graph.addEdgeWithKey(newEdge.id, newEdge.source_node_id, newEdge.target_node_id, attributes); } catch (e) { debug5("Failed to add edge %s: %o", newEdge.id, e); } } } set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); }); var departingEdgesAtom = (0, import_jotai10.atom)(/* @__PURE__ */ new Map()); var EDGE_ANIMATION_DURATION = 300; var removeEdgeWithAnimationAtom = (0, import_jotai10.atom)(null, (get, set, edgeKey) => { const edgeState = get(edgeFamilyAtom(edgeKey)); if (edgeState) { const departing = new Map(get(departingEdgesAtom)); departing.set(edgeKey, edgeState); set(departingEdgesAtom, departing); set(removeEdgeFromLocalGraphAtom, edgeKey); const duration = get(prefersReducedMotionAtom) ? 0 : EDGE_ANIMATION_DURATION; setTimeout(() => { const current = new Map(get(departingEdgesAtom)); current.delete(edgeKey); set(departingEdgesAtom, current); }, duration); } }); var editingEdgeLabelAtom = (0, import_jotai10.atom)(null); var updateEdgeLabelAtom = (0, import_jotai10.atom)(null, (get, set, { edgeKey, label }) => { const graph = get(graphAtom); if (graph.hasEdge(edgeKey)) { graph.setEdgeAttribute(edgeKey, "label", label || void 0); set(graphUpdateVersionAtom, (v) => v + 1); set(nodePositionUpdateCounterAtom, (c) => c + 1); } }); // src/core/graph-mutations-advanced.ts var import_jotai11 = require("jotai"); var debug6 = createDebug("graph:mutations:advanced"); var dropTargetNodeIdAtom = (0, import_jotai11.atom)(null); var splitNodeAtom = (0, import_jotai11.atom)(null, (get, set, { nodeId, position1, position2 }) => { const graph = get(graphAtom); if (!graph.hasNode(nodeId)) return; const attrs = graph.getNodeAttributes(nodeId); const graphId = get(currentGraphIdAtom) || attrs.dbData.graph_id; set(pushHistoryAtom, "Split node"); graph.setNodeAttribute(nodeId, "x", position1.x); graph.setNodeAttribute(nodeId, "y", position1.y); const edges = []; graph.forEachEdge(nodeId, (_key, eAttrs, source, target) => { edges.push({ source, target, attrs: eAttrs }); }); const cloneId = `split-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const cloneDbNode = { ...attrs.dbData, id: cloneId, graph_id: graphId, ui_properties: { ...attrs.dbData.ui_properties || {}, x: position2.x, y: position2.y }, created_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() }; set(addNodeToLocalGraphAtom, cloneDbNode); for (const edge of edges) { const newSource = edge.source === nodeId ? cloneId : edge.source; const newTarget = edge.target === nodeId ? cloneId : edge.target; set(addEdgeToLocalGraphAtom, { ...edge.attrs.dbData, id: `split-e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, source_node_id: newSource, target_node_id: newTarget }); } set(graphUpdateVersionAtom, (v) => v + 1); set(nodePositionUpdateCounterAtom, (c) => c + 1); debug6("Split node %s \u2192 clone %s", nodeId, cloneId); }); var mergeNodesAtom = (0, import_jotai11.atom)(null, (get, set, { nodeIds }) => { if (nodeIds.length < 2) return; const graph = get(graphAtom); const [survivorId, ...doomed] = nodeIds; if (!graph.hasNode(survivorId)) return; set(pushHistoryAtom, `Merge ${nodeIds.length} nodes`); const doomedSet = new Set(doomed); for (const doomedId of doomed) { if (!graph.hasNode(doomedId)) continue; const edges = []; graph.forEachEdge(doomedId, (_key, eAttrs, source, target) => { edges.push({ source, target, attrs: eAttrs }); }); for (const edge of edges) { const newSource = doomedSet.has(edge.source) ? survivorId : edge.source; const newTarget = doomedSet.has(edge.target) ? survivorId : edge.target; if (newSource === newTarget) continue; set(addEdgeToLocalGraphAtom, { ...edge.attrs.dbData, id: `merge-e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, source_node_id: newSource, target_node_id: newTarget }); } set(optimisticDeleteNodeAtom, { nodeId: doomedId }); } set(graphUpdateVersionAtom, (v) => v + 1); debug6("Merged nodes %o \u2192 survivor %s", nodeIds, survivorId); }); // src/core/graph-mutations.ts var debug7 = createDebug("graph:mutations"); var startNodeDragAtom = (0, import_jotai12.atom)(null, (get, set, { nodeId }) => { const graph = get(graphAtom); if (!graph.hasNode(nodeId)) return; const currentAttributes = graph.getNodeAttributes(nodeId); set(preDragNodeAttributesAtom, JSON.parse(JSON.stringify(currentAttributes))); const currentHighestZIndex = get(highestZIndexAtom); const newZIndex = currentHighestZIndex + 1; graph.setNodeAttribute(nodeId, "zIndex", newZIndex); set(draggingNodeIdAtom, nodeId); }); var endNodeDragAtom = (0, import_jotai12.atom)(null, (get, set, _payload) => { const currentDraggingId = get(draggingNodeIdAtom); if (currentDraggingId) { debug7("Node %s drag ended", currentDraggingId); const graph = get(graphAtom); if (graph.hasNode(currentDraggingId)) { const parentId = graph.getNodeAttribute(currentDraggingId, "parentId"); if (parentId) { set(autoResizeGroupAtom, parentId); } } } set(draggingNodeIdAtom, null); set(preDragNodeAttributesAtom, null); }); var optimisticDeleteNodeAtom = (0, import_jotai12.atom)(null, (get, set, { nodeId }) => { const graph = get(graphAtom); if (graph.hasNode(nodeId)) { graph.dropNode(nodeId); set(cleanupNodePositionAtom, nodeId); set(graphAtom, graph.copy()); debug7("Optimistically deleted node %s", nodeId); } }); var optimisticDeleteEdgeAtom = (0, import_jotai12.atom)(null, (get, set, { edgeKey }) => { const graph = get(graphAtom); if (graph.hasEdge(edgeKey)) { graph.dropEdge(edgeKey); set(graphAtom, graph.copy()); debug7("Optimistically deleted edge %s", edgeKey); } }); var addNodeToLocalGraphAtom = (0, import_jotai12.atom)(null, (get, set, newNode) => { const graph = get(graphAtom); if (graph.hasNode(newNode.id)) { debug7("Node %s already exists, skipping", newNode.id); return; } const uiProps = newNode.ui_properties || {}; const attributes = { x: typeof uiProps.x === "number" ? uiProps.x : Math.random() * 800, y: typeof uiProps.y === "number" ? uiProps.y : Math.random() * 600, size: typeof uiProps.size === "number" ? uiProps.size : 15, width: typeof uiProps.width === "number" ? uiProps.width : 500, height: typeof uiProps.height === "number" ? uiProps.height : 500, color: typeof uiProps.color === "string" ? uiProps.color : "#ccc", label: newNode.label || newNode.node_type || newNode.id, zIndex: typeof uiProps.zIndex === "number" ? uiProps.zIndex : 0, dbData: newNode }; debug7("Adding node %s to local graph at (%d, %d)", newNode.id, attributes.x, attributes.y); graph.addNode(newNode.id, attributes); set(graphAtom, graph.copy()); set(graphUpdateVersionAtom, (v) => v + 1); set(nodePositionUpdateCounterAtom, (c) => c + 1); }); var loadGraphFromDbAtom = (0, import_jotai12.atom)(null, (get, set, fetchedNodes, fetchedEdges) => { debug7("========== START SYNC =========="); debug7("Fetched nodes: %d, edges: %d", fetchedNodes.length, fetchedEdges.length); const currentGraphId = get(currentGraphIdAtom); if (fetchedNodes.length > 0 && fetchedNodes[0].graph_id !== currentGraphId) { debug7("Skipping sync - data belongs to different graph"); return; } const existingGraph = get(graphAtom); const isDragging = get(draggingNodeIdAtom) !== null; if (isDragging) { debug7("Skipping sync - drag in progress"); return; } const existingNodeIds = new Set(existingGraph.nodes()); const fetchedNodeIds = new Set(fetchedNodes.map((n) => n.id)); const hasAnyCommonNodes = Array.from(existingNodeIds).some((id) => fetchedNodeIds.has(id)); let graph; if (hasAnyCommonNodes && existingNodeIds.size > 0) { debug7("Merging DB data into existing graph"); graph = existingGraph.copy(); } else { debug7("Creating fresh graph (graph switch detected)"); graph = new import_graphology3.default(graphOptions); } const fetchedEdgeIds = new Set(fetchedEdges.map((e) => e.id)); if (hasAnyCommonNodes && existingNodeIds.size > 0) { graph.forEachNode((nodeId) => { if (!fetchedNodeIds.has(nodeId)) { debug7("Removing deleted node: %s", nodeId); graph.dropNode(nodeId); nodePositionAtomFamily.remove(nodeId); } }); } fetchedNodes.forEach((node) => { const uiProps = node.ui_properties || {}; const newX = typeof uiProps.x === "number" ? uiProps.x : Math.random() * 800; const newY = typeof uiProps.y === "number" ? uiProps.y : Math.random() * 600; if (graph.hasNode(node.id)) { const currentAttrs = graph.getNodeAttributes(node.id); const attributes = { x: newX, y: newY, size: typeof uiProps.size === "number" ? uiProps.size : currentAttrs.size, width: typeof uiProps.width === "number" ? uiProps.width : currentAttrs.width ?? 500, height: typeof uiProps.height === "number" ? uiProps.height : currentAttrs.height ?? 500, color: typeof uiProps.color === "string" ? uiProps.color : currentAttrs.color, label: node.label || node.node_type || node.id, zIndex: typeof uiProps.zIndex === "number" ? uiProps.zIndex : currentAttrs.zIndex, dbData: node }; graph.replaceNodeAttributes(node.id, attributes); } else { const attributes = { x: newX, y: newY, size: typeof uiProps.size === "number" ? uiProps.size : 15, width: typeof uiProps.width === "number" ? uiProps.width : 500, height: typeof uiProps.height === "number" ? uiProps.height : 500, color: typeof uiProps.color === "string" ? uiProps.color : "#ccc", label: node.label || node.node_type || node.id, zIndex: typeof uiProps.zIndex === "number" ? uiProps.zIndex : 0, dbData: node }; graph.addNode(node.id, attributes); } }); graph.forEachEdge((edgeId) => { if (!fetchedEdgeIds.has(edgeId)) { debug7("Removing deleted edge: %s", edgeId); graph.dropEdge(edgeId); } }); fetchedEdges.forEach((edge) => { if (graph.hasNode(edge.source_node_id) && graph.hasNode(edge.target_node_id)) { const uiProps = edge.ui_properties || {}; const attributes = { type: typeof uiProps.style === "string" ? uiProps.style : "solid", color: typeof uiProps.color === "string" ? uiProps.color : "#999", label: edge.edge_type ?? void 0, weight: typeof uiProps.weight === "number" ? uiProps.weight : 1, dbData: edge }; if (graph.hasEdge(edge.id)) { graph.replaceEdgeAttributes(edge.id, attributes); } else { try { graph.addEdgeWithKey(edge.id, edge.source_node_id, edge.target_node_id, attributes); } catch (e) { debug7("Failed to add edge %s: %o", edge.id, e); } } } }); set(graphAtom, graph); set(graphUpdateVersionAtom, (v) => v + 1); debug7("========== SYNC COMPLETE =========="); debug7("Final graph: %d nodes, %d edges", graph.order, graph.size); }); // src/core/sync-store.ts var import_jotai13 = require("jotai"); var debug8 = createDebug("sync"); var syncStatusAtom = (0, import_jotai13.atom)("synced"); var pendingMutationsCountAtom = (0, import_jotai13.atom)(0); var isOnlineAtom = (0, import_jotai13.atom)(typeof navigator !== "undefined" ? navigator.onLine : true); var lastSyncErrorAtom = (0, import_jotai13.atom)(null); var lastSyncTimeAtom = (0, import_jotai13.atom)(Date.now()); var mutationQueueAtom = (0, import_jotai13.atom)([]); var syncStateAtom = (0, import_jotai13.atom)((get) => ({ status: get(syncStatusAtom), pendingMutations: get(pendingMutationsCountAtom), lastError: get(lastSyncErrorAtom), lastSyncTime: get(lastSyncTimeAtom), isOnline: get(isOnlineAtom), queuedMutations: get(mutationQueueAtom).length })); var startMutationAtom = (0, import_jotai13.atom)(null, (get, set) => { const currentCount = get(pendingMutationsCountAtom); const newCount = currentCount + 1; set(pendingMutationsCountAtom, newCount); debug8("Mutation started. Pending count: %d -> %d", currentCount, newCount); if (newCount > 0 && get(syncStatusAtom) !== "syncing") { set(syncStatusAtom, "syncing"); debug8("Status -> syncing"); } }); var completeMutationAtom = (0, import_jotai13.atom)(null, (get, set, success = true) => { const currentCount = get(pendingMutationsCountAtom); const newCount = Math.max(0, currentCount - 1); set(pendingMutationsCountAtom, newCount); debug8("Mutation completed (success: %s). Pending count: %d -> %d", success, currentCount, newCount); if (success) { set(lastSyncTimeAtom, Date.now()); if (newCount === 0) { set(lastSyncErrorAtom, null); } } if (newCount === 0) { const isOnline = get(isOnlineAtom); const hasError = get(lastSyncErrorAtom) !== null; if (hasError) { set(syncStatusAtom, "error"); debug8("Status -> error"); } else if (!isOnline) { set(syncStatusAtom, "offline"); debug8("Status -> offline"); } else { set(syncStatusAtom, "synced"); debug8("Status -> synced"); } } }); var trackMutationErrorAtom = (0, import_jotai13.atom)(null, (_get, set, error) => { set(lastSyncErrorAtom, error); debug8("Mutation failed: %s", error); }); var setOnlineStatusAtom = (0, import_jotai13.atom)(null, (get, set, isOnline) => { set(isOnlineAtom, isOnline); const pendingCount = get(pendingMutationsCountAtom); const hasError = get(lastSyncErrorAtom) !== null; const queueLength = get(mutationQueueAtom).length; if (pendingCount === 0) { if (hasError || queueLength > 0) { set(syncStatusAtom, "error"); } else { set(syncStatusAtom, isOnline ? "synced" : "offline"); } } }); var queueMutationAtom = (0, import_jotai13.atom)(null, (get, set, mutation) => { const queue = get(mutationQueueAtom); const newMutation = { ...mutation, id: crypto.randomUUID(), timestamp: Date.now(), retryCount: 0, maxRetries: mutation.maxRetries ?? 3 }; const newQueue = [...queue, newMutation]; set(mutationQueueAtom, newQueue); debug8("Queued mutation: %s. Queue size: %d", mutation.type, newQueue.length); if (get(pendingMutationsCountAtom) === 0) { set(syncStatusAtom, "error"); } return newMutation.id; }); var dequeueMutationAtom = (0, import_jotai13.atom)(null, (get, set, mutationId) => { const queue = get(mutationQueueAtom); const newQueue = queue.filter((m) => m.id !== mutationId); set(mutationQueueAtom, newQueue); debug8("Dequeued mutation: %s. Queue size: %d", mutationId, newQueue.length); if (newQueue.length === 0 && get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) { set(syncStatusAtom, get(isOnlineAtom) ? "synced" : "offline"); } }); var incrementRetryCountAtom = (0, import_jotai13.atom)(null, (get, set, mutationId) => { const queue = get(mutationQueueAtom); const newQueue = queue.map((m) => m.id === mutationId ? { ...m, retryCount: m.retryCount + 1 } : m); set(mutationQueueAtom, newQueue); }); var getNextQueuedMutationAtom = (0, import_jotai13.atom)((get) => { const queue = get(mutationQueueAtom); return queue.find((m) => m.retryCount < m.maxRetries) ?? null; }); var clearMutationQueueAtom = (0, import_jotai13.atom)(null, (get, set) => { set(mutationQueueAtom, []); debug8("Cleared mutation queue"); if (get(pendingMutationsCountAtom) === 0 && get(lastSyncErrorAtom) === null) { set(syncStatusAtom, get(isOnlineAtom) ? "synced" : "offline"); } }); // src/core/interaction-store.ts var import_jotai14 = require("jotai"); var inputModeAtom = (0, import_jotai14.atom)({ type: "normal" }); var keyboardInteractionModeAtom = (0, import_jotai14.atom)("navigate"); var interactionFeedbackAtom = (0, import_jotai14.atom)(null); var pendingInputResolverAtom = (0, import_jotai14.atom)(null); var resetInputModeAtom = (0, import_jotai14.atom)(null, (_get, set) => { set(inputModeAtom, { type: "normal" }); set(interactionFeedbackAtom, null); set(pendingInputResolverAtom, null); }); var resetKeyboardInteractionModeAtom = (0, import_jotai14.atom)(null, (_get, set) => { set(keyboardInteractionModeAtom, "navigate"); }); var setKeyboardInteractionModeAtom = (0, import_jotai14.atom)(null, (_get, set, mode) => { set(keyboardInteractionModeAtom, mode); }); var startPickNodeAtom = (0, import_jotai14.atom)(null, (_get, set, options) => { set(inputModeAtom, { type: "pickNode", ...options }); }); var startPickNodesAtom = (0, import_jotai14.atom)(null, (_get, set, options) => { set(inputModeAtom, { type: "pickNodes", ...options }); }); var startPickPointAtom = (0, import_jotai14.atom)(null, (_get, set, options) => { set(inputModeAtom, { type: "pickPoint", ...options }); }); var provideInputAtom = (0, import_jotai14.atom)(null, (get, set, value) => { set(pendingInputResolverAtom, value); }); var updateInteractionFeedbackAtom = (0, import_jotai14.atom)(null, (get, set, feedback) => { const current = get(interactionFeedbackAtom); set(interactionFeedbackAtom, { ...current, ...feedback }); }); var isPickingModeAtom = (0, import_jotai14.atom)((get) => { const mode = get(inputModeAtom); return mode.type !== "normal"; }); var isPickNodeModeAtom = (0, import_jotai14.atom)((get) => { const mode = get(inputModeAtom); return mode.type === "pickNode" || mode.type === "pickNodes"; }); // src/core/locked-node-store.ts var import_jotai15 = require("jotai"); var lockedNodeIdAtom = (0, import_jotai15.atom)(null); var lockedNodeDataAtom = (0, import_jotai15.atom)((get) => { const id = get(lockedNodeIdAtom); if (!id) return null; const nodes = get(uiNodesAtom); return nodes.find((n) => n.id === id) || null; }); var lockedNodePageIndexAtom = (0, import_jotai15.atom)(0); var lockedNodePageCountAtom = (0, import_jotai15.atom)(1); var lockNodeAtom = (0, import_jotai15.atom)(null, (_get, set, payload) => { set(lockedNodeIdAtom, payload.nodeId); set(lockedNodePageIndexAtom, 0); }); var unlockNodeAtom = (0, import_jotai15.atom)(null, (_get, set) => { set(lockedNodeIdAtom, null); }); var nextLockedPageAtom = (0, import_jotai15.atom)(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current + 1) % pageCount); }); var prevLockedPageAtom = (0, import_jotai15.atom)(null, (get, set) => { const current = get(lockedNodePageIndexAtom); const pageCount = get(lockedNodePageCountAtom); set(lockedNodePageIndexAtom, (current - 1 + pageCount) % pageCount); }); var goToLockedPageAtom = (0, import_jotai15.atom)(null, (get, set, index) => { const pageCount = get(lockedNodePageCountAtom); if (index >= 0 && index < pageCount) { set(lockedNodePageIndexAtom, index); } }); var hasLockedNodeAtom = (0, import_jotai15.atom)((get) => get(lockedNodeIdAtom) !== null); // src/core/node-type-registry.tsx var import_compiler_runtime = require("react/compiler-runtime"); var import_react = __toESM(require("react")); var import_jsx_runtime = require("react/jsx-runtime"); // src/core/toast-store.ts var import_jotai16 = require("jotai"); var canvasToastAtom = (0, import_jotai16.atom)(null); var showToastAtom = (0, import_jotai16.atom)(null, (_get, set, message) => { const id = `toast-${Date.now()}`; set(canvasToastAtom, { id, message, timestamp: Date.now() }); setTimeout(() => { set(canvasToastAtom, (current) => current?.id === id ? null : current); }, 2e3); }); // src/core/snap-store.ts var import_jotai17 = require("jotai"); var snapEnabledAtom = (0, import_jotai17.atom)(false); var snapGridSizeAtom = (0, import_jotai17.atom)(20); var snapTemporaryDisableAtom = (0, import_jotai17.atom)(false); var isSnappingActiveAtom = (0, import_jotai17.atom)((get) => { return get(snapEnabledAtom) && !get(snapTemporaryDisableAtom); }); var toggleSnapAtom = (0, import_jotai17.atom)(null, (get, set) => { set(snapEnabledAtom, !get(snapEnabledAtom)); }); var setGridSizeAtom = (0, import_jotai17.atom)(null, (_get, set, size) => { set(snapGridSizeAtom, Math.max(5, Math.min(200, size))); }); var snapAlignmentEnabledAtom = (0, import_jotai17.atom)(true); var toggleAlignmentGuidesAtom = (0, import_jotai17.atom)(null, (get, set) => { set(snapAlignmentEnabledAtom, !get(snapAlignmentEnabledAtom)); }); var alignmentGuidesAtom = (0, import_jotai17.atom)({ verticalGuides: [], horizontalGuides: [] }); var clearAlignmentGuidesAtom = (0, import_jotai17.atom)(null, (_get, set) => { set(alignmentGuidesAtom, { verticalGuides: [], horizontalGuides: [] }); }); // src/core/event-types.ts var CanvasEventType = /* @__PURE__ */ (function(CanvasEventType2) { CanvasEventType2["NodeClick"] = "node:click"; CanvasEventType2["NodeDoubleClick"] = "node:double-click"; CanvasEventType2["NodeTripleClick"] = "node:triple-click"; CanvasEventType2["NodeRightClick"] = "node:right-click"; CanvasEventType2["NodeLongPress"] = "node:long-press"; CanvasEventType2["EdgeClick"] = "edge:click"; CanvasEventType2["EdgeDoubleClick"] = "edge:double-click"; CanvasEventType2["EdgeRightClick"] = "edge:right-click"; CanvasEventType2["BackgroundClick"] = "background:click"; CanvasEventType2["BackgroundDoubleClick"] = "background:double-click"; CanvasEventType2["BackgroundRightClick"] = "background:right-click"; CanvasEventType2["BackgroundLongPress"] = "background:long-press"; return CanvasEventType2; })({}); var EVENT_TYPE_INFO = { [CanvasEventType.NodeClick]: { type: CanvasEventType.NodeClick, label: "Click Node", description: "Triggered when clicking on a node", category: "node" }, [CanvasEventType.NodeDoubleClick]: { type: CanvasEventType.NodeDoubleClick, label: "Double-click Node", description: "Triggered when double-clicking on a node", category: "node" }, [CanvasEventType.NodeTripleClick]: { type: CanvasEventType.NodeTripleClick, label: "Triple-click Node", description: "Triggered when triple-clicking on a node", category: "node" }, [CanvasEventType.NodeRightClick]: { type: CanvasEventType.NodeRightClick, label: "Right-click Node", description: "Triggered when right-clicking on a node", category: "node" }, [CanvasEventType.NodeLongPress]: { type: CanvasEventType.NodeLongPress, label: "Long-press Node", description: "Triggered when long-pressing on a node (mobile/touch)", category: "node" }, [CanvasEventType.EdgeClick]: { type: CanvasEventType.EdgeClick, label: "Click Edge", description: "Triggered when clicking on an edge", category: "edge" }, [CanvasEventType.EdgeDoubleClick]: { type: CanvasEventType.EdgeDoubleClick, label: "Double-click Edge", description: "Triggered when double-clicking on an edge", category: "edge" }, [CanvasEventType.EdgeRightClick]: { type: CanvasEventType.EdgeRightClick, label: "Right-click Edge", description: "Triggered when right-clicking on an edge", category: "edge" }, [CanvasEventType.BackgroundClick]: { type: CanvasEventType.BackgroundClick, label: "Click Background", description: "Triggered when clicking on the canvas background", category: "background" }, [CanvasEventType.BackgroundDoubleClick]: { type: CanvasEventType.BackgroundDoubleClick, label: "Double-click Background", description: "Triggered when double-clicking on the canvas background", category: "background" }, [CanvasEventType.BackgroundRightClick]: { type: CanvasEventType.BackgroundRightClick, label: "Right-click Background", description: "Triggered when right-clicking on the canvas background", category: "background" }, [CanvasEventType.BackgroundLongPress]: { type: CanvasEventType.BackgroundLongPress, label: "Long-press Background", description: "Triggered when long-pressing on the canvas background (mobile/touch)", category: "background" } }; // src/core/action-types.ts var ActionCategory = /* @__PURE__ */ (function(ActionCategory2) { ActionCategory2["None"] = "none"; ActionCategory2["Selection"] = "selection"; ActionCategory2["Viewport"] = "viewport"; ActionCategory2["Node"] = "node"; ActionCategory2["Layout"] = "layout"; ActionCategory2["History"] = "history"; ActionCategory2["Custom"] = "custom"; return ActionCategory2; })({}); var BuiltInActionId = { // None None: "none", // Selection SelectNode: "select-node", SelectEdge: "select-edge", AddToSelection: "add-to-selection", ClearSelection: "clear-selection", DeleteSelected: "delete-selected", // Viewport FitToView: "fit-to-view", FitAllToView: "fit-all-to-view", CenterOnNode: "center-on-node", ResetViewport: "reset-viewport", // Node LockNode: "lock-node", UnlockNode: "unlock-node", ToggleLock: "toggle-lock", OpenContextMenu: "open-context-menu", SplitNode: "split-node", GroupNodes: "group-nodes", MergeNodes: "merge-nodes", // Layout ApplyForceLayout: "apply-force-layout", // History Undo: "undo", Redo: "redo", // Creation CreateNode: "create-node" }; // src/core/settings-state-types.ts var DEFAULT_MAPPINGS = { [CanvasEventType.NodeClick]: BuiltInActionId.None, [CanvasEventType.NodeDoubleClick]: BuiltInActionId.FitToView, [CanvasEventType.NodeTripleClick]: BuiltInActionId.ToggleLock, [CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.NodeLongPress]: BuiltInActionId.OpenContextMenu, [CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge, [CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None, [CanvasEventType.EdgeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection, [CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.FitAllToView, [CanvasEventType.BackgroundRightClick]: BuiltInActionId.None, [CanvasEventType.BackgroundLongPress]: BuiltInActionId.CreateNode }; // src/core/actions-node.ts function registerSelectionActions() { registerAction2({ 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); } } }); registerAction2({ 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); } } }); registerAction2({ 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); } } }); registerAction2({ id: BuiltInActionId.ClearSelection, label: "Clear Selection", description: "Deselect all nodes", category: ActionCategory.Selection, icon: "x-square", isBuiltIn: true, handler: (_context, helpers) => { helpers.clearSelection(); } }); registerAction2({ 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() { registerAction2({ 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); } } }); registerAction2({ 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); } } }); registerAction2({ 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); } } }); registerAction2({ 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); } } }); registerAction2({ 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); } } }); registerAction2({ 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); } } }); registerAction2({ 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()); } } }); registerAction2({ id: BuiltInActionId.MergeNodes, label: "Merge Nodes", description: "Merge selected nodes into one", category: ActionCategory.Node, icon: "merge", isBuiltIn: true, handler: async (context, helpers) => { if (helpers.mergeNodes) { await helpers.mergeNodes(context.selectedNodeIds ?? helpers.getSelectedNodeIds()); } } }); } // src/core/actions-viewport.ts function registerViewportActions() { registerAction2({ 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); } } }); registerAction2({ 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"); } }); registerAction2({ 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); } } }); registerAction2({ 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() { registerAction2({ 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(); } } }); registerAction2({ 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(); } } }); registerAction2({ id: BuiltInActionId.ApplyForceLayout, label: "Apply Force Layout", description: "Automatically arrange nodes using force-directed layout", category: ActionCategory.Layout, icon: "layout-grid", isBuiltIn: true, handler: async (_context, helpers) => { await helpers.applyForceLayout(); } }); } // src/core/built-in-actions.ts function registerBuiltInActions() { registerAction2({ id: BuiltInActionId.None, label: "None", description: "Do nothing", category: ActionCategory.None, icon: "ban", isBuiltIn: true, handler: () => { } }); registerSelectionActions(); registerNodeActions(); registerViewportActions(); registerHistoryActions(); } // src/core/action-registry.ts var actionRegistry = /* @__PURE__ */ new Map(); function registerAction2(action) { actionRegistry.set(action.id, action); } registerBuiltInActions(); // src/core/action-executor.ts var debug9 = createDebug("actions"); // src/core/settings-store.ts var import_jotai18 = require("jotai"); var import_utils = require("jotai/utils"); // src/core/settings-presets.ts var BUILT_IN_PRESETS = [{ id: "default", name: "Default", description: "Standard canvas interactions", isBuiltIn: true, mappings: DEFAULT_MAPPINGS }, { id: "minimal", name: "Minimal", description: "Only essential selection and context menu actions", isBuiltIn: true, mappings: { [CanvasEventType.NodeClick]: BuiltInActionId.None, [CanvasEventType.NodeDoubleClick]: BuiltInActionId.None, [CanvasEventType.NodeTripleClick]: BuiltInActionId.None, [CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.NodeLongPress]: BuiltInActionId.OpenContextMenu, [CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge, [CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None, [CanvasEventType.EdgeRightClick]: BuiltInActionId.None, [CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection, [CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.None, [CanvasEventType.BackgroundRightClick]: BuiltInActionId.None, [CanvasEventType.BackgroundLongPress]: BuiltInActionId.None } }, { id: "power-user", name: "Power User", description: "Quick actions for experienced users", isBuiltIn: true, mappings: { [CanvasEventType.NodeClick]: BuiltInActionId.None, [CanvasEventType.NodeDoubleClick]: BuiltInActionId.ToggleLock, [CanvasEventType.NodeTripleClick]: BuiltInActionId.DeleteSelected, [CanvasEventType.NodeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.NodeLongPress]: BuiltInActionId.AddToSelection, [CanvasEventType.EdgeClick]: BuiltInActionId.SelectEdge, [CanvasEventType.EdgeDoubleClick]: BuiltInActionId.None, [CanvasEventType.EdgeRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.BackgroundClick]: BuiltInActionId.ClearSelection, [CanvasEventType.BackgroundDoubleClick]: BuiltInActionId.CreateNode, [CanvasEventType.BackgroundRightClick]: BuiltInActionId.OpenContextMenu, [CanvasEventType.BackgroundLongPress]: BuiltInActionId.ApplyForceLayout } }]; // src/core/settings-store.ts var debug10 = createDebug("settings"); var DEFAULT_STATE = { mappings: DEFAULT_MAPPINGS, activePresetId: "default", customPresets: [], isPanelOpen: false, virtualizationEnabled: true }; var canvasSettingsAtom = (0, import_utils.atomWithStorage)("@blinksgg/canvas/settings", DEFAULT_STATE); var eventMappingsAtom = (0, import_jotai18.atom)((get) => get(canvasSettingsAtom).mappings); var activePresetIdAtom = (0, import_jotai18.atom)((get) => get(canvasSettingsAtom).activePresetId); var allPresetsAtom = (0, import_jotai18.atom)((get) => { const state = get(canvasSettingsAtom); return [...BUILT_IN_PRESETS, ...state.customPresets]; }); var activePresetAtom = (0, import_jotai18.atom)((get) => { const presetId = get(activePresetIdAtom); if (!presetId) return null; const allPresets = get(allPresetsAtom); return allPresets.find((p) => p.id === presetId) || null; }); var isPanelOpenAtom = (0, import_jotai18.atom)((get) => get(canvasSettingsAtom).isPanelOpen); var virtualizationEnabledAtom = (0, import_jotai18.atom)((get) => get(canvasSettingsAtom).virtualizationEnabled ?? true); var hasUnsavedChangesAtom = (0, import_jotai18.atom)((get) => { const state = get(canvasSettingsAtom); const activePreset = get(activePresetAtom); if (!activePreset) return true; const events = Object.values(CanvasEventType); return events.some((event) => state.mappings[event] !== activePreset.mappings[event]); }); var setEventMappingAtom = (0, import_jotai18.atom)(null, (get, set, { event, actionId }) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: { ...current.mappings, [event]: actionId }, // Clear active preset since mappings have changed activePresetId: null }); }); var applyPresetAtom = (0, import_jotai18.atom)(null, (get, set, presetId) => { const allPresets = get(allPresetsAtom); const preset = allPresets.find((p) => p.id === presetId); if (!preset) { debug10.warn("Preset not found: %s", presetId); return; } const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: { ...preset.mappings }, activePresetId: presetId }); }); var saveAsPresetAtom = (0, import_jotai18.atom)(null, (get, set, { name, description }) => { const current = get(canvasSettingsAtom); const id = `custom-${Date.now()}`; const newPreset = { id, name, description, mappings: { ...current.mappings }, isBuiltIn: false }; set(canvasSettingsAtom, { ...current, customPresets: [...current.customPresets, newPreset], activePresetId: id }); return id; }); var updatePresetAtom = (0, import_jotai18.atom)(null, (get, set, presetId) => { const current = get(canvasSettingsAtom); const presetIndex = current.customPresets.findIndex((p) => p.id === presetId); if (presetIndex === -1) { debug10.warn("Cannot update preset: %s (not found or built-in)", presetId); return; } const updatedPresets = [...current.customPresets]; updatedPresets[presetIndex] = { ...updatedPresets[presetIndex], mappings: { ...current.mappings } }; set(canvasSettingsAtom, { ...current, customPresets: updatedPresets, activePresetId: presetId }); }); var deletePresetAtom = (0, import_jotai18.atom)(null, (get, set, presetId) => { const current = get(canvasSettingsAtom); const newCustomPresets = current.customPresets.filter((p) => p.id !== presetId); if (newCustomPresets.length === current.customPresets.length) { debug10.warn("Cannot delete preset: %s (not found or built-in)", presetId); return; } const newActiveId = current.activePresetId === presetId ? "default" : current.activePresetId; const newMappings = newActiveId === "default" ? DEFAULT_MAPPINGS : current.mappings; set(canvasSettingsAtom, { ...current, customPresets: newCustomPresets, activePresetId: newActiveId, mappings: newMappings }); }); var resetSettingsAtom = (0, import_jotai18.atom)(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, mappings: DEFAULT_MAPPINGS, activePresetId: "default" }); }); var togglePanelAtom = (0, import_jotai18.atom)(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: !current.isPanelOpen }); }); var setPanelOpenAtom = (0, import_jotai18.atom)(null, (get, set, isOpen) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, isPanelOpen: isOpen }); }); var setVirtualizationEnabledAtom = (0, import_jotai18.atom)(null, (get, set, enabled) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: enabled }); }); var toggleVirtualizationAtom = (0, import_jotai18.atom)(null, (get, set) => { const current = get(canvasSettingsAtom); set(canvasSettingsAtom, { ...current, virtualizationEnabled: !(current.virtualizationEnabled ?? true) }); }); // src/core/canvas-serializer.ts var import_graphology4 = __toESM(require("graphology")); // src/core/clipboard-store.ts var import_jotai19 = require("jotai"); var debug11 = createDebug("clipboard"); var PASTE_OFFSET = { x: 50, y: 50 }; var clipboardAtom = (0, import_jotai19.atom)(null); var hasClipboardContentAtom = (0, import_jotai19.atom)((get) => get(clipboardAtom) !== null); var clipboardNodeCountAtom = (0, import_jotai19.atom)((get) => { const clipboard = get(clipboardAtom); return clipboard?.nodes.length ?? 0; }); function calculateBounds2(nodes) { if (nodes.length === 0) { return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; } let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (const node of nodes) { minX = Math.min(minX, node.attrs.x); minY = Math.min(minY, node.attrs.y); maxX = Math.max(maxX, node.attrs.x + node.attrs.width); maxY = Math.max(maxY, node.attrs.y + node.attrs.height); } return { minX, minY, maxX, maxY }; } function generatePasteId(index) { return `paste-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 8)}`; } var copyToClipboardAtom = (0, import_jotai19.atom)(null, (get, set, nodeIds) => { const selectedIds = nodeIds ?? Array.from(get(selectedNodeIdsAtom)); if (selectedIds.length === 0) { debug11("Nothing to copy - no nodes selected"); return; } const graph = get(graphAtom); const selectedSet = new Set(selectedIds); const nodes = []; const edges = []; for (const nodeId of selectedIds) { if (!graph.hasNode(nodeId)) { debug11("Node %s not found in graph, skipping", nodeId); continue; } const attrs = graph.getNodeAttributes(nodeId); nodes.push({ attrs: { ...attrs }, dbData: { ...attrs.dbData } }); } graph.forEachEdge((edgeKey, attrs, source, target) => { if (selectedSet.has(source) && selectedSet.has(target)) { edges.push({ source, target, attrs: { ...attrs }, dbData: { ...attrs.dbData } }); } }); const bounds = calculateBounds2(nodes); const clipboardData = { nodes, edges, bounds, timestamp: Date.now() }; set(clipboardAtom, clipboardData); debug11("Copied %d nodes and %d edges to clipboard", nodes.length, edges.length); }); var cutToClipboardAtom = (0, import_jotai19.atom)(null, (get, set, nodeIds) => { const selectedIds = nodeIds ?? Array.from(get(selectedNodeIdsAtom)); if (selectedIds.length === 0) return; set(copyToClipboardAtom, selectedIds); set(pushHistoryAtom, "Cut nodes"); for (const nodeId of selectedIds) { set(optimisticDeleteNodeAtom, { nodeId }); } set(clearSelectionAtom); debug11("Cut %d nodes \u2014 copied to clipboard and deleted from graph", selectedIds.length); }); var pasteFromClipboardAtom = (0, import_jotai19.atom)(null, (get, set, offset) => { const clipboard = get(clipboardAtom); if (!clipboard || clipboard.nodes.length === 0) { debug11("Nothing to paste - clipboard empty"); return []; } const pasteOffset = offset ?? PASTE_OFFSET; const graph = get(graphAtom); set(pushHistoryAtom, "Paste nodes"); const idMap = /* @__PURE__ */ new Map(); const newNodeIds = []; for (let i = 0; i < clipboard.nodes.length; i++) { const nodeData = clipboard.nodes[i]; const newId = generatePasteId(i); idMap.set(nodeData.dbData.id, newId); newNodeIds.push(newId); const newDbNode = { ...nodeData.dbData, id: newId, created_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString(), ui_properties: { ...nodeData.dbData.ui_properties || {}, x: nodeData.attrs.x + pasteOffset.x, y: nodeData.attrs.y + pasteOffset.y } }; debug11("Pasting node %s -> %s at (%d, %d)", nodeData.dbData.id, newId, nodeData.attrs.x + pasteOffset.x, nodeData.attrs.y + pasteOffset.y); set(addNodeToLocalGraphAtom, newDbNode); } for (const edgeData of clipboard.edges) { const newSourceId = idMap.get(edgeData.source); const newTargetId = idMap.get(edgeData.target); if (!newSourceId || !newTargetId) { debug11("Edge %s: source or target not found in id map, skipping", edgeData.dbData.id); continue; } const newEdgeId = generatePasteId(clipboard.edges.indexOf(edgeData) + clipboard.nodes.length); const newDbEdge = { ...edgeData.dbData, id: newEdgeId, source_node_id: newSourceId, target_node_id: newTargetId, created_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() }; debug11("Pasting edge %s -> %s (from %s to %s)", edgeData.dbData.id, newEdgeId, newSourceId, newTargetId); set(addEdgeToLocalGraphAtom, newDbEdge); } set(clearSelectionAtom); set(addNodesToSelectionAtom, newNodeIds); debug11("Pasted %d nodes and %d edges", newNodeIds.length, clipboard.edges.length); return newNodeIds; }); var duplicateSelectionAtom = (0, import_jotai19.atom)(null, (get, set) => { set(copyToClipboardAtom); return set(pasteFromClipboardAtom); }); var clearClipboardAtom = (0, import_jotai19.atom)(null, (_get, set) => { set(clipboardAtom, null); debug11("Clipboard cleared"); }); // src/core/virtualization-store.ts var import_jotai20 = require("jotai"); // src/core/spatial-index.ts var SpatialGrid = class { constructor(cellSize = 500) { /** cell key → set of node IDs in that cell */ __publicField(this, "cells", /* @__PURE__ */ new Map()); /** node ID → entry data (for update/remove) */ __publicField(this, "entries", /* @__PURE__ */ new Map()); this.cellSize = cellSize; } /** Number of tracked entries */ get size() { return this.entries.size; } cellKey(cx, cy) { return `${cx},${cy}`; } getCellRange(x, y, w, h) { const cs = this.cellSize; return { minCX: Math.floor(x / cs), minCY: Math.floor(y / cs), maxCX: Math.floor((x + w) / cs), maxCY: Math.floor((y + h) / cs) }; } /** * Insert a node into the index. * If the node already exists, it is updated. */ insert(id, x, y, width, height) { if (this.entries.has(id)) { this.update(id, x, y, width, height); return; } const entry = { id, x, y, width, height }; this.entries.set(id, entry); const { minCX, minCY, maxCX, maxCY } = this.getCellRange(x, y, width, height); for (let cx = minCX; cx <= maxCX; cx++) { for (let cy = minCY; cy <= maxCY; cy++) { const key = this.cellKey(cx, cy); let cell = this.cells.get(key); if (!cell) { cell = /* @__PURE__ */ new Set(); this.cells.set(key, cell); } cell.add(id); } } } /** * Update a node's position/dimensions. */ update(id, x, y, width, height) { const prev = this.entries.get(id); if (!prev) { this.insert(id, x, y, width, height); return; } const prevRange = this.getCellRange(prev.x, prev.y, prev.width, prev.height); const newRange = this.getCellRange(x, y, width, height); prev.x = x; prev.y = y; prev.width = width; prev.height = height; if (prevRange.minCX === newRange.minCX && prevRange.minCY === newRange.minCY && prevRange.maxCX === newRange.maxCX && prevRange.maxCY === newRange.maxCY) { return; } for (let cx = prevRange.minCX; cx <= prevRange.maxCX; cx++) { for (let cy = prevRange.minCY; cy <= prevRange.maxCY; cy++) { const key = this.cellKey(cx, cy); const cell = this.cells.get(key); if (cell) { cell.delete(id); if (cell.size === 0) this.cells.delete(key); } } } for (let cx = newRange.minCX; cx <= newRange.maxCX; cx++) { for (let cy = newRange.minCY; cy <= newRange.maxCY; cy++) { const key = this.cellKey(cx, cy); let cell = this.cells.get(key); if (!cell) { cell = /* @__PURE__ */ new Set(); this.cells.set(key, cell); } cell.add(id); } } } /** * Remove a node from the index. */ remove(id) { const entry = this.entries.get(id); if (!entry) return; const { minCX, minCY, maxCX, maxCY } = this.getCellRange(entry.x, entry.y, entry.width, entry.height); for (let cx = minCX; cx <= maxCX; cx++) { for (let cy = minCY; cy <= maxCY; cy++) { const key = this.cellKey(cx, cy); const cell = this.cells.get(key); if (cell) { cell.delete(id); if (cell.size === 0) this.cells.delete(key); } } } this.entries.delete(id); } /** * Query all node IDs whose bounding box overlaps the given bounds. * Returns a Set for O(1) membership checks. */ query(bounds) { const result = /* @__PURE__ */ new Set(); const { minCX, minCY, maxCX, maxCY } = this.getCellRange(bounds.minX, bounds.minY, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY); for (let cx = minCX; cx <= maxCX; cx++) { for (let cy = minCY; cy <= maxCY; cy++) { const cell = this.cells.get(this.cellKey(cx, cy)); if (!cell) continue; for (const id of cell) { if (result.has(id)) continue; const entry = this.entries.get(id); const entryRight = entry.x + entry.width; const entryBottom = entry.y + entry.height; if (entry.x <= bounds.maxX && entryRight >= bounds.minX && entry.y <= bounds.maxY && entryBottom >= bounds.minY) { result.add(id); } } } } return result; } /** * Clear all entries. */ clear() { this.cells.clear(); this.entries.clear(); } /** * Check if a node is tracked. */ has(id) { return this.entries.has(id); } }; // src/core/virtualization-store.ts var VIRTUALIZATION_BUFFER = 200; var spatialIndexAtom = (0, import_jotai20.atom)((get) => { get(graphUpdateVersionAtom); get(nodePositionUpdateCounterAtom); const graph = get(graphAtom); const grid = new SpatialGrid(500); graph.forEachNode((nodeId, attrs) => { const a = attrs; grid.insert(nodeId, a.x, a.y, a.width || 200, a.height || 100); }); return grid; }); var visibleBoundsAtom = (0, import_jotai20.atom)((get) => { const viewport = get(viewportRectAtom); const pan = get(panAtom); const zoom = get(zoomAtom); if (!viewport || zoom === 0) { return null; } const buffer = VIRTUALIZATION_BUFFER; return { minX: (-buffer - pan.x) / zoom, minY: (-buffer - pan.y) / zoom, maxX: (viewport.width + buffer - pan.x) / zoom, maxY: (viewport.height + buffer - pan.y) / zoom }; }); var visibleNodeKeysAtom = (0, import_jotai20.atom)((get) => { const end = canvasMark("virtualization-cull"); const enabled = get(virtualizationEnabledAtom); const allKeys = get(nodeKeysAtom); if (!enabled) { end(); return allKeys; } const bounds = get(visibleBoundsAtom); if (!bounds) { end(); return allKeys; } const grid = get(spatialIndexAtom); const visibleSet = grid.query(bounds); const result = allKeys.filter((k) => visibleSet.has(k)); end(); return result; }); var visibleEdgeKeysAtom = (0, import_jotai20.atom)((get) => { const enabled = get(virtualizationEnabledAtom); const allEdgeKeys = get(edgeKeysAtom); const edgeCreation = get(edgeCreationAtom); const remap = get(collapsedEdgeRemapAtom); const tempEdgeKey = edgeCreation.isCreating ? "temp-creating-edge" : null; get(graphUpdateVersionAtom); const graph = get(graphAtom); const filteredEdges = allEdgeKeys.filter((edgeKey) => { const source = graph.source(edgeKey); const target = graph.target(edgeKey); const effectiveSource = remap.get(source) ?? source; const effectiveTarget = remap.get(target) ?? target; if (effectiveSource === effectiveTarget) return false; return true; }); if (!enabled) { return tempEdgeKey ? [...filteredEdges, tempEdgeKey] : filteredEdges; } const visibleNodeKeys = get(visibleNodeKeysAtom); const visibleNodeSet = new Set(visibleNodeKeys); const visibleEdges = filteredEdges.filter((edgeKey) => { const source = graph.source(edgeKey); const target = graph.target(edgeKey); const effectiveSource = remap.get(source) ?? source; const effectiveTarget = remap.get(target) ?? target; return visibleNodeSet.has(effectiveSource) && visibleNodeSet.has(effectiveTarget); }); return tempEdgeKey ? [...visibleEdges, tempEdgeKey] : visibleEdges; }); var virtualizationMetricsAtom = (0, import_jotai20.atom)((get) => { const enabled = get(virtualizationEnabledAtom); const totalNodes = get(nodeKeysAtom).length; const totalEdges = get(edgeKeysAtom).length; const visibleNodes = get(visibleNodeKeysAtom).length; const visibleEdges = get(visibleEdgeKeysAtom).length; const bounds = get(visibleBoundsAtom); return { enabled, totalNodes, totalEdges, visibleNodes, visibleEdges, culledNodes: totalNodes - visibleNodes, culledEdges: totalEdges - visibleEdges, bounds }; }); // src/core/input-store.ts var import_jotai21 = require("jotai"); var activePointersAtom = (0, import_jotai21.atom)(/* @__PURE__ */ new Map()); var primaryInputSourceAtom = (0, import_jotai21.atom)("mouse"); var inputCapabilitiesAtom = (0, import_jotai21.atom)(detectInputCapabilities()); var isStylusActiveAtom = (0, import_jotai21.atom)((get) => { const pointers = get(activePointersAtom); for (const [, pointer] of pointers) { if (pointer.source === "pencil") return true; } return false; }); var isMultiTouchAtom = (0, import_jotai21.atom)((get) => { const pointers = get(activePointersAtom); let fingerCount = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") fingerCount++; } return fingerCount > 1; }); var fingerCountAtom = (0, import_jotai21.atom)((get) => { const pointers = get(activePointersAtom); let count = 0; for (const [, pointer] of pointers) { if (pointer.source === "finger") count++; } return count; }); var isTouchDeviceAtom = (0, import_jotai21.atom)((get) => { const caps = get(inputCapabilitiesAtom); return caps.hasTouch; }); var pointerDownAtom = (0, import_jotai21.atom)(null, (get, set, pointer) => { const pointers = new Map(get(activePointersAtom)); pointers.set(pointer.pointerId, pointer); set(activePointersAtom, pointers); set(primaryInputSourceAtom, pointer.source); if (pointer.source === "pencil") { const caps = get(inputCapabilitiesAtom); if (!caps.hasStylus) { set(inputCapabilitiesAtom, { ...caps, hasStylus: true }); } } }); var pointerUpAtom = (0, import_jotai21.atom)(null, (get, set, pointerId) => { const pointers = new Map(get(activePointersAtom)); pointers.delete(pointerId); set(activePointersAtom, pointers); }); var clearPointersAtom = (0, import_jotai21.atom)(null, (_get, set) => { set(activePointersAtom, /* @__PURE__ */ new Map()); }); // src/core/selection-path-store.ts var import_jotai22 = require("jotai"); var selectionPathAtom = (0, import_jotai22.atom)(null); var isSelectingAtom = (0, import_jotai22.atom)((get) => get(selectionPathAtom) !== null); var startSelectionAtom = (0, import_jotai22.atom)(null, (_get, set, { type, point }) => { set(selectionPathAtom, { type, points: [point] }); }); var updateSelectionAtom = (0, import_jotai22.atom)(null, (get, set, point) => { const current = get(selectionPathAtom); if (!current) return; if (current.type === "rect") { set(selectionPathAtom, { ...current, points: [current.points[0], point] }); } else { set(selectionPathAtom, { ...current, points: [...current.points, point] }); } }); var cancelSelectionAtom = (0, import_jotai22.atom)(null, (_get, set) => { set(selectionPathAtom, null); }); var endSelectionAtom = (0, import_jotai22.atom)(null, (get, set) => { const path = get(selectionPathAtom); if (!path || path.points.length < 2) { set(selectionPathAtom, null); return; } const nodes = get(uiNodesAtom); const selectedIds = []; if (path.type === "rect") { const [p1, p2] = [path.points[0], path.points[path.points.length - 1]]; const minX = Math.min(p1.x, p2.x); const maxX = Math.max(p1.x, p2.x); const minY = Math.min(p1.y, p2.y); const maxY = Math.max(p1.y, p2.y); for (const node of nodes) { const nodeRight = node.position.x + (node.width ?? 200); const nodeBottom = node.position.y + (node.height ?? 100); if (node.position.x < maxX && nodeRight > minX && node.position.y < maxY && nodeBottom > minY) { selectedIds.push(node.id); } } } else { const polygon = path.points; for (const node of nodes) { const cx = node.position.x + (node.width ?? 200) / 2; const cy = node.position.y + (node.height ?? 100) / 2; if (pointInPolygon(cx, cy, polygon)) { selectedIds.push(node.id); } } } set(selectedNodeIdsAtom, new Set(selectedIds)); set(selectionPathAtom, null); }); var selectionRectAtom = (0, import_jotai22.atom)((get) => { const path = get(selectionPathAtom); if (!path || path.type !== "rect" || path.points.length < 2) return null; const [p1, p2] = [path.points[0], path.points[path.points.length - 1]]; return { x: Math.min(p1.x, p2.x), y: Math.min(p1.y, p2.y), width: Math.abs(p2.x - p1.x), height: Math.abs(p2.y - p1.y) }; }); function pointInPolygon(px, py, polygon) { let inside = false; const n = polygon.length; for (let i = 0, j = n - 1; i < n; j = i++) { const xi = polygon[i].x; const yi = polygon[i].y; const xj = polygon[j].x; const yj = polygon[j].y; if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) { inside = !inside; } } return inside; } // src/core/search-store.ts var import_jotai23 = require("jotai"); var searchQueryAtom = (0, import_jotai23.atom)(""); var setSearchQueryAtom = (0, import_jotai23.atom)(null, (_get, set, query) => { set(searchQueryAtom, query); set(highlightedSearchIndexAtom, 0); }); var clearSearchAtom = (0, import_jotai23.atom)(null, (_get, set) => { set(searchQueryAtom, ""); set(highlightedSearchIndexAtom, 0); }); function fuzzyMatch(query, ...haystacks) { const tokens = query.toLowerCase().split(/\s+/).filter(Boolean); if (tokens.length === 0) return false; const combined = haystacks.join(" ").toLowerCase(); return tokens.every((token) => combined.includes(token)); } var searchResultsAtom = (0, import_jotai23.atom)((get) => { const query = get(searchQueryAtom).trim(); if (!query) return /* @__PURE__ */ new Set(); const nodes = get(uiNodesAtom); const matches = /* @__PURE__ */ new Set(); for (const node of nodes) { if (fuzzyMatch(query, node.label || "", node.dbData.node_type || "", node.id)) { matches.add(node.id); } } return matches; }); var searchResultsArrayAtom = (0, import_jotai23.atom)((get) => { return Array.from(get(searchResultsAtom)); }); var searchResultCountAtom = (0, import_jotai23.atom)((get) => { return get(searchResultsAtom).size; }); var searchEdgeResultsAtom = (0, import_jotai23.atom)((get) => { const query = get(searchQueryAtom).trim(); if (!query) return /* @__PURE__ */ new Set(); get(graphUpdateVersionAtom); const graph = get(graphAtom); const matches = /* @__PURE__ */ new Set(); graph.forEachEdge((edgeKey, attrs) => { const label = attrs.label || ""; const edgeType = attrs.dbData?.edge_type || ""; if (fuzzyMatch(query, label, edgeType, edgeKey)) { matches.add(edgeKey); } }); return matches; }); var searchEdgeResultCountAtom = (0, import_jotai23.atom)((get) => { return get(searchEdgeResultsAtom).size; }); var isFilterActiveAtom = (0, import_jotai23.atom)((get) => { return get(searchQueryAtom).trim().length > 0; }); var searchTotalResultCountAtom = (0, import_jotai23.atom)((get) => { return get(searchResultCountAtom) + get(searchEdgeResultCountAtom); }); var highlightedSearchIndexAtom = (0, import_jotai23.atom)(0); var nextSearchResultAtom = (0, import_jotai23.atom)(null, (get, set) => { const results = get(searchResultsArrayAtom); if (results.length === 0) return; const currentIndex = get(highlightedSearchIndexAtom); const nextIndex = (currentIndex + 1) % results.length; set(highlightedSearchIndexAtom, nextIndex); const nodeId = results[nextIndex]; set(centerOnNodeAtom, nodeId); set(selectSingleNodeAtom, nodeId); }); var prevSearchResultAtom = (0, import_jotai23.atom)(null, (get, set) => { const results = get(searchResultsArrayAtom); if (results.length === 0) return; const currentIndex = get(highlightedSearchIndexAtom); const prevIndex = (currentIndex - 1 + results.length) % results.length; set(highlightedSearchIndexAtom, prevIndex); const nodeId = results[prevIndex]; set(centerOnNodeAtom, nodeId); set(selectSingleNodeAtom, nodeId); }); var highlightedSearchNodeIdAtom = (0, import_jotai23.atom)((get) => { const results = get(searchResultsArrayAtom); if (results.length === 0) return null; const index = get(highlightedSearchIndexAtom); return results[index] ?? null; }); // src/core/gesture-rules-defaults.ts function mergeRules(defaults, overrides) { const overrideMap = new Map(overrides.map((r) => [r.id, r])); const result = []; for (const rule of defaults) { const override = overrideMap.get(rule.id); if (override) { result.push(override); overrideMap.delete(rule.id); } else { result.push(rule); } } for (const rule of overrideMap.values()) { result.push(rule); } return result; } var DEFAULT_GESTURE_RULES = [ // ── Tap gestures ────────────────────────────────────────────── { id: "tap-node", pattern: { gesture: "tap", target: "node" }, actionId: "select-node" }, { id: "tap-edge", pattern: { gesture: "tap", target: "edge" }, actionId: "select-edge" }, { id: "tap-port", pattern: { gesture: "tap", target: "port" }, actionId: "select-node" }, { id: "tap-bg", pattern: { gesture: "tap", target: "background" }, actionId: "clear-selection" }, // ── Double-tap ──────────────────────────────────────────────── { id: "dtap-node", pattern: { gesture: "double-tap", target: "node" }, actionId: "fit-to-view" }, { id: "dtap-bg", pattern: { gesture: "double-tap", target: "background" }, actionId: "fit-all-to-view" }, // ── Triple-tap ──────────────────────────────────────────────── { id: "ttap-node", pattern: { gesture: "triple-tap", target: "node" }, actionId: "toggle-lock" }, // ── Left-button drag ────────────────────────────────────────── { id: "drag-node", pattern: { gesture: "drag", target: "node" }, actionId: "move-node" }, { id: "drag-port", pattern: { gesture: "drag", target: "port" }, actionId: "create-edge" }, { id: "drag-bg-finger", pattern: { gesture: "drag", target: "background", source: "finger" }, actionId: "pan" }, { id: "drag-bg-mouse", pattern: { gesture: "drag", target: "background", source: "mouse" }, actionId: "pan" }, { id: "drag-bg-pencil", pattern: { gesture: "drag", target: "background", source: "pencil" }, actionId: "lasso-select" }, // ── Shift+drag overrides ────────────────────────────────────── { id: "shift-drag-bg", pattern: { gesture: "drag", target: "background", modifiers: { shift: true } }, actionId: "rect-select" }, // ── Right-click tap (context menu) ──────────────────────────── { id: "rc-node", pattern: { gesture: "tap", target: "node", button: 2 }, actionId: "open-context-menu" }, { id: "rc-edge", pattern: { gesture: "tap", target: "edge", button: 2 }, actionId: "open-context-menu" }, { id: "rc-bg", pattern: { gesture: "tap", target: "background", button: 2 }, actionId: "open-context-menu" }, // ── Long-press ──────────────────────────────────────────────── { id: "lp-node", pattern: { gesture: "long-press", target: "node" }, actionId: "open-context-menu" }, { id: "lp-bg-finger", pattern: { gesture: "long-press", target: "background", source: "finger" }, actionId: "create-node" }, // ── Right-button drag (defaults to none — consumers override) ─ { id: "rdrag-node", pattern: { gesture: "drag", target: "node", button: 2 }, actionId: "none" }, { id: "rdrag-bg", pattern: { gesture: "drag", target: "background", button: 2 }, actionId: "none" }, // ── Middle-button drag (defaults to none) ───────────────────── { id: "mdrag-node", pattern: { gesture: "drag", target: "node", button: 1 }, actionId: "none" }, { id: "mdrag-bg", pattern: { gesture: "drag", target: "background", button: 1 }, actionId: "none" }, // ── Zoom ────────────────────────────────────────────────────── { id: "pinch-bg", pattern: { gesture: "pinch", target: "background" }, actionId: "zoom" }, { id: "scroll-any", pattern: { gesture: "scroll" }, actionId: "zoom" }, // ── Split ───────────────────────────────────────────────────── { id: "pinch-node", pattern: { gesture: "pinch", target: "node" }, actionId: "split-node" } ]; // src/core/gesture-rules.ts 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; } // src/core/gesture-rule-store.ts var import_jotai24 = require("jotai"); var import_utils2 = require("jotai/utils"); var DEFAULT_RULE_STATE = { customRules: [], palmRejection: true }; var gestureRuleSettingsAtom = (0, import_utils2.atomWithStorage)("canvas-gesture-rules", DEFAULT_RULE_STATE); var consumerGestureRulesAtom = (0, import_jotai24.atom)([]); var gestureRulesAtom = (0, import_jotai24.atom)((get) => { const settings = get(gestureRuleSettingsAtom); const consumerRules = get(consumerGestureRulesAtom); let rules = mergeRules(DEFAULT_GESTURE_RULES, settings.customRules); if (consumerRules.length > 0) { rules = mergeRules(rules, consumerRules); } return rules; }); var gestureRuleIndexAtom = (0, import_jotai24.atom)((get) => { return buildRuleIndex(get(gestureRulesAtom)); }); var palmRejectionEnabledAtom = (0, import_jotai24.atom)((get) => get(gestureRuleSettingsAtom).palmRejection, (get, set, enabled) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, palmRejection: enabled }); }); var addGestureRuleAtom = (0, import_jotai24.atom)(null, (get, set, rule) => { const current = get(gestureRuleSettingsAtom); const existing = current.customRules.findIndex((r) => r.id === rule.id); const newRules = [...current.customRules]; if (existing >= 0) { newRules[existing] = rule; } else { newRules.push(rule); } set(gestureRuleSettingsAtom, { ...current, customRules: newRules }); }); var removeGestureRuleAtom = (0, import_jotai24.atom)(null, (get, set, ruleId) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: current.customRules.filter((r) => r.id !== ruleId) }); }); var updateGestureRuleAtom = (0, import_jotai24.atom)(null, (get, set, { id, updates }) => { const current = get(gestureRuleSettingsAtom); const index = current.customRules.findIndex((r) => r.id === id); if (index < 0) return; const newRules = [...current.customRules]; newRules[index] = { ...newRules[index], ...updates }; set(gestureRuleSettingsAtom, { ...current, customRules: newRules }); }); var resetGestureRulesAtom = (0, import_jotai24.atom)(null, (get, set) => { const current = get(gestureRuleSettingsAtom); set(gestureRuleSettingsAtom, { ...current, customRules: [] }); }); // src/core/external-keyboard-store.ts var import_jotai25 = require("jotai"); var hasExternalKeyboardAtom = (0, import_jotai25.atom)(false); var watchExternalKeyboardAtom = (0, import_jotai25.atom)(null, (get, set) => { if (typeof window === "undefined") return; const handler = (e) => { if (e.key && e.key.length === 1 || ["Tab", "Escape", "Enter", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { set(hasExternalKeyboardAtom, true); window.removeEventListener("keydown", handler); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }); // src/commands/registry.ts var CommandRegistry = class { constructor() { __publicField(this, "commands", /* @__PURE__ */ new Map()); __publicField(this, "aliases", /* @__PURE__ */ new Map()); } // alias -> command name /** * Register a command with the registry. * @param command The command definition to register * @throws Error if command name or alias already exists */ register(command) { if (this.commands.has(command.name)) { throw new Error(`Command "${command.name}" is already registered`); } this.commands.set(command.name, command); if (command.aliases) { for (const alias of command.aliases) { if (this.aliases.has(alias)) { throw new Error(`Alias "${alias}" is already registered for command "${this.aliases.get(alias)}"`); } if (this.commands.has(alias)) { throw new Error(`Alias "${alias}" conflicts with existing command name`); } this.aliases.set(alias, command.name); } } } /** * Unregister a command by name. * @param name The command name to remove */ unregister(name) { const command = this.commands.get(name); if (command) { if (command.aliases) { for (const alias of command.aliases) { this.aliases.delete(alias); } } this.commands.delete(name); } } /** * Get a command by name or alias. * @param nameOrAlias Command name or alias * @returns The command definition or undefined if not found */ get(nameOrAlias) { const direct = this.commands.get(nameOrAlias); if (direct) return direct; const commandName = this.aliases.get(nameOrAlias); if (commandName) { return this.commands.get(commandName); } return void 0; } /** * Check if a command exists by name or alias. * @param nameOrAlias Command name or alias */ has(nameOrAlias) { return this.commands.has(nameOrAlias) || this.aliases.has(nameOrAlias); } /** * Search for commands matching a query. * Searches command names, aliases, and descriptions. * @param query Search query (case-insensitive) * @returns Array of matching commands, sorted by relevance */ search(query) { if (!query.trim()) { return this.all(); } const lowerQuery = query.toLowerCase().trim(); const results = []; const commands = Array.from(this.commands.values()); for (const command of commands) { let score = 0; if (command.name.toLowerCase() === lowerQuery) { score = 100; } else if (command.name.toLowerCase().startsWith(lowerQuery)) { score = 80; } else if (command.name.toLowerCase().includes(lowerQuery)) { score = 60; } else if (command.aliases?.some((a) => a.toLowerCase() === lowerQuery)) { score = 90; } else if (command.aliases?.some((a) => a.toLowerCase().startsWith(lowerQuery))) { score = 70; } else if (command.aliases?.some((a) => a.toLowerCase().includes(lowerQuery))) { score = 50; } else if (command.description.toLowerCase().includes(lowerQuery)) { score = 30; } if (score > 0) { results.push({ command, score }); } } return results.sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name)).map((r) => r.command); } /** * Get all registered commands. * @returns Array of all commands, sorted alphabetically by name */ all() { return Array.from(this.commands.values()).sort((a, b) => a.name.localeCompare(b.name)); } /** * Get commands by category. * @param category The category to filter by * @returns Array of commands in the category */ byCategory(category) { return this.all().filter((cmd) => cmd.category === category); } /** * Get all available categories. * @returns Array of unique categories */ categories() { const categories = /* @__PURE__ */ new Set(); const commands = Array.from(this.commands.values()); for (const command of commands) { categories.add(command.category); } return Array.from(categories).sort(); } /** * Get the count of registered commands. */ get size() { return this.commands.size; } /** * Clear all registered commands. * Useful for testing. */ clear() { this.commands.clear(); this.aliases.clear(); } /** * Get a serializable list of commands for API responses. */ toJSON() { return this.all().map((cmd) => ({ name: cmd.name, aliases: cmd.aliases || [], description: cmd.description, category: cmd.category, inputs: cmd.inputs.map((input) => ({ name: input.name, type: input.type, prompt: input.prompt, required: input.required !== false })) })); } }; var commandRegistry = new CommandRegistry(); // src/core/plugin-registry.ts var debug12 = createDebug("plugins"); // src/commands/store.ts var import_jotai27 = require("jotai"); // src/commands/store-atoms.ts var import_jotai26 = require("jotai"); var import_utils3 = require("jotai/utils"); var inputModeAtom2 = (0, import_jotai26.atom)({ type: "normal" }); var commandLineVisibleAtom = (0, import_jotai26.atom)(false); var commandLineStateAtom = (0, import_jotai26.atom)({ phase: "idle" }); var commandFeedbackAtom = (0, import_jotai26.atom)(null); var commandHistoryAtom = (0, import_utils3.atomWithStorage)("canvas-command-history", []); var selectedSuggestionIndexAtom = (0, import_jotai26.atom)(0); var pendingInputResolverAtom2 = (0, import_jotai26.atom)(null); var isCommandActiveAtom = (0, import_jotai26.atom)((get) => { const state = get(commandLineStateAtom); return state.phase === "collecting" || state.phase === "executing"; }); var currentInputAtom = (0, import_jotai26.atom)((get) => { const state = get(commandLineStateAtom); if (state.phase !== "collecting") return null; return state.command.inputs[state.inputIndex]; }); var commandProgressAtom = (0, import_jotai26.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 = (0, import_jotai27.atom)(null, (get, set) => { set(commandLineVisibleAtom, true); set(commandLineStateAtom, { phase: "searching", query: "", suggestions: commandRegistry.all() }); set(selectedSuggestionIndexAtom, 0); }); var closeCommandLineAtom = (0, import_jotai27.atom)(null, (get, set) => { set(commandLineVisibleAtom, false); set(commandLineStateAtom, { phase: "idle" }); set(inputModeAtom2, { type: "normal" }); set(commandFeedbackAtom, null); set(pendingInputResolverAtom2, null); }); var updateSearchQueryAtom = (0, import_jotai27.atom)(null, (get, set, query) => { const suggestions = commandRegistry.search(query); set(commandLineStateAtom, { phase: "searching", query, suggestions }); set(selectedSuggestionIndexAtom, 0); }); var selectCommandAtom = (0, import_jotai27.atom)(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(inputModeAtom2, inputDefToMode(firstInput)); }); var provideInputAtom2 = (0, import_jotai27.atom)(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(inputModeAtom2, 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(inputModeAtom2, { type: "normal" }); } }); var skipInputAtom = (0, import_jotai27.atom)(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(provideInputAtom2, value); }); var goBackInputAtom = (0, import_jotai27.atom)(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(inputModeAtom2, { 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(inputModeAtom2, inputDefToMode(prevInput, newCollected)); }); var setCommandErrorAtom = (0, import_jotai27.atom)(null, (get, set, message) => { set(commandLineStateAtom, { phase: "error", message }); set(inputModeAtom2, { type: "normal" }); }); var clearCommandErrorAtom = (0, import_jotai27.atom)(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/gestures/modifier-helpers.ts function isRepeatBlocked(event) { return isKeyInputEvent(event) && event.repeat; } function getSelectedNodeIds(store) { return Array.from(store.get(selectedNodeIdsAtom)); } function clearSelectionState(store) { store.set(clearSelectionAtom); store.set(clearEdgeSelectionAtom); } function resolveFocusableNodeId(store) { const focusedNodeId = store.get(focusedNodeIdAtom); if (focusedNodeId) return focusedNodeId; const selected = getSelectedNodeIds(store); return selected[0] ?? null; } function getCurrentSubject(store) { const draggingNodeId = store.get(draggingNodeIdAtom); if (draggingNodeId) return { kind: "node", nodeId: draggingNodeId }; const edgeCreation = store.get(edgeCreationAtom); if (edgeCreation.isCreating && edgeCreation.sourceNodeId) { return { kind: "node", nodeId: edgeCreation.sourceNodeId }; } const focusedNodeId = resolveFocusableNodeId(store); if (focusedNodeId) return { kind: "node", nodeId: focusedNodeId }; const selectedEdgeId = store.get(selectedEdgeIdAtom); if (selectedEdgeId) return { kind: "edge", edgeId: selectedEdgeId }; return { kind: "background" }; } function updateKeySubject(event, store) { if (event.kind === "key" && event.subject === void 0) { event.subject = getCurrentSubject(store); } } // src/gestures/gesture-classification.ts function findNearestNode(currentNodeId, store, direction) { const nodes = store.get(uiNodesAtom); if (nodes.length === 0) return null; if (!currentNodeId) { const sorted = [...nodes].sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x); return sorted[0]?.id ?? null; } const currentNode = nodes.find((node) => node.id === currentNodeId); if (!currentNode) return nodes[0]?.id ?? null; const cx = currentNode.position.x + (currentNode.width ?? 200) / 2; const cy = currentNode.position.y + (currentNode.height ?? 100) / 2; let bestId = null; let bestScore = Number.POSITIVE_INFINITY; for (const candidate of nodes) { if (candidate.id === currentNode.id) continue; const nx = candidate.position.x + (candidate.width ?? 200) / 2; const ny = candidate.position.y + (candidate.height ?? 100) / 2; const dx = nx - cx; const dy = ny - cy; let isValid = false; switch (direction) { case "right": isValid = dx > 0; break; case "left": isValid = dx < 0; break; case "down": isValid = dy > 0; break; case "up": isValid = dy < 0; break; } if (!isValid) continue; const dist = Math.sqrt(dx * dx + dy * dy); const isHorizontal = direction === "left" || direction === "right"; const perpendicularDistance = isHorizontal ? Math.abs(dy) : Math.abs(dx); const score = dist + perpendicularDistance * 0.5; if (score < bestScore) { bestScore = score; bestId = candidate.id; } } return bestId; } function cycleFocus(store, direction) { const nodes = store.get(uiNodesAtom); if (nodes.length === 0) return; const focusedNodeId = store.get(focusedNodeIdAtom); const sorted = [...nodes].sort((a, b) => a.zIndex - b.zIndex); const currentIdx = focusedNodeId ? sorted.findIndex((node) => node.id === focusedNodeId) : -1; const nextIdx = direction === -1 ? currentIdx <= 0 ? sorted.length - 1 : currentIdx - 1 : (currentIdx + 1) % sorted.length; store.set(setFocusedNodeAtom, sorted[nextIdx].id); } function navigateFocus(store, direction) { const nextId = findNearestNode(resolveFocusableNodeId(store), store, direction); if (nextId) { store.set(setFocusedNodeAtom, nextId); } } function activateFocusedNode(store, enterManipulate) { const focusedNodeId = resolveFocusableNodeId(store); if (!focusedNodeId) return; store.set(setFocusedNodeAtom, focusedNodeId); store.set(selectSingleNodeAtom, focusedNodeId); if (enterManipulate) { store.set(setKeyboardInteractionModeAtom, "manipulate"); } } // src/gestures/input-action-helpers.ts var EMPTY_EDGE_CREATION = { isCreating: false, sourceNodeId: null, sourceNodePosition: null, targetPosition: null, hoveredTargetNodeId: null, sourceHandle: null, targetHandle: null, sourcePort: null, targetPort: null, snappedTargetPosition: null }; function nudgeSelection(store, dx, dy, label) { const selected = getSelectedNodeIds(store); if (selected.length === 0) { const focused = store.get(focusedNodeIdAtom); if (focused) { store.set(selectSingleNodeAtom, focused); } } const nodeIds = getSelectedNodeIds(store); if (nodeIds.length === 0) return; const graph = store.get(graphAtom).copy(); store.set(pushHistoryAtom, label); for (const nodeId of nodeIds) { if (!graph.hasNode(nodeId)) continue; const x = graph.getNodeAttribute(nodeId, "x"); const y = graph.getNodeAttribute(nodeId, "y"); graph.setNodeAttribute(nodeId, "x", x + dx); graph.setNodeAttribute(nodeId, "y", y + dy); } store.set(graphAtom, graph); store.set(nodePositionUpdateCounterAtom, (count) => count + 1); } function deleteSelection(store) { const selectedEdgeId = store.get(selectedEdgeIdAtom); if (selectedEdgeId) { store.set(optimisticDeleteEdgeAtom, { edgeKey: selectedEdgeId }); store.set(clearEdgeSelectionAtom); } const selectedNodeIds = getSelectedNodeIds(store); if (selectedNodeIds.length === 0) return; store.set(pushHistoryAtom, selectedNodeIds.length > 1 ? `Delete ${selectedNodeIds.length} nodes` : "Delete node"); const focusedNodeId = store.get(focusedNodeIdAtom); for (const nodeId of selectedNodeIds) { store.set(optimisticDeleteNodeAtom, { nodeId }); } if (focusedNodeId && selectedNodeIds.includes(focusedNodeId)) { store.set(setFocusedNodeAtom, null); } store.set(clearSelectionAtom); } function cutSelection(store) { const selectedNodeIds = getSelectedNodeIds(store); if (selectedNodeIds.length === 0) return; store.set(copyToClipboardAtom, selectedNodeIds); deleteSelection(store); } function cancelActiveInteraction(store) { let didCancel = false; if (store.get(draggingNodeIdAtom) !== null) { store.set(endNodeDragAtom); didCancel = true; } if (store.get(edgeCreationAtom).isCreating) { store.set(edgeCreationAtom, EMPTY_EDGE_CREATION); didCancel = true; } if (store.get(inputModeAtom).type === "pickPoint" || store.get(inputModeAtom).type === "pickNode" || store.get(inputModeAtom).type === "pickNodes") { store.set(resetInputModeAtom); didCancel = true; } const selectionPath = store.get(selectionPathAtom); if (selectionPath !== null) { store.set(cancelSelectionAtom); didCancel = true; } return didCancel; } function escapeInput(store) { if (cancelActiveInteraction(store)) return; if (store.get(keyboardInteractionModeAtom) === "manipulate") { store.set(resetKeyboardInteractionModeAtom); return; } if (store.get(isFilterActiveAtom)) { store.set(clearSearchAtom); return; } if (store.get(commandLineVisibleAtom)) { store.set(closeCommandLineAtom); return; } if (store.get(selectedNodeIdsAtom).size > 0 || store.get(selectedEdgeIdAtom) !== null) { clearSelectionState(store); } } function resolvePickNode(event, store) { updateKeySubject(event, store); if (event.subject?.kind !== "node") return; if (store.get(inputModeAtom).type === "pickNodes") { store.set(toggleNodeInSelectionAtom, event.subject.nodeId); store.set(setFocusedNodeAtom, event.subject.nodeId); return; } store.set(provideInputAtom, event.subject.nodeId); store.set(resetInputModeAtom); } function finishPickNodes(store) { store.set(provideInputAtom, getSelectedNodeIds(store)); store.set(resetInputModeAtom); } function resolvePickPoint(event, store) { if (event.kind !== "pointer") return; store.set(provideInputAtom, event.worldPosition); store.set(resetInputModeAtom); } function selectAll(store) { clearSelectionState(store); store.set(addNodesToSelectionAtom, store.get(nodeKeysAtom)); } // src/gestures/useCanvasGestures.ts var import_react4 = require("react"); var import_jotai29 = require("jotai"); var import_react5 = require("@use-gesture/react"); // src/gestures/useGuardContext.ts var import_react2 = require("react"); var import_jotai28 = require("jotai"); function useGuardContext(heldKeys = NO_HELD_KEYS) { const isStylusActive = (0, import_jotai28.useAtomValue)(isStylusActiveAtom); const fingerCount = (0, import_jotai28.useAtomValue)(fingerCountAtom); const draggingNodeId = (0, import_jotai28.useAtomValue)(draggingNodeIdAtom); const focusedNodeId = (0, import_jotai28.useAtomValue)(focusedNodeIdAtom); const selectedNodeIds = (0, import_jotai28.useAtomValue)(selectedNodeIdsAtom); const inputMode = (0, import_jotai28.useAtomValue)(inputModeAtom); const keyboardInteractionMode = (0, import_jotai28.useAtomValue)(keyboardInteractionModeAtom); const selectionPath = (0, import_jotai28.useAtomValue)(selectionPathAtom); const edgeCreation = (0, import_jotai28.useAtomValue)(edgeCreationAtom); const isSearchActive = (0, import_jotai28.useAtomValue)(isFilterActiveAtom); const commandLineVisible = (0, import_jotai28.useAtomValue)(commandLineVisibleAtom); const guardRef = (0, import_react2.useRef)({ isStylusActive: false, fingerCount: 0, isDragging: false, isResizing: false, isSplitting: false, inputMode: { type: "normal" }, keyboardInteractionMode: "navigate", selectedNodeIds: /* @__PURE__ */ new Set(), focusedNodeId: null, isSearchActive: false, commandLineVisible: false, heldKeys: NO_HELD_KEYS, custom: {} }); guardRef.current = { isStylusActive, fingerCount, isDragging: draggingNodeId !== null, isResizing: false, isSplitting: false, inputMode, keyboardInteractionMode, selectedNodeIds, focusedNodeId, isSearchActive, commandLineVisible, heldKeys, custom: { isSelecting: selectionPath !== null, isCreatingEdge: edgeCreation.isCreating } }; return guardRef; } // src/gestures/useInertia.ts var import_react3 = require("react"); var ZOOM_SNAP_THRESHOLD2 = 0.03; var INERTIA_MIN_SPEED = 2; var VELOCITY_SAMPLE_COUNT2 = 5; function snapZoom(z) { return Math.abs(z - 1) < ZOOM_SNAP_THRESHOLD2 ? 1 : z; } function useInertia() { const panInertiaRef = (0, import_react3.useRef)(null); const zoomInertiaRef = (0, import_react3.useRef)(null); const panSamplerRef = (0, import_react3.useRef)(new VelocitySampler(VELOCITY_SAMPLE_COUNT2)); const zoomSamplerRef = (0, import_react3.useRef)(new VelocitySampler(VELOCITY_SAMPLE_COUNT2)); const pinchPrevOrigin = (0, import_react3.useRef)(null); const lastPinchZoom = (0, import_react3.useRef)(null); const cancelPan = () => { if (panInertiaRef.current) { cancelAnimationFrame(panInertiaRef.current.anim); panInertiaRef.current = null; } }; const cancelZoom = () => { if (zoomInertiaRef.current) { cancelAnimationFrame(zoomInertiaRef.current.anim); zoomInertiaRef.current = null; } }; const cancelAll = () => { cancelPan(); cancelZoom(); }; const startPanInertia = (velocity, setPan) => { cancelPan(); const engine = new PanInertia(velocity); const animate = () => { const tick = engine.tick(); if (!tick) { panInertiaRef.current = null; return; } setPan((prev) => ({ x: prev.x + tick.x, y: prev.y + tick.y })); panInertiaRef.current.anim = requestAnimationFrame(animate); }; panInertiaRef.current = { anim: requestAnimationFrame(animate), engine }; }; const startZoomInertia = (velocity_0, origin, currentZoom, minZoom, maxZoom, setZoom, setPan_0) => { cancelZoom(); const engine_0 = new ZoomInertia(velocity_0, origin); const animate_0 = () => { const tick_0 = engine_0.tick(currentZoom); if (!tick_0) { zoomInertiaRef.current = null; return; } setZoom((prevZoom) => { const newZoom = Math.max(minZoom, Math.min(maxZoom, prevZoom + tick_0.delta)); setPan_0((prevPan) => { const worldX = (tick_0.origin.x - prevPan.x) / prevZoom; const worldY = (tick_0.origin.y - prevPan.y) / prevZoom; return { x: tick_0.origin.x - worldX * newZoom, y: tick_0.origin.y - worldY * newZoom }; }); return snapZoom(newZoom); }); zoomInertiaRef.current.anim = requestAnimationFrame(animate_0); }; zoomInertiaRef.current = { anim: requestAnimationFrame(animate_0), engine: engine_0 }; }; (0, import_react3.useEffect)(() => cancelAll, []); return { startPanInertia, startZoomInertia, cancelAll, cancelPan, cancelZoom, panSampler: panSamplerRef.current, zoomSampler: zoomSamplerRef.current, pinchPrevOrigin, lastPinchZoom }; } // src/gestures/useWheelZoom.ts function createWheelHandler(config) { return ({ event, pinching, delta: [, dy], memo }) => { if (memo === true || pinching || !config.enableZoom || !config.ref.current) return; const target = event.target; const noDrag = target.closest('[data-no-drag="true"]'); if (noDrag) { const draggableNode = target.closest('[data-draggable-node="true"]'); if (draggableNode) { const nodeId = draggableNode.getAttribute("data-node-id"); if (nodeId && config.selectedNodeIds.has(nodeId)) { let el = target; while (el && noDrag.contains(el)) { if (el.scrollHeight > el.clientHeight) { const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1; const atTop = el.scrollTop <= 1; if (!atBottom && dy > 0 || !atTop && dy < 0 || atBottom && dy < 0 || atTop && dy > 0) { return; } } el = el.parentElement; } } } } event.preventDefault(); const rect = config.ref.current.getBoundingClientRect(); const mouseX = event.clientX - rect.left; const mouseY = event.clientY - rect.top; const worldX = (mouseX - config.pan.x) / config.zoom; const worldY = (mouseY - config.pan.y) / config.zoom; const zoomDelta = -dy * config.zoomSensitivity * config.zoom; const newZoom = snapZoom(Math.max(config.minZoom, Math.min(config.maxZoom, config.zoom + zoomDelta))); config.setZoom(newZoom); config.setPan({ x: mouseX - worldX * newZoom, y: mouseY - worldY * newZoom }); }; } // src/gestures/usePinchZoom.ts function createPinchHandlers(config) { const onPinchStart = ({ origin: [ox, oy] }) => { if (!config.enableZoom || !config.ref.current) return true; config.inertia.cancelAll(); config.inertia.pinchPrevOrigin.current = { x: ox, y: oy }; config.inertia.zoomSampler.reset(); config.inertia.lastPinchZoom.current = { zoom: config.zoom, t: performance.now() }; return false; }; const onPinch = ({ offset: [d], origin: [ox, oy], event, memo }) => { if (memo === true || !config.enableZoom || !config.ref.current) return; event.preventDefault(); const rect = config.ref.current.getBoundingClientRect(); const pinchX = ox - rect.left; const pinchY = oy - rect.top; let pdx = 0; let pdy = 0; if (config.inertia.pinchPrevOrigin.current) { pdx = ox - config.inertia.pinchPrevOrigin.current.x; pdy = oy - config.inertia.pinchPrevOrigin.current.y; } config.inertia.pinchPrevOrigin.current = { x: ox, y: oy }; const worldX = (pinchX - config.pan.x) / config.zoom; const worldY = (pinchY - config.pan.y) / config.zoom; const newZoom = snapZoom(Math.max(config.minZoom, Math.min(config.maxZoom, d))); const now = performance.now(); if (config.inertia.lastPinchZoom.current) { const dt = (now - config.inertia.lastPinchZoom.current.t) / 1e3; if (dt > 0) { const v = (newZoom - config.inertia.lastPinchZoom.current.zoom) / dt; config.inertia.zoomSampler.sample(v, 0, now); } } config.inertia.lastPinchZoom.current = { zoom: newZoom, t: now }; config.setZoom(newZoom); config.setPan({ x: pinchX - worldX * newZoom + pdx, y: pinchY - worldY * newZoom + pdy }); }; const onPinchEnd = () => { const avg = config.inertia.zoomSampler.average(); const perFrameV = avg.x / 60; if (!config.reducedMotion && Math.abs(perFrameV) > 1e-3 && config.ref.current) { const rect = config.ref.current.getBoundingClientRect(); const origin = config.inertia.pinchPrevOrigin.current ?? { x: rect.width / 2, y: rect.height / 2 }; config.inertia.startZoomInertia(perFrameV, { x: origin.x - rect.left, y: origin.y - rect.top }, config.zoom, config.minZoom, config.maxZoom, config.setZoom, config.setPan); } config.inertia.pinchPrevOrigin.current = null; config.inertia.zoomSampler.reset(); config.inertia.lastPinchZoom.current = null; }; return { onPinchStart, onPinch, onPinchEnd }; } // src/gestures/useCanvasGestures.ts function useCanvasGestures({ ref, minZoom = 0.1, maxZoom = 5, zoomSensitivity = 15e-4, enablePan = true, enableZoom = true, mappingIndex: externalIndex, contexts: extraContexts, palmRejection = true, onAction, heldKeys = NO_HELD_KEYS }) { const [panVals, setPan] = (0, import_jotai29.useAtom)(panAtom); const [zoomVal, setZoom] = (0, import_jotai29.useAtom)(zoomAtom); const setViewportRect = (0, import_jotai29.useSetAtom)(viewportRectAtom); const selectedNodeIds = (0, import_jotai29.useAtomValue)(selectedNodeIdsAtom); const clearSelection = (0, import_jotai29.useSetAtom)(clearSelectionAtom); const screenToWorld = (0, import_jotai29.useAtomValue)(screenToWorldAtom); const setPointerDown = (0, import_jotai29.useSetAtom)(pointerDownAtom); const setPointerUp = (0, import_jotai29.useSetAtom)(pointerUpAtom); const clearPointers = (0, import_jotai29.useSetAtom)(clearPointersAtom); const primaryInputSource = (0, import_jotai29.useAtomValue)(primaryInputSourceAtom); const reducedMotion = (0, import_jotai29.useAtomValue)(prefersReducedMotionAtom); const startSelection = (0, import_jotai29.useSetAtom)(startSelectionAtom); const updateSelection = (0, import_jotai29.useSetAtom)(updateSelectionAtom); const endSelection = (0, import_jotai29.useSetAtom)(endSelectionAtom); const guardRef = useGuardContext(heldKeys); const inertia = useInertia(); const panStartRef = (0, import_react4.useRef)({ x: 0, y: 0 }); const dragOriginatedOnBg = (0, import_react4.useRef)(false); const dragSourceRef = (0, import_react4.useRef)("mouse"); const buttonRef = (0, import_react4.useRef)(0); const dragIntentRef = (0, import_react4.useRef)("none"); const timedRunner = (0, import_react4.useRef)(new TimedStateRunner()); const internalIndex = (0, import_react4.useMemo)(() => { const ctxList = []; if (palmRejection) ctxList.push(PALM_REJECTION_CONTEXT); if (extraContexts) ctxList.push(...extraContexts); ctxList.push(DEFAULT_CONTEXT); return buildMappingIndex(ctxList); }, [palmRejection, extraContexts]); const mappingIndex = externalIndex ?? internalIndex; const isBackgroundTarget = (target) => { if (!ref.current || !target) return false; const child = ref.current.firstChild; return target === ref.current || target === child; }; const makeGestureEvent = (type, phase, subject, screenX, screenY, extra) => { const worldPos = screenToWorld(screenX, screenY); return { kind: "pointer", type, phase, subject, source: dragSourceRef.current, button: buttonRef.current, modifiers: NO_MODIFIERS, heldKeys, screenPosition: { x: screenX, y: screenY }, worldPosition: worldPos, ...extra }; }; const resolveAndDispatch = (event) => { const resolution = resolve(event, mappingIndex, guardRef.current); if (resolution) { dispatch(event, resolution); onAction?.(event, resolution); } return resolution; }; (0, import_react4.useEffect)(() => { const runner = timedRunner.current; runner.onEmit = (_gestureType) => { }; return () => { runner.destroy(); }; }, []); (0, import_react4.useEffect)(() => { const el = ref.current; if (!el) { setViewportRect(null); return; } setViewportRect(el.getBoundingClientRect()); const observer = new ResizeObserver((entries) => { for (const entry of entries) { setViewportRect(entry.contentRect); } }); observer.observe(el); return () => { observer.unobserve(el); observer.disconnect(); }; }, [setViewportRect]); (0, import_react4.useEffect)(() => { return () => { inertia.cancelAll(); timedRunner.current.destroy(); }; }, []); (0, import_react4.useEffect)(() => { const handleVisChange = () => { if (document.hidden) { inertia.cancelAll(); clearPointers(); timedRunner.current.feed("cancel"); } }; document.addEventListener("visibilitychange", handleVisChange); return () => document.removeEventListener("visibilitychange", handleVisChange); }, [clearPointers]); (0, import_react5.useGesture)({ onPointerDown: (state) => { if (!ref.current) { dragOriginatedOnBg.current = false; return; } const pe = state.event; const classified = classifyPointer(pe); setPointerDown(classified); dragSourceRef.current = classified.source; buttonRef.current = pe.button ?? 0; inertia.cancelAll(); if (isBackgroundTarget(state.event.target)) { dragOriginatedOnBg.current = true; timedRunner.current.feed("down"); const startX = pe.clientX; const startY = pe.clientY; timedRunner.current.onEmit = (gestureType) => { if (gestureType === "long-press") { const worldPos_0 = screenToWorld(startX, startY); const event_0 = makeGestureEvent("long-press", "instant", { kind: "background" }, startX, startY, { modifiers: extractModifiers(pe), worldPosition: worldPos_0 }); resolveAndDispatch(event_0); } }; } else { dragOriginatedOnBg.current = false; } }, onPointerUp: (state_0) => { const pe_0 = state_0.event; setPointerUp(pe_0.pointerId); }, onDragStart: ({ event: event_1 }) => { if (!dragOriginatedOnBg.current) return; const pe_1 = event_1; const modifiers = pe_1 ? extractModifiers(pe_1) : NO_MODIFIERS; const gestureEvent = makeGestureEvent("drag", "start", { kind: "background" }, pe_1?.clientX ?? 0, pe_1?.clientY ?? 0, { modifiers }); const resolution_0 = resolve(gestureEvent, mappingIndex, guardRef.current); dragIntentRef.current = resolution_0?.actionId ?? "none"; if (dragIntentRef.current === "pan" && enablePan) { panStartRef.current = { ...panVals }; inertia.panSampler.reset(); } else if (dragIntentRef.current === "lasso-select" || dragIntentRef.current === "rect-select") { const worldPos_1 = screenToWorld(pe_1?.clientX ?? 0, pe_1?.clientY ?? 0); startSelection({ type: dragIntentRef.current === "lasso-select" ? "lasso" : "rect", point: worldPos_1 }); } if (resolution_0) { dispatch(gestureEvent, resolution_0); onAction?.(gestureEvent, resolution_0); } }, onDrag: ({ movement: [mx, my], tap, active, pinching, event: event_2, velocity: [vx, vy], direction: [dx, dy] }) => { if (tap && dragOriginatedOnBg.current) { const emitted = timedRunner.current.feed("up"); const pe_2 = event_2; const modifiers_0 = extractModifiers(pe_2); const tapType = emitted ?? "tap"; const gestureEvent_0 = makeGestureEvent(tapType, "instant", { kind: "background" }, pe_2.clientX, pe_2.clientY, { modifiers: modifiers_0 }); const resolution_1 = resolve(gestureEvent_0, mappingIndex, guardRef.current); if (resolution_1) { dispatch(gestureEvent_0, resolution_1); onAction?.(gestureEvent_0, resolution_1); } else { clearSelection(); } return; } if (!tap && active && !pinching && dragOriginatedOnBg.current) { timedRunner.current.feed("move-beyond-threshold"); const intent = dragIntentRef.current; if (intent === "pan" && enablePan) { setPan({ x: panStartRef.current.x + mx, y: panStartRef.current.y + my }); const now = performance.now(); inertia.panSampler.sample(vx * dx, vy * dy, now); } else if (intent === "lasso-select" || intent === "rect-select") { const pe_3 = event_2; const worldPos_2 = screenToWorld(pe_3.clientX, pe_3.clientY); updateSelection(worldPos_2); } } }, onDragEnd: () => { timedRunner.current.feed("up"); const intent_0 = dragIntentRef.current; if (intent_0 === "lasso-select" || intent_0 === "rect-select") { endSelection(); } if (!reducedMotion && dragOriginatedOnBg.current && dragSourceRef.current === "finger" && intent_0 === "pan") { const avg = inertia.panSampler.average(); const speed = Math.sqrt(avg.x ** 2 + avg.y ** 2); if (speed > INERTIA_MIN_SPEED) { inertia.startPanInertia(avg, setPan); } } dragOriginatedOnBg.current = false; dragIntentRef.current = "none"; inertia.panSampler.reset(); }, onWheel: createWheelHandler({ ref, minZoom, maxZoom, zoomSensitivity, enableZoom, zoom: zoomVal, pan: panVals, selectedNodeIds, setZoom, setPan }), ...createPinchHandlers({ ref, minZoom, maxZoom, enableZoom, reducedMotion, zoom: zoomVal, pan: panVals, setZoom, setPan, inertia }) }, { target: ref, eventOptions: { passive: false, capture: true }, drag: { filterTaps: true, tapsThreshold: primaryInputSource === "finger" ? 10 : 5, pointer: { touch: true, keys: false, capture: false, buttons: -1 } }, wheel: {}, pinch: { scaleBounds: () => ({ min: minZoom, max: maxZoom }), from: () => [zoomVal, 0] } }); } // src/gestures/useNodeGestures.ts var import_react6 = require("react"); var import_jotai30 = require("jotai"); function useNodeGestures({ nodeId, mappingIndex, onAction, heldKeys = NO_HELD_KEYS }) { const screenToWorld = (0, import_jotai30.useAtomValue)(screenToWorldAtom); const isStylusActive = (0, import_jotai30.useAtomValue)(isStylusActiveAtom); const fingerCount = (0, import_jotai30.useAtomValue)(fingerCountAtom); const inputMode = (0, import_jotai30.useAtomValue)(inputModeAtom); const keyboardInteractionMode = (0, import_jotai30.useAtomValue)(keyboardInteractionModeAtom); const selectedNodeIds = (0, import_jotai30.useAtomValue)(selectedNodeIdsAtom); const focusedNodeId = (0, import_jotai30.useAtomValue)(focusedNodeIdAtom); const draggingNodeId = (0, import_jotai30.useAtomValue)(draggingNodeIdAtom); const edgeCreation = (0, import_jotai30.useAtomValue)(edgeCreationAtom); const selectionPath = (0, import_jotai30.useAtomValue)(selectionPathAtom); const isSearchActive = (0, import_jotai30.useAtomValue)(isFilterActiveAtom); const commandLineVisible = (0, import_jotai30.useAtomValue)(commandLineVisibleAtom); const runner = (0, import_react6.useRef)(new TimedStateRunner()); const sourceRef = (0, import_react6.useRef)("mouse"); const buttonRef = (0, import_react6.useRef)(0); const posRef = (0, import_react6.useRef)({ x: 0, y: 0 }); const modifiersRef = (0, import_react6.useRef)(NO_MODIFIERS); const rightDragSeen = (0, import_react6.useRef)(false); const guardRef = (0, import_react6.useRef)({ isStylusActive: false, fingerCount: 0, isDragging: false, isResizing: false, isSplitting: false, inputMode: { type: "normal" }, keyboardInteractionMode: "navigate", selectedNodeIds: /* @__PURE__ */ new Set(), focusedNodeId: null, isSearchActive: false, commandLineVisible: false, heldKeys: NO_HELD_KEYS, custom: {} }); guardRef.current = { isStylusActive, fingerCount, isDragging: draggingNodeId !== null, isResizing: false, isSplitting: false, inputMode, keyboardInteractionMode, selectedNodeIds, focusedNodeId, isSearchActive, commandLineVisible, heldKeys, custom: {} }; guardRef.current.custom = { isSelecting: selectionPath !== null, isCreatingEdge: edgeCreation.isCreating }; (0, import_react6.useEffect)(() => { return () => runner.current.destroy(); }, []); const resolveAndDispatch = (event) => { const resolution = resolve(event, mappingIndex, guardRef.current); if (resolution) { dispatch(event, resolution); onAction?.(event, resolution); } return resolution; }; const makeEvent = (type, phase, screenX, screenY) => { const worldPos = screenToWorld(screenX, screenY); return { kind: "pointer", type, phase, subject: { kind: "node", nodeId }, source: sourceRef.current, button: buttonRef.current, modifiers: modifiersRef.current, heldKeys, screenPosition: { x: screenX, y: screenY }, worldPosition: worldPos }; }; (0, import_react6.useEffect)(() => { runner.current.onEmit = (gestureType) => { if (gestureType === "long-press") { const event_0 = makeEvent("long-press", "instant", posRef.current.x, posRef.current.y); resolveAndDispatch(event_0); } }; }); const onPointerDown = (e) => { const classified = classifyPointer(e.nativeEvent); sourceRef.current = classified.source; buttonRef.current = e.button ?? 0; posRef.current = { x: e.clientX, y: e.clientY }; modifiersRef.current = extractModifiers(e); rightDragSeen.current = false; runner.current.feed("down"); }; const onPointerUp = (e_0) => { const emitted = runner.current.feed("up"); if (emitted) { const event_1 = makeEvent(emitted, "instant", e_0.clientX, e_0.clientY); resolveAndDispatch(event_1); } }; const onPointerCancel = (_e) => { runner.current.feed("cancel"); }; const onContextMenu = (e_1) => { e_1.preventDefault(); e_1.stopPropagation(); if (rightDragSeen.current) { rightDragSeen.current = false; return; } const event_2 = makeEvent("tap", "instant", e_1.clientX, e_1.clientY); event_2.button = 2; event_2.modifiers = extractModifiers(e_1); resolveAndDispatch(event_2); }; return { onPointerDown, onPointerUp, onPointerCancel, onContextMenu }; } // src/gestures/useGestureSystem.ts var import_compiler_runtime2 = require("react/compiler-runtime"); var import_react7 = require("react"); function useInputSystem(t0) { const $ = (0, import_compiler_runtime2.c)(19); const config = t0 === void 0 ? {} : t0; const { contexts: staticContexts, palmRejection: t1 } = config; const initialPR = t1 === void 0 ? true : t1; let t2; if ($[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel")) { t2 = []; $[0] = t2; } else { t2 = $[0]; } const [dynamicContexts, setDynamicContexts] = (0, import_react7.useState)(t2); const [palmRejection, setPalmRejection] = (0, import_react7.useState)(initialPR); const [heldKeys, setHeldKeys] = (0, import_react7.useState)(NO_HELD_KEYS); const all = []; if (palmRejection) { all.push(PALM_REJECTION_CONTEXT); } all.push(ACTIVE_INTERACTION_CONTEXT, SEARCH_CONTEXT, KEYBOARD_MANIPULATE_CONTEXT, KEYBOARD_NAVIGATE_CONTEXT); if (staticContexts) { all.push(...staticContexts); } all.push(...dynamicContexts); all.push(DEFAULT_CONTEXT); const mappingIndex = buildMappingIndex(all); let t3; if ($[1] !== setDynamicContexts) { t3 = (ctx) => { setDynamicContexts((prev) => { const filtered = prev.filter((c) => c.id !== ctx.id); return [...filtered, ctx]; }); }; $[1] = setDynamicContexts; $[2] = t3; } else { t3 = $[2]; } const pushContext = t3; let t4; if ($[3] !== setDynamicContexts) { t4 = (id) => { setDynamicContexts((prev_0) => prev_0.filter((c_0) => c_0.id !== id)); }; $[3] = setDynamicContexts; $[4] = t4; } else { t4 = $[4]; } const removeContext = t4; let t5; if ($[5] !== setDynamicContexts) { t5 = (id_0, enabled) => { setDynamicContexts((prev_1) => prev_1.map((c_1) => c_1.id === id_0 ? { ...c_1, enabled } : c_1)); }; $[5] = setDynamicContexts; $[6] = t5; } else { t5 = $[6]; } const setContextEnabled = t5; let t6; if ($[7] !== setHeldKeys) { t6 = () => { setHeldKeys(NO_HELD_KEYS); }; $[7] = setHeldKeys; $[8] = t6; } else { t6 = $[8]; } const clearHeldKeys = t6; let t7; if ($[9] !== clearHeldKeys || $[10] !== heldKeys || $[11] !== mappingIndex || $[12] !== palmRejection || $[13] !== pushContext || $[14] !== removeContext || $[15] !== setContextEnabled || $[16] !== setHeldKeys || $[17] !== setPalmRejection) { t7 = { mappingIndex, pushContext, removeContext, setContextEnabled, palmRejection, setPalmRejection, heldKeys, setHeldKeys, clearHeldKeys }; $[9] = clearHeldKeys; $[10] = heldKeys; $[11] = mappingIndex; $[12] = palmRejection; $[13] = pushContext; $[14] = removeContext; $[15] = setContextEnabled; $[16] = setHeldKeys; $[17] = setPalmRejection; $[18] = t7; } else { t7 = $[18]; } return t7; } var useGestureSystem = useInputSystem; // src/gestures/useInputModeGestureContext.ts var import_compiler_runtime3 = require("react/compiler-runtime"); var import_react8 = require("react"); var import_jotai31 = require("jotai"); function useInputModeGestureContext(inputSystem) { const $ = (0, import_compiler_runtime3.c)(4); const inputMode = (0, import_jotai31.useAtomValue)(inputModeAtom); let t0; let t1; if ($[0] !== inputMode.type || $[1] !== inputSystem) { t0 = () => { const modeType = inputMode.type; const ctx = INPUT_MODE_CONTEXTS[modeType] ?? null; if (ctx) { inputSystem.pushContext(ctx); } else { for (const key of Object.keys(INPUT_MODE_CONTEXTS)) { const existing = INPUT_MODE_CONTEXTS[key]; if (existing) { inputSystem.removeContext(existing.id); } } } return () => { if (ctx) { inputSystem.removeContext(ctx.id); } }; }; t1 = [inputMode.type, inputSystem]; $[0] = inputMode.type; $[1] = inputSystem; $[2] = t0; $[3] = t1; } else { t0 = $[2]; t1 = $[3]; } (0, import_react8.useEffect)(t0, t1); } // src/gestures/useRegisterInputActions.ts var import_compiler_runtime4 = require("react/compiler-runtime"); var import_react9 = require("react"); var import_jotai32 = require("jotai"); function register(actionId, handler) { registerAction(actionId, handler); return () => unregisterAction(actionId); } function useRegisterInputActions() { const $ = (0, import_compiler_runtime4.c)(3); const store = (0, import_jotai32.useStore)(); let t0; let t1; if ($[0] !== store) { t0 = () => { const unregister = [register("select-node", (event) => { updateKeySubject(event, store); if (event.subject?.kind !== "node") { return; } store.set(setFocusedNodeAtom, event.subject.nodeId); if (event.kind === "key") { store.set(selectSingleNodeAtom, event.subject.nodeId); } }), register("select-edge", (event_0) => { updateKeySubject(event_0, store); if (event_0.subject?.kind !== "edge") { return; } store.set(selectEdgeAtom, event_0.subject.edgeId); }), register("toggle-selection", (event_1) => { updateKeySubject(event_1, store); if (event_1.subject?.kind !== "node") { return; } store.set(setFocusedNodeAtom, event_1.subject.nodeId); if (event_1.kind === "key") { store.set(toggleNodeInSelectionAtom, event_1.subject.nodeId); } }), register("clear-selection", () => { clearSelectionState(store); }), register("select-all", (event_2) => { if (isRepeatBlocked(event_2)) { return; } selectAll(store); }), register("fit-to-view", (event_3) => { updateKeySubject(event_3, store); if (event_3.subject?.kind === "node") { store.set(centerOnNodeAtom, event_3.subject.nodeId); } }), register("fit-all-to-view", () => { store.set(fitToBoundsAtom, { mode: "graph" }); }), register("toggle-lock", (event_4) => { updateKeySubject(event_4, store); if (event_4.subject?.kind !== "node") { return; } if (store.get(lockedNodeIdAtom) === event_4.subject.nodeId) { store.set(unlockNodeAtom); } else { store.set(lockNodeAtom, { nodeId: event_4.subject.nodeId }); } }), register("open-command-line", (event_5) => { if (isRepeatBlocked(event_5)) { return; } store.set(openCommandLineAtom); }), register("open-search", (event_6) => { if (isRepeatBlocked(event_6)) { return; } store.set(openCommandLineAtom); }), register("search-next-result", () => { store.set(nextSearchResultAtom); }), register("search-prev-result", () => { store.set(prevSearchResultAtom); }), register("copy-selection", (event_7) => { if (isRepeatBlocked(event_7)) { return; } const selectedNodeIds = getSelectedNodeIds(store); if (selectedNodeIds.length > 0) { store.set(copyToClipboardAtom, selectedNodeIds); } }), register("cut-selection", (event_8) => { if (isRepeatBlocked(event_8)) { return; } cutSelection(store); }), register("paste-selection", (event_9) => { if (isRepeatBlocked(event_9)) { return; } store.set(pasteFromClipboardAtom); }), register("duplicate-selection", (event_10) => { if (isRepeatBlocked(event_10)) { return; } store.set(duplicateSelectionAtom); }), register("merge-selection", (event_11) => { if (isRepeatBlocked(event_11)) { return; } const nodeIds = getSelectedNodeIds(store); if (nodeIds.length >= 2) { store.set(mergeNodesAtom, { nodeIds }); } }), register("delete-selection", (event_12) => { if (isRepeatBlocked(event_12)) { return; } deleteSelection(store); }), register("undo", (event_13) => { if (isRepeatBlocked(event_13)) { return; } store.set(undoAtom); }), register("redo", (event_14) => { if (isRepeatBlocked(event_14)) { return; } store.set(redoAtom); }), register("cancel-active-input", () => { cancelActiveInteraction(store); }), register("escape-input", () => { escapeInput(store); }), register("navigate-focus-up", () => { navigateFocus(store, "up"); }), register("navigate-focus-down", () => { navigateFocus(store, "down"); }), register("navigate-focus-left", () => { navigateFocus(store, "left"); }), register("navigate-focus-right", () => { navigateFocus(store, "right"); }), register("cycle-focus-forward", (event_15) => { if (isRepeatBlocked(event_15)) { return; } cycleFocus(store, 1); }), register("cycle-focus-backward", (event_16) => { if (isRepeatBlocked(event_16)) { return; } cycleFocus(store, -1); }), register("activate-focused-node", (event_17) => { if (isRepeatBlocked(event_17)) { return; } activateFocusedNode(store, false); }), register("enter-keyboard-manipulate-mode", (event_18) => { if (isRepeatBlocked(event_18)) { return; } activateFocusedNode(store, true); }), register("exit-keyboard-manipulate-mode", () => { store.set(resetKeyboardInteractionModeAtom); }), register("nudge-selection-up", () => { nudgeSelection(store, 0, -10, "Nudge selection"); }), register("nudge-selection-down", () => { nudgeSelection(store, 0, 10, "Nudge selection"); }), register("nudge-selection-left", () => { nudgeSelection(store, -10, 0, "Nudge selection"); }), register("nudge-selection-right", () => { nudgeSelection(store, 10, 0, "Nudge selection"); }), register("nudge-selection-up-large", () => { nudgeSelection(store, 0, -50, "Nudge selection"); }), register("nudge-selection-down-large", () => { nudgeSelection(store, 0, 50, "Nudge selection"); }), register("nudge-selection-left-large", () => { nudgeSelection(store, -50, 0, "Nudge selection"); }), register("nudge-selection-right-large", () => { nudgeSelection(store, 50, 0, "Nudge selection"); }), register("resolve-pick-node", (event_19) => { resolvePickNode(event_19, store); }), register("finish-pick-nodes", () => { finishPickNodes(store); }), register("resolve-pick-point", (event_20) => { resolvePickPoint(event_20, store); }), register("cancel-pick", () => { store.set(resetInputModeAtom); })]; return () => { for (const cleanup of unregister) { cleanup(); } }; }; t1 = [store]; $[0] = store; $[1] = t0; $[2] = t1; } else { t0 = $[1]; t1 = $[2]; } (0, import_react9.useEffect)(t0, t1); } // src/gestures/GestureProvider.tsx var import_react10 = __toESM(require("react")); var import_jotai33 = require("jotai"); // src/gestures/gesture-provider-utils.ts function isEditableTarget(target) { if (!(target instanceof HTMLElement)) { return false; } if (target.isContentEditable) { return true; } const editable = target.closest('input, textarea, [contenteditable="true"], [data-no-canvas-keyboard="true"]'); return editable !== null; } function setHeldKeyValue(current, key, isHeld) { const previous = current[key] ?? false; if (previous === isHeld) { return current; } const next = { ...current }; if (isHeld) { next[key] = true; } else { delete next[key]; } return next; } function applyHeldKeyDelta(event, current) { const isHeld = event.type === "keydown"; const nextByKey = setHeldKeyValue(current.byKey, event.key, isHeld); const nextByCode = setHeldKeyValue(current.byCode, event.code, isHeld); if (nextByKey === current.byKey && nextByCode === current.byCode) { return current; } return { byKey: nextByKey, byCode: nextByCode }; } function getCurrentSubject2(store) { const draggingNodeId = store.get(draggingNodeIdAtom); if (draggingNodeId) { return { kind: "node", nodeId: draggingNodeId }; } const edgeCreation = store.get(edgeCreationAtom); if (edgeCreation.isCreating && edgeCreation.sourceNodeId) { return { kind: "node", nodeId: edgeCreation.sourceNodeId }; } const focusedNodeId = store.get(focusedNodeIdAtom); if (focusedNodeId) { return { kind: "node", nodeId: focusedNodeId }; } return { kind: "background" }; } function getSubjectPosition(store, root, subject) { const worldToScreen = store.get(worldToScreenAtom); if (subject.kind === "node") { const node = store.get(uiNodesAtom).find((entry) => entry.id === subject.nodeId); if (!node) { return {}; } const worldPosition = { x: node.position.x + (node.width ?? 200) / 2, y: node.position.y + (node.height ?? 100) / 2 }; return { worldPosition, screenPosition: worldToScreen(worldPosition.x, worldPosition.y) }; } if (subject.kind === "background" && root) { const rect = root.getBoundingClientRect(); const screenPosition = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; return { screenPosition, worldPosition: store.get(screenToWorldAtom)(screenPosition.x, screenPosition.y) }; } return {}; } function buildGuardContext(store, system) { const heldKeys = "heldKeys" in system ? system.heldKeys : system; const edgeCreation = store.get(edgeCreationAtom); return { isStylusActive: store.get(isStylusActiveAtom), fingerCount: store.get(fingerCountAtom), isDragging: store.get(draggingNodeIdAtom) !== null, isResizing: false, isSplitting: false, inputMode: store.get(inputModeAtom), keyboardInteractionMode: store.get(keyboardInteractionModeAtom), selectedNodeIds: store.get(selectedNodeIdsAtom), focusedNodeId: store.get(focusedNodeIdAtom), isSearchActive: store.get(isFilterActiveAtom), commandLineVisible: store.get(commandLineVisibleAtom), heldKeys, custom: { isSelecting: store.get(selectionPathAtom) !== null, isCreatingEdge: edgeCreation.isCreating } }; } // src/gestures/GestureProvider.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); var nextOwnerId = 1; var activeOwnerId = null; var InputContext = /* @__PURE__ */ (0, import_react10.createContext)(null); function InputProvider({ children, gestureConfig, onAction }) { const store = (0, import_jotai33.useStore)(); const system = useInputSystem({ contexts: gestureConfig?.contexts, palmRejection: gestureConfig?.palmRejection ?? true }); useInputModeGestureContext(system); useRegisterInputActions(); const [ownerId] = (0, import_react10.useState)(() => nextOwnerId++); const [canvasRoot, setCanvasRoot] = (0, import_react10.useState)(null); const rootRef = (0, import_react10.useRef)(null); const systemRef = (0, import_react10.useRef)(system); systemRef.current = system; const registerCanvasRoot = (node) => { rootRef.current = node; setCanvasRoot(node); }; (0, import_react10.useEffect)(() => { const root = canvasRoot; if (!root) { return; } const activate = (event) => { activeOwnerId = ownerId; if (!isEditableTarget(event.target)) { root.focus({ preventScroll: true }); } }; root.addEventListener("pointerdown", activate, true); root.addEventListener("focusin", activate, true); return () => { root.removeEventListener("pointerdown", activate, true); root.removeEventListener("focusin", activate, true); if (activeOwnerId === ownerId) { activeOwnerId = null; } }; }, [canvasRoot, ownerId]); (0, import_react10.useEffect)(() => { const handleKeyboard = (nativeEvent) => { if (activeOwnerId !== ownerId) { return; } const root_0 = rootRef.current; if (!root_0 || !root_0.isConnected) { return; } const currentSystem = systemRef.current; const editableTarget = isEditableTarget(nativeEvent.target); const shouldTrackHeldKey = nativeEvent.type === "keyup" || !nativeEvent.isComposing && !editableTarget; const nextHeldKeys = shouldTrackHeldKey ? applyHeldKeyDelta(nativeEvent, currentSystem.heldKeys) : currentSystem.heldKeys; if (nextHeldKeys !== currentSystem.heldKeys) { currentSystem.setHeldKeys(nextHeldKeys); } if (nativeEvent.isComposing || editableTarget) { return; } const subject = getCurrentSubject2(store); const { screenPosition, worldPosition } = getSubjectPosition(store, root_0, subject); const inputEvent = { kind: "key", phase: nativeEvent.type === "keydown" ? "down" : "up", key: nativeEvent.key, code: nativeEvent.code, repeat: nativeEvent.repeat, modifiers: { shift: nativeEvent.shiftKey, ctrl: nativeEvent.ctrlKey, alt: nativeEvent.altKey, meta: nativeEvent.metaKey }, heldKeys: nextHeldKeys, subject, screenPosition, worldPosition, originalEvent: nativeEvent }; const resolution = resolve(inputEvent, currentSystem.mappingIndex, buildGuardContext(store, { heldKeys: nextHeldKeys })); if (!resolution) { return; } nativeEvent.preventDefault(); dispatch(inputEvent, resolution); onAction?.(inputEvent, resolution); }; const clearHeldKeys = () => { systemRef.current.clearHeldKeys(); }; window.addEventListener("keydown", handleKeyboard, true); window.addEventListener("keyup", handleKeyboard, true); window.addEventListener("blur", clearHeldKeys); document.addEventListener("visibilitychange", clearHeldKeys); return () => { window.removeEventListener("keydown", handleKeyboard, true); window.removeEventListener("keyup", handleKeyboard, true); window.removeEventListener("blur", clearHeldKeys); document.removeEventListener("visibilitychange", clearHeldKeys); }; }, [onAction, ownerId, store]); return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(InputContext.Provider, { value: { system, onAction, registerCanvasRoot }, children }); } function useInputContext() { const ctx = (0, import_react10.useContext)(InputContext); if (!ctx) { throw new Error("useInputContext must be used within an InputProvider"); } return ctx; } var GestureProvider = InputProvider; var useGestureContext = useInputContext; //# sourceMappingURL=index.js.map