backpack-viewer 0.3.1 → 0.4.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/main.js CHANGED
@@ -517,7 +517,50 @@ async function main() {
517
517
  await deleteSnippet(graphName, snippetId);
518
518
  await refreshSnippets(graphName);
519
519
  },
520
+ onBackpackSwitch: async (name) => {
521
+ await fetch("/api/backpacks/switch", {
522
+ method: "POST",
523
+ headers: { "Content-Type": "application/json" },
524
+ body: JSON.stringify({ name }),
525
+ });
526
+ // The dev WS will fire an active-backpack-change event that
527
+ // triggers refreshBackpacksAndGraphs(). Production server has no
528
+ // live-reload channel, so we refresh immediately as fallback.
529
+ await refreshBackpacksAndGraphs();
530
+ },
531
+ onBackpackRegister: async (name, p, activate) => {
532
+ await fetch("/api/backpacks", {
533
+ method: "POST",
534
+ headers: { "Content-Type": "application/json" },
535
+ body: JSON.stringify({ name, path: p, activate }),
536
+ });
537
+ await refreshBackpacksAndGraphs();
538
+ },
520
539
  });
540
+ async function refreshBackpacksAndGraphs() {
541
+ try {
542
+ const res = await fetch("/api/backpacks");
543
+ const list = await res.json();
544
+ sidebar.setBackpacks(list);
545
+ }
546
+ catch { }
547
+ // Re-fetch ontology list from the (possibly new) active backpack
548
+ try {
549
+ const updated = await listOntologies();
550
+ sidebar.setSummaries(updated);
551
+ // If the previously-active graph no longer exists, clear it
552
+ if (activeOntology && !updated.some((g) => g.name === activeOntology)) {
553
+ activeOntology = "";
554
+ currentData = null;
555
+ canvas.loadGraph({
556
+ metadata: { name: "", description: "", createdAt: "", updatedAt: "" },
557
+ nodes: [],
558
+ edges: [],
559
+ });
560
+ }
561
+ }
562
+ catch { }
563
+ }
521
564
  function syncWalkTrail() {
522
565
  const trail = canvas.getWalkTrail();
523
566
  if (!currentData || trail.length === 0) {
@@ -719,6 +762,15 @@ async function main() {
719
762
  }
720
763
  }
721
764
  }
765
+ // Fetch backpack registry first so the picker shows the right active
766
+ // backpack from the initial render. Fire-and-forget ontology + remote
767
+ // list in parallel.
768
+ try {
769
+ const res = await fetch("/api/backpacks");
770
+ const list = await res.json();
771
+ sidebar.setBackpacks(list);
772
+ }
773
+ catch { }
722
774
  // Load ontology list (local + remote in parallel)
723
775
  const [summaries, remotes] = await Promise.all([
724
776
  listOntologies(),
@@ -871,6 +923,11 @@ async function main() {
871
923
  });
872
924
  // Live reload — when Claude adds nodes via MCP, re-fetch and re-render
873
925
  if (import.meta.hot) {
926
+ // Hot-swap when the active backpack changes (via /api/backpacks/switch
927
+ // or the user clicking the picker on another viewer tab)
928
+ import.meta.hot.on("active-backpack-change", async () => {
929
+ await refreshBackpacksAndGraphs();
930
+ });
874
931
  import.meta.hot.on("ontology-change", async () => {
875
932
  const [updated, updatedRemotes] = await Promise.all([
876
933
  listOntologies(),
package/dist/sidebar.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { LearningGraphSummary } from "backpack-ontology";
2
2
  import type { RemoteSummary } from "./api.js";
3
+ export interface BackpackSummary {
4
+ name: string;
5
+ path: string;
6
+ color: string;
7
+ active?: boolean;
8
+ }
3
9
  export interface SidebarCallbacks {
4
10
  onSelect: (name: string) => void;
5
11
  onRename?: (oldName: string, newName: string) => void;
@@ -8,8 +14,13 @@ export interface SidebarCallbacks {
8
14
  onBranchDelete?: (graphName: string, branchName: string) => void;
9
15
  onSnippetLoad?: (graphName: string, snippetId: string) => void;
10
16
  onSnippetDelete?: (graphName: string, snippetId: string) => void;
17
+ onBackpackSwitch?: (name: string) => void;
18
+ onBackpackRegister?: (name: string, path: string, activate: boolean) => void;
11
19
  }
12
20
  export declare function initSidebar(container: HTMLElement, onSelectOrCallbacks: ((name: string) => void) | SidebarCallbacks): {
21
+ setBackpacks(list: BackpackSummary[]): void;
22
+ setActiveBackpack(entry: BackpackSummary): void;
23
+ getActiveBackpack(): BackpackSummary | null;
13
24
  setSummaries(summaries: LearningGraphSummary[]): void;
14
25
  setActive(name: string): void;
15
26
  setRemotes(remotes: RemoteSummary[]): void;
package/dist/sidebar.js CHANGED
@@ -51,6 +51,112 @@ export function initSidebar(container, onSelectOrCallbacks) {
51
51
  headingRow.appendChild(heading);
52
52
  headingRow.appendChild(collapseBtn);
53
53
  container.appendChild(headingRow);
54
+ // Backpack picker pill — discrete indicator of the active backpack with
55
+ // a dropdown to switch between registered ones.
56
+ const backpackPicker = document.createElement("button");
57
+ backpackPicker.className = "backpack-picker-pill";
58
+ backpackPicker.type = "button";
59
+ backpackPicker.setAttribute("aria-haspopup", "listbox");
60
+ backpackPicker.setAttribute("aria-expanded", "false");
61
+ const pickerDot = document.createElement("span");
62
+ pickerDot.className = "backpack-picker-dot";
63
+ const pickerName = document.createElement("span");
64
+ pickerName.className = "backpack-picker-name";
65
+ pickerName.textContent = "...";
66
+ const pickerCaret = document.createElement("span");
67
+ pickerCaret.className = "backpack-picker-caret";
68
+ pickerCaret.textContent = "▾";
69
+ backpackPicker.appendChild(pickerDot);
70
+ backpackPicker.appendChild(pickerName);
71
+ backpackPicker.appendChild(pickerCaret);
72
+ const pickerDropdown = document.createElement("div");
73
+ pickerDropdown.className = "backpack-picker-dropdown";
74
+ pickerDropdown.hidden = true;
75
+ pickerDropdown.setAttribute("role", "listbox");
76
+ const pickerContainer = document.createElement("div");
77
+ pickerContainer.className = "backpack-picker-container";
78
+ pickerContainer.appendChild(backpackPicker);
79
+ pickerContainer.appendChild(pickerDropdown);
80
+ container.appendChild(pickerContainer);
81
+ let pickerOpen = false;
82
+ function closePicker() {
83
+ pickerOpen = false;
84
+ pickerDropdown.hidden = true;
85
+ backpackPicker.setAttribute("aria-expanded", "false");
86
+ }
87
+ function openPicker() {
88
+ pickerOpen = true;
89
+ pickerDropdown.hidden = false;
90
+ backpackPicker.setAttribute("aria-expanded", "true");
91
+ }
92
+ backpackPicker.addEventListener("click", (e) => {
93
+ e.stopPropagation();
94
+ if (pickerOpen)
95
+ closePicker();
96
+ else
97
+ openPicker();
98
+ });
99
+ // Click outside closes the dropdown
100
+ document.addEventListener("click", (e) => {
101
+ if (!pickerContainer.contains(e.target))
102
+ closePicker();
103
+ });
104
+ let currentBackpacks = [];
105
+ let currentActiveBackpack = null;
106
+ function renderPickerDropdown() {
107
+ pickerDropdown.replaceChildren();
108
+ for (const b of currentBackpacks) {
109
+ const item = document.createElement("button");
110
+ item.className = "backpack-picker-item";
111
+ item.type = "button";
112
+ item.setAttribute("role", "option");
113
+ if (b.active)
114
+ item.classList.add("active");
115
+ const dot = document.createElement("span");
116
+ dot.className = "backpack-picker-item-dot";
117
+ dot.style.setProperty("--backpack-color", b.color);
118
+ const name = document.createElement("span");
119
+ name.className = "backpack-picker-item-name";
120
+ name.textContent = b.name;
121
+ const path = document.createElement("span");
122
+ path.className = "backpack-picker-item-path";
123
+ path.textContent = b.path;
124
+ item.appendChild(dot);
125
+ item.appendChild(name);
126
+ item.appendChild(path);
127
+ item.addEventListener("click", (e) => {
128
+ e.stopPropagation();
129
+ closePicker();
130
+ if (!b.active && cbs.onBackpackSwitch) {
131
+ cbs.onBackpackSwitch(b.name);
132
+ }
133
+ });
134
+ pickerDropdown.appendChild(item);
135
+ }
136
+ // Separator + "Add new backpack..." action
137
+ const divider = document.createElement("div");
138
+ divider.className = "backpack-picker-divider";
139
+ pickerDropdown.appendChild(divider);
140
+ const addItem = document.createElement("button");
141
+ addItem.className = "backpack-picker-item backpack-picker-add";
142
+ addItem.type = "button";
143
+ addItem.textContent = "+ Add new backpack…";
144
+ addItem.addEventListener("click", async (e) => {
145
+ e.stopPropagation();
146
+ closePicker();
147
+ if (!cbs.onBackpackRegister)
148
+ return;
149
+ const name = await showPrompt("Backpack name", "Short kebab-case name (e.g. work, family, project-alpha)", "");
150
+ if (!name)
151
+ return;
152
+ const p = await showPrompt("Backpack path", "Absolute or tilde-expanded path to the graphs directory", "");
153
+ if (!p)
154
+ return;
155
+ const activate = await showConfirm("Switch to new backpack?", `Make "${name}" the active backpack immediately?`);
156
+ cbs.onBackpackRegister(name, p, activate);
157
+ });
158
+ pickerDropdown.appendChild(addItem);
159
+ }
54
160
  // Expand button — inserted into the canvas top-left bar when sidebar is collapsed
55
161
  const expandBtn = document.createElement("button");
56
162
  expandBtn.className = "tools-pane-toggle hidden";
@@ -79,6 +185,36 @@ export function initSidebar(container, onSelectOrCallbacks) {
79
185
  }
80
186
  });
81
187
  return {
188
+ setBackpacks(list) {
189
+ currentBackpacks = list.slice();
190
+ const active = list.find((b) => b.active) ?? null;
191
+ currentActiveBackpack = active;
192
+ if (active) {
193
+ pickerName.textContent = active.name;
194
+ pickerDot.style.setProperty("--backpack-color", active.color);
195
+ container.style.setProperty("--backpack-color", active.color);
196
+ }
197
+ renderPickerDropdown();
198
+ },
199
+ setActiveBackpack(entry) {
200
+ currentActiveBackpack = entry;
201
+ // Update the currentBackpacks list to reflect the new active
202
+ currentBackpacks = currentBackpacks.map((b) => ({
203
+ ...b,
204
+ active: b.name === entry.name,
205
+ }));
206
+ // If this name wasn't in the list (newly registered), include it
207
+ if (!currentBackpacks.some((b) => b.name === entry.name)) {
208
+ currentBackpacks.push({ ...entry, active: true });
209
+ }
210
+ pickerName.textContent = entry.name;
211
+ pickerDot.style.setProperty("--backpack-color", entry.color);
212
+ container.style.setProperty("--backpack-color", entry.color);
213
+ renderPickerDropdown();
214
+ },
215
+ getActiveBackpack() {
216
+ return currentActiveBackpack;
217
+ },
82
218
  setSummaries(summaries) {
83
219
  list.innerHTML = "";
84
220
  // Fetch all locks in one batch request, then distribute to items
package/dist/style.css CHANGED
@@ -110,6 +110,9 @@ body {
110
110
  min-width: 280px;
111
111
  background: var(--bg-surface);
112
112
  border-right: 1px solid var(--border);
113
+ /* Colored left border accents the active backpack — color is set via
114
+ CSS custom property --backpack-color updated on switch */
115
+ border-left: 3px solid var(--backpack-color, transparent);
113
116
  display: flex;
114
117
  flex-direction: column;
115
118
  padding: 16px;
@@ -315,6 +318,129 @@ body {
315
318
  opacity: 1;
316
319
  }
317
320
 
321
+ /* --- Backpack picker (active backpack indicator + switcher) --- */
322
+
323
+ .backpack-picker-container {
324
+ position: relative;
325
+ margin-bottom: 10px;
326
+ }
327
+
328
+ .backpack-picker-pill {
329
+ display: flex;
330
+ align-items: center;
331
+ gap: 6px;
332
+ width: 100%;
333
+ padding: 4px 8px;
334
+ background: var(--bg-base);
335
+ border: 1px solid var(--border);
336
+ border-radius: 999px;
337
+ color: var(--fg);
338
+ font-size: 11px;
339
+ font-family: inherit;
340
+ cursor: pointer;
341
+ text-align: left;
342
+ }
343
+
344
+ .backpack-picker-pill:hover {
345
+ border-color: var(--backpack-color, var(--accent));
346
+ }
347
+
348
+ .backpack-picker-dot {
349
+ display: inline-block;
350
+ width: 8px;
351
+ height: 8px;
352
+ border-radius: 50%;
353
+ background: var(--backpack-color, var(--accent));
354
+ flex-shrink: 0;
355
+ }
356
+
357
+ .backpack-picker-name {
358
+ flex: 1;
359
+ overflow: hidden;
360
+ text-overflow: ellipsis;
361
+ white-space: nowrap;
362
+ font-weight: 500;
363
+ }
364
+
365
+ .backpack-picker-caret {
366
+ opacity: 0.6;
367
+ font-size: 10px;
368
+ }
369
+
370
+ .backpack-picker-dropdown {
371
+ position: absolute;
372
+ top: calc(100% + 4px);
373
+ left: 0;
374
+ right: 0;
375
+ background: var(--bg-surface);
376
+ border: 1px solid var(--border);
377
+ border-radius: 8px;
378
+ box-shadow: 0 4px 16px var(--shadow);
379
+ z-index: 50;
380
+ max-height: 300px;
381
+ overflow-y: auto;
382
+ padding: 4px;
383
+ }
384
+
385
+ .backpack-picker-item {
386
+ display: flex;
387
+ align-items: center;
388
+ gap: 6px;
389
+ width: 100%;
390
+ padding: 6px 8px;
391
+ background: transparent;
392
+ border: none;
393
+ border-radius: 4px;
394
+ color: var(--fg);
395
+ font-family: inherit;
396
+ font-size: 11px;
397
+ cursor: pointer;
398
+ text-align: left;
399
+ }
400
+
401
+ .backpack-picker-item:hover {
402
+ background: var(--bg-hover);
403
+ }
404
+
405
+ .backpack-picker-item.active {
406
+ background: var(--bg-hover);
407
+ font-weight: 600;
408
+ }
409
+
410
+ .backpack-picker-item-dot {
411
+ display: inline-block;
412
+ width: 8px;
413
+ height: 8px;
414
+ border-radius: 50%;
415
+ background: var(--backpack-color, var(--accent));
416
+ flex-shrink: 0;
417
+ }
418
+
419
+ .backpack-picker-item-name {
420
+ flex-shrink: 0;
421
+ }
422
+
423
+ .backpack-picker-item-path {
424
+ flex: 1;
425
+ opacity: 0.55;
426
+ overflow: hidden;
427
+ text-overflow: ellipsis;
428
+ white-space: nowrap;
429
+ font-size: 9px;
430
+ text-align: right;
431
+ }
432
+
433
+ .backpack-picker-divider {
434
+ height: 1px;
435
+ background: var(--border);
436
+ margin: 4px 0;
437
+ }
438
+
439
+ .backpack-picker-add {
440
+ opacity: 0.75;
441
+ font-style: italic;
442
+ }
443
+
318
444
  .sidebar-lock-badge {
319
445
  font-size: 10px;
320
446
  color: #c08c00;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backpack-viewer",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Web-based graph visualizer for backpack-ontology — Canvas 2D, force-directed layout, live reload",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Noah Irzinger",
@@ -23,7 +23,7 @@
23
23
  "release:major": "npm version major && git push && git push --tags"
24
24
  },
25
25
  "dependencies": {
26
- "backpack-ontology": "^0.3.0"
26
+ "backpack-ontology": "^0.4.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^25.5.0",