backpack-viewer 0.2.17 → 0.2.20
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 +147 -0
- package/dist/api.d.ts +27 -0
- package/dist/api.js +71 -0
- package/dist/app/assets/index-BAsAhA_i.js +21 -0
- package/dist/app/assets/index-CvETIueX.css +1 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +16 -1
- package/dist/canvas.js +19 -20
- package/dist/config.js +7 -6
- package/dist/default-config.json +34 -1
- package/dist/dialog.d.ts +9 -0
- package/dist/dialog.js +119 -0
- package/dist/info-panel.js +14 -6
- package/dist/keybindings.d.ts +1 -1
- package/dist/keybindings.js +23 -14
- package/dist/main.js +88 -15
- package/dist/search.d.ts +5 -1
- package/dist/search.js +5 -3
- package/dist/shortcuts.js +1 -0
- package/dist/sidebar.d.ts +8 -0
- package/dist/sidebar.js +108 -2
- package/dist/style.css +262 -6
- package/dist/tools-pane.d.ts +9 -0
- package/dist/tools-pane.js +85 -11
- package/package.json +2 -2
- package/dist/app/assets/index-CKtt38XS.css +0 -1
- package/dist/app/assets/index-D-5q69aO.js +0 -21
package/dist/canvas.js
CHANGED
|
@@ -6,19 +6,18 @@ function cssVar(name) {
|
|
|
6
6
|
}
|
|
7
7
|
const NODE_RADIUS = 20;
|
|
8
8
|
const ALPHA_MIN = 0.001;
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const LOD_HIDE_EDGE_LABELS = 0.35; // hide edge labels even if enabled
|
|
13
|
-
const LOD_SMALL_NODES = 0.2; // shrink nodes to half size
|
|
14
|
-
const LOD_HIDE_ARROWS = 0.15; // hide arrowheads, draw 1px edges
|
|
9
|
+
// Defaults — overridden per-instance via config
|
|
10
|
+
const LOD_DEFAULTS = { hideBadges: 0.4, hideLabels: 0.25, hideEdgeLabels: 0.35, smallNodes: 0.2, hideArrows: 0.15 };
|
|
11
|
+
const NAV_DEFAULTS = { zoomFactor: 1.3, zoomMin: 0.05, zoomMax: 10, panAnimationMs: 300 };
|
|
15
12
|
/** Check if a point is within the visible viewport (with padding). */
|
|
16
13
|
function isInViewport(x, y, camera, canvasW, canvasH, pad = 100) {
|
|
17
14
|
const sx = (x - camera.x) * camera.scale;
|
|
18
15
|
const sy = (y - camera.y) * camera.scale;
|
|
19
16
|
return sx >= -pad && sx <= canvasW + pad && sy >= -pad && sy <= canvasH + pad;
|
|
20
17
|
}
|
|
21
|
-
export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
18
|
+
export function initCanvas(container, onNodeClick, onFocusChange, config) {
|
|
19
|
+
const lod = { ...LOD_DEFAULTS, ...(config?.lod ?? {}) };
|
|
20
|
+
const nav = { ...NAV_DEFAULTS, ...(config?.navigation ?? {}) };
|
|
22
21
|
const canvas = container.querySelector("canvas");
|
|
23
22
|
const ctx = canvas.getContext("2d");
|
|
24
23
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -41,7 +40,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
41
40
|
// Pan animation state
|
|
42
41
|
let panTarget = null;
|
|
43
42
|
let panStart = null;
|
|
44
|
-
const PAN_DURATION =
|
|
43
|
+
const PAN_DURATION = nav.panAnimationMs;
|
|
45
44
|
// --- Sizing ---
|
|
46
45
|
function resize() {
|
|
47
46
|
canvas.width = canvas.clientWidth * dpr;
|
|
@@ -102,7 +101,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
102
101
|
ctx.translate(-camera.x * camera.scale, -camera.y * camera.scale);
|
|
103
102
|
ctx.scale(camera.scale, camera.scale);
|
|
104
103
|
// Draw type hulls (shaded regions behind same-type nodes)
|
|
105
|
-
if (showTypeHulls && camera.scale >=
|
|
104
|
+
if (showTypeHulls && camera.scale >= lod.smallNodes) {
|
|
106
105
|
const typeGroups = new Map();
|
|
107
106
|
for (const node of state.nodes) {
|
|
108
107
|
if (filteredNodeIds !== null && !filteredNodeIds.has(node.id))
|
|
@@ -181,14 +180,14 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
181
180
|
: edgeDimmed
|
|
182
181
|
? edgeDimColor
|
|
183
182
|
: edgeColor;
|
|
184
|
-
ctx.lineWidth = camera.scale <
|
|
183
|
+
ctx.lineWidth = camera.scale < lod.hideArrows ? 1 : highlighted ? 2.5 : 1.5;
|
|
185
184
|
ctx.stroke();
|
|
186
185
|
// Arrowhead
|
|
187
|
-
if (camera.scale >=
|
|
186
|
+
if (camera.scale >= lod.hideArrows) {
|
|
188
187
|
drawArrowhead(source.x, source.y, target.x, target.y, highlighted, arrowColor, arrowHighlight);
|
|
189
188
|
}
|
|
190
189
|
// Edge label at midpoint
|
|
191
|
-
if (showEdgeLabels && camera.scale >=
|
|
190
|
+
if (showEdgeLabels && camera.scale >= lod.hideEdgeLabels) {
|
|
192
191
|
const mx = (source.x + target.x) / 2;
|
|
193
192
|
const my = (source.y + target.y) / 2;
|
|
194
193
|
ctx.fillStyle = highlighted
|
|
@@ -215,7 +214,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
215
214
|
const filteredOut = filteredNodeIds !== null && !filteredNodeIds.has(node.id);
|
|
216
215
|
const dimmed = filteredOut ||
|
|
217
216
|
(selectedNodeIds.size > 0 && !isSelected && !isNeighbor);
|
|
218
|
-
const r = camera.scale <
|
|
217
|
+
const r = camera.scale < lod.smallNodes ? NODE_RADIUS * 0.5 : NODE_RADIUS;
|
|
219
218
|
// Glow for selected node
|
|
220
219
|
if (isSelected) {
|
|
221
220
|
ctx.save();
|
|
@@ -238,7 +237,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
238
237
|
ctx.lineWidth = isSelected ? 3 : 1.5;
|
|
239
238
|
ctx.stroke();
|
|
240
239
|
// Label below
|
|
241
|
-
if (camera.scale >=
|
|
240
|
+
if (camera.scale >= lod.hideLabels) {
|
|
242
241
|
const label = node.label.length > 24 ? node.label.slice(0, 22) + "..." : node.label;
|
|
243
242
|
ctx.fillStyle = dimmed ? nodeLabelDim : nodeLabel;
|
|
244
243
|
ctx.font = "11px system-ui, sans-serif";
|
|
@@ -247,7 +246,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
247
246
|
ctx.fillText(label, node.x, node.y + r + 4);
|
|
248
247
|
}
|
|
249
248
|
// Type badge above
|
|
250
|
-
if (camera.scale >=
|
|
249
|
+
if (camera.scale >= lod.hideBadges) {
|
|
251
250
|
ctx.fillStyle = dimmed ? typeBadgeDim : typeBadge;
|
|
252
251
|
ctx.font = "9px system-ui, sans-serif";
|
|
253
252
|
ctx.textBaseline = "bottom";
|
|
@@ -471,7 +470,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
471
470
|
: e.deltaY > 0
|
|
472
471
|
? 0.9
|
|
473
472
|
: 1.1;
|
|
474
|
-
camera.scale = Math.max(
|
|
473
|
+
camera.scale = Math.max(nav.zoomMin, Math.min(nav.zoomMax, camera.scale * factor));
|
|
475
474
|
camera.x = wx - mx / camera.scale;
|
|
476
475
|
camera.y = wy - my / camera.scale;
|
|
477
476
|
render();
|
|
@@ -504,7 +503,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
504
503
|
if (current.length === 2 && touches.length === 2) {
|
|
505
504
|
const dist = touchDistance(current[0], current[1]);
|
|
506
505
|
const ratio = dist / initialPinchDist;
|
|
507
|
-
camera.scale = Math.max(
|
|
506
|
+
camera.scale = Math.max(nav.zoomMin, Math.min(nav.zoomMax, initialPinchScale * ratio));
|
|
508
507
|
render();
|
|
509
508
|
}
|
|
510
509
|
else if (current.length === 1) {
|
|
@@ -567,7 +566,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
567
566
|
const cx = canvas.clientWidth / 2;
|
|
568
567
|
const cy = canvas.clientHeight / 2;
|
|
569
568
|
const [wx, wy] = screenToWorld(cx, cy);
|
|
570
|
-
camera.scale = Math.min(
|
|
569
|
+
camera.scale = Math.min(nav.zoomMax, camera.scale * nav.zoomFactor);
|
|
571
570
|
camera.x = wx - cx / camera.scale;
|
|
572
571
|
camera.y = wy - cy / camera.scale;
|
|
573
572
|
render();
|
|
@@ -580,7 +579,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
580
579
|
const cx = canvas.clientWidth / 2;
|
|
581
580
|
const cy = canvas.clientHeight / 2;
|
|
582
581
|
const [wx, wy] = screenToWorld(cx, cy);
|
|
583
|
-
camera.scale = Math.max(
|
|
582
|
+
camera.scale = Math.max(nav.zoomMin, camera.scale / nav.zoomFactor);
|
|
584
583
|
camera.x = wx - cx / camera.scale;
|
|
585
584
|
camera.y = wy - cy / camera.scale;
|
|
586
585
|
render();
|
|
@@ -750,7 +749,7 @@ export function initCanvas(container, onNodeClick, onFocusChange) {
|
|
|
750
749
|
const cx = canvas.clientWidth / 2;
|
|
751
750
|
const cy = canvas.clientHeight / 2;
|
|
752
751
|
const [wx, wy] = screenToWorld(cx, cy);
|
|
753
|
-
camera.scale = Math.max(
|
|
752
|
+
camera.scale = Math.max(nav.zoomMin, Math.min(nav.zoomMax, camera.scale * factor));
|
|
754
753
|
camera.x = wx - cx / camera.scale;
|
|
755
754
|
camera.y = wy - cy / camera.scale;
|
|
756
755
|
render();
|
package/dist/config.js
CHANGED
|
@@ -16,13 +16,14 @@ export function loadViewerConfig() {
|
|
|
16
16
|
const filePath = viewerConfigFile();
|
|
17
17
|
try {
|
|
18
18
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
19
|
-
const
|
|
19
|
+
const user = JSON.parse(raw);
|
|
20
20
|
return {
|
|
21
|
-
...defaultConfig,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
21
|
+
keybindings: { ...defaultConfig.keybindings, ...(user.keybindings ?? {}) },
|
|
22
|
+
display: { ...defaultConfig.display, ...(user.display ?? {}) },
|
|
23
|
+
layout: { ...defaultConfig.layout, ...(user.layout ?? {}) },
|
|
24
|
+
navigation: { ...defaultConfig.navigation, ...(user.navigation ?? {}) },
|
|
25
|
+
lod: { ...defaultConfig.lod, ...(user.lod ?? {}) },
|
|
26
|
+
limits: { ...defaultConfig.limits, ...(user.limits ?? {}) },
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
catch {
|
package/dist/default-config.json
CHANGED
|
@@ -28,6 +28,39 @@
|
|
|
28
28
|
"spacingDecrease": "[",
|
|
29
29
|
"spacingIncrease": "]",
|
|
30
30
|
"clusteringDecrease": "{",
|
|
31
|
-
"clusteringIncrease": "}"
|
|
31
|
+
"clusteringIncrease": "}",
|
|
32
|
+
"toggleSidebar": "Tab"
|
|
33
|
+
},
|
|
34
|
+
"display": {
|
|
35
|
+
"edges": true,
|
|
36
|
+
"edgeLabels": true,
|
|
37
|
+
"typeHulls": true,
|
|
38
|
+
"minimap": true,
|
|
39
|
+
"theme": "system"
|
|
40
|
+
},
|
|
41
|
+
"layout": {
|
|
42
|
+
"spacing": 1.5,
|
|
43
|
+
"clustering": 0.08
|
|
44
|
+
},
|
|
45
|
+
"navigation": {
|
|
46
|
+
"panSpeed": 60,
|
|
47
|
+
"panFastMultiplier": 3,
|
|
48
|
+
"zoomFactor": 1.3,
|
|
49
|
+
"zoomMin": 0.05,
|
|
50
|
+
"zoomMax": 10,
|
|
51
|
+
"panAnimationMs": 300
|
|
52
|
+
},
|
|
53
|
+
"lod": {
|
|
54
|
+
"hideBadges": 0.4,
|
|
55
|
+
"hideLabels": 0.25,
|
|
56
|
+
"hideEdgeLabels": 0.35,
|
|
57
|
+
"smallNodes": 0.2,
|
|
58
|
+
"hideArrows": 0.15
|
|
59
|
+
},
|
|
60
|
+
"limits": {
|
|
61
|
+
"maxSearchResults": 8,
|
|
62
|
+
"maxQualityItems": 5,
|
|
63
|
+
"maxMostConnected": 5,
|
|
64
|
+
"searchDebounceMs": 150
|
|
32
65
|
}
|
|
33
66
|
}
|
package/dist/dialog.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Lightweight inline dialog system — replaces native alert/confirm/prompt. */
|
|
2
|
+
/** Show a confirmation dialog. Returns a promise that resolves to true/false. */
|
|
3
|
+
export declare function showConfirm(title: string, message: string): Promise<boolean>;
|
|
4
|
+
/** Show a prompt dialog with an input field. Returns null if cancelled. */
|
|
5
|
+
export declare function showPrompt(title: string, placeholder?: string, defaultValue?: string): Promise<string | null>;
|
|
6
|
+
/** Show a danger confirmation (for destructive actions). */
|
|
7
|
+
export declare function showDangerConfirm(title: string, message: string): Promise<boolean>;
|
|
8
|
+
/** Show a brief toast notification. */
|
|
9
|
+
export declare function showToast(message: string, durationMs?: number): void;
|
package/dist/dialog.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Lightweight inline dialog system — replaces native alert/confirm/prompt. */
|
|
2
|
+
const DIALOG_CSS_CLASS = "bp-dialog-overlay";
|
|
3
|
+
function createOverlay() {
|
|
4
|
+
// Remove any existing dialog
|
|
5
|
+
document.querySelector(`.${DIALOG_CSS_CLASS}`)?.remove();
|
|
6
|
+
const overlay = document.createElement("div");
|
|
7
|
+
overlay.className = DIALOG_CSS_CLASS;
|
|
8
|
+
document.body.appendChild(overlay);
|
|
9
|
+
return overlay;
|
|
10
|
+
}
|
|
11
|
+
function createModal(overlay, title) {
|
|
12
|
+
const modal = document.createElement("div");
|
|
13
|
+
modal.className = "bp-dialog";
|
|
14
|
+
const heading = document.createElement("h4");
|
|
15
|
+
heading.className = "bp-dialog-title";
|
|
16
|
+
heading.textContent = title;
|
|
17
|
+
modal.appendChild(heading);
|
|
18
|
+
overlay.appendChild(modal);
|
|
19
|
+
overlay.addEventListener("click", (e) => {
|
|
20
|
+
if (e.target === overlay)
|
|
21
|
+
overlay.remove();
|
|
22
|
+
});
|
|
23
|
+
return modal;
|
|
24
|
+
}
|
|
25
|
+
function addButtons(modal, buttons) {
|
|
26
|
+
const row = document.createElement("div");
|
|
27
|
+
row.className = "bp-dialog-buttons";
|
|
28
|
+
for (const btn of buttons) {
|
|
29
|
+
const el = document.createElement("button");
|
|
30
|
+
el.className = "bp-dialog-btn";
|
|
31
|
+
if (btn.accent)
|
|
32
|
+
el.classList.add("bp-dialog-btn-accent");
|
|
33
|
+
if (btn.danger)
|
|
34
|
+
el.classList.add("bp-dialog-btn-danger");
|
|
35
|
+
el.textContent = btn.label;
|
|
36
|
+
el.addEventListener("click", btn.onClick);
|
|
37
|
+
row.appendChild(el);
|
|
38
|
+
}
|
|
39
|
+
modal.appendChild(row);
|
|
40
|
+
}
|
|
41
|
+
/** Show a confirmation dialog. Returns a promise that resolves to true/false. */
|
|
42
|
+
export function showConfirm(title, message) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const overlay = createOverlay();
|
|
45
|
+
const modal = createModal(overlay, title);
|
|
46
|
+
const msg = document.createElement("p");
|
|
47
|
+
msg.className = "bp-dialog-message";
|
|
48
|
+
msg.textContent = message;
|
|
49
|
+
modal.appendChild(msg);
|
|
50
|
+
addButtons(modal, [
|
|
51
|
+
{ label: "Cancel", onClick: () => { overlay.remove(); resolve(false); } },
|
|
52
|
+
{ label: "Confirm", accent: true, onClick: () => { overlay.remove(); resolve(true); } },
|
|
53
|
+
]);
|
|
54
|
+
// Focus the confirm button
|
|
55
|
+
modal.querySelector(".bp-dialog-btn-accent")?.focus();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Show a prompt dialog with an input field. Returns null if cancelled. */
|
|
59
|
+
export function showPrompt(title, placeholder, defaultValue) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const overlay = createOverlay();
|
|
62
|
+
const modal = createModal(overlay, title);
|
|
63
|
+
const input = document.createElement("input");
|
|
64
|
+
input.type = "text";
|
|
65
|
+
input.className = "bp-dialog-input";
|
|
66
|
+
input.placeholder = placeholder ?? "";
|
|
67
|
+
input.value = defaultValue ?? "";
|
|
68
|
+
modal.appendChild(input);
|
|
69
|
+
const submit = () => {
|
|
70
|
+
const val = input.value.trim();
|
|
71
|
+
overlay.remove();
|
|
72
|
+
resolve(val || null);
|
|
73
|
+
};
|
|
74
|
+
input.addEventListener("keydown", (e) => {
|
|
75
|
+
if (e.key === "Enter")
|
|
76
|
+
submit();
|
|
77
|
+
if (e.key === "Escape") {
|
|
78
|
+
overlay.remove();
|
|
79
|
+
resolve(null);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
addButtons(modal, [
|
|
83
|
+
{ label: "Cancel", onClick: () => { overlay.remove(); resolve(null); } },
|
|
84
|
+
{ label: "OK", accent: true, onClick: submit },
|
|
85
|
+
]);
|
|
86
|
+
input.focus();
|
|
87
|
+
input.select();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Show a danger confirmation (for destructive actions). */
|
|
91
|
+
export function showDangerConfirm(title, message) {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
const overlay = createOverlay();
|
|
94
|
+
const modal = createModal(overlay, title);
|
|
95
|
+
const msg = document.createElement("p");
|
|
96
|
+
msg.className = "bp-dialog-message";
|
|
97
|
+
msg.textContent = message;
|
|
98
|
+
modal.appendChild(msg);
|
|
99
|
+
addButtons(modal, [
|
|
100
|
+
{ label: "Cancel", onClick: () => { overlay.remove(); resolve(false); } },
|
|
101
|
+
{ label: "Delete", danger: true, onClick: () => { overlay.remove(); resolve(true); } },
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Show a brief toast notification. */
|
|
106
|
+
export function showToast(message, durationMs = 3000) {
|
|
107
|
+
const existing = document.querySelector(".bp-toast");
|
|
108
|
+
if (existing)
|
|
109
|
+
existing.remove();
|
|
110
|
+
const toast = document.createElement("div");
|
|
111
|
+
toast.className = "bp-toast";
|
|
112
|
+
toast.textContent = message;
|
|
113
|
+
document.body.appendChild(toast);
|
|
114
|
+
setTimeout(() => toast.classList.add("bp-toast-visible"), 10);
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
toast.classList.remove("bp-toast-visible");
|
|
117
|
+
setTimeout(() => toast.remove(), 300);
|
|
118
|
+
}, durationMs);
|
|
119
|
+
}
|
package/dist/info-panel.js
CHANGED
|
@@ -132,8 +132,11 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
132
132
|
panel.classList.remove("hidden");
|
|
133
133
|
if (maximized)
|
|
134
134
|
panel.classList.add("info-panel-maximized");
|
|
135
|
+
// Pinned header area (toolbar + node identity)
|
|
136
|
+
const pinnedHeader = document.createElement("div");
|
|
137
|
+
pinnedHeader.className = "info-panel-header";
|
|
135
138
|
// Toolbar (back, forward, maximize, close)
|
|
136
|
-
|
|
139
|
+
pinnedHeader.appendChild(createToolbar());
|
|
137
140
|
// Header: type badge + label
|
|
138
141
|
const header = document.createElement("div");
|
|
139
142
|
header.className = "info-header";
|
|
@@ -187,7 +190,11 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
187
190
|
header.appendChild(typeBadge);
|
|
188
191
|
header.appendChild(label);
|
|
189
192
|
header.appendChild(nodeIdEl);
|
|
190
|
-
|
|
193
|
+
pinnedHeader.appendChild(header);
|
|
194
|
+
panel.appendChild(pinnedHeader);
|
|
195
|
+
// Scrollable body for properties, connections, timestamps
|
|
196
|
+
const body = document.createElement("div");
|
|
197
|
+
body.className = "info-panel-body";
|
|
191
198
|
// Properties section (editable)
|
|
192
199
|
const propKeys = Object.keys(node.properties);
|
|
193
200
|
const propSection = createSection("Properties");
|
|
@@ -272,7 +279,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
272
279
|
});
|
|
273
280
|
propSection.appendChild(addBtn);
|
|
274
281
|
}
|
|
275
|
-
|
|
282
|
+
body.appendChild(propSection);
|
|
276
283
|
// Connections section
|
|
277
284
|
if (connectedEdges.length > 0) {
|
|
278
285
|
const section = createSection(`Connections (${connectedEdges.length})`);
|
|
@@ -341,7 +348,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
341
348
|
list.appendChild(li);
|
|
342
349
|
}
|
|
343
350
|
section.appendChild(list);
|
|
344
|
-
|
|
351
|
+
body.appendChild(section);
|
|
345
352
|
}
|
|
346
353
|
// Timestamps
|
|
347
354
|
const tsSection = createSection("Timestamps");
|
|
@@ -360,7 +367,7 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
360
367
|
timestamps.appendChild(updatedDt);
|
|
361
368
|
timestamps.appendChild(updatedDd);
|
|
362
369
|
tsSection.appendChild(timestamps);
|
|
363
|
-
|
|
370
|
+
body.appendChild(tsSection);
|
|
364
371
|
// Delete node button
|
|
365
372
|
if (callbacks) {
|
|
366
373
|
const deleteSection = document.createElement("div");
|
|
@@ -373,8 +380,9 @@ export function initInfoPanel(container, callbacks, onNavigateToNode, onFocus) {
|
|
|
373
380
|
hide();
|
|
374
381
|
});
|
|
375
382
|
deleteSection.appendChild(deleteBtn);
|
|
376
|
-
|
|
383
|
+
body.appendChild(deleteSection);
|
|
377
384
|
}
|
|
385
|
+
panel.appendChild(body);
|
|
378
386
|
}
|
|
379
387
|
function showMulti(nodeIds, data) {
|
|
380
388
|
const selectedSet = new Set(nodeIds);
|
package/dist/keybindings.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type KeybindingAction = "search" | "searchAlt" | "undo" | "redo" | "help" | "escape" | "focus" | "toggleEdges" | "center" | "nextNode" | "prevNode" | "nextConnection" | "prevConnection" | "historyBack" | "historyForward" | "hopsIncrease" | "hopsDecrease" | "panLeft" | "panDown" | "panUp" | "panRight" | "panFastLeft" | "zoomOut" | "zoomIn" | "panFastRight" | "spacingDecrease" | "spacingIncrease" | "clusteringDecrease" | "clusteringIncrease";
|
|
1
|
+
export type KeybindingAction = "search" | "searchAlt" | "undo" | "redo" | "help" | "escape" | "focus" | "toggleEdges" | "center" | "nextNode" | "prevNode" | "nextConnection" | "prevConnection" | "historyBack" | "historyForward" | "hopsIncrease" | "hopsDecrease" | "panLeft" | "panDown" | "panUp" | "panRight" | "panFastLeft" | "zoomOut" | "zoomIn" | "panFastRight" | "spacingDecrease" | "spacingIncrease" | "clusteringDecrease" | "clusteringIncrease" | "toggleSidebar";
|
|
2
2
|
export type KeybindingMap = Record<KeybindingAction, string>;
|
|
3
3
|
/** Parse a binding string like "ctrl+shift+z" and check if it matches a KeyboardEvent. */
|
|
4
4
|
export declare function matchKey(e: KeyboardEvent, binding: string): boolean;
|
package/dist/keybindings.js
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
/** Parse a binding string like "ctrl+shift+z" and check if it matches a KeyboardEvent. */
|
|
2
2
|
export function matchKey(e, binding) {
|
|
3
|
-
const parts = binding.
|
|
3
|
+
const parts = binding.split("+");
|
|
4
4
|
const key = parts.pop();
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
5
|
+
const modifiers = parts.map((p) => p.toLowerCase());
|
|
6
|
+
const needCtrl = modifiers.includes("ctrl") || modifiers.includes("cmd") || modifiers.includes("meta");
|
|
7
|
+
const explicitShift = modifiers.includes("shift");
|
|
8
|
+
const needAlt = modifiers.includes("alt");
|
|
9
|
+
// Ctrl/meta check
|
|
9
10
|
if (needCtrl !== (e.ctrlKey || e.metaKey))
|
|
10
11
|
return false;
|
|
11
|
-
if (
|
|
12
|
+
if (!needCtrl && (e.ctrlKey || e.metaKey))
|
|
12
13
|
return false;
|
|
13
|
-
|
|
14
|
+
// Only enforce shift when explicitly in the binding (e.g. "ctrl+shift+z").
|
|
15
|
+
// Plain chars like "K", ">", "?" implicitly require shift via their character.
|
|
16
|
+
if (explicitShift && !e.shiftKey)
|
|
14
17
|
return false;
|
|
15
|
-
//
|
|
16
|
-
if (
|
|
18
|
+
// Alt check
|
|
19
|
+
if (needAlt !== e.altKey)
|
|
17
20
|
return false;
|
|
18
|
-
//
|
|
19
|
-
if (key === "escape")
|
|
21
|
+
// Named keys
|
|
22
|
+
if (key.toLowerCase() === "escape")
|
|
20
23
|
return e.key === "Escape";
|
|
21
|
-
if (key.
|
|
22
|
-
return e.key ===
|
|
23
|
-
|
|
24
|
+
if (key.toLowerCase() === "tab")
|
|
25
|
+
return e.key === "Tab";
|
|
26
|
+
// For modified bindings (ctrl+z, ctrl+shift+z), compare case-insensitively
|
|
27
|
+
// because browsers vary on e.key casing when modifiers are held.
|
|
28
|
+
if (modifiers.length > 0)
|
|
29
|
+
return e.key.toLowerCase() === key.toLowerCase();
|
|
30
|
+
// For plain keys, compare exactly — "k" vs "K" distinguishes shift state.
|
|
31
|
+
return e.key === key;
|
|
24
32
|
}
|
|
25
33
|
/** Build a reverse map: for each action, store its binding string. Used by the help modal. */
|
|
26
34
|
export function actionDescriptions() {
|
|
@@ -54,5 +62,6 @@ export function actionDescriptions() {
|
|
|
54
62
|
spacingIncrease: "Increase spacing",
|
|
55
63
|
clusteringDecrease: "Decrease clustering",
|
|
56
64
|
clusteringIncrease: "Increase clustering",
|
|
65
|
+
toggleSidebar: "Toggle sidebar",
|
|
57
66
|
};
|
|
58
67
|
}
|
package/dist/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
@@ -15,20 +15,29 @@ let activeOntology = "";
|
|
|
15
15
|
let currentData = null;
|
|
16
16
|
async function main() {
|
|
17
17
|
const canvasContainer = document.getElementById("canvas-container");
|
|
18
|
-
// --- Load config
|
|
19
|
-
|
|
18
|
+
// --- Load config ---
|
|
19
|
+
const cfg = { ...defaultConfig };
|
|
20
20
|
try {
|
|
21
21
|
const res = await fetch("/api/config");
|
|
22
22
|
if (res.ok) {
|
|
23
|
-
const
|
|
24
|
-
|
|
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 ?? {});
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
catch { /* use defaults */ }
|
|
33
|
+
const bindings = cfg.keybindings;
|
|
28
34
|
// --- Theme toggle (top-right of canvas) ---
|
|
29
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;
|
|
30
39
|
const stored = localStorage.getItem("backpack-theme");
|
|
31
|
-
const initial = stored ??
|
|
40
|
+
const initial = stored ?? themeDefault;
|
|
32
41
|
document.documentElement.setAttribute("data-theme", initial);
|
|
33
42
|
const themeBtn = document.createElement("button");
|
|
34
43
|
themeBtn.className = "theme-toggle";
|
|
@@ -138,8 +147,8 @@ async function main() {
|
|
|
138
147
|
const mobileQuery = window.matchMedia("(max-width: 768px)");
|
|
139
148
|
// Track current selection for keyboard shortcuts
|
|
140
149
|
let currentSelection = [];
|
|
141
|
-
let edgesVisible =
|
|
142
|
-
let panSpeed =
|
|
150
|
+
let edgesVisible = cfg.display.edges;
|
|
151
|
+
let panSpeed = cfg.navigation.panSpeed;
|
|
143
152
|
let viewCycleIndex = -1;
|
|
144
153
|
// --- Focus indicator (top bar pill) ---
|
|
145
154
|
let focusIndicator = null;
|
|
@@ -215,8 +224,11 @@ async function main() {
|
|
|
215
224
|
if (activeOntology)
|
|
216
225
|
updateUrl(activeOntology);
|
|
217
226
|
}
|
|
227
|
+
}, { lod: cfg.lod, navigation: cfg.navigation });
|
|
228
|
+
const search = initSearch(canvasContainer, {
|
|
229
|
+
maxResults: cfg.limits.maxSearchResults,
|
|
230
|
+
debounceMs: cfg.limits.searchDebounceMs,
|
|
218
231
|
});
|
|
219
|
-
const search = initSearch(canvasContainer);
|
|
220
232
|
const toolsPane = initToolsPane(canvasContainer, {
|
|
221
233
|
onFilterByType(type) {
|
|
222
234
|
if (!currentData)
|
|
@@ -293,6 +305,22 @@ async function main() {
|
|
|
293
305
|
link.href = dataUrl;
|
|
294
306
|
link.click();
|
|
295
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
|
+
},
|
|
296
324
|
onOpen() {
|
|
297
325
|
if (mobileQuery.matches)
|
|
298
326
|
infoPanel.hide();
|
|
@@ -354,9 +382,46 @@ async function main() {
|
|
|
354
382
|
toolsPane.setData(currentData);
|
|
355
383
|
}
|
|
356
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
|
+
},
|
|
357
402
|
});
|
|
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
|
+
}
|
|
358
414
|
const shortcuts = initShortcuts(canvasContainer, bindings);
|
|
359
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);
|
|
360
425
|
// --- URL deep linking ---
|
|
361
426
|
function updateUrl(name, nodeIds) {
|
|
362
427
|
const parts = [];
|
|
@@ -403,12 +468,19 @@ async function main() {
|
|
|
403
468
|
search.clear();
|
|
404
469
|
undoHistory.clear();
|
|
405
470
|
currentData = await loadOntology(name);
|
|
406
|
-
|
|
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
|
+
});
|
|
407
476
|
canvas.loadGraph(currentData);
|
|
408
477
|
search.setLearningGraphData(currentData);
|
|
409
478
|
toolsPane.setData(currentData);
|
|
410
479
|
emptyState.hide();
|
|
411
480
|
updateUrl(name);
|
|
481
|
+
// Load branches and snapshots
|
|
482
|
+
await refreshBranches(name);
|
|
483
|
+
await refreshSnapshots(name);
|
|
412
484
|
// Restore focus mode if requested
|
|
413
485
|
if (focusSeedIds?.length && currentData) {
|
|
414
486
|
const validFocus = focusSeedIds.filter((id) => currentData.nodes.some((n) => n.id === id));
|
|
@@ -504,15 +576,16 @@ async function main() {
|
|
|
504
576
|
panDown() { canvas.panBy(0, panSpeed); },
|
|
505
577
|
panUp() { canvas.panBy(0, -panSpeed); },
|
|
506
578
|
panRight() { canvas.panBy(panSpeed, 0); },
|
|
507
|
-
panFastLeft() { canvas.panBy(-panSpeed *
|
|
508
|
-
zoomOut() { canvas.zoomBy(
|
|
509
|
-
zoomIn() { canvas.zoomBy(
|
|
510
|
-
panFastRight() { canvas.panBy(panSpeed *
|
|
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); },
|
|
511
583
|
spacingDecrease() { const p = getLayoutParams(); setLayoutParams({ spacing: Math.max(0.5, p.spacing - 0.5) }); canvas.reheat(); },
|
|
512
584
|
spacingIncrease() { const p = getLayoutParams(); setLayoutParams({ spacing: Math.min(20, p.spacing + 0.5) }); canvas.reheat(); },
|
|
513
585
|
clusteringDecrease() { const p = getLayoutParams(); setLayoutParams({ clusterStrength: Math.max(0, p.clusterStrength - 0.03) }); canvas.reheat(); },
|
|
514
586
|
clusteringIncrease() { const p = getLayoutParams(); setLayoutParams({ clusterStrength: Math.min(1, p.clusterStrength + 0.03) }); canvas.reheat(); },
|
|
515
587
|
help() { shortcuts.toggle(); },
|
|
588
|
+
toggleSidebar() { sidebar.toggle(); },
|
|
516
589
|
escape() { if (canvas.isFocused()) {
|
|
517
590
|
toolsPane.clearFocusSet();
|
|
518
591
|
}
|
|
@@ -525,7 +598,7 @@ async function main() {
|
|
|
525
598
|
return;
|
|
526
599
|
for (const [action, binding] of Object.entries(bindings)) {
|
|
527
600
|
if (matchKey(e, binding)) {
|
|
528
|
-
const needsPrevent = action === "search" || action === "searchAlt" || action === "undo" || action === "redo";
|
|
601
|
+
const needsPrevent = action === "search" || action === "searchAlt" || action === "undo" || action === "redo" || action === "toggleSidebar";
|
|
529
602
|
if (needsPrevent)
|
|
530
603
|
e.preventDefault();
|
|
531
604
|
actions[action]?.();
|
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;
|