backpack-viewer 0.2.20 → 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/main.js CHANGED
@@ -1,4 +1,4 @@
1
- import { listOntologies, loadOntology, saveOntology, renameOntology, listBranches, createBranch, switchBranch, deleteBranch, listSnapshots, createSnapshot, rollbackSnapshot, } from "./api";
1
+ import { listOntologies, loadOntology, saveOntology, renameOntology, listBranches, createBranch, switchBranch, deleteBranch, listSnapshots, createSnapshot, rollbackSnapshot, listSnippets, saveSnippet, loadSnippet, deleteSnippet, } from "./api";
2
2
  import { initSidebar } from "./sidebar";
3
3
  import { initCanvas } from "./canvas";
4
4
  import { initInfoPanel } from "./info-panel";
@@ -9,6 +9,7 @@ import { initShortcuts } from "./shortcuts";
9
9
  import { initEmptyState } from "./empty-state";
10
10
  import { createHistory } from "./history";
11
11
  import { matchKey } from "./keybindings";
12
+ import { initContextMenu } from "./context-menu";
12
13
  import defaultConfig from "./default-config.json";
13
14
  import "./style.css";
14
15
  let activeOntology = "";
@@ -184,10 +185,21 @@ async function main() {
184
185
  exit.textContent = "\u00d7";
185
186
  exit.title = "Exit focus (Esc)";
186
187
  exit.addEventListener("click", () => toolsPane.clearFocusSet());
188
+ const walkBtn = document.createElement("button");
189
+ walkBtn.className = "walk-indicator";
190
+ if (canvas.getWalkMode())
191
+ walkBtn.classList.add("active");
192
+ walkBtn.textContent = "Walk";
193
+ walkBtn.title = "Toggle walk mode (W) — click nodes to traverse";
194
+ walkBtn.addEventListener("click", () => {
195
+ canvas.setWalkMode(!canvas.getWalkMode());
196
+ walkBtn.classList.toggle("active", canvas.getWalkMode());
197
+ });
187
198
  focusIndicator.appendChild(label);
188
199
  focusIndicator.appendChild(minus);
189
200
  focusIndicator.appendChild(hopsLabel);
190
201
  focusIndicator.appendChild(plus);
202
+ focusIndicator.appendChild(walkBtn);
191
203
  focusIndicator.appendChild(exit);
192
204
  }
193
205
  function removeFocusIndicator() {
@@ -196,8 +208,65 @@ async function main() {
196
208
  focusIndicator = null;
197
209
  }
198
210
  }
211
+ // --- Path bar ---
212
+ const pathBar = document.createElement("div");
213
+ pathBar.className = "path-bar hidden";
214
+ canvasContainer.appendChild(pathBar);
215
+ function showPathBar(path) {
216
+ pathBar.innerHTML = "";
217
+ if (!currentData)
218
+ return;
219
+ for (let i = 0; i < path.nodeIds.length; i++) {
220
+ const nodeId = path.nodeIds[i];
221
+ const node = currentData.nodes.find((n) => n.id === nodeId);
222
+ if (!node)
223
+ continue;
224
+ const label = Object.values(node.properties).find((v) => typeof v === "string") ?? node.id;
225
+ // Edge label before this node (except the first)
226
+ if (i > 0) {
227
+ const edgeId = path.edgeIds[i - 1];
228
+ const edge = currentData.edges.find((e) => e.id === edgeId);
229
+ const arrow = document.createElement("span");
230
+ arrow.className = "path-bar-edge";
231
+ arrow.textContent = edge ? `→ ${edge.type} →` : "→";
232
+ pathBar.appendChild(arrow);
233
+ }
234
+ const nodeBtn = document.createElement("span");
235
+ nodeBtn.className = "path-bar-node";
236
+ nodeBtn.textContent = label;
237
+ nodeBtn.addEventListener("click", () => canvas.panToNode(nodeId));
238
+ pathBar.appendChild(nodeBtn);
239
+ }
240
+ const closeBtn = document.createElement("button");
241
+ closeBtn.className = "path-bar-close";
242
+ closeBtn.textContent = "\u00d7";
243
+ closeBtn.addEventListener("click", hidePathBar);
244
+ pathBar.appendChild(closeBtn);
245
+ pathBar.classList.remove("hidden");
246
+ }
247
+ function hidePathBar() {
248
+ pathBar.classList.add("hidden");
249
+ pathBar.innerHTML = "";
250
+ canvas.clearHighlightedPath();
251
+ }
199
252
  canvas = initCanvas(canvasContainer, (nodeIds) => {
200
253
  currentSelection = nodeIds ?? [];
254
+ // Don't touch the path bar when walk mode is active — syncWalkTrail manages it
255
+ if (!canvas.getWalkMode()) {
256
+ if (nodeIds && nodeIds.length === 2) {
257
+ const path = canvas.findPath(nodeIds[0], nodeIds[1]);
258
+ if (path && path.nodeIds.length > 0) {
259
+ canvas.setHighlightedPath(path.nodeIds, path.edgeIds);
260
+ showPathBar(path);
261
+ }
262
+ else {
263
+ hidePathBar();
264
+ }
265
+ }
266
+ else {
267
+ hidePathBar();
268
+ }
269
+ }
201
270
  if (nodeIds && nodeIds.length > 0 && currentData) {
202
271
  infoPanel.show(nodeIds, currentData);
203
272
  if (mobileQuery.matches)
@@ -217,14 +286,16 @@ async function main() {
217
286
  topLeft.appendChild(focusIndicator);
218
287
  updateUrl(activeOntology, focus.seedNodeIds);
219
288
  infoPanel.setFocusDisabled(focus.hops === 0);
289
+ syncWalkTrail();
220
290
  }
221
291
  else {
222
292
  removeFocusIndicator();
223
293
  infoPanel.setFocusDisabled(false);
224
294
  if (activeOntology)
225
295
  updateUrl(activeOntology);
296
+ syncWalkTrail();
226
297
  }
227
- }, { lod: cfg.lod, navigation: cfg.navigation });
298
+ }, { lod: cfg.lod, navigation: cfg.navigation, walk: cfg.walk });
228
299
  const search = initSearch(canvasContainer, {
229
300
  maxResults: cfg.limits.maxSearchResults,
230
301
  debounceMs: cfg.limits.searchDebounceMs,
@@ -248,6 +319,31 @@ async function main() {
248
319
  if (currentData)
249
320
  infoPanel.show([nodeId], currentData);
250
321
  },
322
+ onWalkTrailRemove(nodeId) {
323
+ canvas.removeFromWalkTrail(nodeId);
324
+ syncWalkTrail();
325
+ },
326
+ onWalkIsolate() {
327
+ if (!currentData)
328
+ return;
329
+ const trail = canvas.getWalkTrail();
330
+ if (trail.length === 0)
331
+ return;
332
+ canvas.enterFocus(trail, 0);
333
+ },
334
+ async onWalkSaveSnippet(label) {
335
+ if (!activeOntology || !currentData)
336
+ return;
337
+ const trail = canvas.getWalkTrail();
338
+ if (trail.length < 2)
339
+ return;
340
+ const nodeSet = new Set(trail);
341
+ const edgeIds = currentData.edges
342
+ .filter((e) => nodeSet.has(e.sourceId) && nodeSet.has(e.targetId))
343
+ .map((e) => e.id);
344
+ await saveSnippet(activeOntology, label, trail, edgeIds);
345
+ await refreshSnippets(activeOntology);
346
+ },
251
347
  onFocusChange(seedNodeIds) {
252
348
  if (seedNodeIds && seedNodeIds.length > 0) {
253
349
  canvas.enterFocus(seedNodeIds, 0);
@@ -399,7 +495,53 @@ async function main() {
399
495
  await deleteBranch(graphName, branchName);
400
496
  await refreshBranches(graphName);
401
497
  },
498
+ onSnippetLoad: async (graphName, snippetId) => {
499
+ const snippet = await loadSnippet(graphName, snippetId);
500
+ if (snippet?.nodeIds?.length > 0) {
501
+ canvas.enterFocus(snippet.nodeIds, 0);
502
+ }
503
+ },
504
+ onSnippetDelete: async (graphName, snippetId) => {
505
+ await deleteSnippet(graphName, snippetId);
506
+ await refreshSnippets(graphName);
507
+ },
402
508
  });
509
+ function syncWalkTrail() {
510
+ const trail = canvas.getWalkTrail();
511
+ if (!currentData || trail.length === 0) {
512
+ toolsPane.setWalkTrail([]);
513
+ hidePathBar();
514
+ return;
515
+ }
516
+ const edgeIds = [];
517
+ const items = trail.map((id, i) => {
518
+ const node = currentData.nodes.find((n) => n.id === id);
519
+ let edgeType;
520
+ if (i > 0) {
521
+ const prevId = trail[i - 1];
522
+ const edge = currentData.edges.find((e) => (e.sourceId === prevId && e.targetId === id) ||
523
+ (e.targetId === prevId && e.sourceId === id));
524
+ edgeType = edge?.type;
525
+ if (edge)
526
+ edgeIds.push(edge.id);
527
+ }
528
+ return {
529
+ id,
530
+ label: node ? (Object.values(node.properties).find((v) => typeof v === "string") ?? node.id) : id,
531
+ type: node?.type ?? "?",
532
+ edgeType,
533
+ };
534
+ });
535
+ toolsPane.setWalkTrail(items);
536
+ // Show path bar for walk trail
537
+ if (trail.length >= 2) {
538
+ canvas.setHighlightedPath(trail, edgeIds);
539
+ showPathBar({ nodeIds: trail, edgeIds });
540
+ }
541
+ else {
542
+ hidePathBar();
543
+ }
544
+ }
403
545
  async function refreshBranches(graphName) {
404
546
  const branches = await listBranches(graphName);
405
547
  const active = branches.find((b) => b.active);
@@ -411,8 +553,64 @@ async function main() {
411
553
  const snaps = await listSnapshots(graphName);
412
554
  toolsPane.setSnapshots(snaps);
413
555
  }
556
+ async function refreshSnippets(graphName) {
557
+ const snips = await listSnippets(graphName);
558
+ sidebar.setSnippets(graphName, snips);
559
+ }
560
+ // Insert sidebar expand button into top-left bar (before tools toggle)
561
+ topLeft.insertBefore(sidebar.expandBtn, topLeft.firstChild);
414
562
  const shortcuts = initShortcuts(canvasContainer, bindings);
415
563
  const emptyState = initEmptyState(canvasContainer);
564
+ // Context menu (right-click on nodes)
565
+ const contextMenu = initContextMenu(canvasContainer, {
566
+ onStar(nodeId) {
567
+ if (!currentData)
568
+ return;
569
+ const node = currentData.nodes.find((n) => n.id === nodeId);
570
+ if (!node)
571
+ return;
572
+ const starred = node.properties._starred === true;
573
+ node.properties._starred = !starred;
574
+ saveOntology(activeOntology, currentData);
575
+ canvas.loadGraph(currentData);
576
+ },
577
+ onFocusNode(nodeId) {
578
+ toolsPane.addToFocusSet([nodeId]);
579
+ },
580
+ onExploreInBranch(nodeId) {
581
+ // Create a branch and enter focus
582
+ if (activeOntology) {
583
+ const branchName = `explore-${nodeId.slice(0, 8)}`;
584
+ createBranch(activeOntology, branchName).then(() => {
585
+ switchBranch(activeOntology, branchName).then(() => {
586
+ canvas.enterFocus([nodeId], 1);
587
+ });
588
+ });
589
+ }
590
+ },
591
+ onCopyId(nodeId) {
592
+ navigator.clipboard.writeText(nodeId);
593
+ },
594
+ });
595
+ // Right-click handler for context menu
596
+ canvasContainer.addEventListener("contextmenu", (e) => {
597
+ e.preventDefault();
598
+ const canvasEl = canvasContainer.querySelector("canvas");
599
+ if (!canvasEl || !currentData)
600
+ return;
601
+ const rect = canvasEl.getBoundingClientRect();
602
+ const x = e.clientX - rect.left;
603
+ const y = e.clientY - rect.top;
604
+ const hit = canvas.nodeAtScreen(x, y);
605
+ if (!hit)
606
+ return;
607
+ const node = currentData.nodes.find((n) => n.id === hit.id);
608
+ if (!node)
609
+ return;
610
+ const label = Object.values(node.properties).find((v) => typeof v === "string") ?? node.id;
611
+ const isStarred = node.properties._starred === true;
612
+ contextMenu.show(node.id, label, isStarred, e.clientX - rect.left, e.clientY - rect.top);
613
+ });
416
614
  // Apply display defaults from config
417
615
  if (!cfg.display.edges)
418
616
  canvas.setEdges(false);
@@ -481,6 +679,7 @@ async function main() {
481
679
  // Load branches and snapshots
482
680
  await refreshBranches(name);
483
681
  await refreshSnapshots(name);
682
+ await refreshSnippets(name);
484
683
  // Restore focus mode if requested
485
684
  if (focusSeedIds?.length && currentData) {
486
685
  const validFocus = focusSeedIds.filter((id) => currentData.nodes.some((n) => n.id === id));
@@ -586,6 +785,26 @@ async function main() {
586
785
  clusteringIncrease() { const p = getLayoutParams(); setLayoutParams({ clusterStrength: Math.min(1, p.clusterStrength + 0.03) }); canvas.reheat(); },
587
786
  help() { shortcuts.toggle(); },
588
787
  toggleSidebar() { sidebar.toggle(); },
788
+ walkIsolate() {
789
+ if (!currentData)
790
+ return;
791
+ const trail = canvas.getWalkTrail();
792
+ if (trail.length === 0)
793
+ return;
794
+ // Extract a subgraph of only the trail nodes and edges between them, re-layout as a fresh graph
795
+ canvas.enterFocus(trail, 0);
796
+ },
797
+ walkMode() {
798
+ // If not in focus mode, enter focus on current selection first
799
+ if (!canvas.isFocused() && currentSelection.length > 0) {
800
+ toolsPane.addToFocusSet(currentSelection);
801
+ }
802
+ canvas.setWalkMode(!canvas.getWalkMode());
803
+ const walkBtn = canvasContainer.querySelector(".walk-indicator");
804
+ if (walkBtn)
805
+ walkBtn.classList.toggle("active", canvas.getWalkMode());
806
+ syncWalkTrail();
807
+ },
589
808
  escape() { if (canvas.isFocused()) {
590
809
  toolsPane.clearFocusSet();
591
810
  }
package/dist/shortcuts.js CHANGED
@@ -18,6 +18,8 @@ const ACTION_ORDER = [
18
18
  "spacingDecrease", "spacingIncrease",
19
19
  "clusteringDecrease", "clusteringIncrease",
20
20
  "toggleSidebar",
21
+ "walkMode",
22
+ "walkIsolate",
21
23
  "escape",
22
24
  ];
23
25
  /** Format a binding string for display (e.g. "ctrl+z" → "Ctrl+Z"). */
package/dist/sidebar.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface SidebarCallbacks {
5
5
  onBranchSwitch?: (graphName: string, branchName: string) => void;
6
6
  onBranchCreate?: (graphName: string, branchName: string) => void;
7
7
  onBranchDelete?: (graphName: string, branchName: string) => void;
8
+ onSnippetLoad?: (graphName: string, snippetId: string) => void;
9
+ onSnippetDelete?: (graphName: string, snippetId: string) => void;
8
10
  }
9
11
  export declare function initSidebar(container: HTMLElement, onSelectOrCallbacks: ((name: string) => void) | SidebarCallbacks): {
10
12
  setSummaries(summaries: LearningGraphSummary[]): void;
@@ -13,5 +15,11 @@ export declare function initSidebar(container: HTMLElement, onSelectOrCallbacks:
13
15
  name: string;
14
16
  active: boolean;
15
17
  }[]): void;
18
+ setSnippets(graphName: string, snippets: {
19
+ id: string;
20
+ label: string;
21
+ nodeCount: number;
22
+ }[]): void;
16
23
  toggle: () => void;
24
+ expandBtn: HTMLButtonElement;
17
25
  };
package/dist/sidebar.js CHANGED
@@ -27,9 +27,7 @@ export function initSidebar(container, onSelectOrCallbacks) {
27
27
  function toggleSidebar() {
28
28
  collapsed = !collapsed;
29
29
  container.classList.toggle("sidebar-collapsed", collapsed);
30
- collapseBtn.innerHTML = collapsed
31
- ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 7 18 12 13 17"/><polyline points="6 7 11 12 6 17"/></svg>'
32
- : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></svg>';
30
+ expandBtn.classList.toggle("hidden", !collapsed);
33
31
  }
34
32
  collapseBtn.addEventListener("click", toggleSidebar);
35
33
  const headingRow = document.createElement("div");
@@ -37,6 +35,12 @@ export function initSidebar(container, onSelectOrCallbacks) {
37
35
  headingRow.appendChild(heading);
38
36
  headingRow.appendChild(collapseBtn);
39
37
  container.appendChild(headingRow);
38
+ // Expand button — inserted into the canvas top-left bar when sidebar is collapsed
39
+ const expandBtn = document.createElement("button");
40
+ expandBtn.className = "tools-pane-toggle hidden";
41
+ expandBtn.title = "Show sidebar (Tab)";
42
+ expandBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 7 18 12 13 17"/><polyline points="6 7 11 12 6 17"/></svg>';
43
+ expandBtn.addEventListener("click", toggleSidebar);
40
44
  container.appendChild(input);
41
45
  container.appendChild(list);
42
46
  container.appendChild(footer);
@@ -140,7 +144,44 @@ export function initSidebar(container, onSelectOrCallbacks) {
140
144
  });
141
145
  }
142
146
  },
147
+ setSnippets(graphName, snippets) {
148
+ // Find the graph's list item and add/update snippet pills
149
+ const li = items.find((el) => el.dataset.name === graphName);
150
+ if (!li)
151
+ return;
152
+ // Remove existing snippet list
153
+ li.querySelector(".sidebar-snippets")?.remove();
154
+ if (snippets.length === 0)
155
+ return;
156
+ const snippetList = document.createElement("div");
157
+ snippetList.className = "sidebar-snippets";
158
+ for (const s of snippets) {
159
+ const row = document.createElement("div");
160
+ row.className = "sidebar-snippet";
161
+ const label = document.createElement("span");
162
+ label.className = "sidebar-snippet-label";
163
+ label.textContent = `◆ ${s.label}`;
164
+ label.title = `${s.nodeCount} nodes — click to load`;
165
+ const del = document.createElement("button");
166
+ del.className = "sidebar-snippet-delete";
167
+ del.textContent = "\u00d7";
168
+ del.title = "Delete snippet";
169
+ del.addEventListener("click", (e) => {
170
+ e.stopPropagation();
171
+ cbs.onSnippetDelete?.(graphName, s.id);
172
+ });
173
+ row.appendChild(label);
174
+ row.appendChild(del);
175
+ row.addEventListener("click", (e) => {
176
+ e.stopPropagation();
177
+ cbs.onSnippetLoad?.(graphName, s.id);
178
+ });
179
+ snippetList.appendChild(row);
180
+ }
181
+ li.appendChild(snippetList);
182
+ },
143
183
  toggle: toggleSidebar,
184
+ expandBtn,
144
185
  };
145
186
  function showBranchPicker(graphName, anchor, branches) {
146
187
  // Remove existing picker
package/dist/style.css CHANGED
@@ -45,6 +45,7 @@
45
45
  --canvas-type-badge-dim: rgba(115, 115, 115, 0.15);
46
46
  --canvas-selection-border: #d4d4d4;
47
47
  --canvas-node-border: rgba(255, 255, 255, 0.15);
48
+ --canvas-walk-edge: #e8d5c4;
48
49
  }
49
50
 
50
51
  [data-theme="light"] {
@@ -86,6 +87,7 @@
86
87
  --canvas-type-badge-dim: rgba(87, 83, 78, 0.15);
87
88
  --canvas-selection-border: #292524;
88
89
  --canvas-node-border: rgba(0, 0, 0, 0.1);
90
+ --canvas-walk-edge: #1a1a1a;
89
91
  }
90
92
 
91
93
  body {
@@ -309,6 +311,46 @@ body {
309
311
  padding: 0 4px;
310
312
  }
311
313
 
314
+ .sidebar-snippets {
315
+ margin-top: 4px;
316
+ padding-left: 8px;
317
+ }
318
+
319
+ .sidebar-snippet {
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: space-between;
323
+ padding: 2px 4px;
324
+ border-radius: 4px;
325
+ cursor: pointer;
326
+ }
327
+
328
+ .sidebar-snippet:hover {
329
+ background: var(--bg-hover);
330
+ }
331
+
332
+ .sidebar-snippet-label {
333
+ font-size: 10px;
334
+ color: var(--text-dim);
335
+ overflow: hidden;
336
+ text-overflow: ellipsis;
337
+ white-space: nowrap;
338
+ }
339
+
340
+ .sidebar-snippet-delete {
341
+ background: none;
342
+ border: none;
343
+ color: var(--text-dim);
344
+ font-size: 12px;
345
+ cursor: pointer;
346
+ padding: 0 2px;
347
+ opacity: 0;
348
+ }
349
+
350
+ .sidebar-snippet:hover .sidebar-snippet-delete {
351
+ opacity: 1;
352
+ }
353
+
312
354
  .branch-picker-delete:hover {
313
355
  color: var(--danger, #e55);
314
356
  }
@@ -1841,3 +1883,125 @@ body {
1841
1883
  opacity: 1;
1842
1884
  transform: translateX(-50%) translateY(0);
1843
1885
  }
1886
+
1887
+ /* --- Context Menu --- */
1888
+
1889
+ .context-menu {
1890
+ position: absolute;
1891
+ z-index: 100;
1892
+ background: var(--bg-surface);
1893
+ border: 1px solid var(--border);
1894
+ border-radius: 8px;
1895
+ padding: 4px;
1896
+ min-width: 180px;
1897
+ box-shadow: 0 8px 24px var(--shadow);
1898
+ }
1899
+
1900
+ .context-menu-item {
1901
+ padding: 6px 12px;
1902
+ font-size: 12px;
1903
+ color: var(--text);
1904
+ border-radius: 4px;
1905
+ cursor: pointer;
1906
+ white-space: nowrap;
1907
+ }
1908
+
1909
+ .context-menu-item:hover {
1910
+ background: var(--bg-hover);
1911
+ }
1912
+
1913
+ .context-menu-separator {
1914
+ height: 1px;
1915
+ background: var(--border);
1916
+ margin: 4px 8px;
1917
+ }
1918
+
1919
+ /* --- Path Bar --- */
1920
+
1921
+ .path-bar {
1922
+ position: absolute;
1923
+ bottom: 16px;
1924
+ left: 50%;
1925
+ transform: translateX(-50%);
1926
+ z-index: 25;
1927
+ display: flex;
1928
+ align-items: center;
1929
+ gap: 4px;
1930
+ background: var(--bg-surface);
1931
+ border: 1px solid var(--border);
1932
+ border-radius: 8px;
1933
+ padding: 6px 12px;
1934
+ box-shadow: 0 4px 16px var(--shadow);
1935
+ max-width: 80%;
1936
+ overflow-x: auto;
1937
+ }
1938
+
1939
+ .path-bar.hidden {
1940
+ display: none;
1941
+ }
1942
+
1943
+ .path-bar-node {
1944
+ font-size: 11px;
1945
+ color: var(--text);
1946
+ padding: 2px 8px;
1947
+ border-radius: 4px;
1948
+ cursor: pointer;
1949
+ white-space: nowrap;
1950
+ flex-shrink: 0;
1951
+ }
1952
+
1953
+ .path-bar-node:hover {
1954
+ background: var(--bg-hover);
1955
+ }
1956
+
1957
+ .path-bar-edge {
1958
+ font-size: 9px;
1959
+ color: var(--text-dim);
1960
+ white-space: nowrap;
1961
+ flex-shrink: 0;
1962
+ }
1963
+
1964
+ .path-bar-close {
1965
+ background: none;
1966
+ border: none;
1967
+ color: var(--text-dim);
1968
+ font-size: 14px;
1969
+ cursor: pointer;
1970
+ padding: 0 4px;
1971
+ margin-left: 8px;
1972
+ flex-shrink: 0;
1973
+ }
1974
+
1975
+ .path-bar-close:hover {
1976
+ color: var(--text);
1977
+ }
1978
+
1979
+ /* --- Walk Mode Indicator --- */
1980
+
1981
+ .walk-trail-edge {
1982
+ font-size: 9px;
1983
+ color: var(--text-dim);
1984
+ padding: 1px 0 1px 24px;
1985
+ opacity: 0.7;
1986
+ }
1987
+
1988
+ .walk-indicator {
1989
+ font-size: 10px;
1990
+ color: var(--accent);
1991
+ padding: 2px 8px;
1992
+ border: 1px solid rgba(212, 162, 127, 0.4);
1993
+ border-radius: 4px;
1994
+ cursor: pointer;
1995
+ opacity: 0.4;
1996
+ }
1997
+
1998
+ .walk-indicator.active {
1999
+ opacity: 1;
2000
+ background: rgba(212, 162, 127, 0.15);
2001
+ animation: walk-strobe 2s ease-in-out infinite;
2002
+ }
2003
+
2004
+ @keyframes walk-strobe {
2005
+ 0%, 100% { opacity: 0.6; border-color: rgba(212, 162, 127, 0.3); }
2006
+ 50% { opacity: 1; border-color: rgba(212, 162, 127, 0.8); }
2007
+ }
@@ -3,6 +3,9 @@ interface ToolsPaneCallbacks {
3
3
  onFilterByType: (type: string | null) => void;
4
4
  onNavigateToNode: (nodeId: string) => void;
5
5
  onFocusChange: (seedNodeIds: string[] | null) => void;
6
+ onWalkTrailRemove?: (nodeId: string) => void;
7
+ onWalkIsolate?: () => void;
8
+ onWalkSaveSnippet?: (label: string) => void;
6
9
  onRenameNodeType: (oldType: string, newType: string) => void;
7
10
  onRenameEdgeType: (oldType: string, newType: string) => void;
8
11
  onToggleEdgeLabels: (visible: boolean) => void;
@@ -27,5 +30,11 @@ export declare function initToolsPane(container: HTMLElement, callbacks: ToolsPa
27
30
  edgeCount: number;
28
31
  label?: string;
29
32
  }[]): void;
33
+ setWalkTrail(trail: {
34
+ id: string;
35
+ label: string;
36
+ type: string;
37
+ edgeType?: string;
38
+ }[]): void;
30
39
  };
31
40
  export {};