@loradb/lora-graph-canvas 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ import { DeletionGuard, DeletionSource } from '../types';
2
+ /** Runs an optional deletion guard, returning `true` when the mutation
3
+ * should proceed. Empty batches short-circuit to `true` so callers can
4
+ * use the same code path whether or not anything is actually selected.
5
+ * A thrown error in the guard is swallowed as a cancel — a throwing
6
+ * host should not silently destroy data. */
7
+ export declare function runGuard<T>(guard: DeletionGuard<T> | undefined, items: T[], source: DeletionSource): Promise<boolean>;
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .graph-info-msg{top:50%;width:100%;text-align:center;color:#e6e6fa;opacity:.7;font-size:22px;position:absolute;font-family:Sans-serif}.scene-container .clickable{cursor:pointer}.scene-container .grabbable{cursor:move;cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.scene-container .grabbable:active{cursor:grabbing;cursor:-moz-grabbing;cursor:-webkit-grabbing}.lora-graph-canvas{--lgc-bg: transparent;--lgc-fg: #1c1f23;--lgc-border: #d8dde3;--lgc-accent: #4f8ef7;--lgc-toolbar-bg: rgba(255, 255, 255, .92);--lgc-toolbar-fg: var(--lgc-fg);--lgc-toolbar-border: var(--lgc-border);--lgc-tool-active-bg: rgba(79, 142, 247, .18);--lgc-tool-hover-bg: rgba(0, 0, 0, .05);--lgc-tooltip-bg: rgba(28, 31, 35, .9);--lgc-tooltip-fg: #ffffff;--lgc-menu-bg: #ffffff;--lgc-menu-fg: var(--lgc-fg);--lgc-menu-hover-bg: rgba(0, 0, 0, .06);--lgc-font: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--lgc-font-size: 13px;position:relative;overflow:hidden;background:var(--lgc-bg);color:var(--lgc-fg);font-family:var(--lgc-font);font-size:var(--lgc-font-size)}.lora-graph-canvas .lgc-engine-mount{position:absolute;inset:0}.lora-graph-canvas .lgc-engine-mount>canvas{display:block}.lora-graph-canvas .lgc-toolbar{position:absolute;display:flex;gap:2px;padding:4px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);box-shadow:0 1px 2px #0000000f;z-index:2}.lora-graph-canvas .lgc-toolbar--top,.lora-graph-canvas .lgc-toolbar--top-right,.lora-graph-canvas .lgc-toolbar--top-left{top:12px}.lora-graph-canvas .lgc-toolbar--top{left:50%;transform:translate(-50%)}.lora-graph-canvas .lgc-toolbar--top-right{right:12px}.lora-graph-canvas .lgc-toolbar--top-left{left:12px}.lora-graph-canvas .lgc-toolbar--bottom{bottom:12px;left:50%;transform:translate(-50%)}.lora-graph-canvas .lgc-tool{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;transition:background-color 90ms ease,color 90ms ease}.lora-graph-canvas .lgc-tool:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-tool--active{background:var(--lgc-tool-active-bg);color:var(--lgc-accent)}.lora-graph-canvas .lgc-tool--disabled,.lora-graph-canvas .lgc-tool:disabled{opacity:.35;cursor:not-allowed}.lora-graph-canvas .lgc-tool--disabled:hover,.lora-graph-canvas .lgc-tool:disabled:hover{background:transparent}.lora-graph-canvas .lgc-tool:focus-visible{outline:2px solid var(--lgc-accent);outline-offset:1px}.lora-graph-canvas .lgc-selpanel{position:absolute;top:12px;left:12px;display:flex;align-items:center;gap:4px;padding:4px 6px 4px 10px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);box-shadow:0 1px 2px #0000000f;font-size:12px;z-index:2}.lora-graph-canvas .lgc-selpanel-label{font-weight:500;white-space:nowrap}.lora-graph-canvas .lgc-selpanel-divider{width:1px;height:16px;background:var(--lgc-toolbar-border);margin:0 2px}.lora-graph-canvas .lgc-selpanel-btn{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer;transition:background-color 90ms ease}.lora-graph-canvas .lgc-selpanel-btn:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-selpanel-btn:disabled{opacity:.35;cursor:not-allowed}.lora-graph-canvas .lgc-selpanel-btn:disabled:hover{background:transparent}.lora-graph-canvas .lgc-options-menu{position:absolute;bottom:52px;right:12px;z-index:2}.lora-graph-canvas .lgc-options-trigger{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);box-shadow:0 1px 2px #0000000f;cursor:pointer;transition:background-color 90ms ease}.lora-graph-canvas .lgc-options-trigger:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-options-trigger:focus-visible{outline:2px solid var(--lgc-accent);outline-offset:1px}.lora-graph-canvas .lgc-options-panel{position:absolute;bottom:36px;right:0;min-width:220px;padding:6px;background:var(--lgc-menu-bg);color:var(--lgc-menu-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;box-shadow:0 8px 24px #00000029;font-size:12px;line-height:1.3}.lora-graph-canvas .lgc-options-item{display:flex;align-items:flex-start;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer}.lora-graph-canvas .lgc-options-item:hover{background:var(--lgc-menu-hover-bg)}.lora-graph-canvas .lgc-options-item input[type=checkbox]{margin-top:2px}.lora-graph-canvas .lgc-options-item-text{display:flex;flex-direction:column;gap:2px}.lora-graph-canvas .lgc-options-item-label{font-weight:500}.lora-graph-canvas .lgc-options-item-hint{opacity:.65;font-size:11px}.lora-graph-canvas .lgc-options-select{margin-left:auto;font:inherit;font-size:11px;padding:2px 4px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:4px}.lora-graph-canvas .lgc-mode-toggle{position:absolute;bottom:12px;right:12px;display:inline-flex;align-items:center;gap:6px;padding:5px 10px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);box-shadow:0 1px 2px #0000000f;font:inherit;font-size:11px;font-weight:600;letter-spacing:.04em;cursor:pointer;z-index:2;transition:background-color 90ms ease}.lora-graph-canvas .lgc-mode-toggle:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-mode-toggle:focus-visible{outline:2px solid var(--lgc-accent);outline-offset:1px}.lora-graph-canvas .lgc-mode-toggle-label{font-variant-numeric:tabular-nums}.lora-graph-canvas .lgc-legend{position:absolute;bottom:12px;left:12px;display:flex;flex-direction:column;gap:2px;padding:6px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);font-size:12px;z-index:2;max-height:40%;overflow-y:auto}.lora-graph-canvas .lgc-legend-item{appearance:none;display:flex;align-items:center;gap:8px;padding:2px 8px 2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer;text-align:left;font:inherit}.lora-graph-canvas .lgc-legend-item:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-legend-item--hidden{opacity:.35;text-decoration:line-through}.lora-graph-canvas .lgc-legend-swatch{display:inline-block;width:12px;height:12px;border-radius:3px}.lora-graph-canvas .lgc-marquee{position:absolute;border:1px dashed var(--lgc-accent);background:color-mix(in srgb,var(--lgc-accent) 12%,transparent);border-radius:2px;z-index:2}.lora-graph-canvas .lgc-tooltip{position:absolute;max-width:280px;padding:4px 8px;background:var(--lgc-tooltip-bg);color:var(--lgc-tooltip-fg);border-radius:4px;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;z-index:4;box-shadow:0 2px 6px #0000001f}.lora-graph-canvas .lgc-menu{position:absolute;min-width:180px;padding:4px;background:var(--lgc-menu-bg);color:var(--lgc-menu-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;box-shadow:0 8px 24px #00000029;z-index:3}.lora-graph-canvas .lgc-menu-item{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:4px;cursor:pointer;user-select:none}.lora-graph-canvas .lgc-menu-item:hover{background:var(--lgc-menu-hover-bg)}.lora-graph-canvas .lgc-menu-separator{height:1px;margin:4px 0;background:var(--lgc-toolbar-border)}
1
+ .graph-info-msg{top:50%;width:100%;text-align:center;color:#e6e6fa;opacity:.7;font-size:22px;position:absolute;font-family:Sans-serif}.scene-container .clickable{cursor:pointer}.scene-container .grabbable{cursor:move;cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.scene-container .grabbable:active{cursor:grabbing;cursor:-moz-grabbing;cursor:-webkit-grabbing}.lora-graph-canvas{--lgc-bg: transparent;--lgc-fg: #1c1f23;--lgc-border: #d8dde3;--lgc-accent: #4f8ef7;--lgc-toolbar-bg: rgba(255, 255, 255, .92);--lgc-toolbar-fg: var(--lgc-fg);--lgc-toolbar-border: var(--lgc-border);--lgc-tool-active-bg: rgba(79, 142, 247, .18);--lgc-tool-hover-bg: rgba(0, 0, 0, .05);--lgc-tooltip-bg: rgba(28, 31, 35, .9);--lgc-tooltip-fg: #ffffff;--lgc-menu-bg: #ffffff;--lgc-menu-fg: var(--lgc-fg);--lgc-menu-hover-bg: rgba(0, 0, 0, .06);--lgc-font: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--lgc-font-size: 13px;position:relative;overflow:hidden;background:var(--lgc-bg);color:var(--lgc-fg);font-family:var(--lgc-font);font-size:var(--lgc-font-size)}.lora-graph-canvas .lgc-engine-mount{position:absolute;inset:0}.lora-graph-canvas .lgc-engine-mount>canvas{display:block}.lora-graph-canvas[data-tool=pan] .lgc-engine-mount{cursor:grab}.lora-graph-canvas[data-tool=pan] .lgc-engine-mount:active{cursor:grabbing}.lora-graph-canvas[data-tool=add-node] .lgc-engine-mount,.lora-graph-canvas[data-tool=add-link] .lgc-engine-mount{cursor:crosshair}.lora-graph-canvas[data-paused=true]{box-shadow:inset 0 2px 0 var(--lgc-accent)}.lora-graph-canvas .lgc-toolbar{position:absolute;display:flex;gap:2px;padding:4px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);box-shadow:0 1px 2px #0000000f;z-index:2}.lora-graph-canvas .lgc-toolbar--top,.lora-graph-canvas .lgc-toolbar--top-right,.lora-graph-canvas .lgc-toolbar--top-left{top:12px}.lora-graph-canvas .lgc-toolbar--top{left:50%;transform:translate(-50%)}.lora-graph-canvas .lgc-toolbar--top-right{right:12px}.lora-graph-canvas .lgc-toolbar--top-left{left:12px}.lora-graph-canvas .lgc-toolbar--bottom{bottom:12px;left:50%;transform:translate(-50%)}.lora-graph-canvas .lgc-tool{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;transition:background-color 90ms ease,color 90ms ease}.lora-graph-canvas .lgc-tool:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-tool--active{background:var(--lgc-tool-active-bg);color:var(--lgc-accent)}.lora-graph-canvas .lgc-tool--disabled,.lora-graph-canvas .lgc-tool:disabled{opacity:.35;cursor:not-allowed}.lora-graph-canvas .lgc-tool--disabled:hover,.lora-graph-canvas .lgc-tool:disabled:hover{background:transparent}.lora-graph-canvas .lgc-tool:focus-visible{outline:2px solid var(--lgc-accent);outline-offset:1px}.lora-graph-canvas .lgc-selpanel{position:absolute;top:12px;left:12px;display:flex;align-items:center;gap:4px;padding:4px 6px 4px 10px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);box-shadow:0 1px 2px #0000000f;font-size:12px;z-index:2}.lora-graph-canvas .lgc-selpanel-label{font-weight:500;white-space:nowrap}.lora-graph-canvas .lgc-selpanel-divider{width:1px;height:16px;background:var(--lgc-toolbar-border);margin:0 2px}.lora-graph-canvas .lgc-selpanel-btn{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer;transition:background-color 90ms ease}.lora-graph-canvas .lgc-selpanel-btn:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-selpanel-btn:disabled{opacity:.35;cursor:not-allowed}.lora-graph-canvas .lgc-selpanel-btn:disabled:hover{background:transparent}.lora-graph-canvas .lgc-options-menu{position:absolute;bottom:52px;right:12px;z-index:2}.lora-graph-canvas .lgc-options-trigger{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);box-shadow:0 1px 2px #0000000f;cursor:pointer;transition:background-color 90ms ease}.lora-graph-canvas .lgc-options-trigger:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-options-trigger:focus-visible{outline:2px solid var(--lgc-accent);outline-offset:1px}.lora-graph-canvas .lgc-options-panel{position:absolute;bottom:36px;right:0;min-width:220px;padding:6px;background:var(--lgc-menu-bg);color:var(--lgc-menu-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;box-shadow:0 8px 24px #00000029;font-size:12px;line-height:1.3}.lora-graph-canvas .lgc-options-item{display:flex;align-items:flex-start;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer}.lora-graph-canvas .lgc-options-item:hover{background:var(--lgc-menu-hover-bg)}.lora-graph-canvas .lgc-options-item input[type=checkbox]{margin-top:2px}.lora-graph-canvas .lgc-options-item-text{display:flex;flex-direction:column;gap:2px}.lora-graph-canvas .lgc-options-item-label{font-weight:500}.lora-graph-canvas .lgc-options-item-hint{opacity:.65;font-size:11px}.lora-graph-canvas .lgc-options-select{margin-left:auto;font:inherit;font-size:11px;padding:2px 4px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:4px}.lora-graph-canvas .lgc-mode-toggle{position:absolute;bottom:12px;right:12px;display:inline-flex;align-items:center;gap:6px;padding:5px 10px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);box-shadow:0 1px 2px #0000000f;font:inherit;font-size:11px;font-weight:600;letter-spacing:.04em;cursor:pointer;z-index:2;transition:background-color 90ms ease}.lora-graph-canvas .lgc-mode-toggle:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-mode-toggle:focus-visible{outline:2px solid var(--lgc-accent);outline-offset:1px}.lora-graph-canvas .lgc-mode-toggle-label{font-variant-numeric:tabular-nums}.lora-graph-canvas .lgc-legend{position:absolute;bottom:12px;left:12px;display:flex;flex-direction:column;gap:2px;padding:6px;background:var(--lgc-toolbar-bg);color:var(--lgc-toolbar-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;backdrop-filter:blur(8px);font-size:12px;z-index:2;max-height:40%;overflow-y:auto}.lora-graph-canvas .lgc-legend-item{appearance:none;display:flex;align-items:center;gap:8px;padding:2px 8px 2px 4px;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer;text-align:left;font:inherit}.lora-graph-canvas .lgc-legend-item:hover{background:var(--lgc-tool-hover-bg)}.lora-graph-canvas .lgc-legend-item--hidden{opacity:.35;text-decoration:line-through}.lora-graph-canvas .lgc-legend-swatch{display:inline-block;width:12px;height:12px;border-radius:3px}.lora-graph-canvas .lgc-marquee{position:absolute;border:1px dashed var(--lgc-accent);background:color-mix(in srgb,var(--lgc-accent) 12%,transparent);border-radius:2px;z-index:2}.lora-graph-canvas .lgc-marquee-count{position:absolute;right:-2px;bottom:-22px;display:inline-flex;align-items:center;justify-content:center;padding:1px 6px;font-size:11px;font-weight:600;color:#fff;background:var(--lgc-accent);border-radius:10px;pointer-events:none;font-variant-numeric:tabular-nums}.lora-graph-canvas .lgc-tooltip{position:absolute;max-width:280px;padding:4px 8px;background:var(--lgc-tooltip-bg);color:var(--lgc-tooltip-fg);border-radius:4px;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;z-index:4;box-shadow:0 2px 6px #0000001f}.lora-graph-canvas .lgc-menu{position:absolute;min-width:180px;padding:4px;background:var(--lgc-menu-bg);color:var(--lgc-menu-fg);border:1px solid var(--lgc-toolbar-border);border-radius:8px;box-shadow:0 8px 24px #00000029;z-index:3}.lora-graph-canvas .lgc-menu-item{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:4px;cursor:pointer;user-select:none}.lora-graph-canvas .lgc-menu-item:hover{background:var(--lgc-menu-hover-bg)}.lora-graph-canvas .lgc-menu-separator{height:1px;margin:4px 0;background:var(--lgc-toolbar-border)}
@@ -6,8 +6,14 @@ export interface MarqueeRect {
6
6
  }
7
7
  export interface MarqueeOverlayProps {
8
8
  rect: MarqueeRect | null;
9
+ /** Live count of nodes whose projected position falls inside the
10
+ * rectangle. Hidden when 0 or undefined — small rectangles in dead
11
+ * space shouldn't render an empty "0 nodes" pill. */
12
+ count?: number;
9
13
  }
10
14
  /** Renders the dashed selection rectangle while the user is dragging a
11
15
  * marquee. Positioned absolutely inside the host. Hidden when `rect`
12
- * is null. */
13
- export declare function MarqueeOverlay({ rect }: MarqueeOverlayProps): import("react/jsx-runtime").JSX.Element | null;
16
+ * is null. Shows a live node-count badge in the bottom-right of the
17
+ * rectangle so the user can tell how many nodes they'd grab before
18
+ * releasing. */
19
+ export declare function MarqueeOverlay({ rect, count }: MarqueeOverlayProps): import("react/jsx-runtime").JSX.Element | null;
package/dist/types.d.ts CHANGED
@@ -35,6 +35,16 @@ export interface GraphData<N extends NodeObject = NodeObject, L extends LinkObje
35
35
  links: L[];
36
36
  }
37
37
  export type GraphMode = "2d" | "3d";
38
+ /** Where a delete originated. Hosts often want to gate UI-driven deletes
39
+ * (show a confirm modal) but trust `imperative` calls they made
40
+ * themselves, so the source string lets the guard short-circuit. */
41
+ export type DeletionSource = "keyboard" | "toolbar" | "selectionPanel" | "contextMenu" | "cut" | "imperative";
42
+ /** Returns `true` to allow the deletion, `false` to cancel. Can be
43
+ * async — the caller awaits it before mutating data. A thrown error
44
+ * is treated as a cancel. Called once per batch (not per item). */
45
+ export type DeletionGuard<T> = (items: T[], ctx: {
46
+ source: DeletionSource;
47
+ }) => boolean | Promise<boolean>;
38
48
  /** Built-in tool identifiers. */
39
49
  export type ToolId = "select" | "pan" | "add-node" | "add-link" | "delete" | "duplicate" | "select-all" | "fit" | "zoom-in" | "zoom-out" | "pause" | "resume" | "screenshot" | "export-json" | "import-json" | "toggle-mode";
40
50
  /** Accepts a literal value, a property accessor string, or a function. */
@@ -190,6 +200,12 @@ export interface LoraGraphCanvasProps<N extends NodeObject = NodeObject, L exten
190
200
  * and pins them as a group on release. Set to `false` to keep the
191
201
  * default simulation-pull behaviour. */
192
202
  fixOnDrop?: boolean;
203
+ /** When `true` (default `false`), the camera animates to fit the
204
+ * current selection any time it changes — like the toolbar "fit"
205
+ * button, but scoped to the selected nodes (and the endpoints of
206
+ * any selected links). No-ops when the selection becomes empty;
207
+ * the user keeps their view instead of being snapped back. */
208
+ fitOnSelect?: boolean;
193
209
  /** Auto-tune renderer / simulation settings based on graph size so
194
210
  * the canvas stays responsive into the 50k-100k-node range.
195
211
  *
@@ -253,6 +269,22 @@ export interface LoraGraphCanvasProps<N extends NodeObject = NodeObject, L exten
253
269
  }) => void;
254
270
  onRenderFramePre?: (ctx: CanvasRenderingContext2D, globalScale: number) => void;
255
271
  onRenderFramePost?: (ctx: CanvasRenderingContext2D, globalScale: number) => void;
272
+ /** Optional async gate fired before nodes are removed. Receives every
273
+ * node in the batch (single-node deletes get a 1-length array). Resolve
274
+ * `true` to proceed, `false` to cancel. Throws are treated as cancel.
275
+ * Fires for: keyboard, toolbar, selection-panel, context-menu, cut,
276
+ * and `handle.removeNode(s)` calls — discriminate via `ctx.source`. */
277
+ onBeforeNodeDelete?: DeletionGuard<N>;
278
+ /** Same shape as `onBeforeNodeDelete` for links. */
279
+ onBeforeLinkDelete?: DeletionGuard<L>;
280
+ /** Fires after a node delete has been applied to the data. */
281
+ onNodeDeleted?: (nodes: N[], ctx: {
282
+ source: DeletionSource;
283
+ }) => void;
284
+ /** Fires after a link delete has been applied to the data. */
285
+ onLinkDeleted?: (links: L[], ctx: {
286
+ source: DeletionSource;
287
+ }) => void;
256
288
  /** Fires when the user copies nodes (⌘C) — receives the snapshot. */
257
289
  onCopy?: (nodes: N[]) => void;
258
290
  /** Fires when the user cuts nodes (⌘X) — receives the snapshot
@@ -309,8 +341,14 @@ export interface LoraGraphCanvasHandle<N extends NodeObject = NodeObject, L exte
309
341
  id?: string | number;
310
342
  }>): N[];
311
343
  updateNode(id: string | number, patch: Partial<N>): void;
312
- removeNode(id: string | number): void;
313
- removeNodes(ids: Array<string | number>): void;
344
+ /** Returns a promise that resolves to `true` when the node was removed.
345
+ * When the host passes `onBeforeNodeDelete`, the guard runs with
346
+ * `source: "imperative"` and may cancel — the promise resolves
347
+ * `false` and the graph is unchanged. Hosts that don't pass a guard
348
+ * can ignore the promise; the data mutation is observable on the
349
+ * same tick (only the resolved promise itself is async). */
350
+ removeNode(id: string | number): Promise<boolean>;
351
+ removeNodes(ids: Array<string | number>): Promise<boolean>;
314
352
  addLink(link: {
315
353
  source: string | number;
316
354
  target: string | number;
@@ -320,7 +358,9 @@ export interface LoraGraphCanvasHandle<N extends NodeObject = NodeObject, L exte
320
358
  source: string | number;
321
359
  target: string | number;
322
360
  } & Partial<L>>): L[];
323
- removeLink(predicate: (l: L) => boolean): void;
361
+ /** Same gate semantics as `removeNode` — `onBeforeLinkDelete` runs with
362
+ * `source: "imperative"` and may cancel. */
363
+ removeLink(predicate: (l: L) => boolean): Promise<boolean>;
324
364
  clear(): void;
325
365
  getSelection(): Array<string | number>;
326
366
  setSelection(ids: Array<string | number>): void;
@@ -333,8 +373,32 @@ export interface LoraGraphCanvasHandle<N extends NodeObject = NodeObject, L exte
333
373
  zoom(scale: number, durationMs?: number): void;
334
374
  zoomIn(step?: number): void;
335
375
  zoomOut(step?: number): void;
376
+ /** Translate the view by a world-space delta. In 2D the z component
377
+ * is ignored (the camera height is locked). */
378
+ panBy(delta: {
379
+ x?: number;
380
+ y?: number;
381
+ z?: number;
382
+ }, durationMs?: number): void;
383
+ /** Animate to a world coordinate, preserving the current viewing
384
+ * direction. Use for "jump to coordinates" UI. */
385
+ goTo(target: {
386
+ x: number;
387
+ y: number;
388
+ z?: number;
389
+ }, opts?: {
390
+ durationMs?: number;
391
+ }): void;
392
+ /** Fit the view to a subset of nodes. Empty array → full fit. */
393
+ fitToNodes(nodeIds: ReadonlyArray<string | number>, durationMs?: number, padding?: number): void;
394
+ /** Fit to the current selection (nodes + endpoints of selected links).
395
+ * No-op if nothing is selected. */
396
+ fitToSelection(durationMs?: number, padding?: number): void;
336
397
  copy(): N[];
337
- cut(): N[];
398
+ /** Cut funnels through the same delete-gate as keyboard / toolbar
399
+ * cuts. A rejected guard resolves to an empty array and leaves the
400
+ * graph + clipboard untouched. */
401
+ cut(): Promise<N[]>;
338
402
  paste(opts?: {
339
403
  at?: {
340
404
  x: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loradb/lora-graph-canvas",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "React graph canvas for LoraDB. Unified 2D/3D force-directed component with a built-in tool palette for adding, connecting, selecting, and removing nodes.",
5
5
  "license": "BUSL-1.1",
6
6
  "packageManager": "yarn@4.5.1",
@@ -57,6 +57,7 @@
57
57
  "test": "vitest run",
58
58
  "test:watch": "vitest",
59
59
  "lint": "eslint src test",
60
+ "lint:fix": "eslint src test --fix",
60
61
  "format": "prettier --write src test",
61
62
  "format:check": "prettier --check src test",
62
63
  "storybook": "storybook dev -p 6007",