backpack-viewer 0.2.20 → 0.3.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/README.md +2 -0
- package/bin/serve.js +184 -1
- package/dist/api.d.ts +25 -0
- package/dist/api.js +42 -0
- package/dist/app/assets/index-CBjy2b6N.js +34 -0
- package/dist/app/assets/index-CvkozBSE.css +1 -0
- package/dist/app/assets/layout-worker-BZXiBoiC.js +1 -0
- package/dist/app/index.html +2 -2
- package/dist/canvas.d.ts +19 -0
- package/dist/canvas.js +662 -144
- package/dist/config.js +1 -0
- package/dist/context-menu.d.ts +13 -0
- package/dist/context-menu.js +64 -0
- package/dist/default-config.json +6 -1
- package/dist/empty-state.js +13 -0
- package/dist/info-panel.js +2 -2
- package/dist/keybindings.d.ts +1 -1
- package/dist/keybindings.js +2 -0
- package/dist/label-cache.d.ts +14 -0
- package/dist/label-cache.js +54 -0
- package/dist/layout-worker.d.ts +17 -0
- package/dist/layout-worker.js +78 -0
- package/dist/layout.js +73 -18
- package/dist/main.js +266 -14
- package/dist/quadtree.d.ts +43 -0
- package/dist/quadtree.js +147 -0
- package/dist/shortcuts.js +2 -0
- package/dist/sidebar.d.ts +10 -0
- package/dist/sidebar.js +134 -4
- package/dist/spatial-hash.d.ts +22 -0
- package/dist/spatial-hash.js +67 -0
- package/dist/style.css +357 -0
- package/dist/tools-pane.d.ts +10 -0
- package/dist/tools-pane.js +192 -0
- package/package.json +2 -2
- package/dist/app/assets/index-BAsAhA_i.js +0 -21
- package/dist/app/assets/index-CvETIueX.css +0 -1
package/dist/style.css
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
--canvas-type-badge-dim: rgba(115, 115, 115, 0.15);
|
|
46
46
|
--canvas-selection-border: #d4d4d4;
|
|
47
47
|
--canvas-node-border: rgba(255, 255, 255, 0.15);
|
|
48
|
+
--canvas-walk-edge: #e8d5c4;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
[data-theme="light"] {
|
|
@@ -86,6 +87,7 @@
|
|
|
86
87
|
--canvas-type-badge-dim: rgba(87, 83, 78, 0.15);
|
|
87
88
|
--canvas-selection-border: #292524;
|
|
88
89
|
--canvas-node-border: rgba(0, 0, 0, 0.1);
|
|
90
|
+
--canvas-walk-edge: #1a1a1a;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
body {
|
|
@@ -235,6 +237,54 @@ body {
|
|
|
235
237
|
opacity: 0.7;
|
|
236
238
|
}
|
|
237
239
|
|
|
240
|
+
/* --- Remote graphs section in sidebar --- */
|
|
241
|
+
|
|
242
|
+
.sidebar-section-heading {
|
|
243
|
+
font-size: 10px;
|
|
244
|
+
font-weight: 600;
|
|
245
|
+
color: var(--text-dim);
|
|
246
|
+
letter-spacing: 0.08em;
|
|
247
|
+
margin: 16px 12px 6px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.remote-list {
|
|
251
|
+
list-style: none;
|
|
252
|
+
padding: 0;
|
|
253
|
+
margin: 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.ontology-item-remote .name {
|
|
257
|
+
display: inline;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.remote-name-row {
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: 8px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.remote-badge {
|
|
267
|
+
font-size: 9px;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
color: var(--text-dim);
|
|
270
|
+
background: var(--bg-hover);
|
|
271
|
+
padding: 1px 6px;
|
|
272
|
+
border-radius: 4px;
|
|
273
|
+
text-transform: uppercase;
|
|
274
|
+
letter-spacing: 0.04em;
|
|
275
|
+
border: 1px solid var(--border);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.remote-source {
|
|
279
|
+
display: block;
|
|
280
|
+
font-size: 10px;
|
|
281
|
+
color: var(--text-dim);
|
|
282
|
+
margin-top: 1px;
|
|
283
|
+
overflow: hidden;
|
|
284
|
+
text-overflow: ellipsis;
|
|
285
|
+
white-space: nowrap;
|
|
286
|
+
}
|
|
287
|
+
|
|
238
288
|
.sidebar-edit-btn:hover {
|
|
239
289
|
opacity: 1 !important;
|
|
240
290
|
color: var(--text);
|
|
@@ -265,6 +315,22 @@ body {
|
|
|
265
315
|
opacity: 1;
|
|
266
316
|
}
|
|
267
317
|
|
|
318
|
+
.sidebar-lock-badge {
|
|
319
|
+
font-size: 10px;
|
|
320
|
+
color: #c08c00;
|
|
321
|
+
display: none;
|
|
322
|
+
margin-top: 2px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.sidebar-lock-badge.active {
|
|
326
|
+
display: block;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.sidebar-lock-badge.active::before {
|
|
330
|
+
content: "● ";
|
|
331
|
+
color: #c08c00;
|
|
332
|
+
}
|
|
333
|
+
|
|
268
334
|
.branch-picker {
|
|
269
335
|
background: var(--bg-surface);
|
|
270
336
|
border: 1px solid var(--border);
|
|
@@ -309,6 +375,46 @@ body {
|
|
|
309
375
|
padding: 0 4px;
|
|
310
376
|
}
|
|
311
377
|
|
|
378
|
+
.sidebar-snippets {
|
|
379
|
+
margin-top: 4px;
|
|
380
|
+
padding-left: 8px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.sidebar-snippet {
|
|
384
|
+
display: flex;
|
|
385
|
+
align-items: center;
|
|
386
|
+
justify-content: space-between;
|
|
387
|
+
padding: 2px 4px;
|
|
388
|
+
border-radius: 4px;
|
|
389
|
+
cursor: pointer;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.sidebar-snippet:hover {
|
|
393
|
+
background: var(--bg-hover);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.sidebar-snippet-label {
|
|
397
|
+
font-size: 10px;
|
|
398
|
+
color: var(--text-dim);
|
|
399
|
+
overflow: hidden;
|
|
400
|
+
text-overflow: ellipsis;
|
|
401
|
+
white-space: nowrap;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.sidebar-snippet-delete {
|
|
405
|
+
background: none;
|
|
406
|
+
border: none;
|
|
407
|
+
color: var(--text-dim);
|
|
408
|
+
font-size: 12px;
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
padding: 0 2px;
|
|
411
|
+
opacity: 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.sidebar-snippet:hover .sidebar-snippet-delete {
|
|
415
|
+
opacity: 1;
|
|
416
|
+
}
|
|
417
|
+
|
|
312
418
|
.branch-picker-delete:hover {
|
|
313
419
|
color: var(--danger, #e55);
|
|
314
420
|
}
|
|
@@ -501,6 +607,23 @@ body {
|
|
|
501
607
|
background: var(--bg-hover);
|
|
502
608
|
}
|
|
503
609
|
|
|
610
|
+
/* --- Node Tooltip --- */
|
|
611
|
+
|
|
612
|
+
.node-tooltip {
|
|
613
|
+
position: absolute;
|
|
614
|
+
pointer-events: none;
|
|
615
|
+
background: var(--bg);
|
|
616
|
+
color: var(--text);
|
|
617
|
+
border: 1px solid var(--border);
|
|
618
|
+
border-radius: 6px;
|
|
619
|
+
padding: 4px 8px;
|
|
620
|
+
font-size: 12px;
|
|
621
|
+
white-space: nowrap;
|
|
622
|
+
z-index: 20;
|
|
623
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
624
|
+
opacity: 0.95;
|
|
625
|
+
}
|
|
626
|
+
|
|
504
627
|
/* --- Canvas --- */
|
|
505
628
|
|
|
506
629
|
#canvas-container {
|
|
@@ -863,6 +986,25 @@ body {
|
|
|
863
986
|
margin-bottom: 8px;
|
|
864
987
|
}
|
|
865
988
|
|
|
989
|
+
.info-badge-row {
|
|
990
|
+
display: flex;
|
|
991
|
+
flex-wrap: wrap;
|
|
992
|
+
gap: 4px;
|
|
993
|
+
margin-top: 6px;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.info-empty-message {
|
|
997
|
+
font-size: 12px;
|
|
998
|
+
color: var(--text-dim);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.share-list-message {
|
|
1002
|
+
font-size: 13px;
|
|
1003
|
+
color: var(--text-dim);
|
|
1004
|
+
text-align: center;
|
|
1005
|
+
padding: 12px;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
866
1008
|
.info-label {
|
|
867
1009
|
font-size: 18px;
|
|
868
1010
|
font-weight: 600;
|
|
@@ -1282,6 +1424,41 @@ body {
|
|
|
1282
1424
|
color: var(--text-dim);
|
|
1283
1425
|
}
|
|
1284
1426
|
|
|
1427
|
+
.tools-pane-token-card {
|
|
1428
|
+
padding: 8px 10px;
|
|
1429
|
+
margin-bottom: 10px;
|
|
1430
|
+
border: 1px solid var(--border);
|
|
1431
|
+
border-radius: 6px;
|
|
1432
|
+
background: var(--bg-hover);
|
|
1433
|
+
font-size: 11px;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
.token-card-label {
|
|
1437
|
+
font-weight: 600;
|
|
1438
|
+
color: var(--text);
|
|
1439
|
+
margin-bottom: 4px;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
.token-card-stat {
|
|
1443
|
+
color: var(--text-muted);
|
|
1444
|
+
margin-bottom: 4px;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
.token-card-bar {
|
|
1448
|
+
height: 4px;
|
|
1449
|
+
background: var(--border);
|
|
1450
|
+
border-radius: 2px;
|
|
1451
|
+
margin: 6px 0;
|
|
1452
|
+
overflow: hidden;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.token-card-bar-fill {
|
|
1456
|
+
height: 100%;
|
|
1457
|
+
background: var(--accent);
|
|
1458
|
+
border-radius: 2px;
|
|
1459
|
+
transition: width 0.3s ease;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1285
1462
|
.tools-pane-clickable {
|
|
1286
1463
|
cursor: pointer;
|
|
1287
1464
|
border-radius: 4px;
|
|
@@ -1336,6 +1513,28 @@ body {
|
|
|
1336
1513
|
color: var(--accent);
|
|
1337
1514
|
}
|
|
1338
1515
|
|
|
1516
|
+
.tools-pane-actions {
|
|
1517
|
+
display: flex;
|
|
1518
|
+
gap: 6px;
|
|
1519
|
+
padding-top: 4px;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
.tools-pane-action-btn {
|
|
1523
|
+
background: none;
|
|
1524
|
+
border: 1px solid var(--border);
|
|
1525
|
+
color: var(--text-muted);
|
|
1526
|
+
font-size: 10px;
|
|
1527
|
+
padding: 2px 8px;
|
|
1528
|
+
border-radius: 3px;
|
|
1529
|
+
cursor: pointer;
|
|
1530
|
+
transition: color 0.1s, border-color 0.1s;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.tools-pane-action-btn:hover {
|
|
1534
|
+
color: var(--accent);
|
|
1535
|
+
border-color: var(--accent);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1339
1538
|
.tools-pane-focus-toggle {
|
|
1340
1539
|
opacity: 0.4;
|
|
1341
1540
|
font-size: 11px;
|
|
@@ -1514,16 +1713,52 @@ body {
|
|
|
1514
1713
|
justify-content: center;
|
|
1515
1714
|
z-index: 5;
|
|
1516
1715
|
pointer-events: none;
|
|
1716
|
+
overflow: hidden;
|
|
1517
1717
|
}
|
|
1518
1718
|
|
|
1519
1719
|
.empty-state.hidden {
|
|
1520
1720
|
display: none;
|
|
1521
1721
|
}
|
|
1522
1722
|
|
|
1723
|
+
.empty-state-bg {
|
|
1724
|
+
position: absolute;
|
|
1725
|
+
inset: 0;
|
|
1726
|
+
overflow: hidden;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
.empty-state-circle {
|
|
1730
|
+
position: absolute;
|
|
1731
|
+
border-radius: 50%;
|
|
1732
|
+
background: var(--accent);
|
|
1733
|
+
opacity: 0.07;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
.empty-state-circle.c1 { width: 80px; height: 80px; left: 20%; top: 15%; animation: float-circle 8s ease-in-out infinite; }
|
|
1737
|
+
.empty-state-circle.c2 { width: 50px; height: 50px; right: 25%; top: 25%; animation: float-circle 6s ease-in-out 1s infinite; }
|
|
1738
|
+
.empty-state-circle.c3 { width: 65px; height: 65px; left: 55%; bottom: 20%; animation: float-circle 7s ease-in-out 2s infinite; }
|
|
1739
|
+
.empty-state-circle.c4 { width: 40px; height: 40px; left: 15%; bottom: 30%; animation: float-circle 9s ease-in-out 0.5s infinite; }
|
|
1740
|
+
.empty-state-circle.c5 { width: 55px; height: 55px; right: 15%; bottom: 35%; animation: float-circle 7.5s ease-in-out 1.5s infinite; }
|
|
1741
|
+
|
|
1742
|
+
.empty-state-lines {
|
|
1743
|
+
position: absolute;
|
|
1744
|
+
inset: 0;
|
|
1745
|
+
width: 100%;
|
|
1746
|
+
height: 100%;
|
|
1747
|
+
color: var(--text-dim);
|
|
1748
|
+
animation: float-circle 10s ease-in-out infinite;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
@keyframes float-circle {
|
|
1752
|
+
0%, 100% { transform: translateY(0); }
|
|
1753
|
+
50% { transform: translateY(-12px); }
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1523
1756
|
.empty-state-content {
|
|
1524
1757
|
text-align: center;
|
|
1525
1758
|
max-width: 420px;
|
|
1526
1759
|
padding: 40px 24px;
|
|
1760
|
+
position: relative;
|
|
1761
|
+
z-index: 1;
|
|
1527
1762
|
}
|
|
1528
1763
|
|
|
1529
1764
|
.empty-state-icon {
|
|
@@ -1841,3 +2076,125 @@ body {
|
|
|
1841
2076
|
opacity: 1;
|
|
1842
2077
|
transform: translateX(-50%) translateY(0);
|
|
1843
2078
|
}
|
|
2079
|
+
|
|
2080
|
+
/* --- Context Menu --- */
|
|
2081
|
+
|
|
2082
|
+
.context-menu {
|
|
2083
|
+
position: absolute;
|
|
2084
|
+
z-index: 100;
|
|
2085
|
+
background: var(--bg-surface);
|
|
2086
|
+
border: 1px solid var(--border);
|
|
2087
|
+
border-radius: 8px;
|
|
2088
|
+
padding: 4px;
|
|
2089
|
+
min-width: 180px;
|
|
2090
|
+
box-shadow: 0 8px 24px var(--shadow);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
.context-menu-item {
|
|
2094
|
+
padding: 6px 12px;
|
|
2095
|
+
font-size: 12px;
|
|
2096
|
+
color: var(--text);
|
|
2097
|
+
border-radius: 4px;
|
|
2098
|
+
cursor: pointer;
|
|
2099
|
+
white-space: nowrap;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
.context-menu-item:hover {
|
|
2103
|
+
background: var(--bg-hover);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
.context-menu-separator {
|
|
2107
|
+
height: 1px;
|
|
2108
|
+
background: var(--border);
|
|
2109
|
+
margin: 4px 8px;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
/* --- Path Bar --- */
|
|
2113
|
+
|
|
2114
|
+
.path-bar {
|
|
2115
|
+
position: absolute;
|
|
2116
|
+
bottom: 16px;
|
|
2117
|
+
left: 50%;
|
|
2118
|
+
transform: translateX(-50%);
|
|
2119
|
+
z-index: 25;
|
|
2120
|
+
display: flex;
|
|
2121
|
+
align-items: center;
|
|
2122
|
+
gap: 4px;
|
|
2123
|
+
background: var(--bg-surface);
|
|
2124
|
+
border: 1px solid var(--border);
|
|
2125
|
+
border-radius: 8px;
|
|
2126
|
+
padding: 6px 12px;
|
|
2127
|
+
box-shadow: 0 4px 16px var(--shadow);
|
|
2128
|
+
max-width: 80%;
|
|
2129
|
+
overflow-x: auto;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
.path-bar.hidden {
|
|
2133
|
+
display: none;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
.path-bar-node {
|
|
2137
|
+
font-size: 11px;
|
|
2138
|
+
color: var(--text);
|
|
2139
|
+
padding: 2px 8px;
|
|
2140
|
+
border-radius: 4px;
|
|
2141
|
+
cursor: pointer;
|
|
2142
|
+
white-space: nowrap;
|
|
2143
|
+
flex-shrink: 0;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
.path-bar-node:hover {
|
|
2147
|
+
background: var(--bg-hover);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
.path-bar-edge {
|
|
2151
|
+
font-size: 9px;
|
|
2152
|
+
color: var(--text-dim);
|
|
2153
|
+
white-space: nowrap;
|
|
2154
|
+
flex-shrink: 0;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
.path-bar-close {
|
|
2158
|
+
background: none;
|
|
2159
|
+
border: none;
|
|
2160
|
+
color: var(--text-dim);
|
|
2161
|
+
font-size: 14px;
|
|
2162
|
+
cursor: pointer;
|
|
2163
|
+
padding: 0 4px;
|
|
2164
|
+
margin-left: 8px;
|
|
2165
|
+
flex-shrink: 0;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
.path-bar-close:hover {
|
|
2169
|
+
color: var(--text);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/* --- Walk Mode Indicator --- */
|
|
2173
|
+
|
|
2174
|
+
.walk-trail-edge {
|
|
2175
|
+
font-size: 9px;
|
|
2176
|
+
color: var(--text-dim);
|
|
2177
|
+
padding: 1px 0 1px 24px;
|
|
2178
|
+
opacity: 0.7;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
.walk-indicator {
|
|
2182
|
+
font-size: 10px;
|
|
2183
|
+
color: var(--accent);
|
|
2184
|
+
padding: 2px 8px;
|
|
2185
|
+
border: 1px solid rgba(212, 162, 127, 0.4);
|
|
2186
|
+
border-radius: 4px;
|
|
2187
|
+
cursor: pointer;
|
|
2188
|
+
opacity: 0.4;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
.walk-indicator.active {
|
|
2192
|
+
opacity: 1;
|
|
2193
|
+
background: rgba(212, 162, 127, 0.15);
|
|
2194
|
+
animation: walk-strobe 2s ease-in-out infinite;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
@keyframes walk-strobe {
|
|
2198
|
+
0%, 100% { opacity: 0.6; border-color: rgba(212, 162, 127, 0.3); }
|
|
2199
|
+
50% { opacity: 1; border-color: rgba(212, 162, 127, 0.8); }
|
|
2200
|
+
}
|
package/dist/tools-pane.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ interface ToolsPaneCallbacks {
|
|
|
3
3
|
onFilterByType: (type: string | null) => void;
|
|
4
4
|
onNavigateToNode: (nodeId: string) => void;
|
|
5
5
|
onFocusChange: (seedNodeIds: string[] | null) => void;
|
|
6
|
+
onWalkTrailRemove?: (nodeId: string) => void;
|
|
7
|
+
onWalkIsolate?: () => void;
|
|
8
|
+
onWalkSaveSnippet?: (label: string) => void;
|
|
9
|
+
onStarredSaveSnippet?: (label: string, nodeIds: string[]) => void;
|
|
6
10
|
onRenameNodeType: (oldType: string, newType: string) => void;
|
|
7
11
|
onRenameEdgeType: (oldType: string, newType: string) => void;
|
|
8
12
|
onToggleEdgeLabels: (visible: boolean) => void;
|
|
@@ -27,5 +31,11 @@ export declare function initToolsPane(container: HTMLElement, callbacks: ToolsPa
|
|
|
27
31
|
edgeCount: number;
|
|
28
32
|
label?: string;
|
|
29
33
|
}[]): void;
|
|
34
|
+
setWalkTrail(trail: {
|
|
35
|
+
id: string;
|
|
36
|
+
label: string;
|
|
37
|
+
type: string;
|
|
38
|
+
edgeType?: string;
|
|
39
|
+
}[]): void;
|
|
30
40
|
};
|
|
31
41
|
export {};
|
package/dist/tools-pane.js
CHANGED
|
@@ -12,6 +12,7 @@ export function initToolsPane(container, callbacks) {
|
|
|
12
12
|
let typesSearch = "";
|
|
13
13
|
let qualitySearch = "";
|
|
14
14
|
let snapshots = [];
|
|
15
|
+
let walkTrail = [];
|
|
15
16
|
// Unified focus set — two layers that compose via union
|
|
16
17
|
const focusSet = {
|
|
17
18
|
types: new Set(), // toggled node types (dynamic — resolves to all nodes of type)
|
|
@@ -75,6 +76,37 @@ export function initToolsPane(container, callbacks) {
|
|
|
75
76
|
`<span>${stats.edgeCount} edges</span><span class="tools-pane-sep">·</span>` +
|
|
76
77
|
`<span>${stats.types.length} types</span>`;
|
|
77
78
|
content.appendChild(summary);
|
|
79
|
+
// Token efficiency card
|
|
80
|
+
if (data && stats.nodeCount > 0) {
|
|
81
|
+
const graphTokens = Math.ceil(JSON.stringify(data).length / 4);
|
|
82
|
+
// Estimate a typical search response: ~5 NodeSummary results, each ~30 chars
|
|
83
|
+
const avgNodeTokens = Math.round(graphTokens / stats.nodeCount);
|
|
84
|
+
const searchTokens = Math.max(10, Math.round(avgNodeTokens * 0.3) * Math.min(5, stats.nodeCount));
|
|
85
|
+
const percent = graphTokens > searchTokens ? Math.round((1 - searchTokens / graphTokens) * 100) : 0;
|
|
86
|
+
const tokenCard = document.createElement("div");
|
|
87
|
+
tokenCard.className = "tools-pane-token-card";
|
|
88
|
+
const barWidth = Math.min(100, percent);
|
|
89
|
+
const label = document.createElement("div");
|
|
90
|
+
label.className = "token-card-label";
|
|
91
|
+
label.textContent = "Token Efficiency";
|
|
92
|
+
tokenCard.appendChild(label);
|
|
93
|
+
const storedStat = document.createElement("div");
|
|
94
|
+
storedStat.className = "token-card-stat";
|
|
95
|
+
storedStat.textContent = `~${graphTokens.toLocaleString()} tokens stored`;
|
|
96
|
+
tokenCard.appendChild(storedStat);
|
|
97
|
+
const bar = document.createElement("div");
|
|
98
|
+
bar.className = "token-card-bar";
|
|
99
|
+
const barFill = document.createElement("div");
|
|
100
|
+
barFill.className = "token-card-bar-fill";
|
|
101
|
+
barFill.style.width = `${barWidth}%`;
|
|
102
|
+
bar.appendChild(barFill);
|
|
103
|
+
tokenCard.appendChild(bar);
|
|
104
|
+
const reductionStat = document.createElement("div");
|
|
105
|
+
reductionStat.className = "token-card-stat";
|
|
106
|
+
reductionStat.textContent = `A search returns ~${searchTokens} tokens instead of ~${graphTokens.toLocaleString()} (${percent}% reduction)`;
|
|
107
|
+
tokenCard.appendChild(reductionStat);
|
|
108
|
+
content.appendChild(tokenCard);
|
|
109
|
+
}
|
|
78
110
|
// Tab bar
|
|
79
111
|
const tabBar = document.createElement("div");
|
|
80
112
|
tabBar.className = "tools-pane-tabs";
|
|
@@ -100,6 +132,10 @@ export function initToolsPane(container, callbacks) {
|
|
|
100
132
|
if (!isFocusSetEmpty()) {
|
|
101
133
|
renderFocusedSection();
|
|
102
134
|
}
|
|
135
|
+
// Walk trail (visible when walk mode is active)
|
|
136
|
+
if (walkTrail.length > 0) {
|
|
137
|
+
renderWalkTrail();
|
|
138
|
+
}
|
|
103
139
|
// Search input (for types and quality tabs)
|
|
104
140
|
if (activeTab === "types" && stats.types.length > 5) {
|
|
105
141
|
content.appendChild(makeSearchInput("Filter types...", typesSearch, (v) => {
|
|
@@ -137,6 +173,80 @@ export function initToolsPane(container, callbacks) {
|
|
|
137
173
|
renderControlsTab(tabContainer);
|
|
138
174
|
}
|
|
139
175
|
}
|
|
176
|
+
function renderWalkTrail() {
|
|
177
|
+
content.appendChild(makeSection(`Walk Trail (${walkTrail.length})`, (section) => {
|
|
178
|
+
for (let i = 0; i < walkTrail.length; i++) {
|
|
179
|
+
const item = walkTrail[i];
|
|
180
|
+
const isCurrent = i === walkTrail.length - 1;
|
|
181
|
+
// Edge connector from previous node
|
|
182
|
+
if (item.edgeType) {
|
|
183
|
+
const connector = document.createElement("div");
|
|
184
|
+
connector.className = "walk-trail-edge";
|
|
185
|
+
connector.textContent = `\u2193 ${item.edgeType}`;
|
|
186
|
+
section.appendChild(connector);
|
|
187
|
+
}
|
|
188
|
+
const row = document.createElement("div");
|
|
189
|
+
row.className = "tools-pane-row tools-pane-clickable";
|
|
190
|
+
if (isCurrent)
|
|
191
|
+
row.style.fontWeight = "600";
|
|
192
|
+
const num = document.createElement("span");
|
|
193
|
+
num.className = "tools-pane-count";
|
|
194
|
+
num.style.minWidth = "18px";
|
|
195
|
+
num.textContent = `${i + 1}`;
|
|
196
|
+
const dot = document.createElement("span");
|
|
197
|
+
dot.className = "tools-pane-dot";
|
|
198
|
+
dot.style.backgroundColor = getColor(item.type);
|
|
199
|
+
const name = document.createElement("span");
|
|
200
|
+
name.className = "tools-pane-name";
|
|
201
|
+
name.textContent = item.label;
|
|
202
|
+
const typeBadge = document.createElement("span");
|
|
203
|
+
typeBadge.className = "tools-pane-count";
|
|
204
|
+
typeBadge.textContent = item.type;
|
|
205
|
+
const removeBtn = document.createElement("button");
|
|
206
|
+
removeBtn.className = "tools-pane-edit";
|
|
207
|
+
removeBtn.style.opacity = "1";
|
|
208
|
+
removeBtn.textContent = "\u00d7";
|
|
209
|
+
removeBtn.title = "Remove from trail";
|
|
210
|
+
removeBtn.addEventListener("click", (e) => {
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
callbacks.onWalkTrailRemove?.(item.id);
|
|
213
|
+
});
|
|
214
|
+
row.appendChild(num);
|
|
215
|
+
row.appendChild(dot);
|
|
216
|
+
row.appendChild(name);
|
|
217
|
+
row.appendChild(typeBadge);
|
|
218
|
+
row.appendChild(removeBtn);
|
|
219
|
+
row.addEventListener("click", () => {
|
|
220
|
+
callbacks.onNavigateToNode(item.id);
|
|
221
|
+
});
|
|
222
|
+
section.appendChild(row);
|
|
223
|
+
}
|
|
224
|
+
// Isolate button
|
|
225
|
+
// Action buttons row
|
|
226
|
+
const actionsRow = document.createElement("div");
|
|
227
|
+
actionsRow.className = "tools-pane-export-row";
|
|
228
|
+
if (callbacks.onWalkIsolate) {
|
|
229
|
+
const isolateBtn = document.createElement("button");
|
|
230
|
+
isolateBtn.className = "tools-pane-export-btn";
|
|
231
|
+
isolateBtn.textContent = "Isolate (I)";
|
|
232
|
+
isolateBtn.addEventListener("click", () => callbacks.onWalkIsolate());
|
|
233
|
+
actionsRow.appendChild(isolateBtn);
|
|
234
|
+
}
|
|
235
|
+
if (callbacks.onWalkSaveSnippet && walkTrail.length >= 2) {
|
|
236
|
+
const saveBtn = document.createElement("button");
|
|
237
|
+
saveBtn.className = "tools-pane-export-btn";
|
|
238
|
+
saveBtn.textContent = "Save snippet";
|
|
239
|
+
saveBtn.addEventListener("click", () => {
|
|
240
|
+
showPrompt("Save snippet", "Name for this snippet").then((label) => {
|
|
241
|
+
if (label)
|
|
242
|
+
callbacks.onWalkSaveSnippet(label);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
actionsRow.appendChild(saveBtn);
|
|
246
|
+
}
|
|
247
|
+
section.appendChild(actionsRow);
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
140
250
|
function renderFocusedSection() {
|
|
141
251
|
if (!stats || !data)
|
|
142
252
|
return;
|
|
@@ -363,6 +473,80 @@ export function initToolsPane(container, callbacks) {
|
|
|
363
473
|
if (!stats)
|
|
364
474
|
return;
|
|
365
475
|
const qq = qualitySearch.toLowerCase();
|
|
476
|
+
// Starred nodes — click to navigate, focus button
|
|
477
|
+
const filteredStarred = stats.starred.filter((n) => !qq || n.label.toLowerCase().includes(qq) || n.type.toLowerCase().includes(qq));
|
|
478
|
+
if (filteredStarred.length) {
|
|
479
|
+
target.appendChild(makeSection("\u2605 Starred", (section) => {
|
|
480
|
+
for (const n of filteredStarred) {
|
|
481
|
+
const row = document.createElement("div");
|
|
482
|
+
row.className = "tools-pane-row tools-pane-clickable";
|
|
483
|
+
const dot = document.createElement("span");
|
|
484
|
+
dot.className = "tools-pane-dot";
|
|
485
|
+
dot.style.backgroundColor = getColor(n.type);
|
|
486
|
+
const name = document.createElement("span");
|
|
487
|
+
name.className = "tools-pane-name";
|
|
488
|
+
name.textContent = n.label;
|
|
489
|
+
const focusBtn = document.createElement("button");
|
|
490
|
+
focusBtn.className = "tools-pane-edit tools-pane-focus-toggle";
|
|
491
|
+
if (isNodeFocused(n.id))
|
|
492
|
+
focusBtn.classList.add("tools-pane-focus-active");
|
|
493
|
+
focusBtn.textContent = "\u25CE";
|
|
494
|
+
focusBtn.title = isNodeFocused(n.id)
|
|
495
|
+
? `Remove ${n.label} from focus`
|
|
496
|
+
: `Add ${n.label} to focus`;
|
|
497
|
+
row.appendChild(dot);
|
|
498
|
+
row.appendChild(name);
|
|
499
|
+
row.appendChild(focusBtn);
|
|
500
|
+
row.addEventListener("click", (e) => {
|
|
501
|
+
if (e.target.closest(".tools-pane-edit"))
|
|
502
|
+
return;
|
|
503
|
+
callbacks.onNavigateToNode(n.id);
|
|
504
|
+
});
|
|
505
|
+
focusBtn.addEventListener("click", (e) => {
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
if (focusSet.nodeIds.has(n.id)) {
|
|
508
|
+
focusSet.nodeIds.delete(n.id);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
focusSet.nodeIds.add(n.id);
|
|
512
|
+
}
|
|
513
|
+
emitFocusChange();
|
|
514
|
+
render();
|
|
515
|
+
});
|
|
516
|
+
section.appendChild(row);
|
|
517
|
+
}
|
|
518
|
+
// Action buttons row
|
|
519
|
+
const actions = document.createElement("div");
|
|
520
|
+
actions.className = "tools-pane-row tools-pane-actions";
|
|
521
|
+
const focusAllBtn = document.createElement("button");
|
|
522
|
+
focusAllBtn.className = "tools-pane-action-btn";
|
|
523
|
+
focusAllBtn.textContent = "Focus all";
|
|
524
|
+
focusAllBtn.title = "Enter focus mode with all starred nodes";
|
|
525
|
+
focusAllBtn.addEventListener("click", () => {
|
|
526
|
+
focusSet.nodeIds.clear();
|
|
527
|
+
focusSet.types.clear();
|
|
528
|
+
for (const n of stats.starred)
|
|
529
|
+
focusSet.nodeIds.add(n.id);
|
|
530
|
+
emitFocusChange();
|
|
531
|
+
render();
|
|
532
|
+
});
|
|
533
|
+
actions.appendChild(focusAllBtn);
|
|
534
|
+
if (callbacks.onStarredSaveSnippet) {
|
|
535
|
+
const saveBtn = document.createElement("button");
|
|
536
|
+
saveBtn.className = "tools-pane-action-btn";
|
|
537
|
+
saveBtn.textContent = "Save as snippet";
|
|
538
|
+
saveBtn.title = "Save starred nodes as a reusable snippet";
|
|
539
|
+
saveBtn.addEventListener("click", async () => {
|
|
540
|
+
const label = await showPrompt("Snippet name", "starred");
|
|
541
|
+
if (label) {
|
|
542
|
+
callbacks.onStarredSaveSnippet(label, stats.starred.map((n) => n.id));
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
actions.appendChild(saveBtn);
|
|
546
|
+
}
|
|
547
|
+
section.appendChild(actions);
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
366
550
|
// Most connected nodes — click to navigate, focus button
|
|
367
551
|
const filteredConnected = stats.mostConnected.filter((n) => !qq || n.label.toLowerCase().includes(qq) || n.type.toLowerCase().includes(qq));
|
|
368
552
|
if (filteredConnected.length) {
|
|
@@ -759,6 +943,9 @@ export function initToolsPane(container, callbacks) {
|
|
|
759
943
|
connectedNodes.add(edge.targetId);
|
|
760
944
|
}
|
|
761
945
|
const nodeLabel = (n) => firstStringValue(n.properties) ?? n.id;
|
|
946
|
+
const starred = graphData.nodes
|
|
947
|
+
.filter((n) => n.properties._starred === true)
|
|
948
|
+
.map((n) => ({ id: n.id, label: nodeLabel(n), type: n.type }));
|
|
762
949
|
const orphans = graphData.nodes
|
|
763
950
|
.filter((n) => !connectedNodes.has(n.id))
|
|
764
951
|
.map((n) => ({ id: n.id, label: nodeLabel(n), type: n.type }));
|
|
@@ -787,6 +974,7 @@ export function initToolsPane(container, callbacks) {
|
|
|
787
974
|
edgeTypes: [...edgeTypeCounts.entries()]
|
|
788
975
|
.sort((a, b) => b[1] - a[1])
|
|
789
976
|
.map(([name, count]) => ({ name, count })),
|
|
977
|
+
starred,
|
|
790
978
|
orphans,
|
|
791
979
|
singletons,
|
|
792
980
|
emptyNodes,
|
|
@@ -833,6 +1021,10 @@ export function initToolsPane(container, callbacks) {
|
|
|
833
1021
|
if (activeTab === "controls")
|
|
834
1022
|
renderTabContent();
|
|
835
1023
|
},
|
|
1024
|
+
setWalkTrail(trail) {
|
|
1025
|
+
walkTrail = trail;
|
|
1026
|
+
render();
|
|
1027
|
+
},
|
|
836
1028
|
};
|
|
837
1029
|
}
|
|
838
1030
|
function timeAgo(timestamp) {
|