backpack-viewer 0.2.20 → 0.3.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/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 {
@@ -235,6 +237,54 @@ body {
235
237
  opacity: 0.7;
236
238
  }
237
239
 
240
+ /* --- Remote graphs section in sidebar --- */
241
+
242
+ .sidebar-section-heading {
243
+ font-size: 10px;
244
+ font-weight: 600;
245
+ color: var(--text-dim);
246
+ letter-spacing: 0.08em;
247
+ margin: 16px 12px 6px;
248
+ }
249
+
250
+ .remote-list {
251
+ list-style: none;
252
+ padding: 0;
253
+ margin: 0;
254
+ }
255
+
256
+ .ontology-item-remote .name {
257
+ display: inline;
258
+ }
259
+
260
+ .remote-name-row {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 8px;
264
+ }
265
+
266
+ .remote-badge {
267
+ font-size: 9px;
268
+ font-weight: 600;
269
+ color: var(--text-dim);
270
+ background: var(--bg-hover);
271
+ padding: 1px 6px;
272
+ border-radius: 4px;
273
+ text-transform: uppercase;
274
+ letter-spacing: 0.04em;
275
+ border: 1px solid var(--border);
276
+ }
277
+
278
+ .remote-source {
279
+ display: block;
280
+ font-size: 10px;
281
+ color: var(--text-dim);
282
+ margin-top: 1px;
283
+ overflow: hidden;
284
+ text-overflow: ellipsis;
285
+ white-space: nowrap;
286
+ }
287
+
238
288
  .sidebar-edit-btn:hover {
239
289
  opacity: 1 !important;
240
290
  color: var(--text);
@@ -265,6 +315,22 @@ body {
265
315
  opacity: 1;
266
316
  }
267
317
 
318
+ .sidebar-lock-badge {
319
+ font-size: 10px;
320
+ color: #c08c00;
321
+ display: none;
322
+ margin-top: 2px;
323
+ }
324
+
325
+ .sidebar-lock-badge.active {
326
+ display: block;
327
+ }
328
+
329
+ .sidebar-lock-badge.active::before {
330
+ content: "● ";
331
+ color: #c08c00;
332
+ }
333
+
268
334
  .branch-picker {
269
335
  background: var(--bg-surface);
270
336
  border: 1px solid var(--border);
@@ -309,6 +375,46 @@ body {
309
375
  padding: 0 4px;
310
376
  }
311
377
 
378
+ .sidebar-snippets {
379
+ margin-top: 4px;
380
+ padding-left: 8px;
381
+ }
382
+
383
+ .sidebar-snippet {
384
+ display: flex;
385
+ align-items: center;
386
+ justify-content: space-between;
387
+ padding: 2px 4px;
388
+ border-radius: 4px;
389
+ cursor: pointer;
390
+ }
391
+
392
+ .sidebar-snippet:hover {
393
+ background: var(--bg-hover);
394
+ }
395
+
396
+ .sidebar-snippet-label {
397
+ font-size: 10px;
398
+ color: var(--text-dim);
399
+ overflow: hidden;
400
+ text-overflow: ellipsis;
401
+ white-space: nowrap;
402
+ }
403
+
404
+ .sidebar-snippet-delete {
405
+ background: none;
406
+ border: none;
407
+ color: var(--text-dim);
408
+ font-size: 12px;
409
+ cursor: pointer;
410
+ padding: 0 2px;
411
+ opacity: 0;
412
+ }
413
+
414
+ .sidebar-snippet:hover .sidebar-snippet-delete {
415
+ opacity: 1;
416
+ }
417
+
312
418
  .branch-picker-delete:hover {
313
419
  color: var(--danger, #e55);
314
420
  }
@@ -501,6 +607,23 @@ body {
501
607
  background: var(--bg-hover);
502
608
  }
503
609
 
610
+ /* --- Node Tooltip --- */
611
+
612
+ .node-tooltip {
613
+ position: absolute;
614
+ pointer-events: none;
615
+ background: var(--bg);
616
+ color: var(--text);
617
+ border: 1px solid var(--border);
618
+ border-radius: 6px;
619
+ padding: 4px 8px;
620
+ font-size: 12px;
621
+ white-space: nowrap;
622
+ z-index: 20;
623
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
624
+ opacity: 0.95;
625
+ }
626
+
504
627
  /* --- Canvas --- */
505
628
 
506
629
  #canvas-container {
@@ -863,6 +986,25 @@ body {
863
986
  margin-bottom: 8px;
864
987
  }
865
988
 
989
+ .info-badge-row {
990
+ display: flex;
991
+ flex-wrap: wrap;
992
+ gap: 4px;
993
+ margin-top: 6px;
994
+ }
995
+
996
+ .info-empty-message {
997
+ font-size: 12px;
998
+ color: var(--text-dim);
999
+ }
1000
+
1001
+ .share-list-message {
1002
+ font-size: 13px;
1003
+ color: var(--text-dim);
1004
+ text-align: center;
1005
+ padding: 12px;
1006
+ }
1007
+
866
1008
  .info-label {
867
1009
  font-size: 18px;
868
1010
  font-weight: 600;
@@ -1282,6 +1424,41 @@ body {
1282
1424
  color: var(--text-dim);
1283
1425
  }
1284
1426
 
1427
+ .tools-pane-token-card {
1428
+ padding: 8px 10px;
1429
+ margin-bottom: 10px;
1430
+ border: 1px solid var(--border);
1431
+ border-radius: 6px;
1432
+ background: var(--bg-hover);
1433
+ font-size: 11px;
1434
+ }
1435
+
1436
+ .token-card-label {
1437
+ font-weight: 600;
1438
+ color: var(--text);
1439
+ margin-bottom: 4px;
1440
+ }
1441
+
1442
+ .token-card-stat {
1443
+ color: var(--text-muted);
1444
+ margin-bottom: 4px;
1445
+ }
1446
+
1447
+ .token-card-bar {
1448
+ height: 4px;
1449
+ background: var(--border);
1450
+ border-radius: 2px;
1451
+ margin: 6px 0;
1452
+ overflow: hidden;
1453
+ }
1454
+
1455
+ .token-card-bar-fill {
1456
+ height: 100%;
1457
+ background: var(--accent);
1458
+ border-radius: 2px;
1459
+ transition: width 0.3s ease;
1460
+ }
1461
+
1285
1462
  .tools-pane-clickable {
1286
1463
  cursor: pointer;
1287
1464
  border-radius: 4px;
@@ -1336,6 +1513,28 @@ body {
1336
1513
  color: var(--accent);
1337
1514
  }
1338
1515
 
1516
+ .tools-pane-actions {
1517
+ display: flex;
1518
+ gap: 6px;
1519
+ padding-top: 4px;
1520
+ }
1521
+
1522
+ .tools-pane-action-btn {
1523
+ background: none;
1524
+ border: 1px solid var(--border);
1525
+ color: var(--text-muted);
1526
+ font-size: 10px;
1527
+ padding: 2px 8px;
1528
+ border-radius: 3px;
1529
+ cursor: pointer;
1530
+ transition: color 0.1s, border-color 0.1s;
1531
+ }
1532
+
1533
+ .tools-pane-action-btn:hover {
1534
+ color: var(--accent);
1535
+ border-color: var(--accent);
1536
+ }
1537
+
1339
1538
  .tools-pane-focus-toggle {
1340
1539
  opacity: 0.4;
1341
1540
  font-size: 11px;
@@ -1514,16 +1713,52 @@ body {
1514
1713
  justify-content: center;
1515
1714
  z-index: 5;
1516
1715
  pointer-events: none;
1716
+ overflow: hidden;
1517
1717
  }
1518
1718
 
1519
1719
  .empty-state.hidden {
1520
1720
  display: none;
1521
1721
  }
1522
1722
 
1723
+ .empty-state-bg {
1724
+ position: absolute;
1725
+ inset: 0;
1726
+ overflow: hidden;
1727
+ }
1728
+
1729
+ .empty-state-circle {
1730
+ position: absolute;
1731
+ border-radius: 50%;
1732
+ background: var(--accent);
1733
+ opacity: 0.07;
1734
+ }
1735
+
1736
+ .empty-state-circle.c1 { width: 80px; height: 80px; left: 20%; top: 15%; animation: float-circle 8s ease-in-out infinite; }
1737
+ .empty-state-circle.c2 { width: 50px; height: 50px; right: 25%; top: 25%; animation: float-circle 6s ease-in-out 1s infinite; }
1738
+ .empty-state-circle.c3 { width: 65px; height: 65px; left: 55%; bottom: 20%; animation: float-circle 7s ease-in-out 2s infinite; }
1739
+ .empty-state-circle.c4 { width: 40px; height: 40px; left: 15%; bottom: 30%; animation: float-circle 9s ease-in-out 0.5s infinite; }
1740
+ .empty-state-circle.c5 { width: 55px; height: 55px; right: 15%; bottom: 35%; animation: float-circle 7.5s ease-in-out 1.5s infinite; }
1741
+
1742
+ .empty-state-lines {
1743
+ position: absolute;
1744
+ inset: 0;
1745
+ width: 100%;
1746
+ height: 100%;
1747
+ color: var(--text-dim);
1748
+ animation: float-circle 10s ease-in-out infinite;
1749
+ }
1750
+
1751
+ @keyframes float-circle {
1752
+ 0%, 100% { transform: translateY(0); }
1753
+ 50% { transform: translateY(-12px); }
1754
+ }
1755
+
1523
1756
  .empty-state-content {
1524
1757
  text-align: center;
1525
1758
  max-width: 420px;
1526
1759
  padding: 40px 24px;
1760
+ position: relative;
1761
+ z-index: 1;
1527
1762
  }
1528
1763
 
1529
1764
  .empty-state-icon {
@@ -1841,3 +2076,125 @@ body {
1841
2076
  opacity: 1;
1842
2077
  transform: translateX(-50%) translateY(0);
1843
2078
  }
2079
+
2080
+ /* --- Context Menu --- */
2081
+
2082
+ .context-menu {
2083
+ position: absolute;
2084
+ z-index: 100;
2085
+ background: var(--bg-surface);
2086
+ border: 1px solid var(--border);
2087
+ border-radius: 8px;
2088
+ padding: 4px;
2089
+ min-width: 180px;
2090
+ box-shadow: 0 8px 24px var(--shadow);
2091
+ }
2092
+
2093
+ .context-menu-item {
2094
+ padding: 6px 12px;
2095
+ font-size: 12px;
2096
+ color: var(--text);
2097
+ border-radius: 4px;
2098
+ cursor: pointer;
2099
+ white-space: nowrap;
2100
+ }
2101
+
2102
+ .context-menu-item:hover {
2103
+ background: var(--bg-hover);
2104
+ }
2105
+
2106
+ .context-menu-separator {
2107
+ height: 1px;
2108
+ background: var(--border);
2109
+ margin: 4px 8px;
2110
+ }
2111
+
2112
+ /* --- Path Bar --- */
2113
+
2114
+ .path-bar {
2115
+ position: absolute;
2116
+ bottom: 16px;
2117
+ left: 50%;
2118
+ transform: translateX(-50%);
2119
+ z-index: 25;
2120
+ display: flex;
2121
+ align-items: center;
2122
+ gap: 4px;
2123
+ background: var(--bg-surface);
2124
+ border: 1px solid var(--border);
2125
+ border-radius: 8px;
2126
+ padding: 6px 12px;
2127
+ box-shadow: 0 4px 16px var(--shadow);
2128
+ max-width: 80%;
2129
+ overflow-x: auto;
2130
+ }
2131
+
2132
+ .path-bar.hidden {
2133
+ display: none;
2134
+ }
2135
+
2136
+ .path-bar-node {
2137
+ font-size: 11px;
2138
+ color: var(--text);
2139
+ padding: 2px 8px;
2140
+ border-radius: 4px;
2141
+ cursor: pointer;
2142
+ white-space: nowrap;
2143
+ flex-shrink: 0;
2144
+ }
2145
+
2146
+ .path-bar-node:hover {
2147
+ background: var(--bg-hover);
2148
+ }
2149
+
2150
+ .path-bar-edge {
2151
+ font-size: 9px;
2152
+ color: var(--text-dim);
2153
+ white-space: nowrap;
2154
+ flex-shrink: 0;
2155
+ }
2156
+
2157
+ .path-bar-close {
2158
+ background: none;
2159
+ border: none;
2160
+ color: var(--text-dim);
2161
+ font-size: 14px;
2162
+ cursor: pointer;
2163
+ padding: 0 4px;
2164
+ margin-left: 8px;
2165
+ flex-shrink: 0;
2166
+ }
2167
+
2168
+ .path-bar-close:hover {
2169
+ color: var(--text);
2170
+ }
2171
+
2172
+ /* --- Walk Mode Indicator --- */
2173
+
2174
+ .walk-trail-edge {
2175
+ font-size: 9px;
2176
+ color: var(--text-dim);
2177
+ padding: 1px 0 1px 24px;
2178
+ opacity: 0.7;
2179
+ }
2180
+
2181
+ .walk-indicator {
2182
+ font-size: 10px;
2183
+ color: var(--accent);
2184
+ padding: 2px 8px;
2185
+ border: 1px solid rgba(212, 162, 127, 0.4);
2186
+ border-radius: 4px;
2187
+ cursor: pointer;
2188
+ opacity: 0.4;
2189
+ }
2190
+
2191
+ .walk-indicator.active {
2192
+ opacity: 1;
2193
+ background: rgba(212, 162, 127, 0.15);
2194
+ animation: walk-strobe 2s ease-in-out infinite;
2195
+ }
2196
+
2197
+ @keyframes walk-strobe {
2198
+ 0%, 100% { opacity: 0.6; border-color: rgba(212, 162, 127, 0.3); }
2199
+ 50% { opacity: 1; border-color: rgba(212, 162, 127, 0.8); }
2200
+ }
@@ -3,6 +3,10 @@ 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;
9
+ onStarredSaveSnippet?: (label: string, nodeIds: string[]) => void;
6
10
  onRenameNodeType: (oldType: string, newType: string) => void;
7
11
  onRenameEdgeType: (oldType: string, newType: string) => void;
8
12
  onToggleEdgeLabels: (visible: boolean) => void;
@@ -27,5 +31,11 @@ export declare function initToolsPane(container: HTMLElement, callbacks: ToolsPa
27
31
  edgeCount: number;
28
32
  label?: string;
29
33
  }[]): void;
34
+ setWalkTrail(trail: {
35
+ id: string;
36
+ label: string;
37
+ type: string;
38
+ edgeType?: string;
39
+ }[]): void;
30
40
  };
31
41
  export {};
@@ -12,6 +12,7 @@ export function initToolsPane(container, callbacks) {
12
12
  let typesSearch = "";
13
13
  let qualitySearch = "";
14
14
  let snapshots = [];
15
+ let walkTrail = [];
15
16
  // Unified focus set — two layers that compose via union
16
17
  const focusSet = {
17
18
  types: new Set(), // toggled node types (dynamic — resolves to all nodes of type)
@@ -75,6 +76,37 @@ export function initToolsPane(container, callbacks) {
75
76
  `<span>${stats.edgeCount} edges</span><span class="tools-pane-sep">&middot;</span>` +
76
77
  `<span>${stats.types.length} types</span>`;
77
78
  content.appendChild(summary);
79
+ // Token efficiency card
80
+ if (data && stats.nodeCount > 0) {
81
+ const graphTokens = Math.ceil(JSON.stringify(data).length / 4);
82
+ // Estimate a typical search response: ~5 NodeSummary results, each ~30 chars
83
+ const avgNodeTokens = Math.round(graphTokens / stats.nodeCount);
84
+ const searchTokens = Math.max(10, Math.round(avgNodeTokens * 0.3) * Math.min(5, stats.nodeCount));
85
+ const percent = graphTokens > searchTokens ? Math.round((1 - searchTokens / graphTokens) * 100) : 0;
86
+ const tokenCard = document.createElement("div");
87
+ tokenCard.className = "tools-pane-token-card";
88
+ const barWidth = Math.min(100, percent);
89
+ const label = document.createElement("div");
90
+ label.className = "token-card-label";
91
+ label.textContent = "Token Efficiency";
92
+ tokenCard.appendChild(label);
93
+ const storedStat = document.createElement("div");
94
+ storedStat.className = "token-card-stat";
95
+ storedStat.textContent = `~${graphTokens.toLocaleString()} tokens stored`;
96
+ tokenCard.appendChild(storedStat);
97
+ const bar = document.createElement("div");
98
+ bar.className = "token-card-bar";
99
+ const barFill = document.createElement("div");
100
+ barFill.className = "token-card-bar-fill";
101
+ barFill.style.width = `${barWidth}%`;
102
+ bar.appendChild(barFill);
103
+ tokenCard.appendChild(bar);
104
+ const reductionStat = document.createElement("div");
105
+ reductionStat.className = "token-card-stat";
106
+ reductionStat.textContent = `A search returns ~${searchTokens} tokens instead of ~${graphTokens.toLocaleString()} (${percent}% reduction)`;
107
+ tokenCard.appendChild(reductionStat);
108
+ content.appendChild(tokenCard);
109
+ }
78
110
  // Tab bar
79
111
  const tabBar = document.createElement("div");
80
112
  tabBar.className = "tools-pane-tabs";
@@ -100,6 +132,10 @@ export function initToolsPane(container, callbacks) {
100
132
  if (!isFocusSetEmpty()) {
101
133
  renderFocusedSection();
102
134
  }
135
+ // Walk trail (visible when walk mode is active)
136
+ if (walkTrail.length > 0) {
137
+ renderWalkTrail();
138
+ }
103
139
  // Search input (for types and quality tabs)
104
140
  if (activeTab === "types" && stats.types.length > 5) {
105
141
  content.appendChild(makeSearchInput("Filter types...", typesSearch, (v) => {
@@ -137,6 +173,80 @@ export function initToolsPane(container, callbacks) {
137
173
  renderControlsTab(tabContainer);
138
174
  }
139
175
  }
176
+ function renderWalkTrail() {
177
+ content.appendChild(makeSection(`Walk Trail (${walkTrail.length})`, (section) => {
178
+ for (let i = 0; i < walkTrail.length; i++) {
179
+ const item = walkTrail[i];
180
+ const isCurrent = i === walkTrail.length - 1;
181
+ // Edge connector from previous node
182
+ if (item.edgeType) {
183
+ const connector = document.createElement("div");
184
+ connector.className = "walk-trail-edge";
185
+ connector.textContent = `\u2193 ${item.edgeType}`;
186
+ section.appendChild(connector);
187
+ }
188
+ const row = document.createElement("div");
189
+ row.className = "tools-pane-row tools-pane-clickable";
190
+ if (isCurrent)
191
+ row.style.fontWeight = "600";
192
+ const num = document.createElement("span");
193
+ num.className = "tools-pane-count";
194
+ num.style.minWidth = "18px";
195
+ num.textContent = `${i + 1}`;
196
+ const dot = document.createElement("span");
197
+ dot.className = "tools-pane-dot";
198
+ dot.style.backgroundColor = getColor(item.type);
199
+ const name = document.createElement("span");
200
+ name.className = "tools-pane-name";
201
+ name.textContent = item.label;
202
+ const typeBadge = document.createElement("span");
203
+ typeBadge.className = "tools-pane-count";
204
+ typeBadge.textContent = item.type;
205
+ const removeBtn = document.createElement("button");
206
+ removeBtn.className = "tools-pane-edit";
207
+ removeBtn.style.opacity = "1";
208
+ removeBtn.textContent = "\u00d7";
209
+ removeBtn.title = "Remove from trail";
210
+ removeBtn.addEventListener("click", (e) => {
211
+ e.stopPropagation();
212
+ callbacks.onWalkTrailRemove?.(item.id);
213
+ });
214
+ row.appendChild(num);
215
+ row.appendChild(dot);
216
+ row.appendChild(name);
217
+ row.appendChild(typeBadge);
218
+ row.appendChild(removeBtn);
219
+ row.addEventListener("click", () => {
220
+ callbacks.onNavigateToNode(item.id);
221
+ });
222
+ section.appendChild(row);
223
+ }
224
+ // Isolate button
225
+ // Action buttons row
226
+ const actionsRow = document.createElement("div");
227
+ actionsRow.className = "tools-pane-export-row";
228
+ if (callbacks.onWalkIsolate) {
229
+ const isolateBtn = document.createElement("button");
230
+ isolateBtn.className = "tools-pane-export-btn";
231
+ isolateBtn.textContent = "Isolate (I)";
232
+ isolateBtn.addEventListener("click", () => callbacks.onWalkIsolate());
233
+ actionsRow.appendChild(isolateBtn);
234
+ }
235
+ if (callbacks.onWalkSaveSnippet && walkTrail.length >= 2) {
236
+ const saveBtn = document.createElement("button");
237
+ saveBtn.className = "tools-pane-export-btn";
238
+ saveBtn.textContent = "Save snippet";
239
+ saveBtn.addEventListener("click", () => {
240
+ showPrompt("Save snippet", "Name for this snippet").then((label) => {
241
+ if (label)
242
+ callbacks.onWalkSaveSnippet(label);
243
+ });
244
+ });
245
+ actionsRow.appendChild(saveBtn);
246
+ }
247
+ section.appendChild(actionsRow);
248
+ }));
249
+ }
140
250
  function renderFocusedSection() {
141
251
  if (!stats || !data)
142
252
  return;
@@ -363,6 +473,80 @@ export function initToolsPane(container, callbacks) {
363
473
  if (!stats)
364
474
  return;
365
475
  const qq = qualitySearch.toLowerCase();
476
+ // Starred nodes — click to navigate, focus button
477
+ const filteredStarred = stats.starred.filter((n) => !qq || n.label.toLowerCase().includes(qq) || n.type.toLowerCase().includes(qq));
478
+ if (filteredStarred.length) {
479
+ target.appendChild(makeSection("\u2605 Starred", (section) => {
480
+ for (const n of filteredStarred) {
481
+ const row = document.createElement("div");
482
+ row.className = "tools-pane-row tools-pane-clickable";
483
+ const dot = document.createElement("span");
484
+ dot.className = "tools-pane-dot";
485
+ dot.style.backgroundColor = getColor(n.type);
486
+ const name = document.createElement("span");
487
+ name.className = "tools-pane-name";
488
+ name.textContent = n.label;
489
+ const focusBtn = document.createElement("button");
490
+ focusBtn.className = "tools-pane-edit tools-pane-focus-toggle";
491
+ if (isNodeFocused(n.id))
492
+ focusBtn.classList.add("tools-pane-focus-active");
493
+ focusBtn.textContent = "\u25CE";
494
+ focusBtn.title = isNodeFocused(n.id)
495
+ ? `Remove ${n.label} from focus`
496
+ : `Add ${n.label} to focus`;
497
+ row.appendChild(dot);
498
+ row.appendChild(name);
499
+ row.appendChild(focusBtn);
500
+ row.addEventListener("click", (e) => {
501
+ if (e.target.closest(".tools-pane-edit"))
502
+ return;
503
+ callbacks.onNavigateToNode(n.id);
504
+ });
505
+ focusBtn.addEventListener("click", (e) => {
506
+ e.stopPropagation();
507
+ if (focusSet.nodeIds.has(n.id)) {
508
+ focusSet.nodeIds.delete(n.id);
509
+ }
510
+ else {
511
+ focusSet.nodeIds.add(n.id);
512
+ }
513
+ emitFocusChange();
514
+ render();
515
+ });
516
+ section.appendChild(row);
517
+ }
518
+ // Action buttons row
519
+ const actions = document.createElement("div");
520
+ actions.className = "tools-pane-row tools-pane-actions";
521
+ const focusAllBtn = document.createElement("button");
522
+ focusAllBtn.className = "tools-pane-action-btn";
523
+ focusAllBtn.textContent = "Focus all";
524
+ focusAllBtn.title = "Enter focus mode with all starred nodes";
525
+ focusAllBtn.addEventListener("click", () => {
526
+ focusSet.nodeIds.clear();
527
+ focusSet.types.clear();
528
+ for (const n of stats.starred)
529
+ focusSet.nodeIds.add(n.id);
530
+ emitFocusChange();
531
+ render();
532
+ });
533
+ actions.appendChild(focusAllBtn);
534
+ if (callbacks.onStarredSaveSnippet) {
535
+ const saveBtn = document.createElement("button");
536
+ saveBtn.className = "tools-pane-action-btn";
537
+ saveBtn.textContent = "Save as snippet";
538
+ saveBtn.title = "Save starred nodes as a reusable snippet";
539
+ saveBtn.addEventListener("click", async () => {
540
+ const label = await showPrompt("Snippet name", "starred");
541
+ if (label) {
542
+ callbacks.onStarredSaveSnippet(label, stats.starred.map((n) => n.id));
543
+ }
544
+ });
545
+ actions.appendChild(saveBtn);
546
+ }
547
+ section.appendChild(actions);
548
+ }));
549
+ }
366
550
  // Most connected nodes — click to navigate, focus button
367
551
  const filteredConnected = stats.mostConnected.filter((n) => !qq || n.label.toLowerCase().includes(qq) || n.type.toLowerCase().includes(qq));
368
552
  if (filteredConnected.length) {
@@ -759,6 +943,9 @@ export function initToolsPane(container, callbacks) {
759
943
  connectedNodes.add(edge.targetId);
760
944
  }
761
945
  const nodeLabel = (n) => firstStringValue(n.properties) ?? n.id;
946
+ const starred = graphData.nodes
947
+ .filter((n) => n.properties._starred === true)
948
+ .map((n) => ({ id: n.id, label: nodeLabel(n), type: n.type }));
762
949
  const orphans = graphData.nodes
763
950
  .filter((n) => !connectedNodes.has(n.id))
764
951
  .map((n) => ({ id: n.id, label: nodeLabel(n), type: n.type }));
@@ -787,6 +974,7 @@ export function initToolsPane(container, callbacks) {
787
974
  edgeTypes: [...edgeTypeCounts.entries()]
788
975
  .sort((a, b) => b[1] - a[1])
789
976
  .map(([name, count]) => ({ name, count })),
977
+ starred,
790
978
  orphans,
791
979
  singletons,
792
980
  emptyNodes,
@@ -833,6 +1021,10 @@ export function initToolsPane(container, callbacks) {
833
1021
  if (activeTab === "controls")
834
1022
  renderTabContent();
835
1023
  },
1024
+ setWalkTrail(trail) {
1025
+ walkTrail = trail;
1026
+ render();
1027
+ },
836
1028
  };
837
1029
  }
838
1030
  function timeAgo(timestamp) {