backpack-viewer 0.2.16 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -1
- package/bin/serve.js +155 -0
- package/dist/api.d.ts +27 -0
- package/dist/api.js +71 -0
- package/dist/app/assets/index-CTM-vKgB.js +21 -0
- package/dist/app/assets/index-CjzMJjZ-.css +1 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +22 -1
- package/dist/canvas.js +143 -65
- package/dist/config.d.ts +4 -0
- package/dist/config.js +32 -0
- package/dist/default-config.json +66 -0
- package/dist/dialog.d.ts +9 -0
- package/dist/dialog.js +119 -0
- package/dist/info-panel.d.ts +4 -0
- package/dist/info-panel.js +66 -11
- package/dist/keybindings.d.ts +6 -0
- package/dist/keybindings.js +67 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +18 -8
- package/dist/main.js +175 -42
- package/dist/search.d.ts +5 -1
- package/dist/search.js +46 -85
- package/dist/shortcuts.d.ts +3 -1
- package/dist/shortcuts.js +51 -19
- package/dist/sidebar.d.ts +8 -0
- package/dist/sidebar.js +106 -1
- package/dist/style.css +328 -6
- package/dist/tools-pane.d.ts +10 -0
- package/dist/tools-pane.js +407 -148
- package/package.json +1 -1
- package/dist/app/assets/index-Mi0vDG5K.js +0 -21
- package/dist/app/assets/index-z15vEFEy.css +0 -1
package/dist/main.js
CHANGED
|
@@ -1,22 +1,43 @@
|
|
|
1
|
-
import { listOntologies, loadOntology, saveOntology, renameOntology } from "./api";
|
|
1
|
+
import { listOntologies, loadOntology, saveOntology, renameOntology, listBranches, createBranch, switchBranch, deleteBranch, listSnapshots, createSnapshot, rollbackSnapshot, } from "./api";
|
|
2
2
|
import { initSidebar } from "./sidebar";
|
|
3
3
|
import { initCanvas } from "./canvas";
|
|
4
4
|
import { initInfoPanel } from "./info-panel";
|
|
5
5
|
import { initSearch } from "./search";
|
|
6
6
|
import { initToolsPane } from "./tools-pane";
|
|
7
|
-
import { setLayoutParams } from "./layout";
|
|
7
|
+
import { setLayoutParams, getLayoutParams, autoLayoutParams } from "./layout";
|
|
8
8
|
import { initShortcuts } from "./shortcuts";
|
|
9
9
|
import { initEmptyState } from "./empty-state";
|
|
10
10
|
import { createHistory } from "./history";
|
|
11
|
+
import { matchKey } from "./keybindings";
|
|
12
|
+
import defaultConfig from "./default-config.json";
|
|
11
13
|
import "./style.css";
|
|
12
14
|
let activeOntology = "";
|
|
13
15
|
let currentData = null;
|
|
14
16
|
async function main() {
|
|
15
17
|
const canvasContainer = document.getElementById("canvas-container");
|
|
18
|
+
// --- Load config ---
|
|
19
|
+
const cfg = { ...defaultConfig };
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch("/api/config");
|
|
22
|
+
if (res.ok) {
|
|
23
|
+
const user = await res.json();
|
|
24
|
+
Object.assign(cfg.keybindings, user.keybindings ?? {});
|
|
25
|
+
Object.assign(cfg.display, user.display ?? {});
|
|
26
|
+
Object.assign(cfg.layout, user.layout ?? {});
|
|
27
|
+
Object.assign(cfg.navigation, user.navigation ?? {});
|
|
28
|
+
Object.assign(cfg.lod, user.lod ?? {});
|
|
29
|
+
Object.assign(cfg.limits, user.limits ?? {});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch { /* use defaults */ }
|
|
33
|
+
const bindings = cfg.keybindings;
|
|
16
34
|
// --- Theme toggle (top-right of canvas) ---
|
|
17
35
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
|
|
36
|
+
const themeDefault = cfg.display.theme === "system"
|
|
37
|
+
? (prefersDark.matches ? "dark" : "light")
|
|
38
|
+
: cfg.display.theme;
|
|
18
39
|
const stored = localStorage.getItem("backpack-theme");
|
|
19
|
-
const initial = stored ??
|
|
40
|
+
const initial = stored ?? themeDefault;
|
|
20
41
|
document.documentElement.setAttribute("data-theme", initial);
|
|
21
42
|
const themeBtn = document.createElement("button");
|
|
22
43
|
themeBtn.className = "theme-toggle";
|
|
@@ -126,6 +147,9 @@ async function main() {
|
|
|
126
147
|
const mobileQuery = window.matchMedia("(max-width: 768px)");
|
|
127
148
|
// Track current selection for keyboard shortcuts
|
|
128
149
|
let currentSelection = [];
|
|
150
|
+
let edgesVisible = cfg.display.edges;
|
|
151
|
+
let panSpeed = cfg.navigation.panSpeed;
|
|
152
|
+
let viewCycleIndex = -1;
|
|
129
153
|
// --- Focus indicator (top bar pill) ---
|
|
130
154
|
let focusIndicator = null;
|
|
131
155
|
function buildFocusIndicator(info) {
|
|
@@ -188,19 +212,23 @@ async function main() {
|
|
|
188
212
|
}, (focus) => {
|
|
189
213
|
if (focus) {
|
|
190
214
|
buildFocusIndicator(focus);
|
|
191
|
-
// Insert into top-left, after tools toggle
|
|
192
215
|
const topLeft = canvasContainer.querySelector(".canvas-top-left");
|
|
193
216
|
if (topLeft && focusIndicator)
|
|
194
217
|
topLeft.appendChild(focusIndicator);
|
|
195
218
|
updateUrl(activeOntology, focus.seedNodeIds);
|
|
219
|
+
infoPanel.setFocusDisabled(focus.hops === 0);
|
|
196
220
|
}
|
|
197
221
|
else {
|
|
198
222
|
removeFocusIndicator();
|
|
223
|
+
infoPanel.setFocusDisabled(false);
|
|
199
224
|
if (activeOntology)
|
|
200
225
|
updateUrl(activeOntology);
|
|
201
226
|
}
|
|
227
|
+
}, { lod: cfg.lod, navigation: cfg.navigation });
|
|
228
|
+
const search = initSearch(canvasContainer, {
|
|
229
|
+
maxResults: cfg.limits.maxSearchResults,
|
|
230
|
+
debounceMs: cfg.limits.searchDebounceMs,
|
|
202
231
|
});
|
|
203
|
-
const search = initSearch(canvasContainer);
|
|
204
232
|
const toolsPane = initToolsPane(canvasContainer, {
|
|
205
233
|
onFilterByType(type) {
|
|
206
234
|
if (!currentData)
|
|
@@ -222,7 +250,7 @@ async function main() {
|
|
|
222
250
|
},
|
|
223
251
|
onFocusChange(seedNodeIds) {
|
|
224
252
|
if (seedNodeIds && seedNodeIds.length > 0) {
|
|
225
|
-
canvas.enterFocus(seedNodeIds,
|
|
253
|
+
canvas.enterFocus(seedNodeIds, 0);
|
|
226
254
|
}
|
|
227
255
|
else {
|
|
228
256
|
if (canvas.isFocused())
|
|
@@ -265,6 +293,9 @@ async function main() {
|
|
|
265
293
|
setLayoutParams({ [param]: value });
|
|
266
294
|
canvas.reheat();
|
|
267
295
|
},
|
|
296
|
+
onPanSpeedChange(speed) {
|
|
297
|
+
panSpeed = speed;
|
|
298
|
+
},
|
|
268
299
|
onExport(format) {
|
|
269
300
|
const dataUrl = canvas.exportImage(format);
|
|
270
301
|
if (!dataUrl)
|
|
@@ -274,6 +305,22 @@ async function main() {
|
|
|
274
305
|
link.href = dataUrl;
|
|
275
306
|
link.click();
|
|
276
307
|
},
|
|
308
|
+
onSnapshot: async (label) => {
|
|
309
|
+
if (!activeOntology)
|
|
310
|
+
return;
|
|
311
|
+
await createSnapshot(activeOntology, label);
|
|
312
|
+
await refreshSnapshots(activeOntology);
|
|
313
|
+
},
|
|
314
|
+
onRollback: async (version) => {
|
|
315
|
+
if (!activeOntology)
|
|
316
|
+
return;
|
|
317
|
+
await rollbackSnapshot(activeOntology, version);
|
|
318
|
+
currentData = await loadOntology(activeOntology);
|
|
319
|
+
canvas.loadGraph(currentData);
|
|
320
|
+
search.setLearningGraphData(currentData);
|
|
321
|
+
toolsPane.setData(currentData);
|
|
322
|
+
await refreshSnapshots(activeOntology);
|
|
323
|
+
},
|
|
277
324
|
onOpen() {
|
|
278
325
|
if (mobileQuery.matches)
|
|
279
326
|
infoPanel.hide();
|
|
@@ -335,9 +382,46 @@ async function main() {
|
|
|
335
382
|
toolsPane.setData(currentData);
|
|
336
383
|
}
|
|
337
384
|
},
|
|
385
|
+
onBranchSwitch: async (graphName, branchName) => {
|
|
386
|
+
await switchBranch(graphName, branchName);
|
|
387
|
+
await refreshBranches(graphName);
|
|
388
|
+
currentData = await loadOntology(graphName);
|
|
389
|
+
canvas.loadGraph(currentData);
|
|
390
|
+
search.setLearningGraphData(currentData);
|
|
391
|
+
toolsPane.setData(currentData);
|
|
392
|
+
await refreshSnapshots(graphName);
|
|
393
|
+
},
|
|
394
|
+
onBranchCreate: async (graphName, branchName) => {
|
|
395
|
+
await createBranch(graphName, branchName);
|
|
396
|
+
await refreshBranches(graphName);
|
|
397
|
+
},
|
|
398
|
+
onBranchDelete: async (graphName, branchName) => {
|
|
399
|
+
await deleteBranch(graphName, branchName);
|
|
400
|
+
await refreshBranches(graphName);
|
|
401
|
+
},
|
|
338
402
|
});
|
|
339
|
-
|
|
403
|
+
async function refreshBranches(graphName) {
|
|
404
|
+
const branches = await listBranches(graphName);
|
|
405
|
+
const active = branches.find((b) => b.active);
|
|
406
|
+
if (active) {
|
|
407
|
+
sidebar.setActiveBranch(graphName, active.name, branches);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function refreshSnapshots(graphName) {
|
|
411
|
+
const snaps = await listSnapshots(graphName);
|
|
412
|
+
toolsPane.setSnapshots(snaps);
|
|
413
|
+
}
|
|
414
|
+
const shortcuts = initShortcuts(canvasContainer, bindings);
|
|
340
415
|
const emptyState = initEmptyState(canvasContainer);
|
|
416
|
+
// Apply display defaults from config
|
|
417
|
+
if (!cfg.display.edges)
|
|
418
|
+
canvas.setEdges(false);
|
|
419
|
+
if (!cfg.display.edgeLabels)
|
|
420
|
+
canvas.setEdgeLabels(false);
|
|
421
|
+
if (!cfg.display.typeHulls)
|
|
422
|
+
canvas.setTypeHulls(false);
|
|
423
|
+
if (!cfg.display.minimap)
|
|
424
|
+
canvas.setMinimap(false);
|
|
341
425
|
// --- URL deep linking ---
|
|
342
426
|
function updateUrl(name, nodeIds) {
|
|
343
427
|
const parts = [];
|
|
@@ -384,11 +468,19 @@ async function main() {
|
|
|
384
468
|
search.clear();
|
|
385
469
|
undoHistory.clear();
|
|
386
470
|
currentData = await loadOntology(name);
|
|
471
|
+
const autoParams = autoLayoutParams(currentData.nodes.length);
|
|
472
|
+
setLayoutParams({
|
|
473
|
+
spacing: Math.max(cfg.layout.spacing, autoParams.spacing),
|
|
474
|
+
clusterStrength: Math.max(cfg.layout.clustering, autoParams.clusterStrength),
|
|
475
|
+
});
|
|
387
476
|
canvas.loadGraph(currentData);
|
|
388
477
|
search.setLearningGraphData(currentData);
|
|
389
478
|
toolsPane.setData(currentData);
|
|
390
479
|
emptyState.hide();
|
|
391
480
|
updateUrl(name);
|
|
481
|
+
// Load branches and snapshots
|
|
482
|
+
await refreshBranches(name);
|
|
483
|
+
await refreshSnapshots(name);
|
|
392
484
|
// Restore focus mode if requested
|
|
393
485
|
if (focusSeedIds?.length && currentData) {
|
|
394
486
|
const validFocus = focusSeedIds.filter((id) => currentData.nodes.some((n) => n.id === id));
|
|
@@ -428,48 +520,89 @@ async function main() {
|
|
|
428
520
|
else {
|
|
429
521
|
emptyState.show();
|
|
430
522
|
}
|
|
431
|
-
// Keyboard shortcuts
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
else if (e.key === "z" && (e.metaKey || e.ctrlKey)) {
|
|
448
|
-
e.preventDefault();
|
|
449
|
-
if (currentData) {
|
|
450
|
-
const restored = undoHistory.undo(currentData);
|
|
451
|
-
if (restored)
|
|
452
|
-
applyState(restored);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
else if (e.key === "f" || e.key === "F") {
|
|
456
|
-
// Toggle focus mode on current selection
|
|
523
|
+
// Keyboard shortcuts — dispatched via configurable bindings
|
|
524
|
+
const actions = {
|
|
525
|
+
search() { search.focus(); },
|
|
526
|
+
searchAlt() { search.focus(); },
|
|
527
|
+
undo() { if (currentData) {
|
|
528
|
+
const r = undoHistory.undo(currentData);
|
|
529
|
+
if (r)
|
|
530
|
+
applyState(r);
|
|
531
|
+
} },
|
|
532
|
+
redo() { if (currentData) {
|
|
533
|
+
const r = undoHistory.redo(currentData);
|
|
534
|
+
if (r)
|
|
535
|
+
applyState(r);
|
|
536
|
+
} },
|
|
537
|
+
focus() {
|
|
457
538
|
if (canvas.isFocused()) {
|
|
458
539
|
toolsPane.clearFocusSet();
|
|
459
540
|
}
|
|
460
541
|
else if (currentSelection.length > 0) {
|
|
461
542
|
toolsPane.addToFocusSet(currentSelection);
|
|
462
543
|
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
544
|
+
},
|
|
545
|
+
hopsDecrease() { const i = canvas.getFocusInfo(); if (i && i.hops > 0)
|
|
546
|
+
canvas.enterFocus(i.seedNodeIds, i.hops - 1); },
|
|
547
|
+
hopsIncrease() { const i = canvas.getFocusInfo(); if (i)
|
|
548
|
+
canvas.enterFocus(i.seedNodeIds, i.hops + 1); },
|
|
549
|
+
nextNode() {
|
|
550
|
+
const ids = canvas.getNodeIds();
|
|
551
|
+
if (ids.length > 0) {
|
|
552
|
+
viewCycleIndex = (viewCycleIndex + 1) % ids.length;
|
|
553
|
+
canvas.panToNode(ids[viewCycleIndex]);
|
|
554
|
+
if (currentData)
|
|
555
|
+
infoPanel.show([ids[viewCycleIndex]], currentData);
|
|
470
556
|
}
|
|
471
|
-
|
|
472
|
-
|
|
557
|
+
},
|
|
558
|
+
prevNode() {
|
|
559
|
+
const ids = canvas.getNodeIds();
|
|
560
|
+
if (ids.length > 0) {
|
|
561
|
+
viewCycleIndex = viewCycleIndex <= 0 ? ids.length - 1 : viewCycleIndex - 1;
|
|
562
|
+
canvas.panToNode(ids[viewCycleIndex]);
|
|
563
|
+
if (currentData)
|
|
564
|
+
infoPanel.show([ids[viewCycleIndex]], currentData);
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
nextConnection() { const id = infoPanel.cycleConnection(1); if (id)
|
|
568
|
+
canvas.panToNode(id); },
|
|
569
|
+
prevConnection() { const id = infoPanel.cycleConnection(-1); if (id)
|
|
570
|
+
canvas.panToNode(id); },
|
|
571
|
+
historyBack() { infoPanel.goBack(); },
|
|
572
|
+
historyForward() { infoPanel.goForward(); },
|
|
573
|
+
center() { canvas.centerView(); },
|
|
574
|
+
toggleEdges() { edgesVisible = !edgesVisible; canvas.setEdges(edgesVisible); },
|
|
575
|
+
panLeft() { canvas.panBy(-panSpeed, 0); },
|
|
576
|
+
panDown() { canvas.panBy(0, panSpeed); },
|
|
577
|
+
panUp() { canvas.panBy(0, -panSpeed); },
|
|
578
|
+
panRight() { canvas.panBy(panSpeed, 0); },
|
|
579
|
+
panFastLeft() { canvas.panBy(-panSpeed * cfg.navigation.panFastMultiplier, 0); },
|
|
580
|
+
zoomOut() { canvas.zoomBy(1 / cfg.navigation.zoomFactor); },
|
|
581
|
+
zoomIn() { canvas.zoomBy(cfg.navigation.zoomFactor); },
|
|
582
|
+
panFastRight() { canvas.panBy(panSpeed * cfg.navigation.panFastMultiplier, 0); },
|
|
583
|
+
spacingDecrease() { const p = getLayoutParams(); setLayoutParams({ spacing: Math.max(0.5, p.spacing - 0.5) }); canvas.reheat(); },
|
|
584
|
+
spacingIncrease() { const p = getLayoutParams(); setLayoutParams({ spacing: Math.min(20, p.spacing + 0.5) }); canvas.reheat(); },
|
|
585
|
+
clusteringDecrease() { const p = getLayoutParams(); setLayoutParams({ clusterStrength: Math.max(0, p.clusterStrength - 0.03) }); canvas.reheat(); },
|
|
586
|
+
clusteringIncrease() { const p = getLayoutParams(); setLayoutParams({ clusterStrength: Math.min(1, p.clusterStrength + 0.03) }); canvas.reheat(); },
|
|
587
|
+
help() { shortcuts.toggle(); },
|
|
588
|
+
toggleSidebar() { sidebar.toggle(); },
|
|
589
|
+
escape() { if (canvas.isFocused()) {
|
|
590
|
+
toolsPane.clearFocusSet();
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
shortcuts.hide();
|
|
594
|
+
} },
|
|
595
|
+
};
|
|
596
|
+
document.addEventListener("keydown", (e) => {
|
|
597
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
|
|
598
|
+
return;
|
|
599
|
+
for (const [action, binding] of Object.entries(bindings)) {
|
|
600
|
+
if (matchKey(e, binding)) {
|
|
601
|
+
const needsPrevent = action === "search" || action === "searchAlt" || action === "undo" || action === "redo" || action === "toggleSidebar";
|
|
602
|
+
if (needsPrevent)
|
|
603
|
+
e.preventDefault();
|
|
604
|
+
actions[action]?.();
|
|
605
|
+
return;
|
|
473
606
|
}
|
|
474
607
|
}
|
|
475
608
|
});
|
package/dist/search.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { LearningGraphData } from "backpack-ontology";
|
|
2
|
-
export
|
|
2
|
+
export interface SearchConfig {
|
|
3
|
+
maxResults?: number;
|
|
4
|
+
debounceMs?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function initSearch(container: HTMLElement, config?: SearchConfig): {
|
|
3
7
|
setLearningGraphData(newData: LearningGraphData | null): void;
|
|
4
8
|
onFilterChange(cb: (ids: Set<string> | null) => void): void;
|
|
5
9
|
onNodeSelect(cb: (nodeId: string) => void): void;
|
package/dist/search.js
CHANGED
|
@@ -10,24 +10,22 @@ function nodeLabel(node) {
|
|
|
10
10
|
/** Check if a node matches a search query (case-insensitive across label + all string properties). */
|
|
11
11
|
function matchesQuery(node, query) {
|
|
12
12
|
const q = query.toLowerCase();
|
|
13
|
-
// Check label
|
|
14
13
|
if (nodeLabel(node).toLowerCase().includes(q))
|
|
15
14
|
return true;
|
|
16
|
-
// Check type
|
|
17
15
|
if (node.type.toLowerCase().includes(q))
|
|
18
16
|
return true;
|
|
19
|
-
// Check all string property values
|
|
20
17
|
for (const value of Object.values(node.properties)) {
|
|
21
18
|
if (typeof value === "string" && value.toLowerCase().includes(q))
|
|
22
19
|
return true;
|
|
23
20
|
}
|
|
24
21
|
return false;
|
|
25
22
|
}
|
|
26
|
-
export function initSearch(container) {
|
|
23
|
+
export function initSearch(container, config) {
|
|
24
|
+
const maxResults = config?.maxResults ?? 8;
|
|
25
|
+
const debounceMs = config?.debounceMs ?? 150;
|
|
27
26
|
let data = null;
|
|
28
27
|
let filterCallback = null;
|
|
29
28
|
let selectCallback = null;
|
|
30
|
-
let activeTypes = new Set();
|
|
31
29
|
let debounceTimer = null;
|
|
32
30
|
// --- DOM ---
|
|
33
31
|
const overlay = document.createElement("div");
|
|
@@ -43,83 +41,24 @@ export function initSearch(container) {
|
|
|
43
41
|
const kbd = document.createElement("kbd");
|
|
44
42
|
kbd.className = "search-kbd";
|
|
45
43
|
kbd.textContent = "/";
|
|
46
|
-
const chipToggle = document.createElement("button");
|
|
47
|
-
chipToggle.className = "chip-toggle";
|
|
48
|
-
chipToggle.setAttribute("aria-label", "Toggle filter chips");
|
|
49
|
-
chipToggle.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>';
|
|
50
|
-
let chipsVisible = false;
|
|
51
|
-
chipToggle.addEventListener("click", () => {
|
|
52
|
-
chipsVisible = !chipsVisible;
|
|
53
|
-
chips.classList.toggle("hidden", !chipsVisible);
|
|
54
|
-
chipToggle.classList.toggle("active", chipsVisible);
|
|
55
|
-
});
|
|
56
44
|
inputWrap.appendChild(input);
|
|
57
45
|
inputWrap.appendChild(kbd);
|
|
58
|
-
inputWrap.appendChild(chipToggle);
|
|
59
46
|
const results = document.createElement("ul");
|
|
60
47
|
results.className = "search-results hidden";
|
|
61
|
-
const chips = document.createElement("div");
|
|
62
|
-
chips.className = "type-chips hidden";
|
|
63
48
|
overlay.appendChild(inputWrap);
|
|
64
49
|
overlay.appendChild(results);
|
|
65
|
-
overlay.appendChild(chips);
|
|
66
50
|
container.appendChild(overlay);
|
|
67
|
-
// ---
|
|
68
|
-
function buildChips() {
|
|
69
|
-
chips.innerHTML = "";
|
|
70
|
-
if (!data)
|
|
71
|
-
return;
|
|
72
|
-
// Count nodes per type
|
|
73
|
-
const typeCounts = new Map();
|
|
74
|
-
for (const node of data.nodes) {
|
|
75
|
-
typeCounts.set(node.type, (typeCounts.get(node.type) ?? 0) + 1);
|
|
76
|
-
}
|
|
77
|
-
// Sort alphabetically
|
|
78
|
-
const types = [...typeCounts.keys()].sort();
|
|
79
|
-
activeTypes = new Set(); // None selected = show all
|
|
80
|
-
for (const type of types) {
|
|
81
|
-
const chip = document.createElement("button");
|
|
82
|
-
chip.className = "type-chip";
|
|
83
|
-
chip.dataset.type = type;
|
|
84
|
-
const dot = document.createElement("span");
|
|
85
|
-
dot.className = "type-chip-dot";
|
|
86
|
-
dot.style.backgroundColor = getColor(type);
|
|
87
|
-
const label = document.createElement("span");
|
|
88
|
-
label.textContent = `${type} (${typeCounts.get(type)})`;
|
|
89
|
-
chip.appendChild(dot);
|
|
90
|
-
chip.appendChild(label);
|
|
91
|
-
chip.addEventListener("click", () => {
|
|
92
|
-
if (activeTypes.has(type)) {
|
|
93
|
-
activeTypes.delete(type);
|
|
94
|
-
chip.classList.remove("active");
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
activeTypes.add(type);
|
|
98
|
-
chip.classList.add("active");
|
|
99
|
-
}
|
|
100
|
-
applyFilter();
|
|
101
|
-
});
|
|
102
|
-
chips.appendChild(chip);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// --- Search + filter logic ---
|
|
51
|
+
// --- Search logic ---
|
|
106
52
|
function getMatchingIds() {
|
|
107
53
|
if (!data)
|
|
108
54
|
return null;
|
|
109
55
|
const query = input.value.trim();
|
|
110
|
-
|
|
111
|
-
const noQuery = query.length === 0;
|
|
112
|
-
// No filter active — return null (show all)
|
|
113
|
-
if (noQuery && noChipsSelected)
|
|
56
|
+
if (query.length === 0)
|
|
114
57
|
return null;
|
|
115
58
|
const ids = new Set();
|
|
116
59
|
for (const node of data.nodes) {
|
|
117
|
-
|
|
118
|
-
if (!noChipsSelected && !activeTypes.has(node.type))
|
|
119
|
-
continue;
|
|
120
|
-
if (noQuery || matchesQuery(node, query)) {
|
|
60
|
+
if (matchesQuery(node, query))
|
|
121
61
|
ids.add(node.id);
|
|
122
|
-
}
|
|
123
62
|
}
|
|
124
63
|
return ids;
|
|
125
64
|
}
|
|
@@ -130,19 +69,17 @@ export function initSearch(container) {
|
|
|
130
69
|
}
|
|
131
70
|
function updateResults() {
|
|
132
71
|
results.innerHTML = "";
|
|
72
|
+
activeIndex = -1;
|
|
133
73
|
const query = input.value.trim();
|
|
134
74
|
if (!data || query.length === 0) {
|
|
135
75
|
results.classList.add("hidden");
|
|
136
76
|
return;
|
|
137
77
|
}
|
|
138
|
-
const noChipsSelected = activeTypes.size === 0;
|
|
139
78
|
const matches = [];
|
|
140
79
|
for (const node of data.nodes) {
|
|
141
|
-
if (!noChipsSelected && !activeTypes.has(node.type))
|
|
142
|
-
continue;
|
|
143
80
|
if (matchesQuery(node, query)) {
|
|
144
81
|
matches.push(node);
|
|
145
|
-
if (matches.length >=
|
|
82
|
+
if (matches.length >= maxResults)
|
|
146
83
|
break;
|
|
147
84
|
}
|
|
148
85
|
}
|
|
@@ -180,28 +117,57 @@ export function initSearch(container) {
|
|
|
180
117
|
input.addEventListener("input", () => {
|
|
181
118
|
if (debounceTimer)
|
|
182
119
|
clearTimeout(debounceTimer);
|
|
183
|
-
debounceTimer = setTimeout(applyFilter,
|
|
120
|
+
debounceTimer = setTimeout(applyFilter, debounceMs);
|
|
184
121
|
});
|
|
122
|
+
let activeIndex = -1;
|
|
123
|
+
function updateActiveResult() {
|
|
124
|
+
const items = results.querySelectorAll(".search-result-item");
|
|
125
|
+
items.forEach((el, i) => {
|
|
126
|
+
el.classList.toggle("search-result-active", i === activeIndex);
|
|
127
|
+
});
|
|
128
|
+
if (activeIndex >= 0 && items[activeIndex]) {
|
|
129
|
+
items[activeIndex].scrollIntoView({ block: "nearest" });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
185
132
|
input.addEventListener("keydown", (e) => {
|
|
186
|
-
|
|
133
|
+
const items = results.querySelectorAll(".search-result-item");
|
|
134
|
+
if (e.key === "ArrowDown") {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
if (items.length > 0) {
|
|
137
|
+
activeIndex = Math.min(activeIndex + 1, items.length - 1);
|
|
138
|
+
updateActiveResult();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (e.key === "ArrowUp") {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
if (items.length > 0) {
|
|
144
|
+
activeIndex = Math.max(activeIndex - 1, 0);
|
|
145
|
+
updateActiveResult();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (e.key === "Enter") {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
if (activeIndex >= 0 && items[activeIndex]) {
|
|
151
|
+
items[activeIndex].click();
|
|
152
|
+
}
|
|
153
|
+
else if (items.length > 0) {
|
|
154
|
+
items[0].click();
|
|
155
|
+
}
|
|
156
|
+
input.blur();
|
|
157
|
+
}
|
|
158
|
+
else if (e.key === "Escape") {
|
|
187
159
|
input.value = "";
|
|
188
160
|
input.blur();
|
|
189
161
|
results.classList.add("hidden");
|
|
162
|
+
activeIndex = -1;
|
|
190
163
|
applyFilter();
|
|
191
164
|
}
|
|
192
|
-
else if (e.key === "Enter") {
|
|
193
|
-
// Select first result
|
|
194
|
-
const first = results.querySelector(".search-result-item");
|
|
195
|
-
first?.click();
|
|
196
|
-
}
|
|
197
165
|
});
|
|
198
|
-
// Close results when clicking outside
|
|
199
166
|
document.addEventListener("click", (e) => {
|
|
200
167
|
if (!overlay.contains(e.target)) {
|
|
201
168
|
results.classList.add("hidden");
|
|
202
169
|
}
|
|
203
170
|
});
|
|
204
|
-
// Hide kbd hint when focused
|
|
205
171
|
input.addEventListener("focus", () => kbd.classList.add("hidden"));
|
|
206
172
|
input.addEventListener("blur", () => {
|
|
207
173
|
if (input.value.length === 0)
|
|
@@ -215,7 +181,6 @@ export function initSearch(container) {
|
|
|
215
181
|
results.classList.add("hidden");
|
|
216
182
|
if (data && data.nodes.length > 0) {
|
|
217
183
|
overlay.classList.remove("hidden");
|
|
218
|
-
buildChips();
|
|
219
184
|
}
|
|
220
185
|
else {
|
|
221
186
|
overlay.classList.add("hidden");
|
|
@@ -230,10 +195,6 @@ export function initSearch(container) {
|
|
|
230
195
|
clear() {
|
|
231
196
|
input.value = "";
|
|
232
197
|
results.classList.add("hidden");
|
|
233
|
-
activeTypes.clear();
|
|
234
|
-
chipsVisible = false;
|
|
235
|
-
chips.classList.add("hidden");
|
|
236
|
-
chipToggle.classList.remove("active");
|
|
237
198
|
filterCallback?.(null);
|
|
238
199
|
},
|
|
239
200
|
focus() {
|
package/dist/shortcuts.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { type KeybindingMap } from "./keybindings";
|
|
2
|
+
export declare function initShortcuts(container: HTMLElement, bindings: KeybindingMap): {
|
|
2
3
|
show: () => void;
|
|
3
4
|
hide: () => void;
|
|
5
|
+
toggle: () => void;
|
|
4
6
|
};
|
package/dist/shortcuts.js
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
{ key: "Ctrl+Shift+Z", description: "Redo" },
|
|
5
|
-
{ key: "?", description: "Show this help" },
|
|
6
|
-
{ key: "F", description: "Focus on selected / exit focus" },
|
|
7
|
-
{ key: "Esc", description: "Exit focus / close panel" },
|
|
1
|
+
import { actionDescriptions } from "./keybindings";
|
|
2
|
+
// Non-keyboard actions shown at the bottom of help
|
|
3
|
+
const MOUSE_ACTIONS = [
|
|
8
4
|
{ key: "Click", description: "Select node" },
|
|
9
5
|
{ key: "Ctrl+Click", description: "Multi-select nodes" },
|
|
10
6
|
{ key: "Drag", description: "Pan canvas" },
|
|
11
7
|
{ key: "Scroll", description: "Zoom in/out" },
|
|
12
8
|
];
|
|
13
|
-
|
|
9
|
+
// Group and order for display
|
|
10
|
+
const ACTION_ORDER = [
|
|
11
|
+
"search", "searchAlt", "undo", "redo", "help",
|
|
12
|
+
"focus", "toggleEdges", "center",
|
|
13
|
+
"nextNode", "prevNode", "nextConnection", "prevConnection",
|
|
14
|
+
"historyBack", "historyForward",
|
|
15
|
+
"hopsIncrease", "hopsDecrease",
|
|
16
|
+
"panLeft", "panDown", "panUp", "panRight",
|
|
17
|
+
"panFastLeft", "panFastRight", "zoomIn", "zoomOut",
|
|
18
|
+
"spacingDecrease", "spacingIncrease",
|
|
19
|
+
"clusteringDecrease", "clusteringIncrease",
|
|
20
|
+
"toggleSidebar",
|
|
21
|
+
"escape",
|
|
22
|
+
];
|
|
23
|
+
/** Format a binding string for display (e.g. "ctrl+z" → "Ctrl+Z"). */
|
|
24
|
+
function formatBinding(binding) {
|
|
25
|
+
return binding
|
|
26
|
+
.split("+")
|
|
27
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
28
|
+
.join("+");
|
|
29
|
+
}
|
|
30
|
+
export function initShortcuts(container, bindings) {
|
|
31
|
+
const descriptions = actionDescriptions();
|
|
14
32
|
const overlay = document.createElement("div");
|
|
15
33
|
overlay.className = "shortcuts-overlay hidden";
|
|
16
34
|
const modal = document.createElement("div");
|
|
@@ -20,7 +38,27 @@ export function initShortcuts(container) {
|
|
|
20
38
|
title.textContent = "Keyboard Shortcuts";
|
|
21
39
|
const list = document.createElement("div");
|
|
22
40
|
list.className = "shortcuts-list";
|
|
23
|
-
|
|
41
|
+
// Keybinding actions
|
|
42
|
+
for (const action of ACTION_ORDER) {
|
|
43
|
+
const binding = bindings[action];
|
|
44
|
+
if (!binding)
|
|
45
|
+
continue;
|
|
46
|
+
const row = document.createElement("div");
|
|
47
|
+
row.className = "shortcuts-row";
|
|
48
|
+
const keys = document.createElement("div");
|
|
49
|
+
keys.className = "shortcuts-keys";
|
|
50
|
+
const kbd = document.createElement("kbd");
|
|
51
|
+
kbd.textContent = formatBinding(binding);
|
|
52
|
+
keys.appendChild(kbd);
|
|
53
|
+
const desc = document.createElement("span");
|
|
54
|
+
desc.className = "shortcuts-desc";
|
|
55
|
+
desc.textContent = descriptions[action];
|
|
56
|
+
row.appendChild(keys);
|
|
57
|
+
row.appendChild(desc);
|
|
58
|
+
list.appendChild(row);
|
|
59
|
+
}
|
|
60
|
+
// Mouse actions
|
|
61
|
+
for (const s of MOUSE_ACTIONS) {
|
|
24
62
|
const row = document.createElement("div");
|
|
25
63
|
row.className = "shortcuts-row";
|
|
26
64
|
const keys = document.createElement("div");
|
|
@@ -28,15 +66,6 @@ export function initShortcuts(container) {
|
|
|
28
66
|
const kbd = document.createElement("kbd");
|
|
29
67
|
kbd.textContent = s.key;
|
|
30
68
|
keys.appendChild(kbd);
|
|
31
|
-
if (s.alt) {
|
|
32
|
-
const or = document.createElement("span");
|
|
33
|
-
or.className = "shortcuts-or";
|
|
34
|
-
or.textContent = "or";
|
|
35
|
-
keys.appendChild(or);
|
|
36
|
-
const kbd2 = document.createElement("kbd");
|
|
37
|
-
kbd2.textContent = s.alt;
|
|
38
|
-
keys.appendChild(kbd2);
|
|
39
|
-
}
|
|
40
69
|
const desc = document.createElement("span");
|
|
41
70
|
desc.className = "shortcuts-desc";
|
|
42
71
|
desc.textContent = s.description;
|
|
@@ -58,10 +87,13 @@ export function initShortcuts(container) {
|
|
|
58
87
|
function hide() {
|
|
59
88
|
overlay.classList.add("hidden");
|
|
60
89
|
}
|
|
90
|
+
function toggle() {
|
|
91
|
+
overlay.classList.toggle("hidden");
|
|
92
|
+
}
|
|
61
93
|
closeBtn.addEventListener("click", hide);
|
|
62
94
|
overlay.addEventListener("click", (e) => {
|
|
63
95
|
if (e.target === overlay)
|
|
64
96
|
hide();
|
|
65
97
|
});
|
|
66
|
-
return { show, hide };
|
|
98
|
+
return { show, hide, toggle };
|
|
67
99
|
}
|
package/dist/sidebar.d.ts
CHANGED
|
@@ -2,8 +2,16 @@ import type { LearningGraphSummary } from "backpack-ontology";
|
|
|
2
2
|
export interface SidebarCallbacks {
|
|
3
3
|
onSelect: (name: string) => void;
|
|
4
4
|
onRename?: (oldName: string, newName: string) => void;
|
|
5
|
+
onBranchSwitch?: (graphName: string, branchName: string) => void;
|
|
6
|
+
onBranchCreate?: (graphName: string, branchName: string) => void;
|
|
7
|
+
onBranchDelete?: (graphName: string, branchName: string) => void;
|
|
5
8
|
}
|
|
6
9
|
export declare function initSidebar(container: HTMLElement, onSelectOrCallbacks: ((name: string) => void) | SidebarCallbacks): {
|
|
7
10
|
setSummaries(summaries: LearningGraphSummary[]): void;
|
|
8
11
|
setActive(name: string): void;
|
|
12
|
+
setActiveBranch(graphName: string, branchName: string, allBranches?: {
|
|
13
|
+
name: string;
|
|
14
|
+
active: boolean;
|
|
15
|
+
}[]): void;
|
|
16
|
+
toggle: () => void;
|
|
9
17
|
};
|