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/bin/serve.js +62 -0
- package/dist/api.d.ts +12 -0
- package/dist/api.js +30 -0
- package/dist/app/assets/index-BBfZ1JvO.js +21 -0
- package/dist/app/assets/index-DNiYjxNx.css +1 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +17 -0
- package/dist/canvas.js +251 -45
- package/dist/config.js +1 -0
- package/dist/context-menu.d.ts +13 -0
- package/dist/context-menu.js +64 -0
- package/dist/default-config.json +6 -1
- package/dist/keybindings.d.ts +1 -1
- package/dist/keybindings.js +2 -0
- package/dist/main.js +221 -2
- package/dist/shortcuts.js +2 -0
- package/dist/sidebar.d.ts +8 -0
- package/dist/sidebar.js +44 -3
- package/dist/style.css +164 -0
- package/dist/tools-pane.d.ts +9 -0
- package/dist/tools-pane.js +83 -0
- package/package.json +1 -1
- package/dist/app/assets/index-BAsAhA_i.js +0 -21
- package/dist/app/assets/index-CvETIueX.css +0 -1
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
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
|
-
|
|
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
|
+
}
|
package/dist/tools-pane.d.ts
CHANGED
|
@@ -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 {};
|