@reqlan/analytical 0.6.2 → 0.7.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 +17 -0
- package/out/analysis-api.d.ts +2 -2
- package/out/analysis-api.js +4 -3
- package/out/analysis-api.js.map +1 -1
- package/out/core/filter-specials.d.ts +35 -0
- package/out/core/filter-specials.js +98 -0
- package/out/core/filter-specials.js.map +1 -0
- package/out/core/types.d.ts +10 -0
- package/out/core/types.js.map +1 -1
- package/out/export/build-export-snapshot.d.ts +2 -2
- package/out/export/build-export-snapshot.js +43 -16
- package/out/export/build-export-snapshot.js.map +1 -1
- package/out/export/export-html.d.ts +2 -2
- package/out/export/export-html.js +7 -3
- package/out/export/export-html.js.map +1 -1
- package/out/export/html-export-assets.d.ts +1 -1
- package/out/export/html-export-assets.js +580 -33
- package/out/export/html-export-assets.js.map +1 -1
- package/out/export/html-export-template.js +71 -14
- package/out/export/html-export-template.js.map +1 -1
- package/out/export/html-export-utils.js +15 -5
- package/out/export/html-export-utils.js.map +1 -1
- package/out/export/types.d.ts +19 -0
- package/out/export/write-html-export.d.ts +2 -2
- package/out/export/write-html-export.js +61 -29
- package/out/export/write-html-export.js.map +1 -1
- package/out/graph/physics-core-source.d.ts +5 -0
- package/out/graph/physics-core-source.js +8 -0
- package/out/graph/physics-core-source.js.map +1 -0
- package/out/index-store/index-diagnostics-store.js +1 -2
- package/out/index-store/index-diagnostics-store.js.map +1 -1
- package/out/index-store/sqlite-store.js +4 -2
- package/out/index-store/sqlite-store.js.map +1 -1
- package/out/index-store/webview-graph-queries.d.ts +8 -2
- package/out/index-store/webview-graph-queries.js +77 -14
- package/out/index-store/webview-graph-queries.js.map +1 -1
- package/out/index-store/workspace-index.d.ts +2 -1
- package/out/index-store/workspace-index.js +4 -1
- package/out/index-store/workspace-index.js.map +1 -1
- package/out/index.d.ts +3 -1
- package/out/index.js +1 -0
- package/out/index.js.map +1 -1
- package/package.json +7 -3
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
1
|
+
import { PHYSICS_CORE_CLASSIC_SOURCE } from '../graph/physics-core-source.js';
|
|
2
|
+
/**
|
|
3
|
+
* physics-core.js with `export` keywords stripped, inlined at build time by
|
|
4
|
+
* scripts/generate-physics-core-source.mjs. Using a pre-generated string
|
|
5
|
+
* constant rather than readFileSync(import.meta.url) keeps this bundler-safe:
|
|
6
|
+
* esbuild with format cjs + target es2017 empties import.meta.url, and the
|
|
7
|
+
* source file does not exist on disk relative to the bundled main.cjs anyway.
|
|
8
|
+
*/
|
|
3
9
|
function embedPhysicsCoreSource() {
|
|
4
|
-
return
|
|
5
|
-
.replace(/^export\s+/gm, '');
|
|
10
|
+
return PHYSICS_CORE_CLASSIC_SOURCE;
|
|
6
11
|
}
|
|
7
12
|
export const SHARED_STYLES = `
|
|
8
13
|
:root {
|
|
@@ -127,6 +132,165 @@ a.pill:hover { color: var(--accent-strong); border-color: color-mix(in srgb, var
|
|
|
127
132
|
flex: 1 1 180px;
|
|
128
133
|
min-width: 160px;
|
|
129
134
|
}
|
|
135
|
+
.scd {
|
|
136
|
+
position: relative;
|
|
137
|
+
flex: 1 1 180px;
|
|
138
|
+
min-width: 160px;
|
|
139
|
+
}
|
|
140
|
+
.scd-trigger {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
gap: 0.55rem;
|
|
145
|
+
width: 100%;
|
|
146
|
+
min-height: 2.35rem;
|
|
147
|
+
padding: 0.4rem 0.75rem;
|
|
148
|
+
border: 1px solid var(--line);
|
|
149
|
+
border-radius: 8px;
|
|
150
|
+
background: var(--bg-soft);
|
|
151
|
+
color: var(--fg);
|
|
152
|
+
font: inherit;
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
text-align: left;
|
|
155
|
+
}
|
|
156
|
+
.scd.is-open .scd-trigger,
|
|
157
|
+
.scd-trigger:hover {
|
|
158
|
+
border-color: var(--accent-dim);
|
|
159
|
+
}
|
|
160
|
+
.scd.has-selection .scd-trigger {
|
|
161
|
+
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
|
162
|
+
}
|
|
163
|
+
.scd.is-searching .scd-trigger {
|
|
164
|
+
border-color: var(--line);
|
|
165
|
+
}
|
|
166
|
+
.scd.is-loading .scd-trigger {
|
|
167
|
+
opacity: 0.72;
|
|
168
|
+
}
|
|
169
|
+
.scd-trigger-label {
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
text-overflow: ellipsis;
|
|
172
|
+
white-space: nowrap;
|
|
173
|
+
}
|
|
174
|
+
.scd-chevron {
|
|
175
|
+
width: 0;
|
|
176
|
+
height: 0;
|
|
177
|
+
border-left: 4px solid transparent;
|
|
178
|
+
border-right: 4px solid transparent;
|
|
179
|
+
border-top: 5px solid currentColor;
|
|
180
|
+
opacity: 0.7;
|
|
181
|
+
flex: 0 0 auto;
|
|
182
|
+
}
|
|
183
|
+
.scd.is-open .scd-chevron { transform: rotate(180deg); }
|
|
184
|
+
.scd-panel {
|
|
185
|
+
position: absolute;
|
|
186
|
+
z-index: 50;
|
|
187
|
+
top: calc(100% + 0.35rem);
|
|
188
|
+
left: 0;
|
|
189
|
+
right: auto;
|
|
190
|
+
width: max(100%, 15rem);
|
|
191
|
+
max-width: min(20rem, 80vw);
|
|
192
|
+
max-height: 18rem;
|
|
193
|
+
display: flex;
|
|
194
|
+
flex-direction: column;
|
|
195
|
+
border: 1px solid var(--line);
|
|
196
|
+
border-radius: 10px;
|
|
197
|
+
background: var(--bg-elev);
|
|
198
|
+
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
}
|
|
201
|
+
/* Author display:flex overrides the UA [hidden] rule; keep closed panels invisible. */
|
|
202
|
+
.scd-panel[hidden] {
|
|
203
|
+
display: none;
|
|
204
|
+
}
|
|
205
|
+
.scd-search-row {
|
|
206
|
+
display: flex;
|
|
207
|
+
gap: 0.45rem;
|
|
208
|
+
padding: 0.65rem;
|
|
209
|
+
border-bottom: 1px solid var(--line);
|
|
210
|
+
opacity: 0.72;
|
|
211
|
+
}
|
|
212
|
+
.scd-search-row.is-active { opacity: 1; }
|
|
213
|
+
.scd-search {
|
|
214
|
+
flex: 1;
|
|
215
|
+
min-width: 0;
|
|
216
|
+
padding: 0.4rem 0.55rem;
|
|
217
|
+
border: 1px solid transparent;
|
|
218
|
+
border-radius: 6px;
|
|
219
|
+
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
220
|
+
color: var(--muted);
|
|
221
|
+
font: inherit;
|
|
222
|
+
}
|
|
223
|
+
.scd.is-searching .scd-search,
|
|
224
|
+
.scd-search:focus {
|
|
225
|
+
border-color: var(--accent-dim);
|
|
226
|
+
background: var(--bg);
|
|
227
|
+
color: var(--fg);
|
|
228
|
+
outline: none;
|
|
229
|
+
}
|
|
230
|
+
.scd-clear {
|
|
231
|
+
border: none;
|
|
232
|
+
background: transparent;
|
|
233
|
+
color: var(--accent-strong);
|
|
234
|
+
font: inherit;
|
|
235
|
+
cursor: pointer;
|
|
236
|
+
}
|
|
237
|
+
.scd-list {
|
|
238
|
+
overflow: auto;
|
|
239
|
+
padding: 0.25rem 0 0.55rem;
|
|
240
|
+
}
|
|
241
|
+
.scd-group-label {
|
|
242
|
+
padding: 0.55rem 0.85rem 0.25rem;
|
|
243
|
+
font-size: 0.72rem;
|
|
244
|
+
letter-spacing: 0.06em;
|
|
245
|
+
text-transform: uppercase;
|
|
246
|
+
color: var(--muted);
|
|
247
|
+
}
|
|
248
|
+
.scd-option {
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
gap: 0.55rem;
|
|
252
|
+
padding: 0.45rem 0.85rem;
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
color: var(--fg);
|
|
255
|
+
}
|
|
256
|
+
.scd-option:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
257
|
+
.scd-option input { margin: 0; }
|
|
258
|
+
.scd-option-label {
|
|
259
|
+
flex: 1;
|
|
260
|
+
min-width: 0;
|
|
261
|
+
overflow: hidden;
|
|
262
|
+
text-overflow: ellipsis;
|
|
263
|
+
white-space: nowrap;
|
|
264
|
+
}
|
|
265
|
+
.scd-option-count {
|
|
266
|
+
flex: 0 0 auto;
|
|
267
|
+
font-variant-numeric: tabular-nums;
|
|
268
|
+
font-size: 0.85em;
|
|
269
|
+
color: var(--muted);
|
|
270
|
+
opacity: 0.65;
|
|
271
|
+
}
|
|
272
|
+
.scd-option.is-special .scd-option-label { font-style: italic; }
|
|
273
|
+
.scd-option.is-not-present .scd-option-label { color: var(--faint); }
|
|
274
|
+
.scd-option.is-empty .scd-option-label { color: var(--accent-strong); }
|
|
275
|
+
.scd-option.is-unspecified .scd-option-label { color: var(--rust-muted); }
|
|
276
|
+
.scd-empty {
|
|
277
|
+
margin: 0;
|
|
278
|
+
padding: 0.85rem;
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
font-size: 0.9rem;
|
|
281
|
+
}
|
|
282
|
+
.rollup-list .rollup-special dt {
|
|
283
|
+
font-style: italic;
|
|
284
|
+
}
|
|
285
|
+
.rollup-list .rollup-not-present dt {
|
|
286
|
+
color: var(--faint);
|
|
287
|
+
}
|
|
288
|
+
.rollup-list .rollup-empty dt {
|
|
289
|
+
color: var(--accent-strong);
|
|
290
|
+
}
|
|
291
|
+
.rollup-list .rollup-unspecified dt {
|
|
292
|
+
color: var(--rust-muted);
|
|
293
|
+
}
|
|
130
294
|
.graph-action {
|
|
131
295
|
border: 1px solid var(--line);
|
|
132
296
|
background: var(--bg-soft);
|
|
@@ -269,6 +433,37 @@ body[data-runtime-mode="interactive"] .scroll-window thead .column-filter-row.is
|
|
|
269
433
|
background: linear-gradient(90deg, var(--accent-dim), var(--accent-strong));
|
|
270
434
|
min-width: 0;
|
|
271
435
|
}
|
|
436
|
+
.graph-root {
|
|
437
|
+
position: relative;
|
|
438
|
+
min-height: 620px;
|
|
439
|
+
}
|
|
440
|
+
.graph-root.is-booting {
|
|
441
|
+
display: grid;
|
|
442
|
+
place-items: center;
|
|
443
|
+
border: 1px solid var(--line);
|
|
444
|
+
border-radius: 10px;
|
|
445
|
+
background: var(--bg);
|
|
446
|
+
}
|
|
447
|
+
.graph-boot {
|
|
448
|
+
display: grid;
|
|
449
|
+
justify-items: center;
|
|
450
|
+
gap: 0.75rem;
|
|
451
|
+
padding: 1.5rem;
|
|
452
|
+
text-align: center;
|
|
453
|
+
color: var(--muted);
|
|
454
|
+
}
|
|
455
|
+
.graph-boot p { margin: 0; }
|
|
456
|
+
.graph-boot-spinner {
|
|
457
|
+
width: 1.35rem;
|
|
458
|
+
height: 1.35rem;
|
|
459
|
+
border: 2px solid color-mix(in srgb, var(--muted) 35%, transparent);
|
|
460
|
+
border-top-color: var(--accent-strong);
|
|
461
|
+
border-radius: 50%;
|
|
462
|
+
animation: graph-boot-spin 0.7s linear infinite;
|
|
463
|
+
}
|
|
464
|
+
@keyframes graph-boot-spin {
|
|
465
|
+
to { transform: rotate(360deg); }
|
|
466
|
+
}
|
|
272
467
|
.graph-root canvas {
|
|
273
468
|
display: block;
|
|
274
469
|
width: 100%;
|
|
@@ -391,6 +586,267 @@ function lower(value) {
|
|
|
391
586
|
return String(value || '').toLowerCase();
|
|
392
587
|
}
|
|
393
588
|
|
|
589
|
+
function mountSearchableCheckboxDropdown(host, onChange) {
|
|
590
|
+
if (!host) {
|
|
591
|
+
return {
|
|
592
|
+
getSelected: () => [],
|
|
593
|
+
clear: () => undefined,
|
|
594
|
+
setOnChange: () => undefined
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (host.__scdApi) {
|
|
598
|
+
host.__scdApi.setOnChange(onChange);
|
|
599
|
+
return host.__scdApi;
|
|
600
|
+
}
|
|
601
|
+
let options = [];
|
|
602
|
+
try {
|
|
603
|
+
options = JSON.parse(host.dataset.options || '[]');
|
|
604
|
+
} catch {
|
|
605
|
+
options = [];
|
|
606
|
+
}
|
|
607
|
+
const label = host.dataset.label || 'Filter';
|
|
608
|
+
const placeholder = host.dataset.placeholder || 'Search…';
|
|
609
|
+
let selected = new Set();
|
|
610
|
+
let open = false;
|
|
611
|
+
let searching = false;
|
|
612
|
+
let query = '';
|
|
613
|
+
let panel = null;
|
|
614
|
+
let search = null;
|
|
615
|
+
let searchRow = null;
|
|
616
|
+
let clearBtn = null;
|
|
617
|
+
let list = null;
|
|
618
|
+
let listenersBound = false;
|
|
619
|
+
let changeHandler = onChange;
|
|
620
|
+
|
|
621
|
+
let trigger = host.querySelector('.scd-trigger');
|
|
622
|
+
let triggerLabel = host.querySelector('.scd-trigger-label');
|
|
623
|
+
if (!trigger) {
|
|
624
|
+
trigger = document.createElement('button');
|
|
625
|
+
trigger.type = 'button';
|
|
626
|
+
trigger.className = 'scd-trigger';
|
|
627
|
+
trigger.setAttribute('aria-haspopup', 'listbox');
|
|
628
|
+
triggerLabel = document.createElement('span');
|
|
629
|
+
triggerLabel.className = 'scd-trigger-label';
|
|
630
|
+
const chevron = document.createElement('span');
|
|
631
|
+
chevron.className = 'scd-chevron';
|
|
632
|
+
chevron.setAttribute('aria-hidden', 'true');
|
|
633
|
+
trigger.append(triggerLabel, chevron);
|
|
634
|
+
host.appendChild(trigger);
|
|
635
|
+
}
|
|
636
|
+
if (!trigger.querySelector('.scd-chevron')) {
|
|
637
|
+
const chevron = document.createElement('span');
|
|
638
|
+
chevron.className = 'scd-chevron';
|
|
639
|
+
chevron.setAttribute('aria-hidden', 'true');
|
|
640
|
+
trigger.appendChild(chevron);
|
|
641
|
+
}
|
|
642
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
643
|
+
trigger.setAttribute('aria-busy', 'false');
|
|
644
|
+
host.classList.remove('is-loading');
|
|
645
|
+
host.dataset.scdReady = '1';
|
|
646
|
+
|
|
647
|
+
function emitChange() {
|
|
648
|
+
changeHandler?.([...selected]);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function syncTrigger() {
|
|
652
|
+
const values = [...selected];
|
|
653
|
+
host.dataset.selected = JSON.stringify(values);
|
|
654
|
+
host.classList.toggle('has-selection', values.length > 0);
|
|
655
|
+
if (!triggerLabel) return;
|
|
656
|
+
if (values.length === 0) triggerLabel.textContent = label;
|
|
657
|
+
else if (values.length === 1) {
|
|
658
|
+
const match = options.find(option => option.value === values[0]);
|
|
659
|
+
triggerLabel.textContent = match?.label || values[0];
|
|
660
|
+
} else triggerLabel.textContent = \`\${values.length} selected\`;
|
|
661
|
+
if (clearBtn) clearBtn.hidden = values.length === 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function setSearching(next) {
|
|
665
|
+
searching = next;
|
|
666
|
+
host.classList.toggle('is-searching', searching);
|
|
667
|
+
if (searchRow) searchRow.classList.toggle('is-active', searching);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function endSearching() {
|
|
671
|
+
query = '';
|
|
672
|
+
if (search) search.value = '';
|
|
673
|
+
setSearching(false);
|
|
674
|
+
if (search && document.activeElement === search) search.blur();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function ensurePanel() {
|
|
678
|
+
if (panel) return;
|
|
679
|
+
panel = document.createElement('div');
|
|
680
|
+
panel.className = 'scd-panel';
|
|
681
|
+
panel.hidden = true;
|
|
682
|
+
panel.setAttribute('role', 'listbox');
|
|
683
|
+
panel.setAttribute('aria-multiselectable', 'true');
|
|
684
|
+
panel.setAttribute('aria-label', label);
|
|
685
|
+
|
|
686
|
+
searchRow = document.createElement('div');
|
|
687
|
+
searchRow.className = 'scd-search-row';
|
|
688
|
+
search = document.createElement('input');
|
|
689
|
+
search.type = 'search';
|
|
690
|
+
search.className = 'scd-search';
|
|
691
|
+
search.placeholder = placeholder;
|
|
692
|
+
search.setAttribute('aria-label', \`Search \${label}\`);
|
|
693
|
+
clearBtn = document.createElement('button');
|
|
694
|
+
clearBtn.type = 'button';
|
|
695
|
+
clearBtn.className = 'scd-clear';
|
|
696
|
+
clearBtn.textContent = 'Clear';
|
|
697
|
+
clearBtn.hidden = true;
|
|
698
|
+
searchRow.append(search, clearBtn);
|
|
699
|
+
|
|
700
|
+
list = document.createElement('div');
|
|
701
|
+
list.className = 'scd-list';
|
|
702
|
+
panel.append(searchRow, list);
|
|
703
|
+
host.appendChild(panel);
|
|
704
|
+
|
|
705
|
+
search.addEventListener('focus', () => setSearching(true));
|
|
706
|
+
search.addEventListener('blur', () => {
|
|
707
|
+
if (!query.trim()) setSearching(false);
|
|
708
|
+
});
|
|
709
|
+
search.addEventListener('input', () => {
|
|
710
|
+
query = search.value;
|
|
711
|
+
if (query.trim()) setSearching(true);
|
|
712
|
+
renderList();
|
|
713
|
+
});
|
|
714
|
+
clearBtn.addEventListener('click', () => {
|
|
715
|
+
selected.clear();
|
|
716
|
+
syncTrigger();
|
|
717
|
+
renderList();
|
|
718
|
+
emitChange();
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function renderList() {
|
|
723
|
+
if (!list) return;
|
|
724
|
+
const needle = lower(query.trim());
|
|
725
|
+
const visible = options.filter(option => !needle || lower(option.label).includes(needle));
|
|
726
|
+
list.innerHTML = '';
|
|
727
|
+
if (visible.length === 0) {
|
|
728
|
+
const empty = document.createElement('p');
|
|
729
|
+
empty.className = 'scd-empty';
|
|
730
|
+
empty.textContent = 'No matches';
|
|
731
|
+
list.appendChild(empty);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
let lastGroup = '';
|
|
735
|
+
for (const option of visible) {
|
|
736
|
+
const group = option.special ? 'Special' : label;
|
|
737
|
+
if (group !== lastGroup) {
|
|
738
|
+
const groupLabel = document.createElement('div');
|
|
739
|
+
groupLabel.className = 'scd-group-label';
|
|
740
|
+
groupLabel.textContent = group;
|
|
741
|
+
list.appendChild(groupLabel);
|
|
742
|
+
lastGroup = group;
|
|
743
|
+
}
|
|
744
|
+
const row = document.createElement('label');
|
|
745
|
+
row.className = 'scd-option';
|
|
746
|
+
if (option.kind) row.classList.add(\`is-\${option.kind}\`);
|
|
747
|
+
if (option.special) row.classList.add('is-special');
|
|
748
|
+
const box = document.createElement('input');
|
|
749
|
+
box.type = 'checkbox';
|
|
750
|
+
box.checked = selected.has(option.value);
|
|
751
|
+
box.addEventListener('change', () => {
|
|
752
|
+
if (box.checked) selected.add(option.value);
|
|
753
|
+
else selected.delete(option.value);
|
|
754
|
+
syncTrigger();
|
|
755
|
+
emitChange();
|
|
756
|
+
});
|
|
757
|
+
const text = document.createElement('span');
|
|
758
|
+
text.className = 'scd-option-label';
|
|
759
|
+
text.textContent = option.label;
|
|
760
|
+
row.append(box, text);
|
|
761
|
+
if (typeof option.count === 'number') {
|
|
762
|
+
const count = document.createElement('span');
|
|
763
|
+
count.className = 'scd-option-count';
|
|
764
|
+
count.textContent = String(option.count);
|
|
765
|
+
row.appendChild(count);
|
|
766
|
+
}
|
|
767
|
+
list.appendChild(row);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function onDocPointerDown(event) {
|
|
772
|
+
if (!open) return;
|
|
773
|
+
if (!host.contains(event.target)) setOpen(false);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function onKeyDown(event) {
|
|
777
|
+
if (event.key !== 'Escape' || !open) return;
|
|
778
|
+
if (searching || query.trim()) {
|
|
779
|
+
endSearching();
|
|
780
|
+
renderList();
|
|
781
|
+
event.stopPropagation();
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
setOpen(false);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function onPeerOpen(event) {
|
|
788
|
+
if (!open) return;
|
|
789
|
+
if (event.detail?.source && event.detail.source !== host) setOpen(false);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function bindListeners() {
|
|
793
|
+
if (listenersBound) return;
|
|
794
|
+
document.addEventListener('pointerdown', onDocPointerDown);
|
|
795
|
+
document.addEventListener('keydown', onKeyDown);
|
|
796
|
+
document.addEventListener('reqlan-scd-open', onPeerOpen);
|
|
797
|
+
listenersBound = true;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function unbindListeners() {
|
|
801
|
+
if (!listenersBound) return;
|
|
802
|
+
document.removeEventListener('pointerdown', onDocPointerDown);
|
|
803
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
804
|
+
document.removeEventListener('reqlan-scd-open', onPeerOpen);
|
|
805
|
+
listenersBound = false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function setOpen(next) {
|
|
809
|
+
open = next;
|
|
810
|
+
host.classList.toggle('is-open', open);
|
|
811
|
+
if (open) {
|
|
812
|
+
ensurePanel();
|
|
813
|
+
panel.hidden = false;
|
|
814
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
815
|
+
setSearching(false);
|
|
816
|
+
renderList();
|
|
817
|
+
bindListeners();
|
|
818
|
+
document.dispatchEvent(new CustomEvent('reqlan-scd-open', { detail: { source: host } }));
|
|
819
|
+
} else {
|
|
820
|
+
if (panel) panel.hidden = true;
|
|
821
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
822
|
+
endSearching();
|
|
823
|
+
unbindListeners();
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
trigger.addEventListener('click', () => setOpen(!open));
|
|
828
|
+
syncTrigger();
|
|
829
|
+
const api = {
|
|
830
|
+
getSelected: () => [...selected],
|
|
831
|
+
clear: () => {
|
|
832
|
+
selected.clear();
|
|
833
|
+
syncTrigger();
|
|
834
|
+
if (open) renderList();
|
|
835
|
+
},
|
|
836
|
+
setOnChange: (fn) => {
|
|
837
|
+
changeHandler = fn;
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
host.__scdApi = api;
|
|
841
|
+
return api;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function enhanceScdPlaceholders(root) {
|
|
845
|
+
for (const host of root.querySelectorAll('[data-graph-status-scd], [data-graph-tag-scd]')) {
|
|
846
|
+
mountSearchableCheckboxDropdown(host, null);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
394
850
|
function scoreDocument(doc, query) {
|
|
395
851
|
const haystack = [
|
|
396
852
|
doc.title,
|
|
@@ -635,6 +1091,10 @@ const GRAPH_NODE_RADIUS = 7;
|
|
|
635
1091
|
const GRAPH_SUBJECT_RADIUS = 10;
|
|
636
1092
|
const GRAPH_LABEL_MAX_WIDTH = 132;
|
|
637
1093
|
const GRAPH_HIT_PAD = 6;
|
|
1094
|
+
// Auto-mode opacity ramp: fully hidden below START, fully opaque at END (brief muted band between).
|
|
1095
|
+
const GRAPH_LABEL_FADE_START = 0.62;
|
|
1096
|
+
const GRAPH_LABEL_FADE_END = 0.82;
|
|
1097
|
+
const GRAPH_LABEL_MODES = ['auto', 'on', 'off'];
|
|
638
1098
|
|
|
639
1099
|
function wireGraph(root) {
|
|
640
1100
|
const graphRoots = root.querySelectorAll('[data-graph-json]');
|
|
@@ -653,21 +1113,37 @@ function wireGraph(root) {
|
|
|
653
1113
|
controls = document.createElement('div');
|
|
654
1114
|
controls.className = 'graph-controls-bar';
|
|
655
1115
|
controls.dataset.graphControls = scriptId;
|
|
656
|
-
controls.innerHTML = '<button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button><button type="button" class="graph-action" data-graph-fit>Fit</button><span class="graph-status" data-graph-status-text></span>';
|
|
1116
|
+
controls.innerHTML = '<button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button><button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button><button type="button" class="graph-action" data-graph-fit>Fit</button><span class="graph-status" data-graph-status-text></span>';
|
|
657
1117
|
element.parentElement?.insertBefore(controls, element);
|
|
658
1118
|
}
|
|
659
1119
|
const searchInput = controls.querySelector('[data-graph-search]');
|
|
660
1120
|
const pathInput = controls.querySelector('[data-graph-path]');
|
|
661
|
-
const
|
|
662
|
-
const
|
|
1121
|
+
const statusHost = controls.querySelector('[data-graph-status-scd]');
|
|
1122
|
+
const tagHost = controls.querySelector('[data-graph-tag-scd]');
|
|
663
1123
|
const toggleExternal = controls.querySelector('[data-graph-toggle-external]');
|
|
664
1124
|
const toggleIdeasets = controls.querySelector('[data-graph-toggle-ideasets]');
|
|
1125
|
+
const toggleLabels = controls.querySelector('[data-graph-toggle-labels]');
|
|
665
1126
|
const togglePhysics = controls.querySelector('[data-graph-toggle-physics]');
|
|
666
1127
|
const fitButton = controls.querySelector('[data-graph-fit]');
|
|
667
1128
|
const resetButton = controls.querySelector('[data-graph-reset]');
|
|
668
1129
|
const statusText = controls.querySelector('[data-graph-status-text]');
|
|
1130
|
+
if (statusText) statusText.textContent = 'Initialising graph…';
|
|
1131
|
+
element.classList.add('is-booting');
|
|
669
1132
|
let hideExternal = false;
|
|
670
1133
|
let hideIdeasets = false;
|
|
1134
|
+
let labelMode = 'auto';
|
|
1135
|
+
let statusSelected = [];
|
|
1136
|
+
let tagSelected = [];
|
|
1137
|
+
const statusFilter = mountSearchableCheckboxDropdown(statusHost, (values) => {
|
|
1138
|
+
statusSelected = values;
|
|
1139
|
+
refresh();
|
|
1140
|
+
});
|
|
1141
|
+
const tagFilter = mountSearchableCheckboxDropdown(tagHost, (values) => {
|
|
1142
|
+
tagSelected = values;
|
|
1143
|
+
refresh();
|
|
1144
|
+
});
|
|
1145
|
+
statusSelected = statusFilter.getSelected();
|
|
1146
|
+
tagSelected = tagFilter.getSelected();
|
|
671
1147
|
let livePhysics = false;
|
|
672
1148
|
let animationFrame = 0;
|
|
673
1149
|
let calmTicks = 0;
|
|
@@ -788,7 +1264,8 @@ function wireGraph(root) {
|
|
|
788
1264
|
minX = Math.min(minX, pos.x - r);
|
|
789
1265
|
minY = Math.min(minY, pos.y - r);
|
|
790
1266
|
maxX = Math.max(maxX, pos.x + r);
|
|
791
|
-
|
|
1267
|
+
// Reserve label space only when labels are forced on; auto hides when fitted/zoomed out.
|
|
1268
|
+
maxY = Math.max(maxY, pos.y + r + (labelMode === 'on' ? 54 : 0));
|
|
792
1269
|
}
|
|
793
1270
|
if (!Number.isFinite(minX)) {
|
|
794
1271
|
return { minX: 0, minY: 0, maxX: 1200, maxY: 640 };
|
|
@@ -796,19 +1273,56 @@ function wireGraph(root) {
|
|
|
796
1273
|
return { minX, minY, maxX, maxY };
|
|
797
1274
|
}
|
|
798
1275
|
|
|
1276
|
+
function syncLabelsButton() {
|
|
1277
|
+
if (!toggleLabels) return;
|
|
1278
|
+
const label = labelMode === 'auto' ? 'Labels: auto' : labelMode === 'on' ? 'Labels: on' : 'Labels: off';
|
|
1279
|
+
toggleLabels.textContent = label;
|
|
1280
|
+
toggleLabels.dataset.labelMode = labelMode;
|
|
1281
|
+
toggleLabels.classList.toggle('is-active', labelMode !== 'off');
|
|
1282
|
+
toggleLabels.setAttribute('aria-pressed', labelMode === 'auto' ? 'mixed' : labelMode === 'on' ? 'true' : 'false');
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function labelOpacityAtScale(scale) {
|
|
1286
|
+
if (labelMode === 'on') return 1;
|
|
1287
|
+
if (labelMode === 'off') return 0;
|
|
1288
|
+
if (scale <= GRAPH_LABEL_FADE_START) return 0;
|
|
1289
|
+
if (scale >= GRAPH_LABEL_FADE_END) return 1;
|
|
1290
|
+
return (scale - GRAPH_LABEL_FADE_START) / (GRAPH_LABEL_FADE_END - GRAPH_LABEL_FADE_START);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
799
1293
|
function fitView(force = false) {
|
|
800
1294
|
if (userViewport && !force) return;
|
|
801
1295
|
const bounds = contentBounds();
|
|
802
1296
|
const pad = 56;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1297
|
+
let x = bounds.minX - pad;
|
|
1298
|
+
let y = bounds.minY - pad;
|
|
1299
|
+
let w = Math.max(360, bounds.maxX - bounds.minX + pad * 2);
|
|
1300
|
+
let h = Math.max(360, bounds.maxY - bounds.minY + pad * 2);
|
|
1301
|
+
// Match canvas aspect so uniform scale fills without squashing (SVG viewBox meet).
|
|
1302
|
+
const canvasAspect = cssWidth / Math.max(1, cssHeight);
|
|
1303
|
+
const viewAspect = w / h;
|
|
1304
|
+
if (viewAspect < canvasAspect) {
|
|
1305
|
+
const widened = h * canvasAspect;
|
|
1306
|
+
x -= (widened - w) / 2;
|
|
1307
|
+
w = widened;
|
|
1308
|
+
} else if (viewAspect > canvasAspect) {
|
|
1309
|
+
const heightened = w / canvasAspect;
|
|
1310
|
+
y -= (heightened - h) / 2;
|
|
1311
|
+
h = heightened;
|
|
1312
|
+
}
|
|
1313
|
+
view = { x, y, w, h };
|
|
809
1314
|
userViewport = false;
|
|
810
1315
|
}
|
|
811
1316
|
|
|
1317
|
+
function viewportTransform() {
|
|
1318
|
+
const scale = Math.min(cssWidth / Math.max(1, view.w), cssHeight / Math.max(1, view.h));
|
|
1319
|
+
return {
|
|
1320
|
+
scale,
|
|
1321
|
+
offsetX: (cssWidth - view.w * scale) / 2,
|
|
1322
|
+
offsetY: (cssHeight - view.h * scale) / 2
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
|
|
812
1326
|
function wrapLines(text, maxWidth, font) {
|
|
813
1327
|
if (!text) return [];
|
|
814
1328
|
ctx.font = font;
|
|
@@ -871,13 +1385,14 @@ function wireGraph(root) {
|
|
|
871
1385
|
ctx.fillStyle = '#14100e';
|
|
872
1386
|
ctx.fillRect(0, 0, cssWidth, cssHeight);
|
|
873
1387
|
|
|
874
|
-
const
|
|
875
|
-
const
|
|
1388
|
+
const { scale, offsetX, offsetY } = viewportTransform();
|
|
1389
|
+
const ambientLabelOpacity = labelOpacityAtScale(scale);
|
|
876
1390
|
ctx.save();
|
|
877
|
-
ctx.translate(
|
|
878
|
-
ctx.scale(
|
|
1391
|
+
ctx.translate(offsetX, offsetY);
|
|
1392
|
+
ctx.scale(scale, scale);
|
|
1393
|
+
ctx.translate(-view.x, -view.y);
|
|
879
1394
|
|
|
880
|
-
ctx.lineWidth = 1.1 /
|
|
1395
|
+
ctx.lineWidth = 1.1 / scale;
|
|
881
1396
|
ctx.strokeStyle = 'rgba(61,47,40,0.85)';
|
|
882
1397
|
ctx.globalAlpha = 0.85;
|
|
883
1398
|
for (const edge of simEdges) {
|
|
@@ -909,10 +1424,13 @@ function wireGraph(root) {
|
|
|
909
1424
|
ctx.arc(pos.x, pos.y, r, 0, Math.PI * 2);
|
|
910
1425
|
ctx.fillStyle = nodeFill(node);
|
|
911
1426
|
ctx.fill();
|
|
912
|
-
ctx.lineWidth = (isSubject ? 2.2 : hover ? 2 : 1.15) /
|
|
1427
|
+
ctx.lineWidth = (isSubject ? 2.2 : hover ? 2 : 1.15) / scale;
|
|
913
1428
|
ctx.strokeStyle = nodeStroke(node, hover);
|
|
914
1429
|
ctx.stroke();
|
|
915
1430
|
|
|
1431
|
+
// Auto: continuous opacity from zoom; hovered/dragged nodes stay fully opaque.
|
|
1432
|
+
const labelOpacity = hover && labelMode === 'auto' ? 1 : ambientLabelOpacity;
|
|
1433
|
+
if (labelOpacity <= 0) continue;
|
|
916
1434
|
const meta = formatNodeMeta(node);
|
|
917
1435
|
const attrs = formatNodeAttrs(node);
|
|
918
1436
|
const nameFont = isSubject ? '600 11px Inter, system-ui, sans-serif' : '600 10px Inter, system-ui, sans-serif';
|
|
@@ -921,12 +1439,14 @@ function wireGraph(root) {
|
|
|
921
1439
|
const metaLines = wrapLines(meta, GRAPH_LABEL_MAX_WIDTH, metaFont);
|
|
922
1440
|
const attrLines = wrapLines(attrs, GRAPH_LABEL_MAX_WIDTH, metaFont);
|
|
923
1441
|
let ty = pos.y + r + 12;
|
|
1442
|
+
ctx.save();
|
|
1443
|
+
ctx.globalAlpha = labelOpacity;
|
|
924
1444
|
ctx.textAlign = 'center';
|
|
925
1445
|
ctx.textBaseline = 'top';
|
|
926
1446
|
ctx.fillStyle = isSubject ? '#0bbefb' : '#ebe4de';
|
|
927
1447
|
ctx.font = nameFont;
|
|
928
1448
|
for (const line of nameLines) {
|
|
929
|
-
ctx.lineWidth = 3 /
|
|
1449
|
+
ctx.lineWidth = 3 / scale;
|
|
930
1450
|
ctx.strokeStyle = '#14100e';
|
|
931
1451
|
ctx.strokeText(line, pos.x, ty);
|
|
932
1452
|
ctx.fillText(line, pos.x, ty);
|
|
@@ -943,6 +1463,7 @@ function wireGraph(root) {
|
|
|
943
1463
|
ctx.fillText(line, pos.x, ty);
|
|
944
1464
|
ty += 11;
|
|
945
1465
|
}
|
|
1466
|
+
ctx.restore();
|
|
946
1467
|
}
|
|
947
1468
|
ctx.restore();
|
|
948
1469
|
}
|
|
@@ -1035,6 +1556,7 @@ function wireGraph(root) {
|
|
|
1035
1556
|
canvas.setAttribute('aria-label', \`Requirement graph with \${filteredNodes.length} nodes\`);
|
|
1036
1557
|
ctx = canvas.getContext('2d');
|
|
1037
1558
|
element.innerHTML = '';
|
|
1559
|
+
element.classList.remove('is-booting');
|
|
1038
1560
|
element.appendChild(canvas);
|
|
1039
1561
|
resizeCanvas();
|
|
1040
1562
|
wireViewport(canvas);
|
|
@@ -1053,9 +1575,13 @@ function wireGraph(root) {
|
|
|
1053
1575
|
|
|
1054
1576
|
function clientToGraph(event) {
|
|
1055
1577
|
const rect = canvas.getBoundingClientRect();
|
|
1056
|
-
const
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1578
|
+
const { scale, offsetX, offsetY } = viewportTransform();
|
|
1579
|
+
const cssX = ((event.clientX - rect.left) / rect.width) * cssWidth;
|
|
1580
|
+
const cssY = ((event.clientY - rect.top) / rect.height) * cssHeight;
|
|
1581
|
+
return {
|
|
1582
|
+
x: view.x + (cssX - offsetX) / scale,
|
|
1583
|
+
y: view.y + (cssY - offsetY) / scale
|
|
1584
|
+
};
|
|
1059
1585
|
}
|
|
1060
1586
|
|
|
1061
1587
|
function wireViewport(target) {
|
|
@@ -1104,8 +1630,9 @@ function wireGraph(root) {
|
|
|
1104
1630
|
}
|
|
1105
1631
|
if (panMode && panOrigin) {
|
|
1106
1632
|
const rect = target.getBoundingClientRect();
|
|
1107
|
-
const
|
|
1108
|
-
const
|
|
1633
|
+
const { scale } = viewportTransform();
|
|
1634
|
+
const dx = ((event.clientX - panOrigin.x) / rect.width) * cssWidth / scale;
|
|
1635
|
+
const dy = ((event.clientY - panOrigin.y) / rect.height) * cssHeight / scale;
|
|
1109
1636
|
view.x = panOrigin.viewX - dx;
|
|
1110
1637
|
view.y = panOrigin.viewY - dy;
|
|
1111
1638
|
userViewport = true;
|
|
@@ -1164,17 +1691,24 @@ function wireGraph(root) {
|
|
|
1164
1691
|
}
|
|
1165
1692
|
|
|
1166
1693
|
function matches(node) {
|
|
1694
|
+
const FILTER_NOT_PRESENT = '__not_present__';
|
|
1167
1695
|
const query = lower(searchInput?.value?.trim());
|
|
1168
1696
|
const pathQuery = lower(pathInput?.value?.trim());
|
|
1169
|
-
const statusQuery = lower(statusSelect?.value?.trim());
|
|
1170
|
-
const tagQuery = lower(tagSelect?.value?.trim());
|
|
1171
1697
|
if (hideExternal && node.isExternal) return false;
|
|
1172
1698
|
if (hideIdeasets && node.kind === 'ideaset') return false;
|
|
1173
1699
|
const haystack = [node.name, node.kind, node.fileUri, node.status || '', ...(node.tags || [])].map(lower).join(' ');
|
|
1174
1700
|
if (query && !haystack.includes(query)) return false;
|
|
1175
1701
|
if (pathQuery && !lower(node.fileUri).includes(pathQuery)) return false;
|
|
1176
|
-
if (
|
|
1177
|
-
|
|
1702
|
+
if (statusSelected.length) {
|
|
1703
|
+
const key = node.statusKey || (String(node.status || '').trim() ? String(node.status).trim() : FILTER_NOT_PRESENT);
|
|
1704
|
+
if (!statusSelected.includes(key)) return false;
|
|
1705
|
+
}
|
|
1706
|
+
if (tagSelected.length) {
|
|
1707
|
+
const keys = Array.isArray(node.tagsKeys) && node.tagsKeys.length
|
|
1708
|
+
? node.tagsKeys
|
|
1709
|
+
: (Array.isArray(node.tags) && node.tags.length ? node.tags : [FILTER_NOT_PRESENT]);
|
|
1710
|
+
if (!tagSelected.some(value => keys.includes(value))) return false;
|
|
1711
|
+
}
|
|
1178
1712
|
return true;
|
|
1179
1713
|
}
|
|
1180
1714
|
|
|
@@ -1187,7 +1721,7 @@ function wireGraph(root) {
|
|
|
1187
1721
|
mountGraph(visibleNodes, visibleEdges);
|
|
1188
1722
|
}
|
|
1189
1723
|
|
|
1190
|
-
for (const control of [searchInput, pathInput
|
|
1724
|
+
for (const control of [searchInput, pathInput]) {
|
|
1191
1725
|
control?.addEventListener('input', refresh);
|
|
1192
1726
|
control?.addEventListener('change', refresh);
|
|
1193
1727
|
}
|
|
@@ -1203,6 +1737,12 @@ function wireGraph(root) {
|
|
|
1203
1737
|
toggleIdeasets.textContent = hideIdeasets ? 'Show ideasets' : 'Hide ideasets';
|
|
1204
1738
|
refresh();
|
|
1205
1739
|
});
|
|
1740
|
+
toggleLabels?.addEventListener('click', () => {
|
|
1741
|
+
const index = GRAPH_LABEL_MODES.indexOf(labelMode);
|
|
1742
|
+
labelMode = GRAPH_LABEL_MODES[(index + 1) % GRAPH_LABEL_MODES.length];
|
|
1743
|
+
syncLabelsButton();
|
|
1744
|
+
paint();
|
|
1745
|
+
});
|
|
1206
1746
|
togglePhysics?.addEventListener('click', () => {
|
|
1207
1747
|
livePhysics = !livePhysics;
|
|
1208
1748
|
togglePhysics.classList.toggle('is-active', livePhysics);
|
|
@@ -1221,12 +1761,16 @@ function wireGraph(root) {
|
|
|
1221
1761
|
resetButton?.addEventListener('click', () => {
|
|
1222
1762
|
if (searchInput) searchInput.value = '';
|
|
1223
1763
|
if (pathInput) pathInput.value = '';
|
|
1224
|
-
|
|
1225
|
-
|
|
1764
|
+
statusSelected = [];
|
|
1765
|
+
tagSelected = [];
|
|
1766
|
+
statusFilter.clear();
|
|
1767
|
+
tagFilter.clear();
|
|
1226
1768
|
hideExternal = false;
|
|
1227
1769
|
hideIdeasets = false;
|
|
1770
|
+
labelMode = 'auto';
|
|
1228
1771
|
toggleExternal?.classList.remove('is-active');
|
|
1229
1772
|
toggleIdeasets?.classList.remove('is-active');
|
|
1773
|
+
syncLabelsButton();
|
|
1230
1774
|
if (toggleExternal) toggleExternal.textContent = 'Hide external';
|
|
1231
1775
|
if (toggleIdeasets) toggleIdeasets.textContent = 'Hide ideasets';
|
|
1232
1776
|
positions.clear();
|
|
@@ -1235,12 +1779,15 @@ function wireGraph(root) {
|
|
|
1235
1779
|
userViewport = false;
|
|
1236
1780
|
refresh();
|
|
1237
1781
|
});
|
|
1782
|
+
syncLabelsButton();
|
|
1238
1783
|
refresh();
|
|
1239
1784
|
}
|
|
1240
1785
|
}
|
|
1241
1786
|
|
|
1242
1787
|
function boot() {
|
|
1243
1788
|
const root = document.body;
|
|
1789
|
+
// Interactive Status/Tags triggers before heavy table/graph init.
|
|
1790
|
+
enhanceScdPlaceholders(root);
|
|
1244
1791
|
wireTables(root);
|
|
1245
1792
|
wireGraph(root);
|
|
1246
1793
|
void wireGlobalSearch(root);
|