backpack-viewer 0.2.14 → 0.2.16
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/app/assets/index-Mi0vDG5K.js +21 -0
- package/dist/app/assets/index-z15vEFEy.css +1 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +10 -1
- package/dist/canvas.js +81 -2
- package/dist/info-panel.d.ts +1 -1
- package/dist/info-panel.js +32 -11
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +26 -0
- package/dist/main.js +135 -10
- package/dist/shortcuts.js +2 -1
- package/dist/style.css +88 -2
- package/dist/tools-pane.d.ts +3 -0
- package/dist/tools-pane.js +167 -5
- package/package.json +1 -1
- package/dist/app/assets/index-CR8Iepyw.js +0 -21
- package/dist/app/assets/index-FMdnOuXa.css +0 -1
package/dist/main.js
CHANGED
|
@@ -120,9 +120,60 @@ async function main() {
|
|
|
120
120
|
},
|
|
121
121
|
}, (nodeId) => {
|
|
122
122
|
canvas.panToNode(nodeId);
|
|
123
|
+
}, (nodeIds) => {
|
|
124
|
+
toolsPane.addToFocusSet(nodeIds);
|
|
123
125
|
});
|
|
124
126
|
const mobileQuery = window.matchMedia("(max-width: 768px)");
|
|
127
|
+
// Track current selection for keyboard shortcuts
|
|
128
|
+
let currentSelection = [];
|
|
129
|
+
// --- Focus indicator (top bar pill) ---
|
|
130
|
+
let focusIndicator = null;
|
|
131
|
+
function buildFocusIndicator(info) {
|
|
132
|
+
if (focusIndicator)
|
|
133
|
+
focusIndicator.remove();
|
|
134
|
+
focusIndicator = document.createElement("div");
|
|
135
|
+
focusIndicator.className = "focus-indicator";
|
|
136
|
+
const label = document.createElement("span");
|
|
137
|
+
label.className = "focus-indicator-label";
|
|
138
|
+
label.textContent = `Focused: ${info.totalNodes} nodes`;
|
|
139
|
+
const hopsLabel = document.createElement("span");
|
|
140
|
+
hopsLabel.className = "focus-indicator-hops";
|
|
141
|
+
hopsLabel.textContent = `${info.hops}`;
|
|
142
|
+
const minus = document.createElement("button");
|
|
143
|
+
minus.className = "focus-indicator-btn";
|
|
144
|
+
minus.textContent = "\u2212";
|
|
145
|
+
minus.title = "Fewer hops";
|
|
146
|
+
minus.disabled = info.hops === 0;
|
|
147
|
+
minus.addEventListener("click", () => {
|
|
148
|
+
canvas.enterFocus(info.seedNodeIds, Math.max(0, info.hops - 1));
|
|
149
|
+
});
|
|
150
|
+
const plus = document.createElement("button");
|
|
151
|
+
plus.className = "focus-indicator-btn";
|
|
152
|
+
plus.textContent = "+";
|
|
153
|
+
plus.title = "More hops";
|
|
154
|
+
plus.disabled = false;
|
|
155
|
+
plus.addEventListener("click", () => {
|
|
156
|
+
canvas.enterFocus(info.seedNodeIds, info.hops + 1);
|
|
157
|
+
});
|
|
158
|
+
const exit = document.createElement("button");
|
|
159
|
+
exit.className = "focus-indicator-btn focus-indicator-exit";
|
|
160
|
+
exit.textContent = "\u00d7";
|
|
161
|
+
exit.title = "Exit focus (Esc)";
|
|
162
|
+
exit.addEventListener("click", () => toolsPane.clearFocusSet());
|
|
163
|
+
focusIndicator.appendChild(label);
|
|
164
|
+
focusIndicator.appendChild(minus);
|
|
165
|
+
focusIndicator.appendChild(hopsLabel);
|
|
166
|
+
focusIndicator.appendChild(plus);
|
|
167
|
+
focusIndicator.appendChild(exit);
|
|
168
|
+
}
|
|
169
|
+
function removeFocusIndicator() {
|
|
170
|
+
if (focusIndicator) {
|
|
171
|
+
focusIndicator.remove();
|
|
172
|
+
focusIndicator = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
125
175
|
canvas = initCanvas(canvasContainer, (nodeIds) => {
|
|
176
|
+
currentSelection = nodeIds ?? [];
|
|
126
177
|
if (nodeIds && nodeIds.length > 0 && currentData) {
|
|
127
178
|
infoPanel.show(nodeIds, currentData);
|
|
128
179
|
if (mobileQuery.matches)
|
|
@@ -134,6 +185,20 @@ async function main() {
|
|
|
134
185
|
if (activeOntology)
|
|
135
186
|
updateUrl(activeOntology);
|
|
136
187
|
}
|
|
188
|
+
}, (focus) => {
|
|
189
|
+
if (focus) {
|
|
190
|
+
buildFocusIndicator(focus);
|
|
191
|
+
// Insert into top-left, after tools toggle
|
|
192
|
+
const topLeft = canvasContainer.querySelector(".canvas-top-left");
|
|
193
|
+
if (topLeft && focusIndicator)
|
|
194
|
+
topLeft.appendChild(focusIndicator);
|
|
195
|
+
updateUrl(activeOntology, focus.seedNodeIds);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
removeFocusIndicator();
|
|
199
|
+
if (activeOntology)
|
|
200
|
+
updateUrl(activeOntology);
|
|
201
|
+
}
|
|
137
202
|
});
|
|
138
203
|
const search = initSearch(canvasContainer);
|
|
139
204
|
const toolsPane = initToolsPane(canvasContainer, {
|
|
@@ -155,6 +220,15 @@ async function main() {
|
|
|
155
220
|
if (currentData)
|
|
156
221
|
infoPanel.show([nodeId], currentData);
|
|
157
222
|
},
|
|
223
|
+
onFocusChange(seedNodeIds) {
|
|
224
|
+
if (seedNodeIds && seedNodeIds.length > 0) {
|
|
225
|
+
canvas.enterFocus(seedNodeIds, 1);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
if (canvas.isFocused())
|
|
229
|
+
canvas.exitFocus();
|
|
230
|
+
}
|
|
231
|
+
},
|
|
158
232
|
onRenameNodeType(oldType, newType) {
|
|
159
233
|
if (!currentData)
|
|
160
234
|
return;
|
|
@@ -235,6 +309,10 @@ async function main() {
|
|
|
235
309
|
canvas.setFilteredNodeIds(ids);
|
|
236
310
|
});
|
|
237
311
|
search.onNodeSelect((nodeId) => {
|
|
312
|
+
// If focused and the node isn't in the subgraph, exit focus first
|
|
313
|
+
if (canvas.isFocused()) {
|
|
314
|
+
toolsPane.clearFocusSet();
|
|
315
|
+
}
|
|
238
316
|
canvas.panToNode(nodeId);
|
|
239
317
|
if (currentData) {
|
|
240
318
|
infoPanel.show([nodeId], currentData);
|
|
@@ -262,29 +340,47 @@ async function main() {
|
|
|
262
340
|
const emptyState = initEmptyState(canvasContainer);
|
|
263
341
|
// --- URL deep linking ---
|
|
264
342
|
function updateUrl(name, nodeIds) {
|
|
343
|
+
const parts = [];
|
|
344
|
+
if (nodeIds?.length) {
|
|
345
|
+
parts.push("node=" + nodeIds.map(encodeURIComponent).join(","));
|
|
346
|
+
}
|
|
347
|
+
const focusInfo = canvas.getFocusInfo();
|
|
348
|
+
if (focusInfo) {
|
|
349
|
+
parts.push("focus=" + focusInfo.seedNodeIds.map(encodeURIComponent).join(","));
|
|
350
|
+
parts.push("hops=" + focusInfo.hops);
|
|
351
|
+
}
|
|
265
352
|
const hash = "#" + encodeURIComponent(name) +
|
|
266
|
-
(
|
|
353
|
+
(parts.length ? "?" + parts.join("&") : "");
|
|
267
354
|
history.replaceState(null, "", hash);
|
|
268
355
|
}
|
|
269
356
|
function parseUrl() {
|
|
270
357
|
const hash = window.location.hash.slice(1);
|
|
271
358
|
if (!hash)
|
|
272
|
-
return { graph: null, nodes: [] };
|
|
359
|
+
return { graph: null, nodes: [], focus: [], hops: 1 };
|
|
273
360
|
const [graphPart, queryPart] = hash.split("?");
|
|
274
361
|
const graph = graphPart ? decodeURIComponent(graphPart) : null;
|
|
275
362
|
let nodes = [];
|
|
363
|
+
let focus = [];
|
|
364
|
+
let hops = 1;
|
|
276
365
|
if (queryPart) {
|
|
277
366
|
const params = new URLSearchParams(queryPart);
|
|
278
367
|
const nodeParam = params.get("node");
|
|
279
368
|
if (nodeParam)
|
|
280
369
|
nodes = nodeParam.split(",").map(decodeURIComponent);
|
|
370
|
+
const focusParam = params.get("focus");
|
|
371
|
+
if (focusParam)
|
|
372
|
+
focus = focusParam.split(",").map(decodeURIComponent);
|
|
373
|
+
const hopsParam = params.get("hops");
|
|
374
|
+
if (hopsParam)
|
|
375
|
+
hops = Math.max(0, parseInt(hopsParam, 10) || 1);
|
|
281
376
|
}
|
|
282
|
-
return { graph, nodes };
|
|
377
|
+
return { graph, nodes, focus, hops };
|
|
283
378
|
}
|
|
284
|
-
async function selectGraph(name,
|
|
379
|
+
async function selectGraph(name, panToNodeIds, focusSeedIds, focusHops) {
|
|
285
380
|
activeOntology = name;
|
|
286
381
|
sidebar.setActive(name);
|
|
287
382
|
infoPanel.hide();
|
|
383
|
+
removeFocusIndicator();
|
|
288
384
|
search.clear();
|
|
289
385
|
undoHistory.clear();
|
|
290
386
|
currentData = await loadOntology(name);
|
|
@@ -293,9 +389,19 @@ async function main() {
|
|
|
293
389
|
toolsPane.setData(currentData);
|
|
294
390
|
emptyState.hide();
|
|
295
391
|
updateUrl(name);
|
|
296
|
-
//
|
|
297
|
-
if (
|
|
298
|
-
const
|
|
392
|
+
// Restore focus mode if requested
|
|
393
|
+
if (focusSeedIds?.length && currentData) {
|
|
394
|
+
const validFocus = focusSeedIds.filter((id) => currentData.nodes.some((n) => n.id === id));
|
|
395
|
+
if (validFocus.length) {
|
|
396
|
+
setTimeout(() => {
|
|
397
|
+
canvas.enterFocus(validFocus, focusHops ?? 1);
|
|
398
|
+
}, 500);
|
|
399
|
+
return; // enterFocus handles the URL update
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Pan to specific nodes if requested
|
|
403
|
+
if (panToNodeIds?.length && currentData) {
|
|
404
|
+
const validIds = panToNodeIds.filter((id) => currentData.nodes.some((n) => n.id === id));
|
|
299
405
|
if (validIds.length) {
|
|
300
406
|
setTimeout(() => {
|
|
301
407
|
canvas.panToNodes(validIds);
|
|
@@ -317,7 +423,7 @@ async function main() {
|
|
|
317
423
|
? summaries[0].name
|
|
318
424
|
: null;
|
|
319
425
|
if (initialName) {
|
|
320
|
-
await selectGraph(initialName, initialUrl.nodes.length ? initialUrl.nodes : undefined);
|
|
426
|
+
await selectGraph(initialName, initialUrl.nodes.length ? initialUrl.nodes : undefined, initialUrl.focus.length ? initialUrl.focus : undefined, initialUrl.hops);
|
|
321
427
|
}
|
|
322
428
|
else {
|
|
323
429
|
emptyState.show();
|
|
@@ -346,20 +452,39 @@ async function main() {
|
|
|
346
452
|
applyState(restored);
|
|
347
453
|
}
|
|
348
454
|
}
|
|
455
|
+
else if (e.key === "f" || e.key === "F") {
|
|
456
|
+
// Toggle focus mode on current selection
|
|
457
|
+
if (canvas.isFocused()) {
|
|
458
|
+
toolsPane.clearFocusSet();
|
|
459
|
+
}
|
|
460
|
+
else if (currentSelection.length > 0) {
|
|
461
|
+
toolsPane.addToFocusSet(currentSelection);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
349
464
|
else if (e.key === "?") {
|
|
350
465
|
shortcuts.show();
|
|
351
466
|
}
|
|
352
467
|
else if (e.key === "Escape") {
|
|
353
|
-
|
|
468
|
+
if (canvas.isFocused()) {
|
|
469
|
+
toolsPane.clearFocusSet();
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
shortcuts.hide();
|
|
473
|
+
}
|
|
354
474
|
}
|
|
355
475
|
});
|
|
356
476
|
// Handle browser back/forward
|
|
357
477
|
window.addEventListener("hashchange", () => {
|
|
358
478
|
const url = parseUrl();
|
|
359
479
|
if (url.graph && url.graph !== activeOntology) {
|
|
360
|
-
selectGraph(url.graph, url.nodes.length ? url.nodes : undefined);
|
|
480
|
+
selectGraph(url.graph, url.nodes.length ? url.nodes : undefined, url.focus.length ? url.focus : undefined, url.hops);
|
|
481
|
+
}
|
|
482
|
+
else if (url.graph && url.focus.length && currentData) {
|
|
483
|
+
canvas.enterFocus(url.focus, url.hops);
|
|
361
484
|
}
|
|
362
485
|
else if (url.graph && url.nodes.length && currentData) {
|
|
486
|
+
if (canvas.isFocused())
|
|
487
|
+
canvas.exitFocus();
|
|
363
488
|
const validIds = url.nodes.filter((id) => currentData.nodes.some((n) => n.id === id));
|
|
364
489
|
if (validIds.length) {
|
|
365
490
|
canvas.panToNodes(validIds);
|
package/dist/shortcuts.js
CHANGED
|
@@ -3,7 +3,8 @@ const SHORTCUTS = [
|
|
|
3
3
|
{ key: "Ctrl+Z", description: "Undo" },
|
|
4
4
|
{ key: "Ctrl+Shift+Z", description: "Redo" },
|
|
5
5
|
{ key: "?", description: "Show this help" },
|
|
6
|
-
{ key: "
|
|
6
|
+
{ key: "F", description: "Focus on selected / exit focus" },
|
|
7
|
+
{ key: "Esc", description: "Exit focus / close panel" },
|
|
7
8
|
{ key: "Click", description: "Select node" },
|
|
8
9
|
{ key: "Ctrl+Click", description: "Multi-select nodes" },
|
|
9
10
|
{ key: "Drag", description: "Pan canvas" },
|
package/dist/style.css
CHANGED
|
@@ -274,6 +274,71 @@ body {
|
|
|
274
274
|
justify-content: center;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/* --- Focus Indicator --- */
|
|
278
|
+
|
|
279
|
+
.focus-indicator {
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
gap: 2px;
|
|
283
|
+
background: var(--bg-surface);
|
|
284
|
+
border: 1px solid rgba(212, 162, 127, 0.4);
|
|
285
|
+
border-radius: 8px;
|
|
286
|
+
padding: 4px 6px 4px 10px;
|
|
287
|
+
box-shadow: 0 2px 8px var(--shadow);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.focus-indicator-label {
|
|
291
|
+
font-size: 11px;
|
|
292
|
+
color: var(--accent);
|
|
293
|
+
font-weight: 500;
|
|
294
|
+
white-space: nowrap;
|
|
295
|
+
margin-right: 4px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.focus-indicator-hops {
|
|
299
|
+
font-size: 11px;
|
|
300
|
+
color: var(--text-muted);
|
|
301
|
+
font-family: monospace;
|
|
302
|
+
min-width: 12px;
|
|
303
|
+
text-align: center;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.focus-indicator-btn {
|
|
307
|
+
background: none;
|
|
308
|
+
border: none;
|
|
309
|
+
color: var(--text-muted);
|
|
310
|
+
font-size: 14px;
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
padding: 2px 4px;
|
|
313
|
+
line-height: 1;
|
|
314
|
+
border-radius: 4px;
|
|
315
|
+
transition: color 0.15s, background 0.15s;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.focus-indicator-btn:hover:not(:disabled) {
|
|
319
|
+
color: var(--text);
|
|
320
|
+
background: var(--bg-hover);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.focus-indicator-btn:disabled {
|
|
324
|
+
color: var(--text-dim);
|
|
325
|
+
cursor: default;
|
|
326
|
+
opacity: 0.3;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.focus-indicator-exit {
|
|
330
|
+
font-size: 16px;
|
|
331
|
+
margin-left: 2px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.focus-indicator-exit:hover {
|
|
335
|
+
color: #ef4444 !important;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.info-focus-btn {
|
|
339
|
+
font-size: 14px;
|
|
340
|
+
}
|
|
341
|
+
|
|
277
342
|
/* --- Theme Toggle --- */
|
|
278
343
|
|
|
279
344
|
.theme-toggle {
|
|
@@ -852,9 +917,14 @@ body {
|
|
|
852
917
|
border-radius: 4px;
|
|
853
918
|
padding: 3px 6px;
|
|
854
919
|
font-size: 12px;
|
|
920
|
+
font-family: inherit;
|
|
855
921
|
color: var(--text);
|
|
856
922
|
flex: 1;
|
|
857
923
|
min-width: 0;
|
|
924
|
+
resize: vertical;
|
|
925
|
+
overflow: hidden;
|
|
926
|
+
line-height: 1.4;
|
|
927
|
+
max-height: 300px;
|
|
858
928
|
}
|
|
859
929
|
|
|
860
930
|
.info-edit-input:focus {
|
|
@@ -997,9 +1067,9 @@ body {
|
|
|
997
1067
|
position: absolute;
|
|
998
1068
|
top: 56px;
|
|
999
1069
|
left: 16px;
|
|
1070
|
+
bottom: 16px;
|
|
1000
1071
|
z-index: 20;
|
|
1001
1072
|
width: 200px;
|
|
1002
|
-
max-height: calc(100vh - 72px);
|
|
1003
1073
|
overflow-y: auto;
|
|
1004
1074
|
background: var(--bg-surface);
|
|
1005
1075
|
border: 1px solid var(--border);
|
|
@@ -1127,6 +1197,22 @@ body {
|
|
|
1127
1197
|
color: var(--accent);
|
|
1128
1198
|
}
|
|
1129
1199
|
|
|
1200
|
+
.tools-pane-focus-toggle {
|
|
1201
|
+
opacity: 0.4;
|
|
1202
|
+
font-size: 11px;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.tools-pane-focus-active {
|
|
1206
|
+
opacity: 1 !important;
|
|
1207
|
+
color: var(--accent) !important;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.tools-pane-focus-clear {
|
|
1211
|
+
margin-top: 4px;
|
|
1212
|
+
border-top: 1px solid var(--border);
|
|
1213
|
+
padding-top: 6px;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1130
1216
|
.tools-pane-editing {
|
|
1131
1217
|
background: none !important;
|
|
1132
1218
|
}
|
|
@@ -1417,8 +1503,8 @@ body {
|
|
|
1417
1503
|
.tools-pane-content {
|
|
1418
1504
|
top: 48px;
|
|
1419
1505
|
left: 8px;
|
|
1506
|
+
bottom: 80px;
|
|
1420
1507
|
width: 160px;
|
|
1421
|
-
max-height: calc(100% - 200px);
|
|
1422
1508
|
max-width: calc(100vw - 24px);
|
|
1423
1509
|
}
|
|
1424
1510
|
|
package/dist/tools-pane.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { LearningGraphData } from "backpack-ontology";
|
|
|
2
2
|
interface ToolsPaneCallbacks {
|
|
3
3
|
onFilterByType: (type: string | null) => void;
|
|
4
4
|
onNavigateToNode: (nodeId: string) => void;
|
|
5
|
+
onFocusChange: (seedNodeIds: string[] | null) => void;
|
|
5
6
|
onRenameNodeType: (oldType: string, newType: string) => void;
|
|
6
7
|
onRenameEdgeType: (oldType: string, newType: string) => void;
|
|
7
8
|
onToggleEdgeLabels: (visible: boolean) => void;
|
|
@@ -13,6 +14,8 @@ interface ToolsPaneCallbacks {
|
|
|
13
14
|
}
|
|
14
15
|
export declare function initToolsPane(container: HTMLElement, callbacks: ToolsPaneCallbacks): {
|
|
15
16
|
collapse(): void;
|
|
17
|
+
addToFocusSet(nodeIds: string[]): void;
|
|
18
|
+
clearFocusSet(): void;
|
|
16
19
|
setData(newData: LearningGraphData | null): void;
|
|
17
20
|
};
|
|
18
21
|
export {};
|
package/dist/tools-pane.js
CHANGED
|
@@ -7,6 +7,39 @@ export function initToolsPane(container, callbacks) {
|
|
|
7
7
|
let edgeLabelsVisible = true;
|
|
8
8
|
let typeHullsVisible = true;
|
|
9
9
|
let minimapVisible = true;
|
|
10
|
+
// Unified focus set — two layers that compose via union
|
|
11
|
+
const focusSet = {
|
|
12
|
+
types: new Set(), // toggled node types (dynamic — resolves to all nodes of type)
|
|
13
|
+
nodeIds: new Set(), // individually toggled node IDs
|
|
14
|
+
};
|
|
15
|
+
/** Resolve the focus set to a flat array of node IDs. */
|
|
16
|
+
function resolveFocusSet() {
|
|
17
|
+
if (!data)
|
|
18
|
+
return [];
|
|
19
|
+
const ids = new Set();
|
|
20
|
+
for (const node of data.nodes) {
|
|
21
|
+
if (focusSet.types.has(node.type))
|
|
22
|
+
ids.add(node.id);
|
|
23
|
+
}
|
|
24
|
+
for (const id of focusSet.nodeIds)
|
|
25
|
+
ids.add(id);
|
|
26
|
+
return [...ids];
|
|
27
|
+
}
|
|
28
|
+
/** Check if a node is in the focus set (directly or via its type). */
|
|
29
|
+
function isNodeFocused(nodeId) {
|
|
30
|
+
if (focusSet.nodeIds.has(nodeId))
|
|
31
|
+
return true;
|
|
32
|
+
const node = data?.nodes.find((n) => n.id === nodeId);
|
|
33
|
+
return node ? focusSet.types.has(node.type) : false;
|
|
34
|
+
}
|
|
35
|
+
function isFocusSetEmpty() {
|
|
36
|
+
return focusSet.types.size === 0 && focusSet.nodeIds.size === 0;
|
|
37
|
+
}
|
|
38
|
+
/** Emit the resolved focus set to the callback. */
|
|
39
|
+
function emitFocusChange() {
|
|
40
|
+
const resolved = resolveFocusSet();
|
|
41
|
+
callbacks.onFocusChange(resolved.length > 0 ? resolved : null);
|
|
42
|
+
}
|
|
10
43
|
// --- DOM ---
|
|
11
44
|
const toggle = document.createElement("button");
|
|
12
45
|
toggle.className = "tools-pane-toggle hidden";
|
|
@@ -37,7 +70,7 @@ export function initToolsPane(container, callbacks) {
|
|
|
37
70
|
`<span>${stats.edgeCount} edges</span><span class="tools-pane-sep">·</span>` +
|
|
38
71
|
`<span>${stats.types.length} types</span>`;
|
|
39
72
|
content.appendChild(summary);
|
|
40
|
-
// Node types — click to filter,
|
|
73
|
+
// Node types — click to filter, bullseye to toggle focus set, pencil to rename
|
|
41
74
|
if (stats.types.length) {
|
|
42
75
|
content.appendChild(makeSection("Node Types", (section) => {
|
|
43
76
|
for (const t of stats.types) {
|
|
@@ -54,6 +87,14 @@ export function initToolsPane(container, callbacks) {
|
|
|
54
87
|
const count = document.createElement("span");
|
|
55
88
|
count.className = "tools-pane-count";
|
|
56
89
|
count.textContent = String(t.count);
|
|
90
|
+
const focusBtn = document.createElement("button");
|
|
91
|
+
focusBtn.className = "tools-pane-edit tools-pane-focus-toggle";
|
|
92
|
+
if (focusSet.types.has(t.name))
|
|
93
|
+
focusBtn.classList.add("tools-pane-focus-active");
|
|
94
|
+
focusBtn.textContent = "\u25CE";
|
|
95
|
+
focusBtn.title = focusSet.types.has(t.name)
|
|
96
|
+
? `Remove ${t.name} from focus`
|
|
97
|
+
: `Add ${t.name} to focus`;
|
|
57
98
|
const editBtn = document.createElement("button");
|
|
58
99
|
editBtn.className = "tools-pane-edit";
|
|
59
100
|
editBtn.textContent = "\u270E";
|
|
@@ -61,6 +102,7 @@ export function initToolsPane(container, callbacks) {
|
|
|
61
102
|
row.appendChild(dot);
|
|
62
103
|
row.appendChild(name);
|
|
63
104
|
row.appendChild(count);
|
|
105
|
+
row.appendChild(focusBtn);
|
|
64
106
|
row.appendChild(editBtn);
|
|
65
107
|
row.addEventListener("click", (e) => {
|
|
66
108
|
if (e.target.closest(".tools-pane-edit"))
|
|
@@ -75,6 +117,17 @@ export function initToolsPane(container, callbacks) {
|
|
|
75
117
|
}
|
|
76
118
|
render();
|
|
77
119
|
});
|
|
120
|
+
focusBtn.addEventListener("click", (e) => {
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
if (focusSet.types.has(t.name)) {
|
|
123
|
+
focusSet.types.delete(t.name);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
focusSet.types.add(t.name);
|
|
127
|
+
}
|
|
128
|
+
emitFocusChange();
|
|
129
|
+
render();
|
|
130
|
+
});
|
|
78
131
|
editBtn.addEventListener("click", (e) => {
|
|
79
132
|
e.stopPropagation();
|
|
80
133
|
startInlineEdit(row, t.name, (newName) => {
|
|
@@ -85,6 +138,26 @@ export function initToolsPane(container, callbacks) {
|
|
|
85
138
|
});
|
|
86
139
|
section.appendChild(row);
|
|
87
140
|
}
|
|
141
|
+
// Show clear button when types are focused
|
|
142
|
+
if (focusSet.types.size > 0) {
|
|
143
|
+
const clearRow = document.createElement("div");
|
|
144
|
+
clearRow.className = "tools-pane-row tools-pane-clickable tools-pane-focus-clear";
|
|
145
|
+
const label = document.createElement("span");
|
|
146
|
+
label.className = "tools-pane-name";
|
|
147
|
+
label.style.color = "var(--accent)";
|
|
148
|
+
label.textContent = `${focusSet.types.size} type${focusSet.types.size > 1 ? "s" : ""} focused`;
|
|
149
|
+
const clearBtn = document.createElement("span");
|
|
150
|
+
clearBtn.className = "tools-pane-badge";
|
|
151
|
+
clearBtn.textContent = "clear types";
|
|
152
|
+
clearRow.appendChild(label);
|
|
153
|
+
clearRow.appendChild(clearBtn);
|
|
154
|
+
clearRow.addEventListener("click", () => {
|
|
155
|
+
focusSet.types.clear();
|
|
156
|
+
emitFocusChange();
|
|
157
|
+
render();
|
|
158
|
+
});
|
|
159
|
+
section.appendChild(clearRow);
|
|
160
|
+
}
|
|
88
161
|
}));
|
|
89
162
|
}
|
|
90
163
|
// Edge types — with rename
|
|
@@ -118,7 +191,7 @@ export function initToolsPane(container, callbacks) {
|
|
|
118
191
|
}
|
|
119
192
|
}));
|
|
120
193
|
}
|
|
121
|
-
// Most connected nodes — click to navigate
|
|
194
|
+
// Most connected nodes — click to navigate, focus button
|
|
122
195
|
if (stats.mostConnected.length) {
|
|
123
196
|
content.appendChild(makeSection("Most Connected", (section) => {
|
|
124
197
|
for (const n of stats.mostConnected) {
|
|
@@ -133,12 +206,34 @@ export function initToolsPane(container, callbacks) {
|
|
|
133
206
|
const count = document.createElement("span");
|
|
134
207
|
count.className = "tools-pane-count";
|
|
135
208
|
count.textContent = `${n.connections}`;
|
|
209
|
+
const focusBtn = document.createElement("button");
|
|
210
|
+
focusBtn.className = "tools-pane-edit tools-pane-focus-toggle";
|
|
211
|
+
if (isNodeFocused(n.id))
|
|
212
|
+
focusBtn.classList.add("tools-pane-focus-active");
|
|
213
|
+
focusBtn.textContent = "\u25CE";
|
|
214
|
+
focusBtn.title = isNodeFocused(n.id)
|
|
215
|
+
? `Remove ${n.label} from focus`
|
|
216
|
+
: `Add ${n.label} to focus`;
|
|
136
217
|
row.appendChild(dot);
|
|
137
218
|
row.appendChild(name);
|
|
138
219
|
row.appendChild(count);
|
|
139
|
-
row.
|
|
220
|
+
row.appendChild(focusBtn);
|
|
221
|
+
row.addEventListener("click", (e) => {
|
|
222
|
+
if (e.target.closest(".tools-pane-edit"))
|
|
223
|
+
return;
|
|
140
224
|
callbacks.onNavigateToNode(n.id);
|
|
141
225
|
});
|
|
226
|
+
focusBtn.addEventListener("click", (e) => {
|
|
227
|
+
e.stopPropagation();
|
|
228
|
+
if (focusSet.nodeIds.has(n.id)) {
|
|
229
|
+
focusSet.nodeIds.delete(n.id);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
focusSet.nodeIds.add(n.id);
|
|
233
|
+
}
|
|
234
|
+
emitFocusChange();
|
|
235
|
+
render();
|
|
236
|
+
});
|
|
142
237
|
section.appendChild(row);
|
|
143
238
|
}
|
|
144
239
|
}));
|
|
@@ -153,7 +248,7 @@ export function initToolsPane(container, callbacks) {
|
|
|
153
248
|
issues.push(`${stats.emptyNodes.length} empty node${stats.emptyNodes.length > 1 ? "s" : ""}`);
|
|
154
249
|
if (issues.length) {
|
|
155
250
|
content.appendChild(makeSection("Quality", (section) => {
|
|
156
|
-
// Orphans — click to navigate
|
|
251
|
+
// Orphans — click to navigate, focus button
|
|
157
252
|
for (const o of stats.orphans.slice(0, 5)) {
|
|
158
253
|
const row = document.createElement("div");
|
|
159
254
|
row.className = "tools-pane-row tools-pane-clickable tools-pane-issue";
|
|
@@ -166,12 +261,34 @@ export function initToolsPane(container, callbacks) {
|
|
|
166
261
|
const badge = document.createElement("span");
|
|
167
262
|
badge.className = "tools-pane-badge";
|
|
168
263
|
badge.textContent = "orphan";
|
|
264
|
+
const focusBtn = document.createElement("button");
|
|
265
|
+
focusBtn.className = "tools-pane-edit tools-pane-focus-toggle";
|
|
266
|
+
if (isNodeFocused(o.id))
|
|
267
|
+
focusBtn.classList.add("tools-pane-focus-active");
|
|
268
|
+
focusBtn.textContent = "\u25CE";
|
|
269
|
+
focusBtn.title = isNodeFocused(o.id)
|
|
270
|
+
? `Remove ${o.label} from focus`
|
|
271
|
+
: `Add ${o.label} to focus`;
|
|
169
272
|
row.appendChild(dot);
|
|
170
273
|
row.appendChild(name);
|
|
171
274
|
row.appendChild(badge);
|
|
172
|
-
row.
|
|
275
|
+
row.appendChild(focusBtn);
|
|
276
|
+
row.addEventListener("click", (e) => {
|
|
277
|
+
if (e.target.closest(".tools-pane-edit"))
|
|
278
|
+
return;
|
|
173
279
|
callbacks.onNavigateToNode(o.id);
|
|
174
280
|
});
|
|
281
|
+
focusBtn.addEventListener("click", (e) => {
|
|
282
|
+
e.stopPropagation();
|
|
283
|
+
if (focusSet.nodeIds.has(o.id)) {
|
|
284
|
+
focusSet.nodeIds.delete(o.id);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
focusSet.nodeIds.add(o.id);
|
|
288
|
+
}
|
|
289
|
+
emitFocusChange();
|
|
290
|
+
render();
|
|
291
|
+
});
|
|
175
292
|
section.appendChild(row);
|
|
176
293
|
}
|
|
177
294
|
if (stats.orphans.length > 5) {
|
|
@@ -200,6 +317,37 @@ export function initToolsPane(container, callbacks) {
|
|
|
200
317
|
}
|
|
201
318
|
}));
|
|
202
319
|
}
|
|
320
|
+
// Unified focus summary (if anything from any section is focused)
|
|
321
|
+
if (!isFocusSetEmpty()) {
|
|
322
|
+
const resolved = resolveFocusSet();
|
|
323
|
+
const summaryParts = [];
|
|
324
|
+
if (focusSet.types.size > 0)
|
|
325
|
+
summaryParts.push(`${focusSet.types.size} type${focusSet.types.size > 1 ? "s" : ""}`);
|
|
326
|
+
if (focusSet.nodeIds.size > 0)
|
|
327
|
+
summaryParts.push(`${focusSet.nodeIds.size} node${focusSet.nodeIds.size > 1 ? "s" : ""}`);
|
|
328
|
+
content.appendChild(makeSection("Focus", (section) => {
|
|
329
|
+
const row = document.createElement("div");
|
|
330
|
+
row.className = "tools-pane-row";
|
|
331
|
+
const label = document.createElement("span");
|
|
332
|
+
label.className = "tools-pane-name";
|
|
333
|
+
label.style.color = "var(--accent)";
|
|
334
|
+
label.textContent = `${summaryParts.join(" + ")} (${resolved.length} total)`;
|
|
335
|
+
const clearBtn = document.createElement("button");
|
|
336
|
+
clearBtn.className = "tools-pane-edit tools-pane-focus-active";
|
|
337
|
+
clearBtn.style.opacity = "1";
|
|
338
|
+
clearBtn.textContent = "\u00d7";
|
|
339
|
+
clearBtn.title = "Clear all focus";
|
|
340
|
+
clearBtn.addEventListener("click", () => {
|
|
341
|
+
focusSet.types.clear();
|
|
342
|
+
focusSet.nodeIds.clear();
|
|
343
|
+
emitFocusChange();
|
|
344
|
+
render();
|
|
345
|
+
});
|
|
346
|
+
row.appendChild(label);
|
|
347
|
+
row.appendChild(clearBtn);
|
|
348
|
+
section.appendChild(row);
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
203
351
|
// Controls section
|
|
204
352
|
content.appendChild(makeSection("Controls", (section) => {
|
|
205
353
|
// Edge labels toggle
|
|
@@ -411,9 +559,23 @@ export function initToolsPane(container, callbacks) {
|
|
|
411
559
|
content.classList.add("hidden");
|
|
412
560
|
toggle.classList.remove("active");
|
|
413
561
|
},
|
|
562
|
+
addToFocusSet(nodeIds) {
|
|
563
|
+
for (const id of nodeIds)
|
|
564
|
+
focusSet.nodeIds.add(id);
|
|
565
|
+
emitFocusChange();
|
|
566
|
+
render();
|
|
567
|
+
},
|
|
568
|
+
clearFocusSet() {
|
|
569
|
+
focusSet.types.clear();
|
|
570
|
+
focusSet.nodeIds.clear();
|
|
571
|
+
emitFocusChange();
|
|
572
|
+
render();
|
|
573
|
+
},
|
|
414
574
|
setData(newData) {
|
|
415
575
|
data = newData;
|
|
416
576
|
activeTypeFilter = null;
|
|
577
|
+
focusSet.types.clear();
|
|
578
|
+
focusSet.nodeIds.clear();
|
|
417
579
|
if (data && data.nodes.length > 0) {
|
|
418
580
|
stats = deriveStats(data);
|
|
419
581
|
toggle.classList.remove("hidden");
|
package/package.json
CHANGED