backpack-viewer 0.2.16 → 0.2.17
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 +8 -0
- package/dist/app/assets/index-CKtt38XS.css +1 -0
- package/dist/app/assets/index-D-5q69aO.js +21 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +6 -0
- package/dist/canvas.js +138 -59
- package/dist/config.d.ts +4 -0
- package/dist/config.js +31 -0
- package/dist/default-config.json +33 -0
- package/dist/info-panel.d.ts +4 -0
- package/dist/info-panel.js +52 -5
- package/dist/keybindings.d.ts +6 -0
- package/dist/keybindings.js +58 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +18 -8
- package/dist/main.js +99 -39
- package/dist/search.js +41 -82
- package/dist/shortcuts.d.ts +3 -1
- package/dist/shortcuts.js +50 -19
- package/dist/style.css +74 -1
- package/dist/tools-pane.d.ts +1 -0
- package/dist/tools-pane.js +334 -149
- 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
|
@@ -4,15 +4,27 @@ 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 (keybindings) ---
|
|
19
|
+
let bindings = defaultConfig.keybindings;
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch("/api/config");
|
|
22
|
+
if (res.ok) {
|
|
23
|
+
const config = await res.json();
|
|
24
|
+
bindings = { ...bindings, ...(config.keybindings ?? {}) };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch { /* use defaults */ }
|
|
16
28
|
// --- Theme toggle (top-right of canvas) ---
|
|
17
29
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
|
|
18
30
|
const stored = localStorage.getItem("backpack-theme");
|
|
@@ -126,6 +138,9 @@ async function main() {
|
|
|
126
138
|
const mobileQuery = window.matchMedia("(max-width: 768px)");
|
|
127
139
|
// Track current selection for keyboard shortcuts
|
|
128
140
|
let currentSelection = [];
|
|
141
|
+
let edgesVisible = true;
|
|
142
|
+
let panSpeed = 60;
|
|
143
|
+
let viewCycleIndex = -1;
|
|
129
144
|
// --- Focus indicator (top bar pill) ---
|
|
130
145
|
let focusIndicator = null;
|
|
131
146
|
function buildFocusIndicator(info) {
|
|
@@ -188,14 +203,15 @@ async function main() {
|
|
|
188
203
|
}, (focus) => {
|
|
189
204
|
if (focus) {
|
|
190
205
|
buildFocusIndicator(focus);
|
|
191
|
-
// Insert into top-left, after tools toggle
|
|
192
206
|
const topLeft = canvasContainer.querySelector(".canvas-top-left");
|
|
193
207
|
if (topLeft && focusIndicator)
|
|
194
208
|
topLeft.appendChild(focusIndicator);
|
|
195
209
|
updateUrl(activeOntology, focus.seedNodeIds);
|
|
210
|
+
infoPanel.setFocusDisabled(focus.hops === 0);
|
|
196
211
|
}
|
|
197
212
|
else {
|
|
198
213
|
removeFocusIndicator();
|
|
214
|
+
infoPanel.setFocusDisabled(false);
|
|
199
215
|
if (activeOntology)
|
|
200
216
|
updateUrl(activeOntology);
|
|
201
217
|
}
|
|
@@ -222,7 +238,7 @@ async function main() {
|
|
|
222
238
|
},
|
|
223
239
|
onFocusChange(seedNodeIds) {
|
|
224
240
|
if (seedNodeIds && seedNodeIds.length > 0) {
|
|
225
|
-
canvas.enterFocus(seedNodeIds,
|
|
241
|
+
canvas.enterFocus(seedNodeIds, 0);
|
|
226
242
|
}
|
|
227
243
|
else {
|
|
228
244
|
if (canvas.isFocused())
|
|
@@ -265,6 +281,9 @@ async function main() {
|
|
|
265
281
|
setLayoutParams({ [param]: value });
|
|
266
282
|
canvas.reheat();
|
|
267
283
|
},
|
|
284
|
+
onPanSpeedChange(speed) {
|
|
285
|
+
panSpeed = speed;
|
|
286
|
+
},
|
|
268
287
|
onExport(format) {
|
|
269
288
|
const dataUrl = canvas.exportImage(format);
|
|
270
289
|
if (!dataUrl)
|
|
@@ -336,7 +355,7 @@ async function main() {
|
|
|
336
355
|
}
|
|
337
356
|
},
|
|
338
357
|
});
|
|
339
|
-
const shortcuts = initShortcuts(canvasContainer);
|
|
358
|
+
const shortcuts = initShortcuts(canvasContainer, bindings);
|
|
340
359
|
const emptyState = initEmptyState(canvasContainer);
|
|
341
360
|
// --- URL deep linking ---
|
|
342
361
|
function updateUrl(name, nodeIds) {
|
|
@@ -384,6 +403,7 @@ async function main() {
|
|
|
384
403
|
search.clear();
|
|
385
404
|
undoHistory.clear();
|
|
386
405
|
currentData = await loadOntology(name);
|
|
406
|
+
setLayoutParams(autoLayoutParams(currentData.nodes.length));
|
|
387
407
|
canvas.loadGraph(currentData);
|
|
388
408
|
search.setLearningGraphData(currentData);
|
|
389
409
|
toolsPane.setData(currentData);
|
|
@@ -428,48 +448,88 @@ async function main() {
|
|
|
428
448
|
else {
|
|
429
449
|
emptyState.show();
|
|
430
450
|
}
|
|
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
|
|
451
|
+
// Keyboard shortcuts — dispatched via configurable bindings
|
|
452
|
+
const actions = {
|
|
453
|
+
search() { search.focus(); },
|
|
454
|
+
searchAlt() { search.focus(); },
|
|
455
|
+
undo() { if (currentData) {
|
|
456
|
+
const r = undoHistory.undo(currentData);
|
|
457
|
+
if (r)
|
|
458
|
+
applyState(r);
|
|
459
|
+
} },
|
|
460
|
+
redo() { if (currentData) {
|
|
461
|
+
const r = undoHistory.redo(currentData);
|
|
462
|
+
if (r)
|
|
463
|
+
applyState(r);
|
|
464
|
+
} },
|
|
465
|
+
focus() {
|
|
457
466
|
if (canvas.isFocused()) {
|
|
458
467
|
toolsPane.clearFocusSet();
|
|
459
468
|
}
|
|
460
469
|
else if (currentSelection.length > 0) {
|
|
461
470
|
toolsPane.addToFocusSet(currentSelection);
|
|
462
471
|
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
472
|
+
},
|
|
473
|
+
hopsDecrease() { const i = canvas.getFocusInfo(); if (i && i.hops > 0)
|
|
474
|
+
canvas.enterFocus(i.seedNodeIds, i.hops - 1); },
|
|
475
|
+
hopsIncrease() { const i = canvas.getFocusInfo(); if (i)
|
|
476
|
+
canvas.enterFocus(i.seedNodeIds, i.hops + 1); },
|
|
477
|
+
nextNode() {
|
|
478
|
+
const ids = canvas.getNodeIds();
|
|
479
|
+
if (ids.length > 0) {
|
|
480
|
+
viewCycleIndex = (viewCycleIndex + 1) % ids.length;
|
|
481
|
+
canvas.panToNode(ids[viewCycleIndex]);
|
|
482
|
+
if (currentData)
|
|
483
|
+
infoPanel.show([ids[viewCycleIndex]], currentData);
|
|
470
484
|
}
|
|
471
|
-
|
|
472
|
-
|
|
485
|
+
},
|
|
486
|
+
prevNode() {
|
|
487
|
+
const ids = canvas.getNodeIds();
|
|
488
|
+
if (ids.length > 0) {
|
|
489
|
+
viewCycleIndex = viewCycleIndex <= 0 ? ids.length - 1 : viewCycleIndex - 1;
|
|
490
|
+
canvas.panToNode(ids[viewCycleIndex]);
|
|
491
|
+
if (currentData)
|
|
492
|
+
infoPanel.show([ids[viewCycleIndex]], currentData);
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
nextConnection() { const id = infoPanel.cycleConnection(1); if (id)
|
|
496
|
+
canvas.panToNode(id); },
|
|
497
|
+
prevConnection() { const id = infoPanel.cycleConnection(-1); if (id)
|
|
498
|
+
canvas.panToNode(id); },
|
|
499
|
+
historyBack() { infoPanel.goBack(); },
|
|
500
|
+
historyForward() { infoPanel.goForward(); },
|
|
501
|
+
center() { canvas.centerView(); },
|
|
502
|
+
toggleEdges() { edgesVisible = !edgesVisible; canvas.setEdges(edgesVisible); },
|
|
503
|
+
panLeft() { canvas.panBy(-panSpeed, 0); },
|
|
504
|
+
panDown() { canvas.panBy(0, panSpeed); },
|
|
505
|
+
panUp() { canvas.panBy(0, -panSpeed); },
|
|
506
|
+
panRight() { canvas.panBy(panSpeed, 0); },
|
|
507
|
+
panFastLeft() { canvas.panBy(-panSpeed * 3, 0); },
|
|
508
|
+
zoomOut() { canvas.zoomBy(0.8); },
|
|
509
|
+
zoomIn() { canvas.zoomBy(1.25); },
|
|
510
|
+
panFastRight() { canvas.panBy(panSpeed * 3, 0); },
|
|
511
|
+
spacingDecrease() { const p = getLayoutParams(); setLayoutParams({ spacing: Math.max(0.5, p.spacing - 0.5) }); canvas.reheat(); },
|
|
512
|
+
spacingIncrease() { const p = getLayoutParams(); setLayoutParams({ spacing: Math.min(20, p.spacing + 0.5) }); canvas.reheat(); },
|
|
513
|
+
clusteringDecrease() { const p = getLayoutParams(); setLayoutParams({ clusterStrength: Math.max(0, p.clusterStrength - 0.03) }); canvas.reheat(); },
|
|
514
|
+
clusteringIncrease() { const p = getLayoutParams(); setLayoutParams({ clusterStrength: Math.min(1, p.clusterStrength + 0.03) }); canvas.reheat(); },
|
|
515
|
+
help() { shortcuts.toggle(); },
|
|
516
|
+
escape() { if (canvas.isFocused()) {
|
|
517
|
+
toolsPane.clearFocusSet();
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
shortcuts.hide();
|
|
521
|
+
} },
|
|
522
|
+
};
|
|
523
|
+
document.addEventListener("keydown", (e) => {
|
|
524
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
|
|
525
|
+
return;
|
|
526
|
+
for (const [action, binding] of Object.entries(bindings)) {
|
|
527
|
+
if (matchKey(e, binding)) {
|
|
528
|
+
const needsPrevent = action === "search" || action === "searchAlt" || action === "undo" || action === "redo";
|
|
529
|
+
if (needsPrevent)
|
|
530
|
+
e.preventDefault();
|
|
531
|
+
actions[action]?.();
|
|
532
|
+
return;
|
|
473
533
|
}
|
|
474
534
|
}
|
|
475
535
|
});
|
package/dist/search.js
CHANGED
|
@@ -10,13 +10,10 @@ 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;
|
|
@@ -27,7 +24,6 @@ export function initSearch(container) {
|
|
|
27
24
|
let data = null;
|
|
28
25
|
let filterCallback = null;
|
|
29
26
|
let selectCallback = null;
|
|
30
|
-
let activeTypes = new Set();
|
|
31
27
|
let debounceTimer = null;
|
|
32
28
|
// --- DOM ---
|
|
33
29
|
const overlay = document.createElement("div");
|
|
@@ -43,83 +39,24 @@ export function initSearch(container) {
|
|
|
43
39
|
const kbd = document.createElement("kbd");
|
|
44
40
|
kbd.className = "search-kbd";
|
|
45
41
|
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
42
|
inputWrap.appendChild(input);
|
|
57
43
|
inputWrap.appendChild(kbd);
|
|
58
|
-
inputWrap.appendChild(chipToggle);
|
|
59
44
|
const results = document.createElement("ul");
|
|
60
45
|
results.className = "search-results hidden";
|
|
61
|
-
const chips = document.createElement("div");
|
|
62
|
-
chips.className = "type-chips hidden";
|
|
63
46
|
overlay.appendChild(inputWrap);
|
|
64
47
|
overlay.appendChild(results);
|
|
65
|
-
overlay.appendChild(chips);
|
|
66
48
|
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 ---
|
|
49
|
+
// --- Search logic ---
|
|
106
50
|
function getMatchingIds() {
|
|
107
51
|
if (!data)
|
|
108
52
|
return null;
|
|
109
53
|
const query = input.value.trim();
|
|
110
|
-
|
|
111
|
-
const noQuery = query.length === 0;
|
|
112
|
-
// No filter active — return null (show all)
|
|
113
|
-
if (noQuery && noChipsSelected)
|
|
54
|
+
if (query.length === 0)
|
|
114
55
|
return null;
|
|
115
56
|
const ids = new Set();
|
|
116
57
|
for (const node of data.nodes) {
|
|
117
|
-
|
|
118
|
-
if (!noChipsSelected && !activeTypes.has(node.type))
|
|
119
|
-
continue;
|
|
120
|
-
if (noQuery || matchesQuery(node, query)) {
|
|
58
|
+
if (matchesQuery(node, query))
|
|
121
59
|
ids.add(node.id);
|
|
122
|
-
}
|
|
123
60
|
}
|
|
124
61
|
return ids;
|
|
125
62
|
}
|
|
@@ -130,16 +67,14 @@ export function initSearch(container) {
|
|
|
130
67
|
}
|
|
131
68
|
function updateResults() {
|
|
132
69
|
results.innerHTML = "";
|
|
70
|
+
activeIndex = -1;
|
|
133
71
|
const query = input.value.trim();
|
|
134
72
|
if (!data || query.length === 0) {
|
|
135
73
|
results.classList.add("hidden");
|
|
136
74
|
return;
|
|
137
75
|
}
|
|
138
|
-
const noChipsSelected = activeTypes.size === 0;
|
|
139
76
|
const matches = [];
|
|
140
77
|
for (const node of data.nodes) {
|
|
141
|
-
if (!noChipsSelected && !activeTypes.has(node.type))
|
|
142
|
-
continue;
|
|
143
78
|
if (matchesQuery(node, query)) {
|
|
144
79
|
matches.push(node);
|
|
145
80
|
if (matches.length >= 8)
|
|
@@ -182,26 +117,55 @@ export function initSearch(container) {
|
|
|
182
117
|
clearTimeout(debounceTimer);
|
|
183
118
|
debounceTimer = setTimeout(applyFilter, 150);
|
|
184
119
|
});
|
|
120
|
+
let activeIndex = -1;
|
|
121
|
+
function updateActiveResult() {
|
|
122
|
+
const items = results.querySelectorAll(".search-result-item");
|
|
123
|
+
items.forEach((el, i) => {
|
|
124
|
+
el.classList.toggle("search-result-active", i === activeIndex);
|
|
125
|
+
});
|
|
126
|
+
if (activeIndex >= 0 && items[activeIndex]) {
|
|
127
|
+
items[activeIndex].scrollIntoView({ block: "nearest" });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
185
130
|
input.addEventListener("keydown", (e) => {
|
|
186
|
-
|
|
131
|
+
const items = results.querySelectorAll(".search-result-item");
|
|
132
|
+
if (e.key === "ArrowDown") {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
if (items.length > 0) {
|
|
135
|
+
activeIndex = Math.min(activeIndex + 1, items.length - 1);
|
|
136
|
+
updateActiveResult();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (e.key === "ArrowUp") {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
if (items.length > 0) {
|
|
142
|
+
activeIndex = Math.max(activeIndex - 1, 0);
|
|
143
|
+
updateActiveResult();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (e.key === "Enter") {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
if (activeIndex >= 0 && items[activeIndex]) {
|
|
149
|
+
items[activeIndex].click();
|
|
150
|
+
}
|
|
151
|
+
else if (items.length > 0) {
|
|
152
|
+
items[0].click();
|
|
153
|
+
}
|
|
154
|
+
input.blur();
|
|
155
|
+
}
|
|
156
|
+
else if (e.key === "Escape") {
|
|
187
157
|
input.value = "";
|
|
188
158
|
input.blur();
|
|
189
159
|
results.classList.add("hidden");
|
|
160
|
+
activeIndex = -1;
|
|
190
161
|
applyFilter();
|
|
191
162
|
}
|
|
192
|
-
else if (e.key === "Enter") {
|
|
193
|
-
// Select first result
|
|
194
|
-
const first = results.querySelector(".search-result-item");
|
|
195
|
-
first?.click();
|
|
196
|
-
}
|
|
197
163
|
});
|
|
198
|
-
// Close results when clicking outside
|
|
199
164
|
document.addEventListener("click", (e) => {
|
|
200
165
|
if (!overlay.contains(e.target)) {
|
|
201
166
|
results.classList.add("hidden");
|
|
202
167
|
}
|
|
203
168
|
});
|
|
204
|
-
// Hide kbd hint when focused
|
|
205
169
|
input.addEventListener("focus", () => kbd.classList.add("hidden"));
|
|
206
170
|
input.addEventListener("blur", () => {
|
|
207
171
|
if (input.value.length === 0)
|
|
@@ -215,7 +179,6 @@ export function initSearch(container) {
|
|
|
215
179
|
results.classList.add("hidden");
|
|
216
180
|
if (data && data.nodes.length > 0) {
|
|
217
181
|
overlay.classList.remove("hidden");
|
|
218
|
-
buildChips();
|
|
219
182
|
}
|
|
220
183
|
else {
|
|
221
184
|
overlay.classList.add("hidden");
|
|
@@ -230,10 +193,6 @@ export function initSearch(container) {
|
|
|
230
193
|
clear() {
|
|
231
194
|
input.value = "";
|
|
232
195
|
results.classList.add("hidden");
|
|
233
|
-
activeTypes.clear();
|
|
234
|
-
chipsVisible = false;
|
|
235
|
-
chips.classList.add("hidden");
|
|
236
|
-
chipToggle.classList.remove("active");
|
|
237
196
|
filterCallback?.(null);
|
|
238
197
|
},
|
|
239
198
|
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,33 @@
|
|
|
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
|
+
"escape",
|
|
21
|
+
];
|
|
22
|
+
/** Format a binding string for display (e.g. "ctrl+z" → "Ctrl+Z"). */
|
|
23
|
+
function formatBinding(binding) {
|
|
24
|
+
return binding
|
|
25
|
+
.split("+")
|
|
26
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
27
|
+
.join("+");
|
|
28
|
+
}
|
|
29
|
+
export function initShortcuts(container, bindings) {
|
|
30
|
+
const descriptions = actionDescriptions();
|
|
14
31
|
const overlay = document.createElement("div");
|
|
15
32
|
overlay.className = "shortcuts-overlay hidden";
|
|
16
33
|
const modal = document.createElement("div");
|
|
@@ -20,7 +37,27 @@ export function initShortcuts(container) {
|
|
|
20
37
|
title.textContent = "Keyboard Shortcuts";
|
|
21
38
|
const list = document.createElement("div");
|
|
22
39
|
list.className = "shortcuts-list";
|
|
23
|
-
|
|
40
|
+
// Keybinding actions
|
|
41
|
+
for (const action of ACTION_ORDER) {
|
|
42
|
+
const binding = bindings[action];
|
|
43
|
+
if (!binding)
|
|
44
|
+
continue;
|
|
45
|
+
const row = document.createElement("div");
|
|
46
|
+
row.className = "shortcuts-row";
|
|
47
|
+
const keys = document.createElement("div");
|
|
48
|
+
keys.className = "shortcuts-keys";
|
|
49
|
+
const kbd = document.createElement("kbd");
|
|
50
|
+
kbd.textContent = formatBinding(binding);
|
|
51
|
+
keys.appendChild(kbd);
|
|
52
|
+
const desc = document.createElement("span");
|
|
53
|
+
desc.className = "shortcuts-desc";
|
|
54
|
+
desc.textContent = descriptions[action];
|
|
55
|
+
row.appendChild(keys);
|
|
56
|
+
row.appendChild(desc);
|
|
57
|
+
list.appendChild(row);
|
|
58
|
+
}
|
|
59
|
+
// Mouse actions
|
|
60
|
+
for (const s of MOUSE_ACTIONS) {
|
|
24
61
|
const row = document.createElement("div");
|
|
25
62
|
row.className = "shortcuts-row";
|
|
26
63
|
const keys = document.createElement("div");
|
|
@@ -28,15 +65,6 @@ export function initShortcuts(container) {
|
|
|
28
65
|
const kbd = document.createElement("kbd");
|
|
29
66
|
kbd.textContent = s.key;
|
|
30
67
|
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
68
|
const desc = document.createElement("span");
|
|
41
69
|
desc.className = "shortcuts-desc";
|
|
42
70
|
desc.textContent = s.description;
|
|
@@ -58,10 +86,13 @@ export function initShortcuts(container) {
|
|
|
58
86
|
function hide() {
|
|
59
87
|
overlay.classList.add("hidden");
|
|
60
88
|
}
|
|
89
|
+
function toggle() {
|
|
90
|
+
overlay.classList.toggle("hidden");
|
|
91
|
+
}
|
|
61
92
|
closeBtn.addEventListener("click", hide);
|
|
62
93
|
overlay.addEventListener("click", (e) => {
|
|
63
94
|
if (e.target === overlay)
|
|
64
95
|
hide();
|
|
65
96
|
});
|
|
66
|
-
return { show, hide };
|
|
97
|
+
return { show, hide, toggle };
|
|
67
98
|
}
|
package/dist/style.css
CHANGED
|
@@ -269,6 +269,10 @@ body {
|
|
|
269
269
|
gap: 4px;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
.canvas-top-left {
|
|
273
|
+
flex-shrink: 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
272
276
|
.canvas-top-center {
|
|
273
277
|
flex: 1;
|
|
274
278
|
justify-content: center;
|
|
@@ -510,7 +514,8 @@ body {
|
|
|
510
514
|
transition: background 0.1s;
|
|
511
515
|
}
|
|
512
516
|
|
|
513
|
-
.search-result-item:hover
|
|
517
|
+
.search-result-item:hover,
|
|
518
|
+
.search-result-active {
|
|
514
519
|
background: var(--bg-hover);
|
|
515
520
|
}
|
|
516
521
|
|
|
@@ -837,6 +842,11 @@ body {
|
|
|
837
842
|
flex-wrap: wrap;
|
|
838
843
|
}
|
|
839
844
|
|
|
845
|
+
.info-connection-active {
|
|
846
|
+
outline: 1.5px solid var(--accent);
|
|
847
|
+
background: var(--bg-hover);
|
|
848
|
+
}
|
|
849
|
+
|
|
840
850
|
.info-target-dot {
|
|
841
851
|
width: 8px;
|
|
842
852
|
height: 8px;
|
|
@@ -1071,6 +1081,7 @@ body {
|
|
|
1071
1081
|
z-index: 20;
|
|
1072
1082
|
width: 200px;
|
|
1073
1083
|
overflow-y: auto;
|
|
1084
|
+
overflow-x: hidden;
|
|
1074
1085
|
background: var(--bg-surface);
|
|
1075
1086
|
border: 1px solid var(--border);
|
|
1076
1087
|
border-radius: 10px;
|
|
@@ -1296,6 +1307,68 @@ body {
|
|
|
1296
1307
|
padding: 8px 0;
|
|
1297
1308
|
}
|
|
1298
1309
|
|
|
1310
|
+
.tools-pane-tabs {
|
|
1311
|
+
display: flex;
|
|
1312
|
+
gap: 2px;
|
|
1313
|
+
margin-bottom: 10px;
|
|
1314
|
+
padding-bottom: 8px;
|
|
1315
|
+
border-bottom: 1px solid var(--border);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.tools-pane-tab {
|
|
1319
|
+
flex: 1;
|
|
1320
|
+
padding: 4px 0;
|
|
1321
|
+
font-size: 10px;
|
|
1322
|
+
font-weight: 600;
|
|
1323
|
+
text-transform: uppercase;
|
|
1324
|
+
letter-spacing: 0.03em;
|
|
1325
|
+
background: none;
|
|
1326
|
+
border: 1px solid transparent;
|
|
1327
|
+
border-radius: 5px;
|
|
1328
|
+
color: var(--text-dim);
|
|
1329
|
+
cursor: pointer;
|
|
1330
|
+
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.tools-pane-tab:hover {
|
|
1334
|
+
color: var(--text-muted);
|
|
1335
|
+
background: var(--bg-hover);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.tools-pane-tab-active {
|
|
1339
|
+
color: var(--text);
|
|
1340
|
+
background: var(--bg-hover);
|
|
1341
|
+
border-color: var(--border);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.tools-pane-search {
|
|
1345
|
+
width: 100%;
|
|
1346
|
+
padding: 4px 8px;
|
|
1347
|
+
font-size: 11px;
|
|
1348
|
+
background: var(--bg);
|
|
1349
|
+
border: 1px solid var(--border);
|
|
1350
|
+
border-radius: 6px;
|
|
1351
|
+
color: var(--text);
|
|
1352
|
+
outline: none;
|
|
1353
|
+
margin-bottom: 8px;
|
|
1354
|
+
box-sizing: border-box;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
.tools-pane-search:focus {
|
|
1358
|
+
border-color: var(--accent);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.tools-pane-search::placeholder {
|
|
1362
|
+
color: var(--text-dim);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.tools-pane-empty-msg {
|
|
1366
|
+
font-size: 11px;
|
|
1367
|
+
color: var(--text-dim);
|
|
1368
|
+
text-align: center;
|
|
1369
|
+
padding: 16px 0;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1299
1372
|
/* --- Empty State --- */
|
|
1300
1373
|
|
|
1301
1374
|
.empty-state {
|
package/dist/tools-pane.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface ToolsPaneCallbacks {
|
|
|
9
9
|
onToggleTypeHulls: (visible: boolean) => void;
|
|
10
10
|
onToggleMinimap: (visible: boolean) => void;
|
|
11
11
|
onLayoutChange: (param: string, value: number) => void;
|
|
12
|
+
onPanSpeedChange: (speed: number) => void;
|
|
12
13
|
onExport: (format: "png" | "svg") => void;
|
|
13
14
|
onOpen?: () => void;
|
|
14
15
|
}
|