@loradb/lora-graph-canvas 0.10.1 → 0.11.0

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.
package/README.md CHANGED
@@ -114,6 +114,37 @@ import { LoraGraphCanvas, darkTheme } from "@loradb/lora-graph-canvas";
114
114
 
115
115
  Two presets are exported: `lightTheme` and `darkTheme`.
116
116
 
117
+ ## Confirm-before-delete
118
+
119
+ Gate every node / link removal — keyboard, toolbar, context menu,
120
+ selection panel, cut, and the imperative `removeNode` / `removeLink`
121
+ handle methods — through an async guard:
122
+
123
+ ```tsx
124
+ <LoraGraphCanvas
125
+ onBeforeNodeDelete={(nodes, { source }) =>
126
+ source === "imperative"
127
+ ? true // trust your own code
128
+ : openMyConfirmDialog(nodes) // returns Promise<boolean>
129
+ }
130
+ onBeforeLinkDelete={(links) => openMyConfirmDialog([], links)}
131
+ onNodeDeleted={(nodes, { source }) => analytics.track("nodes.removed", {
132
+ n: nodes.length,
133
+ source,
134
+ })}
135
+ />
136
+ ```
137
+
138
+ The guard receives every item in the batch (one selection-wide call,
139
+ not per-item), the originating `source` (`"keyboard" | "toolbar" |
140
+ "contextMenu" | "selectionPanel" | "cut" | "imperative"`), and may
141
+ return either a boolean or a `Promise<boolean>`. A thrown error is
142
+ treated as a cancel — your host won't silently destroy data.
143
+
144
+ When no guard is wired, deletion happens immediately as before; the
145
+ imperative methods become `Promise<boolean>` but resolve on the same
146
+ tick.
147
+
117
148
  ## Performance knobs
118
149
 
119
150
  For large graphs, cap `cooldownTicks` (default ∞) and increase
@@ -21,3 +21,4 @@ export declare const Beeswarm: Story;
21
21
  export declare const EmitParticle: Story;
22
22
  export declare const RandomGraphStory: Story;
23
23
  export declare const LabeledStory: Story;
24
+ export declare const ConfirmDeleteStory: Story;
@@ -8,6 +8,10 @@
8
8
  * the animation at the current frame (no further `step` invocations).
9
9
  * Returns null when running in an environment without
10
10
  * `requestAnimationFrame` (jsdom in unit tests). */
11
- export declare function runAnim(durationMs: number, step: (t: number) => void, onDone?: () => void): () => void;
11
+ export declare function runAnim(durationMs: number, step: (t: number) => void, onDone?: () => void, ease?: (t: number) => number): () => void;
12
12
  export declare function easeOutQuad(t: number): number;
13
+ /** Smoother S-curve — slow start, fast middle, slow end. Reads as
14
+ * more "cinematic" than easeOutQuad for longer cross-mode camera
15
+ * tweens where the user is watching the whole motion. */
16
+ export declare function easeInOutCubic(t: number): number;
13
17
  export declare function lerp(a: number, b: number, t: number): number;
@@ -2,10 +2,12 @@ import { MutableRefObject } from 'react';
2
2
  import { GraphEngine } from '../engines/types';
3
3
  import { LinkObject, NodeObject } from '../types';
4
4
  import { GraphDataApi } from './useGraphData';
5
+ import { GraphDeleteGateApi } from './useGraphDeleteGate';
5
6
  import { SelectionApi } from './useGraphSelection';
6
7
  export interface UseGraphClipboardParams<N extends NodeObject, L extends LinkObject> {
7
8
  enableClipboard: boolean;
8
9
  dataApi: GraphDataApi<N, L>;
10
+ deleteGate: GraphDeleteGateApi<N, L>;
9
11
  selection: SelectionApi;
10
12
  setSelectedLinkIds: React.Dispatch<React.SetStateAction<Array<string | number>>>;
11
13
  engineRef: MutableRefObject<GraphEngine<N, L> | null>;
@@ -23,7 +25,11 @@ export interface GraphClipboardApi<N extends NodeObject> {
23
25
  * callers that need to react on every keystroke. */
24
26
  hasClipboard(): boolean;
25
27
  copy(): N[];
26
- cut(): N[];
28
+ /** Async because cut funnels through the host's delete guard — if the
29
+ * guard rejects, the cut becomes a no-op (clipboard untouched, nodes
30
+ * not removed). Callers that fire-and-forget can ignore the
31
+ * promise. */
32
+ cut(): Promise<N[]>;
27
33
  paste(at?: {
28
34
  x: number;
29
35
  y: number;
@@ -0,0 +1,38 @@
1
+ import { DeletionGuard, DeletionSource, LinkObject, NodeObject } from '../types';
2
+ import { GraphDataApi } from './useGraphData';
3
+ export interface UseGraphDeleteGateParams<N extends NodeObject, L extends LinkObject> {
4
+ dataApi: GraphDataApi<N, L>;
5
+ beforeNode?: DeletionGuard<N>;
6
+ beforeLink?: DeletionGuard<L>;
7
+ onNodeDeleted?: (nodes: N[], ctx: {
8
+ source: DeletionSource;
9
+ }) => void;
10
+ onLinkDeleted?: (links: L[], ctx: {
11
+ source: DeletionSource;
12
+ }) => void;
13
+ /** Called after a successful node delete so the caller can clear its
14
+ * own selection / hover state. Skipped if the guard rejected. */
15
+ afterNodeDelete?: (ids: Array<string | number>) => void;
16
+ afterLinkDelete?: (ids: Array<string | number>) => void;
17
+ }
18
+ export interface GraphDeleteGateApi<N extends NodeObject, L extends LinkObject> {
19
+ /** Resolves the selected node ids against current data, runs the guard,
20
+ * and removes them. Returns `false` if the guard rejected or nothing
21
+ * matched. */
22
+ requestNodeDelete: (ids: Array<string | number>, source: DeletionSource) => Promise<boolean>;
23
+ /** Same, for links. Accepts either an id list or a predicate so context
24
+ * menus that hold the link reference can still target it precisely
25
+ * (links sometimes lack an id). */
26
+ requestLinkDelete: (target: Array<string | number> | ((l: L) => boolean), source: DeletionSource) => Promise<boolean>;
27
+ /** Convenience: run node + link guards in sequence. Used by the
28
+ * "delete selection" sites (toolbar / selection panel / keyboard)
29
+ * where a mixed selection is common. Each guard fires independently;
30
+ * rejecting one doesn't cancel the other. Returns true if anything
31
+ * was actually deleted. */
32
+ requestMixedDelete: (nodeIds: Array<string | number>, linkIds: Array<string | number>, source: DeletionSource) => Promise<boolean>;
33
+ }
34
+ /** Single chokepoint for every gated delete in the canvas. Centralising
35
+ * here keeps the guard semantics (batched calls, post-delete callbacks,
36
+ * selection cleanup) consistent across keyboard, toolbar, context menu,
37
+ * selection panel, and imperative paths. */
38
+ export declare function useGraphDeleteGate<N extends NodeObject, L extends LinkObject>(params: UseGraphDeleteGateParams<N, L>): GraphDeleteGateApi<N, L>;
@@ -1,10 +1,13 @@
1
+ import { RefObject } from 'react';
1
2
  import { GraphEngine } from '../engines/types';
2
3
  import { GraphMode, LinkObject, NodeObject, ToolId } from '../types';
3
4
  import { GraphDataApi } from './useGraphData';
5
+ import { GraphDeleteGateApi } from './useGraphDeleteGate';
4
6
  import { SelectionApi } from './useGraphSelection';
5
7
  export interface UseGraphKeybindingsParams<N extends NodeObject, L extends LinkObject> {
6
8
  engine: GraphEngine<N, L> | null;
7
9
  dataApi: GraphDataApi<N, L>;
10
+ deleteGate: GraphDeleteGateApi<N, L>;
8
11
  selection: SelectionApi;
9
12
  mode: GraphMode;
10
13
  setMode: (next: GraphMode) => void;
@@ -19,6 +22,10 @@ export interface UseGraphKeybindingsParams<N extends NodeObject, L extends LinkO
19
22
  duplicate: () => unknown;
20
23
  addConnectedNode: () => unknown;
21
24
  togglePin: (id: string | number) => void;
25
+ /** Host element. Bindings only fire while focus is inside this
26
+ * element — otherwise hitting `f` while typing into a sibling text
27
+ * field on the page would trigger the canvas fit shortcut. */
28
+ hostRef: RefObject<HTMLElement | null>;
22
29
  }
23
30
  /** Global keyboard shortcuts for the canvas. The listener is bound once
24
31
  * per mount; live state is read through a ref so we avoid the
@@ -1,11 +1,13 @@
1
1
  import { GraphEngine } from '../engines/types';
2
2
  import { GraphMode, LinkObject, LoraGraphCanvasHandle, NodeObject } from '../types';
3
3
  import { GraphDataApi } from './useGraphData';
4
+ import { GraphDeleteGateApi } from './useGraphDeleteGate';
4
5
  import { SelectionApi } from './useGraphSelection';
5
6
  import { GraphClipboardApi } from './useGraphClipboard';
6
7
  export interface UseImperativeGraphHandleParams<N extends NodeObject, L extends LinkObject> {
7
8
  ref: React.Ref<LoraGraphCanvasHandle<N, L>>;
8
9
  dataApi: GraphDataApi<N, L>;
10
+ deleteGate: GraphDeleteGateApi<N, L>;
9
11
  selection: SelectionApi;
10
12
  engine: GraphEngine<N, L> | null;
11
13
  mode: GraphMode;
@@ -8,6 +8,11 @@ export interface MarqueeRect {
8
8
  x1: number;
9
9
  y1: number;
10
10
  additive: boolean;
11
+ /** Live count of nodes inside the rectangle. Updated on rAF
12
+ * throttle during drag — cheap to compute (one graph2Screen per
13
+ * node) but we still coalesce by frame so a 5k-node graph doesn't
14
+ * re-render the host 60×/s. Undefined while inactive. */
15
+ count?: number;
11
16
  }
12
17
  export interface UseMarqueeAndCursorParams<N extends NodeObject, L extends LinkObject> {
13
18
  mount: HTMLDivElement | null;
@@ -0,0 +1,10 @@
1
+ /** Track the user's `prefers-reduced-motion` media query. Returns
2
+ * `true` when the user has asked the OS to minimise non-essential
3
+ * motion — our camera tweens (intro zoom, mode transition, focus
4
+ * fly-in) skip the animation in that case and snap directly to the
5
+ * final state.
6
+ *
7
+ * Re-reads on media-query change so we react when the user flips the
8
+ * setting mid-session. Returns `false` in non-browser environments
9
+ * (SSR / jsdom without matchMedia) so animations play by default. */
10
+ export declare function usePrefersReducedMotion(): boolean;