backpack-viewer 0.2.19 → 0.2.21

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/dist/canvas.js CHANGED
@@ -18,6 +18,7 @@ function isInViewport(x, y, camera, canvasW, canvasH, pad = 100) {
18
18
  export function initCanvas(container, onNodeClick, onFocusChange, config) {
19
19
  const lod = { ...LOD_DEFAULTS, ...(config?.lod ?? {}) };
20
20
  const nav = { ...NAV_DEFAULTS, ...(config?.navigation ?? {}) };
21
+ const walkCfg = { pulseSpeed: 0.02, ...(config?.walk ?? {}) };
21
22
  const canvas = container.querySelector("canvas");
22
23
  const ctx = canvas.getContext("2d");
23
24
  const dpr = window.devicePixelRatio || 1;
@@ -37,6 +38,12 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
37
38
  let focusHops = 1;
38
39
  let savedFullState = null;
39
40
  let savedFullCamera = null;
41
+ // Highlighted path state (for path finding visualization)
42
+ let highlightedPath = null;
43
+ // Walk mode state
44
+ let walkMode = false;
45
+ let walkTrail = []; // node IDs visited during walk, most recent last
46
+ let pulsePhase = 0; // animation counter for pulse effect
40
47
  // Pan animation state
41
48
  let panTarget = null;
42
49
  let panStart = null;
@@ -79,6 +86,11 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
79
86
  ctx.clearRect(0, 0, canvas.width, canvas.height);
80
87
  return;
81
88
  }
89
+ // Advance pulse animation for walk mode
90
+ if (walkMode && walkTrail.length > 0) {
91
+ pulsePhase += walkCfg.pulseSpeed;
92
+ }
93
+ const walkTrailSet = walkMode ? new Set(walkTrail) : null;
82
94
  // Read theme colors from CSS variables each frame
83
95
  const edgeColor = cssVar("--canvas-edge");
84
96
  const edgeHighlight = cssVar("--canvas-edge-highlight");
@@ -166,6 +178,11 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
166
178
  (selectedNodeIds.has(edge.sourceId) || selectedNodeIds.has(edge.targetId));
167
179
  const highlighted = isConnected || (filteredNodeIds !== null && bothMatch);
168
180
  const edgeDimmed = filteredNodeIds !== null && !bothMatch;
181
+ const isWalkEdge = walkTrailSet !== null && walkTrailSet.has(edge.sourceId) && walkTrailSet.has(edge.targetId);
182
+ // Check if this edge is part of the highlighted path
183
+ const fullEdge = highlightedPath ? lastLoadedData?.edges.find(e => (e.sourceId === edge.sourceId && e.targetId === edge.targetId) ||
184
+ (e.targetId === edge.sourceId && e.sourceId === edge.targetId)) : null;
185
+ const isPathEdge = highlightedPath && fullEdge && highlightedPath.edgeIds.has(fullEdge.id);
169
186
  // Self-loop
170
187
  if (edge.sourceId === edge.targetId) {
171
188
  drawSelfLoop(source, edge.type, highlighted, edgeColor, edgeHighlight, edgeLabel, edgeLabelHighlight);
@@ -175,13 +192,25 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
175
192
  ctx.beginPath();
176
193
  ctx.moveTo(source.x, source.y);
177
194
  ctx.lineTo(target.x, target.y);
178
- ctx.strokeStyle = highlighted
179
- ? edgeHighlight
180
- : edgeDimmed
181
- ? edgeDimColor
182
- : edgeColor;
183
- ctx.lineWidth = camera.scale < lod.hideArrows ? 1 : highlighted ? 2.5 : 1.5;
195
+ const accent = cssVar("--accent") || "#d4a27f";
196
+ const walkEdgeColor = cssVar("--canvas-walk-edge") || "#1a1a1a";
197
+ ctx.strokeStyle = isPathEdge
198
+ ? accent
199
+ : isWalkEdge
200
+ ? walkEdgeColor
201
+ : highlighted
202
+ ? edgeHighlight
203
+ : edgeDimmed
204
+ ? edgeDimColor
205
+ : edgeColor;
206
+ ctx.lineWidth = isPathEdge || isWalkEdge
207
+ ? 3
208
+ : camera.scale < lod.hideArrows ? 1 : highlighted ? 2.5 : 1.5;
209
+ if (isWalkEdge) {
210
+ ctx.globalAlpha = 0.5 + 0.5 * Math.sin(pulsePhase);
211
+ }
184
212
  ctx.stroke();
213
+ ctx.globalAlpha = 1;
185
214
  // Arrowhead
186
215
  if (camera.scale >= lod.hideArrows) {
187
216
  drawArrowhead(source.x, source.y, target.x, target.y, highlighted, arrowColor, arrowHighlight);
@@ -215,6 +244,20 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
215
244
  const dimmed = filteredOut ||
216
245
  (selectedNodeIds.size > 0 && !isSelected && !isNeighbor);
217
246
  const r = camera.scale < lod.smallNodes ? NODE_RADIUS * 0.5 : NODE_RADIUS;
247
+ // Walk trail effect — all visited nodes pulse together
248
+ if (walkTrailSet?.has(node.id)) {
249
+ const isCurrent = walkTrail[walkTrail.length - 1] === node.id;
250
+ const pulse = 0.5 + 0.5 * Math.sin(pulsePhase);
251
+ const accent = cssVar("--accent") || "#d4a27f";
252
+ ctx.save();
253
+ ctx.strokeStyle = accent;
254
+ ctx.lineWidth = isCurrent ? 3 : 2;
255
+ ctx.globalAlpha = isCurrent ? 0.5 + 0.5 * pulse : 0.3 + 0.4 * pulse;
256
+ ctx.beginPath();
257
+ ctx.arc(node.x, node.y, r + (isCurrent ? 6 : 4), 0, Math.PI * 2);
258
+ ctx.stroke();
259
+ ctx.restore();
260
+ }
218
261
  // Glow for selected node
219
262
  if (isSelected) {
220
263
  ctx.save();
@@ -236,6 +279,29 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
236
279
  ctx.strokeStyle = isSelected ? selectionBorder : nodeBorder;
237
280
  ctx.lineWidth = isSelected ? 3 : 1.5;
238
281
  ctx.stroke();
282
+ // Highlighted path glow
283
+ if (highlightedPath && highlightedPath.nodeIds.has(node.id) && !isSelected) {
284
+ ctx.save();
285
+ ctx.shadowColor = cssVar("--accent") || "#d4a27f";
286
+ ctx.shadowBlur = 15;
287
+ ctx.beginPath();
288
+ ctx.arc(node.x, node.y, r + 2, 0, Math.PI * 2);
289
+ ctx.strokeStyle = cssVar("--accent") || "#d4a27f";
290
+ ctx.globalAlpha = 0.5;
291
+ ctx.lineWidth = 2;
292
+ ctx.stroke();
293
+ ctx.restore();
294
+ }
295
+ // Star indicator for starred nodes
296
+ const originalNode = lastLoadedData?.nodes.find(n => n.id === node.id);
297
+ const isStarred = originalNode?.properties?._starred === true;
298
+ if (isStarred) {
299
+ ctx.fillStyle = "#ffd700";
300
+ ctx.font = "10px system-ui, sans-serif";
301
+ ctx.textAlign = "left";
302
+ ctx.textBaseline = "bottom";
303
+ ctx.fillText("\u2605", node.x + r - 2, node.y - r + 2);
304
+ }
239
305
  // Label below
240
306
  if (camera.scale >= lod.hideLabels) {
241
307
  const label = node.label.length > 24 ? node.label.slice(0, 22) + "..." : node.label;
@@ -385,9 +451,47 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
385
451
  panStart = null;
386
452
  }
387
453
  }
454
+ let walkAnimFrame = 0;
455
+ function walkAnimate() {
456
+ if (!walkMode || walkTrail.length === 0) {
457
+ walkAnimFrame = 0;
458
+ return;
459
+ }
460
+ render();
461
+ walkAnimFrame = requestAnimationFrame(walkAnimate);
462
+ }
463
+ function fitToNodes() {
464
+ if (!state || state.nodes.length === 0)
465
+ return;
466
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
467
+ for (const n of state.nodes) {
468
+ if (n.x < minX)
469
+ minX = n.x;
470
+ if (n.y < minY)
471
+ minY = n.y;
472
+ if (n.x > maxX)
473
+ maxX = n.x;
474
+ if (n.y > maxY)
475
+ maxY = n.y;
476
+ }
477
+ const pad = NODE_RADIUS * 4;
478
+ const graphW = (maxX - minX) + pad * 2;
479
+ const graphH = (maxY - minY) + pad * 2;
480
+ const scaleX = canvas.clientWidth / Math.max(graphW, 1);
481
+ const scaleY = canvas.clientHeight / Math.max(graphH, 1);
482
+ camera.scale = Math.min(scaleX, scaleY, 2);
483
+ camera.x = (minX + maxX) / 2 - canvas.clientWidth / (2 * camera.scale);
484
+ camera.y = (minY + maxY) / 2 - canvas.clientHeight / (2 * camera.scale);
485
+ render();
486
+ }
388
487
  function simulate() {
389
- if (!state || alpha < ALPHA_MIN)
488
+ if (!state || alpha < ALPHA_MIN) {
489
+ // Start walk animation loop if simulation stopped but walk mode is active
490
+ if (walkMode && walkTrail.length > 0 && !walkAnimFrame) {
491
+ walkAnimFrame = requestAnimationFrame(walkAnimate);
492
+ }
390
493
  return;
494
+ }
391
495
  alpha = tick(state, alpha);
392
496
  render();
393
497
  animFrame = requestAnimationFrame(simulate);
@@ -408,7 +512,7 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
408
512
  return;
409
513
  const dx = e.clientX - lastX;
410
514
  const dy = e.clientY - lastY;
411
- if (Math.abs(dx) > 2 || Math.abs(dy) > 2)
515
+ if (Math.abs(dx) > 5 || Math.abs(dy) > 5)
412
516
  didDrag = true;
413
517
  camera.x -= dx / camera.scale;
414
518
  camera.y -= dy / camera.scale;
@@ -426,6 +530,60 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
426
530
  const my = e.clientY - rect.top;
427
531
  const hit = nodeAtScreen(mx, my);
428
532
  const multiSelect = e.ctrlKey || e.metaKey;
533
+ if (walkMode && focusSeedIds && hit && state) {
534
+ // Walk mode: find path from current position to clicked node
535
+ const currentId = walkTrail.length > 0 ? walkTrail[walkTrail.length - 1] : focusSeedIds[0];
536
+ // BFS in the current subgraph to find path
537
+ const visited = new Set([currentId]);
538
+ const queue = [{ id: currentId, path: [currentId] }];
539
+ let pathToTarget = null;
540
+ while (queue.length > 0) {
541
+ const { id, path } = queue.shift();
542
+ if (id === hit.id) {
543
+ pathToTarget = path;
544
+ break;
545
+ }
546
+ for (const edge of state.edges) {
547
+ let neighbor = null;
548
+ if (edge.sourceId === id)
549
+ neighbor = edge.targetId;
550
+ else if (edge.targetId === id)
551
+ neighbor = edge.sourceId;
552
+ if (neighbor && !visited.has(neighbor)) {
553
+ visited.add(neighbor);
554
+ queue.push({ id: neighbor, path: [...path, neighbor] });
555
+ }
556
+ }
557
+ }
558
+ // No path found — node is unreachable, ignore click
559
+ if (!pathToTarget)
560
+ return;
561
+ // Add all intermediate nodes to the trail (skip first since it's already the current position)
562
+ for (const id of pathToTarget.slice(1)) {
563
+ if (!walkTrail.includes(id))
564
+ walkTrail.push(id);
565
+ }
566
+ focusSeedIds = [hit.id];
567
+ const walkHops = Math.max(1, focusHops);
568
+ focusHops = walkHops;
569
+ const subgraph = extractSubgraph(lastLoadedData, [hit.id], walkHops);
570
+ cancelAnimationFrame(animFrame);
571
+ state = createLayout(subgraph);
572
+ alpha = 1;
573
+ selectedNodeIds = new Set([hit.id]);
574
+ filteredNodeIds = null;
575
+ camera = { x: 0, y: 0, scale: 1 };
576
+ simulate();
577
+ // Center after physics settle
578
+ setTimeout(() => {
579
+ if (!state)
580
+ return;
581
+ fitToNodes();
582
+ }, 300);
583
+ onFocusChange?.({ seedNodeIds: [hit.id], hops: walkHops, totalNodes: subgraph.nodes.length });
584
+ onNodeClick?.([hit.id]);
585
+ return; // skip normal selection
586
+ }
429
587
  if (hit) {
430
588
  if (multiSelect) {
431
589
  // Toggle node in/out of multi-selection
@@ -720,25 +878,7 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
720
878
  render();
721
879
  },
722
880
  centerView() {
723
- if (!state)
724
- return;
725
- camera = { x: 0, y: 0, scale: 1 };
726
- if (state.nodes.length > 0) {
727
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
728
- for (const n of state.nodes) {
729
- if (n.x < minX)
730
- minX = n.x;
731
- if (n.y < minY)
732
- minY = n.y;
733
- if (n.x > maxX)
734
- maxX = n.x;
735
- if (n.y > maxY)
736
- maxY = n.y;
737
- }
738
- camera.x = (minX + maxX) / 2 - canvas.clientWidth / 2;
739
- camera.y = (minY + maxY) / 2 - canvas.clientHeight / 2;
740
- }
741
- render();
881
+ fitToNodes();
742
882
  },
743
883
  panBy(dx, dy) {
744
884
  camera.x += dx / camera.scale;
@@ -804,26 +944,15 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
804
944
  alpha = 1;
805
945
  selectedNodeIds = new Set(seedNodeIds);
806
946
  filteredNodeIds = null;
807
- // Center camera
947
+ // Start simulation, then center after layout settles
808
948
  camera = { x: 0, y: 0, scale: 1 };
809
- if (state.nodes.length > 0) {
810
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
811
- for (const n of state.nodes) {
812
- if (n.x < minX)
813
- minX = n.x;
814
- if (n.y < minY)
815
- minY = n.y;
816
- if (n.x > maxX)
817
- maxX = n.x;
818
- if (n.y > maxY)
819
- maxY = n.y;
820
- }
821
- const cx = (minX + maxX) / 2;
822
- const cy = (minY + maxY) / 2;
823
- camera.x = cx - canvas.clientWidth / 2;
824
- camera.y = cy - canvas.clientHeight / 2;
825
- }
826
949
  simulate();
950
+ // Center + fit after physics settle
951
+ setTimeout(() => {
952
+ if (!state || !focusSeedIds)
953
+ return;
954
+ fitToNodes();
955
+ }, 300);
827
956
  onFocusChange?.({
828
957
  seedNodeIds,
829
958
  hops,
@@ -856,6 +985,83 @@ export function initCanvas(container, onNodeClick, onFocusChange, config) {
856
985
  totalNodes: state.nodes.length,
857
986
  };
858
987
  },
988
+ findPath(sourceId, targetId) {
989
+ if (!state)
990
+ return null;
991
+ const visited = new Set([sourceId]);
992
+ const queue = [
993
+ { nodeId: sourceId, path: [sourceId], edges: [] }
994
+ ];
995
+ while (queue.length > 0) {
996
+ const { nodeId, path, edges } = queue.shift();
997
+ if (nodeId === targetId)
998
+ return { nodeIds: path, edgeIds: edges };
999
+ for (const edge of state.edges) {
1000
+ let neighbor = null;
1001
+ if (edge.sourceId === nodeId)
1002
+ neighbor = edge.targetId;
1003
+ else if (edge.targetId === nodeId)
1004
+ neighbor = edge.sourceId;
1005
+ if (neighbor && !visited.has(neighbor)) {
1006
+ visited.add(neighbor);
1007
+ const fullEdge = lastLoadedData?.edges.find(e => (e.sourceId === edge.sourceId && e.targetId === edge.targetId) ||
1008
+ (e.targetId === edge.sourceId && e.sourceId === edge.targetId));
1009
+ queue.push({
1010
+ nodeId: neighbor,
1011
+ path: [...path, neighbor],
1012
+ edges: [...edges, fullEdge?.id ?? ""]
1013
+ });
1014
+ }
1015
+ }
1016
+ }
1017
+ return null;
1018
+ },
1019
+ setHighlightedPath(nodeIds, edgeIds) {
1020
+ if (nodeIds && edgeIds) {
1021
+ highlightedPath = { nodeIds: new Set(nodeIds), edgeIds: new Set(edgeIds) };
1022
+ }
1023
+ else {
1024
+ highlightedPath = null;
1025
+ }
1026
+ render();
1027
+ },
1028
+ clearHighlightedPath() {
1029
+ highlightedPath = null;
1030
+ render();
1031
+ },
1032
+ setWalkMode(enabled) {
1033
+ walkMode = enabled;
1034
+ if (enabled) {
1035
+ walkTrail = focusSeedIds ? [...focusSeedIds] : [...selectedNodeIds];
1036
+ if (!walkAnimFrame)
1037
+ walkAnimFrame = requestAnimationFrame(walkAnimate);
1038
+ }
1039
+ else {
1040
+ walkTrail = [];
1041
+ if (walkAnimFrame) {
1042
+ cancelAnimationFrame(walkAnimFrame);
1043
+ walkAnimFrame = 0;
1044
+ }
1045
+ }
1046
+ render();
1047
+ },
1048
+ getWalkMode() {
1049
+ return walkMode;
1050
+ },
1051
+ getWalkTrail() {
1052
+ return [...walkTrail];
1053
+ },
1054
+ getFilteredNodeIds() {
1055
+ return filteredNodeIds;
1056
+ },
1057
+ removeFromWalkTrail(nodeId) {
1058
+ walkTrail = walkTrail.filter((id) => id !== nodeId);
1059
+ render();
1060
+ },
1061
+ /** Hit-test a screen coordinate against nodes. Returns the node or null. */
1062
+ nodeAtScreen(sx, sy) {
1063
+ return nodeAtScreen(sx, sy);
1064
+ },
859
1065
  /** Get all node IDs in the current layout (subgraph if focused, full graph otherwise). Seed nodes first. */
860
1066
  getNodeIds() {
861
1067
  if (!state)
package/dist/config.js CHANGED
@@ -23,6 +23,7 @@ export function loadViewerConfig() {
23
23
  layout: { ...defaultConfig.layout, ...(user.layout ?? {}) },
24
24
  navigation: { ...defaultConfig.navigation, ...(user.navigation ?? {}) },
25
25
  lod: { ...defaultConfig.lod, ...(user.lod ?? {}) },
26
+ walk: { ...defaultConfig.walk, ...(user.walk ?? {}) },
26
27
  limits: { ...defaultConfig.limits, ...(user.limits ?? {}) },
27
28
  };
28
29
  }
@@ -0,0 +1,13 @@
1
+ export interface ContextMenuCallbacks {
2
+ onStar: (nodeId: string) => void;
3
+ onFocusNode: (nodeId: string) => void;
4
+ onExploreInBranch: (nodeId: string) => void;
5
+ onCopyId: (nodeId: string) => void;
6
+ onExpand?: (nodeId: string) => void;
7
+ onExplainPath?: (nodeId: string) => void;
8
+ onEnrich?: (nodeId: string) => void;
9
+ }
10
+ export declare function initContextMenu(container: HTMLElement, callbacks: ContextMenuCallbacks): {
11
+ show: (nodeId: string, nodeLabel: string, isStarred: boolean, screenX: number, screenY: number) => void;
12
+ hide: () => void;
13
+ };
@@ -0,0 +1,64 @@
1
+ export function initContextMenu(container, callbacks) {
2
+ let menuEl = null;
3
+ function show(nodeId, nodeLabel, isStarred, screenX, screenY) {
4
+ hide();
5
+ menuEl = document.createElement("div");
6
+ menuEl.className = "context-menu";
7
+ menuEl.style.left = `${screenX}px`;
8
+ menuEl.style.top = `${screenY}px`;
9
+ const items = [
10
+ { label: isStarred ? "\u2605 Unstar" : "\u2606 Star", action: () => callbacks.onStar(nodeId), premium: false },
11
+ { label: "\u25CE Focus on node", action: () => callbacks.onFocusNode(nodeId), premium: false },
12
+ { label: "\u2442 Explore in branch", action: () => callbacks.onExploreInBranch(nodeId), premium: false },
13
+ { label: "\u2398 Copy ID", action: () => callbacks.onCopyId(nodeId), premium: false },
14
+ ];
15
+ if (callbacks.onExpand)
16
+ items.push({ label: "\u2295 Expand node", action: () => callbacks.onExpand(nodeId), premium: true });
17
+ if (callbacks.onExplainPath)
18
+ items.push({ label: "\u2194 Explain path to\u2026", action: () => callbacks.onExplainPath(nodeId), premium: true });
19
+ if (callbacks.onEnrich)
20
+ items.push({ label: "\u2261 Enrich from web", action: () => callbacks.onEnrich(nodeId), premium: true });
21
+ let addedSep = false;
22
+ for (const item of items) {
23
+ if (!addedSep && item.premium) {
24
+ const sep = document.createElement("div");
25
+ sep.className = "context-menu-separator";
26
+ menuEl.appendChild(sep);
27
+ addedSep = true;
28
+ }
29
+ const row = document.createElement("div");
30
+ row.className = "context-menu-item";
31
+ row.textContent = item.label;
32
+ row.addEventListener("click", () => {
33
+ item.action();
34
+ hide();
35
+ });
36
+ menuEl.appendChild(row);
37
+ }
38
+ container.appendChild(menuEl);
39
+ // Clamp to viewport
40
+ const rect = menuEl.getBoundingClientRect();
41
+ if (rect.right > window.innerWidth) {
42
+ menuEl.style.left = `${screenX - rect.width}px`;
43
+ }
44
+ if (rect.bottom > window.innerHeight) {
45
+ menuEl.style.top = `${screenY - rect.height}px`;
46
+ }
47
+ // Close on outside click (delayed to not catch the opening right-click)
48
+ setTimeout(() => document.addEventListener("click", hide), 0);
49
+ document.addEventListener("keydown", handleEscape);
50
+ }
51
+ function hide() {
52
+ if (menuEl) {
53
+ menuEl.remove();
54
+ menuEl = null;
55
+ }
56
+ document.removeEventListener("click", hide);
57
+ document.removeEventListener("keydown", handleEscape);
58
+ }
59
+ function handleEscape(e) {
60
+ if (e.key === "Escape")
61
+ hide();
62
+ }
63
+ return { show, hide };
64
+ }
@@ -29,7 +29,9 @@
29
29
  "spacingIncrease": "]",
30
30
  "clusteringDecrease": "{",
31
31
  "clusteringIncrease": "}",
32
- "toggleSidebar": "Tab"
32
+ "toggleSidebar": "Tab",
33
+ "walkMode": "w",
34
+ "walkIsolate": "i"
33
35
  },
34
36
  "display": {
35
37
  "edges": true,
@@ -57,6 +59,9 @@
57
59
  "smallNodes": 0.2,
58
60
  "hideArrows": 0.15
59
61
  },
62
+ "walk": {
63
+ "pulseSpeed": 0.02
64
+ },
60
65
  "limits": {
61
66
  "maxSearchResults": 8,
62
67
  "maxQualityItems": 5,
@@ -1,4 +1,4 @@
1
- export type KeybindingAction = "search" | "searchAlt" | "undo" | "redo" | "help" | "escape" | "focus" | "toggleEdges" | "center" | "nextNode" | "prevNode" | "nextConnection" | "prevConnection" | "historyBack" | "historyForward" | "hopsIncrease" | "hopsDecrease" | "panLeft" | "panDown" | "panUp" | "panRight" | "panFastLeft" | "zoomOut" | "zoomIn" | "panFastRight" | "spacingDecrease" | "spacingIncrease" | "clusteringDecrease" | "clusteringIncrease" | "toggleSidebar";
1
+ export type KeybindingAction = "search" | "searchAlt" | "undo" | "redo" | "help" | "escape" | "focus" | "toggleEdges" | "center" | "nextNode" | "prevNode" | "nextConnection" | "prevConnection" | "historyBack" | "historyForward" | "hopsIncrease" | "hopsDecrease" | "panLeft" | "panDown" | "panUp" | "panRight" | "panFastLeft" | "zoomOut" | "zoomIn" | "panFastRight" | "spacingDecrease" | "spacingIncrease" | "clusteringDecrease" | "clusteringIncrease" | "toggleSidebar" | "walkMode" | "walkIsolate";
2
2
  export type KeybindingMap = Record<KeybindingAction, string>;
3
3
  /** Parse a binding string like "ctrl+shift+z" and check if it matches a KeyboardEvent. */
4
4
  export declare function matchKey(e: KeyboardEvent, binding: string): boolean;
@@ -63,5 +63,7 @@ export function actionDescriptions() {
63
63
  clusteringDecrease: "Decrease clustering",
64
64
  clusteringIncrease: "Increase clustering",
65
65
  toggleSidebar: "Toggle sidebar",
66
+ walkMode: "Toggle walk mode (in focus)",
67
+ walkIsolate: "Isolate walk trail nodes",
66
68
  };
67
69
  }