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/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 ?? (prefersDark.matches ? "dark" : "light");
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, 1);
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
- const shortcuts = initShortcuts(canvasContainer);
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
- document.addEventListener("keydown", (e) => {
433
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
434
- return;
435
- if (e.key === "/" || (e.key === "k" && (e.metaKey || e.ctrlKey))) {
436
- e.preventDefault();
437
- search.focus();
438
- }
439
- else if (e.key === "z" && (e.metaKey || e.ctrlKey) && e.shiftKey) {
440
- e.preventDefault();
441
- if (currentData) {
442
- const restored = undoHistory.redo(currentData);
443
- if (restored)
444
- applyState(restored);
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
- else if (e.key === "?") {
465
- shortcuts.show();
466
- }
467
- else if (e.key === "Escape") {
468
- if (canvas.isFocused()) {
469
- toolsPane.clearFocusSet();
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
- else {
472
- shortcuts.hide();
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 declare function initSearch(container: HTMLElement): {
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
- // --- Type chips ---
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
- const noChipsSelected = activeTypes.size === 0;
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
- // If chips are selected, only include those types
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 >= 8)
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, 150);
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
- if (e.key === "Escape") {
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() {
@@ -1,4 +1,6 @@
1
- export declare function initShortcuts(container: HTMLElement): {
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
- const SHORTCUTS = [
2
- { key: "/", alt: "Ctrl+K", description: "Focus search" },
3
- { key: "Ctrl+Z", description: "Undo" },
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
- export function initShortcuts(container) {
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
- for (const s of SHORTCUTS) {
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
  };