@reqlan/analytical 0.6.3 → 0.8.1

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.
Files changed (90) hide show
  1. package/README.md +30 -0
  2. package/out/analysis/fuzzy-search-analyser.d.ts +17 -0
  3. package/out/analysis/fuzzy-search-analyser.js +25 -0
  4. package/out/analysis/fuzzy-search-analyser.js.map +1 -0
  5. package/out/analysis/fuzzy-search-worker-client.d.ts +39 -0
  6. package/out/analysis/fuzzy-search-worker-client.js +178 -0
  7. package/out/analysis/fuzzy-search-worker-client.js.map +1 -0
  8. package/out/analysis/fuzzy-search-worker.d.ts +32 -0
  9. package/out/analysis/fuzzy-search-worker.js +81 -0
  10. package/out/analysis/fuzzy-search-worker.js.map +1 -0
  11. package/out/analysis/fuzzy-search.d.ts +38 -0
  12. package/out/analysis/fuzzy-search.js +217 -0
  13. package/out/analysis/fuzzy-search.js.map +1 -0
  14. package/out/analysis/git-dates-analyser.js +12 -3
  15. package/out/analysis/git-dates-analyser.js.map +1 -1
  16. package/out/analysis-api.d.ts +16 -2
  17. package/out/analysis-api.js +36 -3
  18. package/out/analysis-api.js.map +1 -1
  19. package/out/core/barrel-page.d.ts +46 -0
  20. package/out/core/barrel-page.js +162 -0
  21. package/out/core/barrel-page.js.map +1 -0
  22. package/out/core/filter-specials.d.ts +37 -0
  23. package/out/core/filter-specials.js +113 -0
  24. package/out/core/filter-specials.js.map +1 -0
  25. package/out/core/types.d.ts +27 -1
  26. package/out/core/types.js +1 -0
  27. package/out/core/types.js.map +1 -1
  28. package/out/create-runtime.js +2 -0
  29. package/out/create-runtime.js.map +1 -1
  30. package/out/export/build-export-snapshot.d.ts +2 -2
  31. package/out/export/build-export-snapshot.js +43 -16
  32. package/out/export/build-export-snapshot.js.map +1 -1
  33. package/out/export/export-csv.d.ts +4 -0
  34. package/out/export/export-csv.js +13 -0
  35. package/out/export/export-csv.js.map +1 -0
  36. package/out/export/export-html.d.ts +2 -2
  37. package/out/export/export-html.js +7 -3
  38. package/out/export/export-html.js.map +1 -1
  39. package/out/export/export-json.d.ts +6 -0
  40. package/out/export/export-json.js +28 -0
  41. package/out/export/export-json.js.map +1 -0
  42. package/out/export/export-markdown.d.ts +4 -0
  43. package/out/export/export-markdown.js +23 -0
  44. package/out/export/export-markdown.js.map +1 -0
  45. package/out/export/html-export-assets.d.ts +1 -1
  46. package/out/export/html-export-assets.js +663 -20
  47. package/out/export/html-export-assets.js.map +1 -1
  48. package/out/export/html-export-template.js +160 -61
  49. package/out/export/html-export-template.js.map +1 -1
  50. package/out/export/html-export-utils.d.ts +33 -3
  51. package/out/export/html-export-utils.js +121 -13
  52. package/out/export/html-export-utils.js.map +1 -1
  53. package/out/export/types.d.ts +20 -1
  54. package/out/export/write-csv-export.d.ts +5 -0
  55. package/out/export/write-csv-export.js +132 -0
  56. package/out/export/write-csv-export.js.map +1 -0
  57. package/out/export/write-html-export.d.ts +2 -2
  58. package/out/export/write-html-export.js +61 -29
  59. package/out/export/write-html-export.js.map +1 -1
  60. package/out/export/write-json-export.d.ts +3 -0
  61. package/out/export/write-json-export.js +92 -0
  62. package/out/export/write-json-export.js.map +1 -0
  63. package/out/export/write-markdown-export.d.ts +5 -0
  64. package/out/export/write-markdown-export.js +177 -0
  65. package/out/export/write-markdown-export.js.map +1 -0
  66. package/out/graph/physics-core-source.d.ts +5 -0
  67. package/out/graph/physics-core-source.js +8 -0
  68. package/out/graph/physics-core-source.js.map +1 -0
  69. package/out/index-store/idea-extractor.d.ts +6 -1
  70. package/out/index-store/idea-extractor.js +56 -11
  71. package/out/index-store/idea-extractor.js.map +1 -1
  72. package/out/index-store/index-diagnostics-store.js +1 -2
  73. package/out/index-store/index-diagnostics-store.js.map +1 -1
  74. package/out/index-store/sqlite-store.d.ts +16 -3
  75. package/out/index-store/sqlite-store.js +107 -9
  76. package/out/index-store/sqlite-store.js.map +1 -1
  77. package/out/index-store/webview-graph-queries.d.ts +13 -2
  78. package/out/index-store/webview-graph-queries.js +98 -18
  79. package/out/index-store/webview-graph-queries.js.map +1 -1
  80. package/out/index-store/webview-table-queries.js +1 -0
  81. package/out/index-store/webview-table-queries.js.map +1 -1
  82. package/out/index-store/workspace-index-file.js +22 -1
  83. package/out/index-store/workspace-index-file.js.map +1 -1
  84. package/out/index-store/workspace-index.d.ts +2 -1
  85. package/out/index-store/workspace-index.js +4 -1
  86. package/out/index-store/workspace-index.js.map +1 -1
  87. package/out/index.d.ts +17 -2
  88. package/out/index.js +10 -1
  89. package/out/index.js.map +1 -1
  90. package/package.json +7 -3
@@ -1,8 +1,13 @@
1
- import { readFileSync } from 'node:fs';
2
- /** Embed shared physics core into the classic-script export app.js (strip ESM exports). */
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 readFileSync(new URL('../graph/physics-core.js', import.meta.url), 'utf8')
5
- .replace(/^export\s+/gm, '');
10
+ return PHYSICS_CORE_CLASSIC_SOURCE;
6
11
  }
7
12
  export const SHARED_STYLES = `
8
13
  :root {
@@ -26,6 +31,10 @@ export const SHARED_STYLES = `
26
31
  font-family: Inter, system-ui, sans-serif;
27
32
  }
28
33
  * { box-sizing: border-box; }
34
+ html {
35
+ /* Keep fragment targets below the sticky .topbar (nav + padding ≈ 4.5–5rem). */
36
+ scroll-padding-top: 5.5rem;
37
+ }
29
38
  body {
30
39
  margin: 0;
31
40
  background: var(--bg);
@@ -127,6 +136,165 @@ a.pill:hover { color: var(--accent-strong); border-color: color-mix(in srgb, var
127
136
  flex: 1 1 180px;
128
137
  min-width: 160px;
129
138
  }
139
+ .scd {
140
+ position: relative;
141
+ flex: 1 1 180px;
142
+ min-width: 160px;
143
+ }
144
+ .scd-trigger {
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: space-between;
148
+ gap: 0.55rem;
149
+ width: 100%;
150
+ min-height: 2.35rem;
151
+ padding: 0.4rem 0.75rem;
152
+ border: 1px solid var(--line);
153
+ border-radius: 8px;
154
+ background: var(--bg-soft);
155
+ color: var(--fg);
156
+ font: inherit;
157
+ cursor: pointer;
158
+ text-align: left;
159
+ }
160
+ .scd.is-open .scd-trigger,
161
+ .scd-trigger:hover {
162
+ border-color: var(--accent-dim);
163
+ }
164
+ .scd.has-selection .scd-trigger {
165
+ border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
166
+ }
167
+ .scd.is-searching .scd-trigger {
168
+ border-color: var(--line);
169
+ }
170
+ .scd.is-loading .scd-trigger {
171
+ opacity: 0.72;
172
+ }
173
+ .scd-trigger-label {
174
+ overflow: hidden;
175
+ text-overflow: ellipsis;
176
+ white-space: nowrap;
177
+ }
178
+ .scd-chevron {
179
+ width: 0;
180
+ height: 0;
181
+ border-left: 4px solid transparent;
182
+ border-right: 4px solid transparent;
183
+ border-top: 5px solid currentColor;
184
+ opacity: 0.7;
185
+ flex: 0 0 auto;
186
+ }
187
+ .scd.is-open .scd-chevron { transform: rotate(180deg); }
188
+ .scd-panel {
189
+ position: absolute;
190
+ z-index: 50;
191
+ top: calc(100% + 0.35rem);
192
+ left: 0;
193
+ right: auto;
194
+ width: max(100%, 15rem);
195
+ max-width: min(20rem, 80vw);
196
+ max-height: 18rem;
197
+ display: flex;
198
+ flex-direction: column;
199
+ border: 1px solid var(--line);
200
+ border-radius: 10px;
201
+ background: var(--bg-elev);
202
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
203
+ overflow: hidden;
204
+ }
205
+ /* Author display:flex overrides the UA [hidden] rule; keep closed panels invisible. */
206
+ .scd-panel[hidden] {
207
+ display: none;
208
+ }
209
+ .scd-search-row {
210
+ display: flex;
211
+ gap: 0.45rem;
212
+ padding: 0.65rem;
213
+ border-bottom: 1px solid var(--line);
214
+ opacity: 0.72;
215
+ }
216
+ .scd-search-row.is-active { opacity: 1; }
217
+ .scd-search {
218
+ flex: 1;
219
+ min-width: 0;
220
+ padding: 0.4rem 0.55rem;
221
+ border: 1px solid transparent;
222
+ border-radius: 6px;
223
+ background: color-mix(in srgb, var(--bg) 88%, transparent);
224
+ color: var(--muted);
225
+ font: inherit;
226
+ }
227
+ .scd.is-searching .scd-search,
228
+ .scd-search:focus {
229
+ border-color: var(--accent-dim);
230
+ background: var(--bg);
231
+ color: var(--fg);
232
+ outline: none;
233
+ }
234
+ .scd-clear {
235
+ border: none;
236
+ background: transparent;
237
+ color: var(--accent-strong);
238
+ font: inherit;
239
+ cursor: pointer;
240
+ }
241
+ .scd-list {
242
+ overflow: auto;
243
+ padding: 0.25rem 0 0.55rem;
244
+ }
245
+ .scd-group-label {
246
+ padding: 0.55rem 0.85rem 0.25rem;
247
+ font-size: 0.72rem;
248
+ letter-spacing: 0.06em;
249
+ text-transform: uppercase;
250
+ color: var(--muted);
251
+ }
252
+ .scd-option {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 0.55rem;
256
+ padding: 0.45rem 0.85rem;
257
+ cursor: pointer;
258
+ color: var(--fg);
259
+ }
260
+ .scd-option:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
261
+ .scd-option input { margin: 0; }
262
+ .scd-option-label {
263
+ flex: 1;
264
+ min-width: 0;
265
+ overflow: hidden;
266
+ text-overflow: ellipsis;
267
+ white-space: nowrap;
268
+ }
269
+ .scd-option-count {
270
+ flex: 0 0 auto;
271
+ font-variant-numeric: tabular-nums;
272
+ font-size: 0.85em;
273
+ color: var(--muted);
274
+ opacity: 0.65;
275
+ }
276
+ .scd-option.is-special .scd-option-label { font-style: italic; }
277
+ .scd-option.is-not-present .scd-option-label { color: var(--faint); }
278
+ .scd-option.is-empty .scd-option-label { color: var(--accent-strong); }
279
+ .scd-option.is-unspecified .scd-option-label { color: var(--rust-muted); }
280
+ .scd-empty {
281
+ margin: 0;
282
+ padding: 0.85rem;
283
+ color: var(--muted);
284
+ font-size: 0.9rem;
285
+ }
286
+ .rollup-list .rollup-special dt {
287
+ font-style: italic;
288
+ }
289
+ .rollup-list .rollup-not-present dt {
290
+ color: var(--faint);
291
+ }
292
+ .rollup-list .rollup-empty dt {
293
+ color: var(--accent-strong);
294
+ }
295
+ .rollup-list .rollup-unspecified dt {
296
+ color: var(--rust-muted);
297
+ }
130
298
  .graph-action {
131
299
  border: 1px solid var(--line);
132
300
  background: var(--bg-soft);
@@ -239,14 +407,68 @@ body[data-runtime-mode="interactive"] .scroll-window thead .column-filter-row.is
239
407
  background: var(--bg-raised);
240
408
  box-shadow: 0 1px 0 var(--line);
241
409
  }
242
- .entity-list { display: grid; gap: 1.5rem; }
410
+ .entity-list {
411
+ display: grid;
412
+ grid-template-columns: minmax(0, 1fr);
413
+ gap: 1.5rem;
414
+ }
243
415
  .entity-card {
244
416
  border: 1px solid var(--line); border-radius: 10px; padding: 0.9rem 1rem; background: var(--bg);
245
417
  color: inherit;
418
+ min-width: 0;
419
+ max-width: 100%;
420
+ overflow-wrap: anywhere;
421
+ word-break: break-word;
246
422
  }
247
423
  .entity-card:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
248
424
  .entity-card h3, .entity-card p { margin: 0; }
249
425
  .entity-card p + p, .entity-card strong + p, .entity-card h3 + p { margin-top: 0.35rem; }
426
+ .print-card {
427
+ min-width: 0;
428
+ max-width: 100%;
429
+ overflow-wrap: anywhere;
430
+ word-break: break-word;
431
+ scroll-margin-top: 5.5rem;
432
+ }
433
+ .print-card > h3 { overflow-wrap: anywhere; }
434
+ .print-attrs {
435
+ display: grid;
436
+ gap: 0.45rem;
437
+ margin: 0.75rem 0 0;
438
+ }
439
+ .print-attrs div {
440
+ display: grid;
441
+ grid-template-columns: minmax(0, 8rem) minmax(0, 1fr);
442
+ gap: 0.65rem;
443
+ border-bottom: 1px solid var(--line);
444
+ padding-bottom: 0.35rem;
445
+ }
446
+ .print-attrs dt {
447
+ margin: 0;
448
+ color: var(--muted);
449
+ font-weight: 600;
450
+ overflow-wrap: anywhere;
451
+ }
452
+ .print-attrs dd {
453
+ margin: 0;
454
+ overflow-wrap: anywhere;
455
+ word-break: break-word;
456
+ }
457
+ .print-button {
458
+ appearance: none;
459
+ border: 1px solid var(--line);
460
+ border-radius: 8px;
461
+ background: color-mix(in srgb, var(--accent) 18%, var(--bg-raised));
462
+ color: var(--fg);
463
+ font: inherit;
464
+ font-weight: 600;
465
+ padding: 0.45rem 0.9rem;
466
+ cursor: pointer;
467
+ }
468
+ .print-button:hover {
469
+ border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
470
+ background: color-mix(in srgb, var(--accent) 28%, var(--bg-raised));
471
+ }
250
472
  .panel > h2, .table-shell > h2, .graph-shell > h2, .print-card > h3 { margin-top: 0; margin-bottom: 0.75rem; }
251
473
  .toolbar h2 { margin: 0; }
252
474
  .rollup-list { display: grid; gap: 0.5rem; }
@@ -254,6 +476,14 @@ body[data-runtime-mode="interactive"] .scroll-window thead .column-filter-row.is
254
476
  display: flex; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--line); padding-bottom: 0.4rem;
255
477
  }
256
478
  .rollup-list dd { margin: 0; }
479
+ .rollup-list a.rollup-link {
480
+ color: inherit;
481
+ text-decoration: none;
482
+ }
483
+ .rollup-list a.rollup-link:hover {
484
+ color: var(--accent);
485
+ text-decoration: underline;
486
+ }
257
487
  .distribution-track {
258
488
  height: 0.7rem;
259
489
  min-width: 6rem;
@@ -269,6 +499,37 @@ body[data-runtime-mode="interactive"] .scroll-window thead .column-filter-row.is
269
499
  background: linear-gradient(90deg, var(--accent-dim), var(--accent-strong));
270
500
  min-width: 0;
271
501
  }
502
+ .graph-root {
503
+ position: relative;
504
+ min-height: 620px;
505
+ }
506
+ .graph-root.is-booting {
507
+ display: grid;
508
+ place-items: center;
509
+ border: 1px solid var(--line);
510
+ border-radius: 10px;
511
+ background: var(--bg);
512
+ }
513
+ .graph-boot {
514
+ display: grid;
515
+ justify-items: center;
516
+ gap: 0.75rem;
517
+ padding: 1.5rem;
518
+ text-align: center;
519
+ color: var(--muted);
520
+ }
521
+ .graph-boot p { margin: 0; }
522
+ .graph-boot-spinner {
523
+ width: 1.35rem;
524
+ height: 1.35rem;
525
+ border: 2px solid color-mix(in srgb, var(--muted) 35%, transparent);
526
+ border-top-color: var(--accent-strong);
527
+ border-radius: 50%;
528
+ animation: graph-boot-spin 0.7s linear infinite;
529
+ }
530
+ @keyframes graph-boot-spin {
531
+ to { transform: rotate(360deg); }
532
+ }
272
533
  .graph-root canvas {
273
534
  display: block;
274
535
  width: 100%;
@@ -299,7 +560,8 @@ body[data-runtime-mode="interactive"] .scroll-window thead .column-filter-row.is
299
560
  color: var(--accent-strong);
300
561
  font-weight: 600;
301
562
  text-decoration: none;
302
- white-space: nowrap;
563
+ white-space: normal;
564
+ overflow-wrap: anywhere;
303
565
  }
304
566
  a.idea-ref:hover {
305
567
  color: var(--fg);
@@ -329,6 +591,9 @@ pre.code-like {
329
591
  @media (max-width: 900px) {
330
592
  .hero, .topbar-inner { grid-template-columns: 1fr; }
331
593
  }
594
+ @media (max-width: 520px) {
595
+ .print-attrs div { grid-template-columns: 1fr; }
596
+ }
332
597
  @media print {
333
598
  :root { color-scheme: light; }
334
599
  body { background: white; color: black; }
@@ -391,6 +656,267 @@ function lower(value) {
391
656
  return String(value || '').toLowerCase();
392
657
  }
393
658
 
659
+ function mountSearchableCheckboxDropdown(host, onChange) {
660
+ if (!host) {
661
+ return {
662
+ getSelected: () => [],
663
+ clear: () => undefined,
664
+ setOnChange: () => undefined
665
+ };
666
+ }
667
+ if (host.__scdApi) {
668
+ host.__scdApi.setOnChange(onChange);
669
+ return host.__scdApi;
670
+ }
671
+ let options = [];
672
+ try {
673
+ options = JSON.parse(host.dataset.options || '[]');
674
+ } catch {
675
+ options = [];
676
+ }
677
+ const label = host.dataset.label || 'Filter';
678
+ const placeholder = host.dataset.placeholder || 'Search…';
679
+ let selected = new Set();
680
+ let open = false;
681
+ let searching = false;
682
+ let query = '';
683
+ let panel = null;
684
+ let search = null;
685
+ let searchRow = null;
686
+ let clearBtn = null;
687
+ let list = null;
688
+ let listenersBound = false;
689
+ let changeHandler = onChange;
690
+
691
+ let trigger = host.querySelector('.scd-trigger');
692
+ let triggerLabel = host.querySelector('.scd-trigger-label');
693
+ if (!trigger) {
694
+ trigger = document.createElement('button');
695
+ trigger.type = 'button';
696
+ trigger.className = 'scd-trigger';
697
+ trigger.setAttribute('aria-haspopup', 'listbox');
698
+ triggerLabel = document.createElement('span');
699
+ triggerLabel.className = 'scd-trigger-label';
700
+ const chevron = document.createElement('span');
701
+ chevron.className = 'scd-chevron';
702
+ chevron.setAttribute('aria-hidden', 'true');
703
+ trigger.append(triggerLabel, chevron);
704
+ host.appendChild(trigger);
705
+ }
706
+ if (!trigger.querySelector('.scd-chevron')) {
707
+ const chevron = document.createElement('span');
708
+ chevron.className = 'scd-chevron';
709
+ chevron.setAttribute('aria-hidden', 'true');
710
+ trigger.appendChild(chevron);
711
+ }
712
+ trigger.setAttribute('aria-expanded', 'false');
713
+ trigger.setAttribute('aria-busy', 'false');
714
+ host.classList.remove('is-loading');
715
+ host.dataset.scdReady = '1';
716
+
717
+ function emitChange() {
718
+ changeHandler?.([...selected]);
719
+ }
720
+
721
+ function syncTrigger() {
722
+ const values = [...selected];
723
+ host.dataset.selected = JSON.stringify(values);
724
+ host.classList.toggle('has-selection', values.length > 0);
725
+ if (!triggerLabel) return;
726
+ if (values.length === 0) triggerLabel.textContent = label;
727
+ else if (values.length === 1) {
728
+ const match = options.find(option => option.value === values[0]);
729
+ triggerLabel.textContent = match?.label || values[0];
730
+ } else triggerLabel.textContent = \`\${values.length} selected\`;
731
+ if (clearBtn) clearBtn.hidden = values.length === 0;
732
+ }
733
+
734
+ function setSearching(next) {
735
+ searching = next;
736
+ host.classList.toggle('is-searching', searching);
737
+ if (searchRow) searchRow.classList.toggle('is-active', searching);
738
+ }
739
+
740
+ function endSearching() {
741
+ query = '';
742
+ if (search) search.value = '';
743
+ setSearching(false);
744
+ if (search && document.activeElement === search) search.blur();
745
+ }
746
+
747
+ function ensurePanel() {
748
+ if (panel) return;
749
+ panel = document.createElement('div');
750
+ panel.className = 'scd-panel';
751
+ panel.hidden = true;
752
+ panel.setAttribute('role', 'listbox');
753
+ panel.setAttribute('aria-multiselectable', 'true');
754
+ panel.setAttribute('aria-label', label);
755
+
756
+ searchRow = document.createElement('div');
757
+ searchRow.className = 'scd-search-row';
758
+ search = document.createElement('input');
759
+ search.type = 'search';
760
+ search.className = 'scd-search';
761
+ search.placeholder = placeholder;
762
+ search.setAttribute('aria-label', \`Search \${label}\`);
763
+ clearBtn = document.createElement('button');
764
+ clearBtn.type = 'button';
765
+ clearBtn.className = 'scd-clear';
766
+ clearBtn.textContent = 'Clear';
767
+ clearBtn.hidden = true;
768
+ searchRow.append(search, clearBtn);
769
+
770
+ list = document.createElement('div');
771
+ list.className = 'scd-list';
772
+ panel.append(searchRow, list);
773
+ host.appendChild(panel);
774
+
775
+ search.addEventListener('focus', () => setSearching(true));
776
+ search.addEventListener('blur', () => {
777
+ if (!query.trim()) setSearching(false);
778
+ });
779
+ search.addEventListener('input', () => {
780
+ query = search.value;
781
+ if (query.trim()) setSearching(true);
782
+ renderList();
783
+ });
784
+ clearBtn.addEventListener('click', () => {
785
+ selected.clear();
786
+ syncTrigger();
787
+ renderList();
788
+ emitChange();
789
+ });
790
+ }
791
+
792
+ function renderList() {
793
+ if (!list) return;
794
+ const needle = lower(query.trim());
795
+ const visible = options.filter(option => !needle || lower(option.label).includes(needle));
796
+ list.innerHTML = '';
797
+ if (visible.length === 0) {
798
+ const empty = document.createElement('p');
799
+ empty.className = 'scd-empty';
800
+ empty.textContent = 'No matches';
801
+ list.appendChild(empty);
802
+ return;
803
+ }
804
+ let lastGroup = '';
805
+ for (const option of visible) {
806
+ const group = option.special ? 'Special' : label;
807
+ if (group !== lastGroup) {
808
+ const groupLabel = document.createElement('div');
809
+ groupLabel.className = 'scd-group-label';
810
+ groupLabel.textContent = group;
811
+ list.appendChild(groupLabel);
812
+ lastGroup = group;
813
+ }
814
+ const row = document.createElement('label');
815
+ row.className = 'scd-option';
816
+ if (option.kind) row.classList.add(\`is-\${option.kind}\`);
817
+ if (option.special) row.classList.add('is-special');
818
+ const box = document.createElement('input');
819
+ box.type = 'checkbox';
820
+ box.checked = selected.has(option.value);
821
+ box.addEventListener('change', () => {
822
+ if (box.checked) selected.add(option.value);
823
+ else selected.delete(option.value);
824
+ syncTrigger();
825
+ emitChange();
826
+ });
827
+ const text = document.createElement('span');
828
+ text.className = 'scd-option-label';
829
+ text.textContent = option.label;
830
+ row.append(box, text);
831
+ if (typeof option.count === 'number') {
832
+ const count = document.createElement('span');
833
+ count.className = 'scd-option-count';
834
+ count.textContent = String(option.count);
835
+ row.appendChild(count);
836
+ }
837
+ list.appendChild(row);
838
+ }
839
+ }
840
+
841
+ function onDocPointerDown(event) {
842
+ if (!open) return;
843
+ if (!host.contains(event.target)) setOpen(false);
844
+ }
845
+
846
+ function onKeyDown(event) {
847
+ if (event.key !== 'Escape' || !open) return;
848
+ if (searching || query.trim()) {
849
+ endSearching();
850
+ renderList();
851
+ event.stopPropagation();
852
+ return;
853
+ }
854
+ setOpen(false);
855
+ }
856
+
857
+ function onPeerOpen(event) {
858
+ if (!open) return;
859
+ if (event.detail?.source && event.detail.source !== host) setOpen(false);
860
+ }
861
+
862
+ function bindListeners() {
863
+ if (listenersBound) return;
864
+ document.addEventListener('pointerdown', onDocPointerDown);
865
+ document.addEventListener('keydown', onKeyDown);
866
+ document.addEventListener('reqlan-scd-open', onPeerOpen);
867
+ listenersBound = true;
868
+ }
869
+
870
+ function unbindListeners() {
871
+ if (!listenersBound) return;
872
+ document.removeEventListener('pointerdown', onDocPointerDown);
873
+ document.removeEventListener('keydown', onKeyDown);
874
+ document.removeEventListener('reqlan-scd-open', onPeerOpen);
875
+ listenersBound = false;
876
+ }
877
+
878
+ function setOpen(next) {
879
+ open = next;
880
+ host.classList.toggle('is-open', open);
881
+ if (open) {
882
+ ensurePanel();
883
+ panel.hidden = false;
884
+ trigger.setAttribute('aria-expanded', 'true');
885
+ setSearching(false);
886
+ renderList();
887
+ bindListeners();
888
+ document.dispatchEvent(new CustomEvent('reqlan-scd-open', { detail: { source: host } }));
889
+ } else {
890
+ if (panel) panel.hidden = true;
891
+ trigger.setAttribute('aria-expanded', 'false');
892
+ endSearching();
893
+ unbindListeners();
894
+ }
895
+ }
896
+
897
+ trigger.addEventListener('click', () => setOpen(!open));
898
+ syncTrigger();
899
+ const api = {
900
+ getSelected: () => [...selected],
901
+ clear: () => {
902
+ selected.clear();
903
+ syncTrigger();
904
+ if (open) renderList();
905
+ },
906
+ setOnChange: (fn) => {
907
+ changeHandler = fn;
908
+ }
909
+ };
910
+ host.__scdApi = api;
911
+ return api;
912
+ }
913
+
914
+ function enhanceScdPlaceholders(root) {
915
+ for (const host of root.querySelectorAll('[data-graph-status-scd], [data-graph-tag-scd]')) {
916
+ mountSearchableCheckboxDropdown(host, null);
917
+ }
918
+ }
919
+
394
920
  function scoreDocument(doc, query) {
395
921
  const haystack = [
396
922
  doc.title,
@@ -501,6 +1027,7 @@ function ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState
501
1027
  function wireTables(root) {
502
1028
  const tables = root.querySelectorAll('table');
503
1029
  const tableControllers = [];
1030
+ const params = new URLSearchParams(typeof location !== 'undefined' ? location.search : '');
504
1031
 
505
1032
  for (const table of tables) {
506
1033
  const thead = table.tHead;
@@ -550,8 +1077,12 @@ function wireTables(root) {
550
1077
 
551
1078
  headers.forEach((th, index) => {
552
1079
  const label = String(th.textContent || '').trim() || \`Column \${index + 1}\`;
1080
+ const filterKey = String(th.getAttribute('data-filter-key') || '').trim();
553
1081
  th.classList.add('sortable-th');
554
1082
  th.replaceChildren();
1083
+ if (filterKey) {
1084
+ th.setAttribute('data-filter-key', filterKey);
1085
+ }
555
1086
  const button = document.createElement('button');
556
1087
  button.type = 'button';
557
1088
  button.className = 'sort-button';
@@ -597,12 +1128,16 @@ function wireTables(root) {
597
1128
  filterRow.className = 'column-filter-row';
598
1129
  headers.forEach((th, index) => {
599
1130
  const label = String(th.querySelector('.sort-button span')?.textContent || \`Column \${index + 1}\`);
1131
+ const filterKey = String(th.getAttribute('data-filter-key') || '').trim().toLowerCase();
600
1132
  const cell = document.createElement('th');
601
1133
  const input = document.createElement('input');
602
1134
  input.type = 'search';
603
1135
  input.className = 'column-filter';
604
1136
  input.placeholder = 'Filter…';
605
1137
  input.dataset.colFilter = String(index);
1138
+ if (filterKey) {
1139
+ input.dataset.filterKey = filterKey;
1140
+ }
606
1141
  input.setAttribute('aria-label', \`Filter \${label}\`);
607
1142
  input.addEventListener('input', () => controller.applyFilters());
608
1143
  filterInputs.push(input);
@@ -612,6 +1147,32 @@ function wireTables(root) {
612
1147
  thead.appendChild(filterRow);
613
1148
  filterToggle = ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState);
614
1149
  tableControllers.push(controller);
1150
+
1151
+ let seeded = false;
1152
+ for (const input of filterInputs) {
1153
+ const key = String(input.dataset.filterKey || '').trim().toLowerCase();
1154
+ if (!key) continue;
1155
+ const aliases = [key];
1156
+ if (key === 'tags') aliases.push('tag');
1157
+ if (key === 'status') aliases.push('statuses');
1158
+ if (key === 'cluster') aliases.push('label');
1159
+ let value = '';
1160
+ for (const alias of aliases) {
1161
+ if (params.has(alias)) {
1162
+ value = params.get(alias) || '';
1163
+ break;
1164
+ }
1165
+ }
1166
+ if (!value) continue;
1167
+ input.value = value;
1168
+ seeded = true;
1169
+ }
1170
+ if (seeded) {
1171
+ filterRow.classList.add('is-open');
1172
+ filterToggle.classList.add('is-active', 'has-filters');
1173
+ filterToggle.setAttribute('aria-expanded', 'true');
1174
+ controller.applyFilters();
1175
+ }
615
1176
  }
616
1177
 
617
1178
  const inputs = root.querySelectorAll('[data-filter-input]');
@@ -635,6 +1196,10 @@ const GRAPH_NODE_RADIUS = 7;
635
1196
  const GRAPH_SUBJECT_RADIUS = 10;
636
1197
  const GRAPH_LABEL_MAX_WIDTH = 132;
637
1198
  const GRAPH_HIT_PAD = 6;
1199
+ // Auto-mode opacity ramp: fully hidden below START, fully opaque at END (brief muted band between).
1200
+ const GRAPH_LABEL_FADE_START = 0.62;
1201
+ const GRAPH_LABEL_FADE_END = 0.82;
1202
+ const GRAPH_LABEL_MODES = ['auto', 'on', 'off'];
638
1203
 
639
1204
  function wireGraph(root) {
640
1205
  const graphRoots = root.querySelectorAll('[data-graph-json]');
@@ -653,21 +1218,39 @@ function wireGraph(root) {
653
1218
  controls = document.createElement('div');
654
1219
  controls.className = 'graph-controls-bar';
655
1220
  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>';
1221
+ 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
1222
  element.parentElement?.insertBefore(controls, element);
658
1223
  }
659
1224
  const searchInput = controls.querySelector('[data-graph-search]');
660
1225
  const pathInput = controls.querySelector('[data-graph-path]');
661
- const statusSelect = controls.querySelector('[data-graph-status]');
662
- const tagSelect = controls.querySelector('[data-graph-tag]');
1226
+ const statusHost = controls.querySelector('[data-graph-status-scd]');
1227
+ const tagHost = controls.querySelector('[data-graph-tag-scd]');
663
1228
  const toggleExternal = controls.querySelector('[data-graph-toggle-external]');
664
1229
  const toggleIdeasets = controls.querySelector('[data-graph-toggle-ideasets]');
1230
+ const toggleWildcard = controls.querySelector('[data-graph-toggle-wildcard]');
1231
+ const toggleLabels = controls.querySelector('[data-graph-toggle-labels]');
665
1232
  const togglePhysics = controls.querySelector('[data-graph-toggle-physics]');
666
1233
  const fitButton = controls.querySelector('[data-graph-fit]');
667
1234
  const resetButton = controls.querySelector('[data-graph-reset]');
668
1235
  const statusText = controls.querySelector('[data-graph-status-text]');
1236
+ if (statusText) statusText.textContent = 'Initialising graph…';
1237
+ element.classList.add('is-booting');
669
1238
  let hideExternal = false;
670
1239
  let hideIdeasets = false;
1240
+ let includeWildcardRefs = true;
1241
+ let labelMode = 'auto';
1242
+ let statusSelected = [];
1243
+ let tagSelected = [];
1244
+ const statusFilter = mountSearchableCheckboxDropdown(statusHost, (values) => {
1245
+ statusSelected = values;
1246
+ refresh();
1247
+ });
1248
+ const tagFilter = mountSearchableCheckboxDropdown(tagHost, (values) => {
1249
+ tagSelected = values;
1250
+ refresh();
1251
+ });
1252
+ statusSelected = statusFilter.getSelected();
1253
+ tagSelected = tagFilter.getSelected();
671
1254
  let livePhysics = false;
672
1255
  let animationFrame = 0;
673
1256
  let calmTicks = 0;
@@ -788,7 +1371,8 @@ function wireGraph(root) {
788
1371
  minX = Math.min(minX, pos.x - r);
789
1372
  minY = Math.min(minY, pos.y - r);
790
1373
  maxX = Math.max(maxX, pos.x + r);
791
- maxY = Math.max(maxY, pos.y + r + 54);
1374
+ // Reserve label space only when labels are forced on; auto hides when fitted/zoomed out.
1375
+ maxY = Math.max(maxY, pos.y + r + (labelMode === 'on' ? 54 : 0));
792
1376
  }
793
1377
  if (!Number.isFinite(minX)) {
794
1378
  return { minX: 0, minY: 0, maxX: 1200, maxY: 640 };
@@ -796,6 +1380,23 @@ function wireGraph(root) {
796
1380
  return { minX, minY, maxX, maxY };
797
1381
  }
798
1382
 
1383
+ function syncLabelsButton() {
1384
+ if (!toggleLabels) return;
1385
+ const label = labelMode === 'auto' ? 'Labels: auto' : labelMode === 'on' ? 'Labels: on' : 'Labels: off';
1386
+ toggleLabels.textContent = label;
1387
+ toggleLabels.dataset.labelMode = labelMode;
1388
+ toggleLabels.classList.toggle('is-active', labelMode !== 'off');
1389
+ toggleLabels.setAttribute('aria-pressed', labelMode === 'auto' ? 'mixed' : labelMode === 'on' ? 'true' : 'false');
1390
+ }
1391
+
1392
+ function labelOpacityAtScale(scale) {
1393
+ if (labelMode === 'on') return 1;
1394
+ if (labelMode === 'off') return 0;
1395
+ if (scale <= GRAPH_LABEL_FADE_START) return 0;
1396
+ if (scale >= GRAPH_LABEL_FADE_END) return 1;
1397
+ return (scale - GRAPH_LABEL_FADE_START) / (GRAPH_LABEL_FADE_END - GRAPH_LABEL_FADE_START);
1398
+ }
1399
+
799
1400
  function fitView(force = false) {
800
1401
  if (userViewport && !force) return;
801
1402
  const bounds = contentBounds();
@@ -892,6 +1493,7 @@ function wireGraph(root) {
892
1493
  ctx.fillRect(0, 0, cssWidth, cssHeight);
893
1494
 
894
1495
  const { scale, offsetX, offsetY } = viewportTransform();
1496
+ const ambientLabelOpacity = labelOpacityAtScale(scale);
895
1497
  ctx.save();
896
1498
  ctx.translate(offsetX, offsetY);
897
1499
  ctx.scale(scale, scale);
@@ -904,11 +1506,14 @@ function wireGraph(root) {
904
1506
  const source = positions.get(edge.sourceId);
905
1507
  const target = positions.get(edge.targetId);
906
1508
  if (!source || !target) continue;
1509
+ const isWildcard = edge.kind === 'wildcard_reference';
1510
+ ctx.setLineDash(isWildcard ? [2 / scale, 3.5 / scale] : []);
907
1511
  ctx.beginPath();
908
1512
  ctx.moveTo(source.x, source.y);
909
1513
  ctx.lineTo(target.x, target.y);
910
1514
  ctx.stroke();
911
1515
  }
1516
+ ctx.setLineDash([]);
912
1517
  ctx.globalAlpha = 1;
913
1518
 
914
1519
  pulsePhase = (pulsePhase + 0.035) % (Math.PI * 2);
@@ -933,6 +1538,9 @@ function wireGraph(root) {
933
1538
  ctx.strokeStyle = nodeStroke(node, hover);
934
1539
  ctx.stroke();
935
1540
 
1541
+ // Auto: continuous opacity from zoom; hovered/dragged nodes stay fully opaque.
1542
+ const labelOpacity = hover && labelMode === 'auto' ? 1 : ambientLabelOpacity;
1543
+ if (labelOpacity <= 0) continue;
936
1544
  const meta = formatNodeMeta(node);
937
1545
  const attrs = formatNodeAttrs(node);
938
1546
  const nameFont = isSubject ? '600 11px Inter, system-ui, sans-serif' : '600 10px Inter, system-ui, sans-serif';
@@ -941,6 +1549,8 @@ function wireGraph(root) {
941
1549
  const metaLines = wrapLines(meta, GRAPH_LABEL_MAX_WIDTH, metaFont);
942
1550
  const attrLines = wrapLines(attrs, GRAPH_LABEL_MAX_WIDTH, metaFont);
943
1551
  let ty = pos.y + r + 12;
1552
+ ctx.save();
1553
+ ctx.globalAlpha = labelOpacity;
944
1554
  ctx.textAlign = 'center';
945
1555
  ctx.textBaseline = 'top';
946
1556
  ctx.fillStyle = isSubject ? '#0bbefb' : '#ebe4de';
@@ -963,6 +1573,7 @@ function wireGraph(root) {
963
1573
  ctx.fillText(line, pos.x, ty);
964
1574
  ty += 11;
965
1575
  }
1576
+ ctx.restore();
966
1577
  }
967
1578
  ctx.restore();
968
1579
  }
@@ -1055,6 +1666,7 @@ function wireGraph(root) {
1055
1666
  canvas.setAttribute('aria-label', \`Requirement graph with \${filteredNodes.length} nodes\`);
1056
1667
  ctx = canvas.getContext('2d');
1057
1668
  element.innerHTML = '';
1669
+ element.classList.remove('is-booting');
1058
1670
  element.appendChild(canvas);
1059
1671
  resizeCanvas();
1060
1672
  wireViewport(canvas);
@@ -1189,30 +1801,39 @@ function wireGraph(root) {
1189
1801
  }
1190
1802
 
1191
1803
  function matches(node) {
1804
+ const FILTER_NOT_PRESENT = '__not_present__';
1192
1805
  const query = lower(searchInput?.value?.trim());
1193
1806
  const pathQuery = lower(pathInput?.value?.trim());
1194
- const statusQuery = lower(statusSelect?.value?.trim());
1195
- const tagQuery = lower(tagSelect?.value?.trim());
1196
1807
  if (hideExternal && node.isExternal) return false;
1197
1808
  if (hideIdeasets && node.kind === 'ideaset') return false;
1198
1809
  const haystack = [node.name, node.kind, node.fileUri, node.status || '', ...(node.tags || [])].map(lower).join(' ');
1199
1810
  if (query && !haystack.includes(query)) return false;
1200
1811
  if (pathQuery && !lower(node.fileUri).includes(pathQuery)) return false;
1201
- if (statusQuery && lower(node.status || '') !== statusQuery) return false;
1202
- if (tagQuery && !(node.tags || []).some(tag => lower(tag) === tagQuery)) return false;
1812
+ if (statusSelected.length) {
1813
+ const key = node.statusKey || (String(node.status || '').trim() ? String(node.status).trim() : FILTER_NOT_PRESENT);
1814
+ if (!statusSelected.includes(key)) return false;
1815
+ }
1816
+ if (tagSelected.length) {
1817
+ const keys = Array.isArray(node.tagsKeys) && node.tagsKeys.length
1818
+ ? node.tagsKeys
1819
+ : (Array.isArray(node.tags) && node.tags.length ? node.tags : [FILTER_NOT_PRESENT]);
1820
+ if (!tagSelected.some(value => keys.includes(value))) return false;
1821
+ }
1203
1822
  return true;
1204
1823
  }
1205
1824
 
1206
1825
  function refresh() {
1207
1826
  const visibleNodes = (graph.nodes || []).filter(matches);
1208
1827
  const visibleIds = new Set(visibleNodes.map(node => node.id));
1209
- const visibleEdges = (graph.edges || []).filter(edge =>
1210
- visibleIds.has(edge.sourceId) && visibleIds.has(edge.targetId)
1211
- );
1828
+ const visibleEdges = (graph.edges || []).filter(edge => {
1829
+ if (!visibleIds.has(edge.sourceId) || !visibleIds.has(edge.targetId)) return false;
1830
+ if (!includeWildcardRefs && edge.kind === 'wildcard_reference') return false;
1831
+ return true;
1832
+ });
1212
1833
  mountGraph(visibleNodes, visibleEdges);
1213
1834
  }
1214
1835
 
1215
- for (const control of [searchInput, pathInput, statusSelect, tagSelect]) {
1836
+ for (const control of [searchInput, pathInput]) {
1216
1837
  control?.addEventListener('input', refresh);
1217
1838
  control?.addEventListener('change', refresh);
1218
1839
  }
@@ -1228,6 +1849,18 @@ function wireGraph(root) {
1228
1849
  toggleIdeasets.textContent = hideIdeasets ? 'Show ideasets' : 'Hide ideasets';
1229
1850
  refresh();
1230
1851
  });
1852
+ toggleWildcard?.addEventListener('click', () => {
1853
+ includeWildcardRefs = !includeWildcardRefs;
1854
+ toggleWildcard.classList.toggle('is-active', includeWildcardRefs);
1855
+ toggleWildcard.setAttribute('aria-pressed', includeWildcardRefs ? 'true' : 'false');
1856
+ refresh();
1857
+ });
1858
+ toggleLabels?.addEventListener('click', () => {
1859
+ const index = GRAPH_LABEL_MODES.indexOf(labelMode);
1860
+ labelMode = GRAPH_LABEL_MODES[(index + 1) % GRAPH_LABEL_MODES.length];
1861
+ syncLabelsButton();
1862
+ paint();
1863
+ });
1231
1864
  togglePhysics?.addEventListener('click', () => {
1232
1865
  livePhysics = !livePhysics;
1233
1866
  togglePhysics.classList.toggle('is-active', livePhysics);
@@ -1246,12 +1879,19 @@ function wireGraph(root) {
1246
1879
  resetButton?.addEventListener('click', () => {
1247
1880
  if (searchInput) searchInput.value = '';
1248
1881
  if (pathInput) pathInput.value = '';
1249
- if (statusSelect) statusSelect.value = '';
1250
- if (tagSelect) tagSelect.value = '';
1882
+ statusSelected = [];
1883
+ tagSelected = [];
1884
+ statusFilter.clear();
1885
+ tagFilter.clear();
1251
1886
  hideExternal = false;
1252
1887
  hideIdeasets = false;
1888
+ includeWildcardRefs = true;
1889
+ labelMode = 'auto';
1253
1890
  toggleExternal?.classList.remove('is-active');
1254
1891
  toggleIdeasets?.classList.remove('is-active');
1892
+ toggleWildcard?.classList.add('is-active');
1893
+ toggleWildcard?.setAttribute('aria-pressed', 'true');
1894
+ syncLabelsButton();
1255
1895
  if (toggleExternal) toggleExternal.textContent = 'Hide external';
1256
1896
  if (toggleIdeasets) toggleIdeasets.textContent = 'Hide ideasets';
1257
1897
  positions.clear();
@@ -1260,12 +1900,15 @@ function wireGraph(root) {
1260
1900
  userViewport = false;
1261
1901
  refresh();
1262
1902
  });
1903
+ syncLabelsButton();
1263
1904
  refresh();
1264
1905
  }
1265
1906
  }
1266
1907
 
1267
1908
  function boot() {
1268
1909
  const root = document.body;
1910
+ // Interactive Status/Tags triggers before heavy table/graph init.
1911
+ enhanceScdPlaceholders(root);
1269
1912
  wireTables(root);
1270
1913
  wireGraph(root);
1271
1914
  void wireGlobalSearch(root);