@reqlan/analytical 0.8.2 → 0.10.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.
Files changed (179) hide show
  1. package/README.md +27 -2
  2. package/README.template.md +1 -1
  3. package/out/core/barrel-page.d.ts +2 -1
  4. package/out/core/barrel-page.js +18 -83
  5. package/out/core/barrel-page.js.map +1 -1
  6. package/out/core/base-discovery.d.ts +6 -0
  7. package/out/core/base-discovery.js +56 -4
  8. package/out/core/base-discovery.js.map +1 -1
  9. package/out/core/create-base.js +10 -14
  10. package/out/core/create-base.js.map +1 -1
  11. package/out/core/file-reference-resolve.js +8 -2
  12. package/out/core/file-reference-resolve.js.map +1 -1
  13. package/out/core/index-error.d.ts +3 -3
  14. package/out/core/index-error.js.map +1 -1
  15. package/out/core/index-state-types.d.ts +47 -0
  16. package/out/core/index-state-types.js +11 -0
  17. package/out/core/index-state-types.js.map +1 -0
  18. package/out/core/path-relative.d.ts +15 -0
  19. package/out/core/path-relative.js +84 -0
  20. package/out/core/path-relative.js.map +1 -0
  21. package/out/core/rqignore.js +62 -62
  22. package/out/core/rqignore.js.map +1 -1
  23. package/out/core/types.d.ts +12 -3
  24. package/out/core/types.js +4 -1
  25. package/out/core/types.js.map +1 -1
  26. package/out/core/workspace-paths.d.ts +1 -1
  27. package/out/core/workspace-paths.js +8 -44
  28. package/out/core/workspace-paths.js.map +1 -1
  29. package/out/export/build-export-snapshot.js +1 -1
  30. package/out/export/build-export-snapshot.js.map +1 -1
  31. package/out/export/export-utils.d.ts +5 -0
  32. package/out/export/export-utils.js +38 -0
  33. package/out/export/export-utils.js.map +1 -0
  34. package/out/export/write-csv-export.js +1 -1
  35. package/out/export/write-csv-export.js.map +1 -1
  36. package/out/export/write-markdown-export.js +1 -1
  37. package/out/export/write-markdown-export.js.map +1 -1
  38. package/out/index-store/base-registry.d.ts +2 -3
  39. package/out/index-store/base-registry.js +8 -4
  40. package/out/index-store/base-registry.js.map +1 -1
  41. package/out/index-store/fuzzy-search-hit.d.ts +10 -0
  42. package/out/index-store/fuzzy-search-hit.js +2 -0
  43. package/out/index-store/fuzzy-search-hit.js.map +1 -0
  44. package/out/index-store/index-diagnostics-store.d.ts +4 -0
  45. package/out/index-store/index-diagnostics-store.js +44 -88
  46. package/out/index-store/index-diagnostics-store.js.map +1 -1
  47. package/out/index-store/index-file-error.d.ts +1 -1
  48. package/out/index-store/index-parse-issues.d.ts +4 -3
  49. package/out/index-store/index-parse-issues.js +0 -38
  50. package/out/index-store/index-parse-issues.js.map +1 -1
  51. package/out/index-store/index-status.d.ts +1 -1
  52. package/out/index-store/schema.d.ts +1 -1
  53. package/out/index-store/schema.js +6 -2
  54. package/out/index-store/schema.js.map +1 -1
  55. package/out/index-store/sqlite-store.d.ts +10 -4
  56. package/out/index-store/sqlite-store.js +81 -606
  57. package/out/index-store/sqlite-store.js.map +1 -1
  58. package/out/index-store/webview-graph-queries.d.ts +7 -0
  59. package/out/index-store/webview-graph-queries.js.map +1 -1
  60. package/out/index-store/webview-table-queries.d.ts +1 -1
  61. package/out/index-store/webview-table-queries.js +9 -0
  62. package/out/index-store/webview-table-queries.js.map +1 -1
  63. package/out/index-store/workspace-index-file.d.ts +3 -14
  64. package/out/index-store/workspace-index-file.js +1 -118
  65. package/out/index-store/workspace-index-file.js.map +1 -1
  66. package/out/index-store/workspace-index-sync.d.ts +4 -17
  67. package/out/index-store/workspace-index-sync.js +29 -11
  68. package/out/index-store/workspace-index-sync.js.map +1 -1
  69. package/out/index-store/workspace-index.d.ts +36 -9
  70. package/out/index-store/workspace-index.js +223 -120
  71. package/out/index-store/workspace-index.js.map +1 -1
  72. package/out/index.d.ts +16 -39
  73. package/out/index.js +9 -23
  74. package/out/index.js.map +1 -1
  75. package/out/native/generated.d.ts +102 -0
  76. package/out/native/generated.js +4 -0
  77. package/out/native/generated.js.map +1 -0
  78. package/out/native/index.d.ts +17 -0
  79. package/out/native/index.js +12 -0
  80. package/out/native/index.js.map +1 -0
  81. package/out/native/load-native.d.ts +50 -0
  82. package/out/native/load-native.js +152 -0
  83. package/out/native/load-native.js.map +1 -0
  84. package/out/{analysis-api.d.ts → native/native-analysis-api.d.ts} +24 -20
  85. package/out/native/native-analysis-api.js +212 -0
  86. package/out/native/native-analysis-api.js.map +1 -0
  87. package/out/native/native-index-store.d.ts +5 -0
  88. package/out/native/native-index-store.js +6 -0
  89. package/out/native/native-index-store.js.map +1 -0
  90. package/out/native/native-sql-db.d.ts +135 -0
  91. package/out/native/native-sql-db.js +217 -0
  92. package/out/native/native-sql-db.js.map +1 -0
  93. package/out/native/native-workspace-index.d.ts +164 -0
  94. package/out/native/native-workspace-index.js +283 -0
  95. package/out/native/native-workspace-index.js.map +1 -0
  96. package/out/native/open-analysis-api.d.ts +16 -0
  97. package/out/native/open-analysis-api.js +21 -0
  98. package/out/native/open-analysis-api.js.map +1 -0
  99. package/out/native/parse-source.d.ts +24 -0
  100. package/out/native/parse-source.js +26 -0
  101. package/out/native/parse-source.js.map +1 -0
  102. package/package.json +18 -22
  103. package/out/analysis/analyser-registry.d.ts +0 -21
  104. package/out/analysis/analyser-registry.js +0 -33
  105. package/out/analysis/analyser-registry.js.map +0 -1
  106. package/out/analysis/completion-tracking-analyser.d.ts +0 -3
  107. package/out/analysis/completion-tracking-analyser.js +0 -37
  108. package/out/analysis/completion-tracking-analyser.js.map +0 -1
  109. package/out/analysis/deprecation-impact-analyser.d.ts +0 -3
  110. package/out/analysis/deprecation-impact-analyser.js +0 -21
  111. package/out/analysis/deprecation-impact-analyser.js.map +0 -1
  112. package/out/analysis/file-related-analyser.d.ts +0 -5
  113. package/out/analysis/file-related-analyser.js +0 -41
  114. package/out/analysis/file-related-analyser.js.map +0 -1
  115. package/out/analysis/fuzzy-search-analyser.d.ts +0 -17
  116. package/out/analysis/fuzzy-search-analyser.js +0 -25
  117. package/out/analysis/fuzzy-search-analyser.js.map +0 -1
  118. package/out/analysis/fuzzy-search-worker-client.d.ts +0 -39
  119. package/out/analysis/fuzzy-search-worker-client.js +0 -178
  120. package/out/analysis/fuzzy-search-worker-client.js.map +0 -1
  121. package/out/analysis/fuzzy-search-worker.d.ts +0 -32
  122. package/out/analysis/fuzzy-search-worker.js +0 -81
  123. package/out/analysis/fuzzy-search-worker.js.map +0 -1
  124. package/out/analysis/fuzzy-search.d.ts +0 -38
  125. package/out/analysis/fuzzy-search.js +0 -217
  126. package/out/analysis/fuzzy-search.js.map +0 -1
  127. package/out/analysis/git-dates-analyser.d.ts +0 -9
  128. package/out/analysis/git-dates-analyser.js +0 -77
  129. package/out/analysis/git-dates-analyser.js.map +0 -1
  130. package/out/analysis/list-ideas-analyser.d.ts +0 -3
  131. package/out/analysis/list-ideas-analyser.js +0 -7
  132. package/out/analysis/list-ideas-analyser.js.map +0 -1
  133. package/out/analysis/local-graph-analyser.d.ts +0 -6
  134. package/out/analysis/local-graph-analyser.js +0 -42
  135. package/out/analysis/local-graph-analyser.js.map +0 -1
  136. package/out/analysis/semantic-search-analyser.d.ts +0 -8
  137. package/out/analysis/semantic-search-analyser.js +0 -51
  138. package/out/analysis/semantic-search-analyser.js.map +0 -1
  139. package/out/analysis-api.js +0 -190
  140. package/out/analysis-api.js.map +0 -1
  141. package/out/core/analytical-store.d.ts +0 -85
  142. package/out/core/analytical-store.js +0 -143
  143. package/out/core/analytical-store.js.map +0 -1
  144. package/out/core/context-model.d.ts +0 -204
  145. package/out/core/context-model.js +0 -36
  146. package/out/core/context-model.js.map +0 -1
  147. package/out/core/context-signals.d.ts +0 -149
  148. package/out/core/context-signals.js +0 -397
  149. package/out/core/context-signals.js.map +0 -1
  150. package/out/create-runtime.d.ts +0 -47
  151. package/out/create-runtime.js +0 -79
  152. package/out/create-runtime.js.map +0 -1
  153. package/out/export/export-html.d.ts +0 -4
  154. package/out/export/export-html.js +0 -12
  155. package/out/export/export-html.js.map +0 -1
  156. package/out/export/html-export-assets.d.ts +0 -3
  157. package/out/export/html-export-assets.js +0 -1926
  158. package/out/export/html-export-assets.js.map +0 -1
  159. package/out/export/html-export-template.d.ts +0 -18
  160. package/out/export/html-export-template.js +0 -1139
  161. package/out/export/html-export-template.js.map +0 -1
  162. package/out/export/html-export-utils.d.ts +0 -86
  163. package/out/export/html-export-utils.js +0 -485
  164. package/out/export/html-export-utils.js.map +0 -1
  165. package/out/export/write-html-export.d.ts +0 -2
  166. package/out/export/write-html-export.js +0 -181
  167. package/out/export/write-html-export.js.map +0 -1
  168. package/out/graph/physics-core-source.d.ts +0 -5
  169. package/out/graph/physics-core-source.js +0 -8
  170. package/out/graph/physics-core-source.js.map +0 -1
  171. package/out/graph/physics-core.d.ts +0 -141
  172. package/out/graph/physics-core.js +0 -237
  173. package/out/graph/physics-core.js.map +0 -1
  174. package/out/index-store/idea-extractor.d.ts +0 -11
  175. package/out/index-store/idea-extractor.js +0 -351
  176. package/out/index-store/idea-extractor.js.map +0 -1
  177. package/out/index-store/overview-coverage.d.ts +0 -23
  178. package/out/index-store/overview-coverage.js +0 -254
  179. package/out/index-store/overview-coverage.js.map +0 -1
@@ -1,1926 +0,0 @@
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
- */
9
- function embedPhysicsCoreSource() {
10
- return PHYSICS_CORE_CLASSIC_SOURCE;
11
- }
12
- export const SHARED_STYLES = `
13
- :root {
14
- color-scheme: dark;
15
- --bg: #14100e;
16
- --bg-elev: #1f1815;
17
- --bg-soft: #1f1815;
18
- --bg-raised: #2a211d;
19
- --fg: #ebe4de;
20
- --muted: #a89488;
21
- --faint: #6e5f56;
22
- --line: #3d2f28;
23
- --accent: #07a0e5;
24
- --accent-strong: #0bbefb;
25
- --accent-dim: #0371c1;
26
- --rust: #b85c38;
27
- --rust-muted: #8a4530;
28
- --good: #4ade80;
29
- --warn: #fbbf24;
30
- --bad: #fb7185;
31
- font-family: Inter, system-ui, sans-serif;
32
- }
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
- }
38
- body {
39
- margin: 0;
40
- background: var(--bg);
41
- color: var(--fg);
42
- line-height: 1.55;
43
- }
44
- a { color: var(--accent-strong); text-decoration: none; }
45
- a:hover { color: var(--accent); }
46
- main.layout {
47
- max-width: 1360px; margin: 0 auto; padding: 1.5rem;
48
- display: flex; flex-direction: column; gap: 1.5rem;
49
- }
50
- .topbar {
51
- position: sticky; top: 0; z-index: 10;
52
- background: var(--bg-elev); border-bottom: 1px solid var(--line); margin: 0 -1.5rem;
53
- padding: 0.9rem 1.5rem;
54
- }
55
- .topbar-inner, .hero, .split, .grid, .detail-grid { display: grid; gap: 1.5rem; }
56
- .topbar-inner { grid-template-columns: 1.3fr auto; align-items: center; }
57
- .nav { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
58
- .nav a {
59
- padding: 0.45rem 0.75rem; border-radius: 8px;
60
- background: var(--bg-raised); color: var(--muted); border: 1px solid var(--line);
61
- }
62
- .nav a:hover { color: var(--fg); border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); }
63
- .nav a.active {
64
- background: color-mix(in srgb, var(--rust) 28%, var(--bg-raised));
65
- color: var(--fg); border-color: var(--rust);
66
- }
67
- .nav a.brand-link {
68
- background: transparent;
69
- border-color: transparent;
70
- color: var(--fg);
71
- font-weight: 650;
72
- letter-spacing: 0.02em;
73
- padding-left: 0.15rem;
74
- padding-right: 0.9rem;
75
- }
76
- .nav a.brand-link:hover {
77
- color: var(--accent);
78
- border-color: transparent;
79
- background: transparent;
80
- }
81
- .hero { grid-template-columns: 2fr 1fr; }
82
- .grid {
83
- display: grid;
84
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr));
85
- gap: 1.5rem;
86
- row-gap: 1.5rem;
87
- column-gap: 1.5rem;
88
- }
89
- .split, .detail-grid {
90
- display: grid;
91
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
92
- gap: 1.5rem;
93
- }
94
- .panel, .metric, .card, .table-shell, .graph-shell, .print-card, .search-result {
95
- background: var(--bg-raised);
96
- border: 1px solid var(--line);
97
- border-radius: 10px;
98
- }
99
- .panel, .card, .table-shell, .graph-shell, .print-card, .search-result { padding: 1rem 1.1rem; }
100
- .metric { padding: 0; overflow: hidden; }
101
- .metric-link, .metric > span, .metric > strong {
102
- display: flex; flex-direction: column; gap: 0.35rem; padding: 1rem; color: inherit;
103
- }
104
- .metric-link:hover { background: color-mix(in srgb, var(--accent) 10%, var(--bg-raised)); }
105
- .metric-label, .subtle, .eyebrow, .breadcrumbs { color: var(--muted); }
106
- .metric-value { font-size: 1.75rem; color: var(--accent-strong); }
107
- .eyebrow { text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.75rem; color: var(--rust); }
108
- .page-header { display: grid; gap: 0.55rem; }
109
- .page-header h1 { margin: 0; }
110
- .page-header .eyebrow { margin: 0; }
111
- .page-header .subtle { margin: 0; }
112
- .toolbar {
113
- display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; justify-content: space-between;
114
- margin-bottom: 0.9rem;
115
- }
116
- .toolbar .actions, .pill-row, .breadcrumbs { display: flex; gap: 0.75rem; row-gap: 0.85rem; flex-wrap: wrap; align-items: center; }
117
- .chip, .pill {
118
- display: inline-flex; align-items: center; gap: 0.35rem;
119
- padding: 0.28rem 0.6rem; border-radius: 8px;
120
- border: 1px solid var(--line); background: var(--bg); color: var(--muted);
121
- }
122
- a.pill:hover { color: var(--accent-strong); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
123
- .graph-controls-bar {
124
- display: flex;
125
- flex-wrap: wrap;
126
- gap: 0.65rem;
127
- align-items: center;
128
- margin-bottom: 0.9rem;
129
- padding: 0.8rem;
130
- border: 1px solid var(--line);
131
- border-radius: 10px;
132
- background: var(--bg);
133
- }
134
- .graph-shell > .pill-row { margin-bottom: 0.9rem; }
135
- .graph-filter {
136
- flex: 1 1 180px;
137
- min-width: 160px;
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
- }
298
- .graph-action {
299
- border: 1px solid var(--line);
300
- background: var(--bg-soft);
301
- color: var(--fg);
302
- border-radius: 8px;
303
- padding: 0.7rem 0.9rem;
304
- cursor: pointer;
305
- }
306
- .graph-action:hover { border-color: var(--accent-dim); color: var(--accent-strong); }
307
- .graph-action.is-active {
308
- background: color-mix(in srgb, var(--rust) 28%, var(--bg-soft));
309
- border-color: var(--rust);
310
- color: var(--fg);
311
- }
312
- .graph-status {
313
- color: var(--muted);
314
- font-size: 0.92rem;
315
- margin-left: auto;
316
- }
317
- .searchbar, select, input[type="search"] {
318
- width: min(100%, 440px); background: var(--bg); color: var(--fg);
319
- border: 1px solid var(--line); border-radius: 8px; padding: 0.75rem 0.9rem;
320
- }
321
- .searchbar:focus, select:focus, input[type="search"]:focus {
322
- outline: none;
323
- border-color: var(--accent);
324
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 25%, transparent);
325
- }
326
- table { width: 100%; border-collapse: collapse; }
327
- th, td {
328
- padding: 0.75rem 0.65rem; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top;
329
- }
330
- th { color: var(--muted); font-size: 0.94rem; }
331
- tbody tr:hover { background: var(--bg-raised); }
332
- .table-shell { overflow-x: auto; }
333
- .scroll-window {
334
- --scroll-window-max: min(28rem, 50vh);
335
- }
336
- body[data-runtime-mode="interactive"] .scroll-window {
337
- max-height: var(--scroll-window-max);
338
- overflow: auto;
339
- border: 1px solid var(--line);
340
- border-radius: 8px;
341
- background: var(--bg);
342
- }
343
- body[data-runtime-mode="interactive"] .scroll-window > table {
344
- margin: 0;
345
- }
346
- body[data-runtime-mode="interactive"] .scroll-window .entity-list,
347
- body[data-runtime-mode="interactive"] .scroll-window .rollup-list {
348
- padding: 0.75rem;
349
- }
350
- .sortable-th { padding: 0.45rem 0.4rem; }
351
- .sort-button {
352
- display: inline-flex; align-items: center; gap: 0.35rem;
353
- width: 100%; margin: 0; padding: 0.3rem 0.25rem;
354
- border: 0; background: transparent; color: inherit;
355
- font: inherit; text-align: left; cursor: pointer;
356
- }
357
- .sort-button:hover { color: var(--fg); }
358
- .sort-button.sort-active { color: var(--accent-strong); }
359
- .sort-indicator { color: var(--faint); font-size: 0.8em; min-width: 1em; }
360
- .sort-button.sort-active .sort-indicator { color: var(--accent-strong); }
361
- .table-filter-toggle {
362
- display: inline-flex; align-items: center; justify-content: center;
363
- width: 2.25rem; height: 2.25rem; padding: 0;
364
- border: 1px solid var(--line); border-radius: 8px;
365
- background: var(--bg-raised); color: var(--muted); cursor: pointer;
366
- }
367
- .table-filter-toggle:hover { color: var(--fg); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
368
- .table-filter-toggle.is-active {
369
- color: var(--fg);
370
- border-color: var(--accent);
371
- background: color-mix(in srgb, var(--accent) 18%, var(--bg-raised));
372
- }
373
- .table-filter-toggle.has-filters:not(.is-active) {
374
- color: var(--accent-strong);
375
- border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
376
- }
377
- .table-filter-toggle svg { width: 1rem; height: 1rem; display: block; }
378
- .table-filter-actions {
379
- display: flex; justify-content: flex-end; gap: 0.55rem;
380
- margin-bottom: 0.65rem;
381
- }
382
- .column-filter-row { display: none; }
383
- .column-filter-row.is-open { display: table-row; }
384
- .column-filter-row th {
385
- padding: 0.35rem 0.4rem 0.75rem; border-bottom: 1px solid var(--line); font-weight: 400;
386
- }
387
- .column-filter {
388
- width: 100%; min-width: 4.5rem; background: var(--bg); color: var(--fg);
389
- border: 1px solid var(--line); border-radius: 6px; padding: 0.4rem 0.55rem; font-size: 0.85rem;
390
- }
391
- .column-filter:focus {
392
- outline: none;
393
- border-color: var(--accent);
394
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 25%, transparent);
395
- }
396
- body[data-runtime-mode="interactive"] .scroll-window thead tr:first-child th {
397
- position: sticky;
398
- top: 0;
399
- z-index: 2;
400
- background: var(--bg-raised);
401
- box-shadow: 0 1px 0 var(--line);
402
- }
403
- body[data-runtime-mode="interactive"] .scroll-window thead .column-filter-row.is-open th {
404
- position: sticky;
405
- top: 2.65rem;
406
- z-index: 1;
407
- background: var(--bg-raised);
408
- box-shadow: 0 1px 0 var(--line);
409
- }
410
- .entity-list {
411
- display: grid;
412
- grid-template-columns: minmax(0, 1fr);
413
- gap: 1.5rem;
414
- }
415
- .entity-card {
416
- border: 1px solid var(--line); border-radius: 10px; padding: 0.9rem 1rem; background: var(--bg);
417
- color: inherit;
418
- min-width: 0;
419
- max-width: 100%;
420
- overflow-wrap: anywhere;
421
- word-break: break-word;
422
- }
423
- .entity-card:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
424
- .entity-card h3, .entity-card p { margin: 0; }
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
- }
472
- .panel > h2, .table-shell > h2, .graph-shell > h2, .print-card > h3 { margin-top: 0; margin-bottom: 0.75rem; }
473
- .toolbar h2 { margin: 0; }
474
- .rollup-list { display: grid; gap: 0.5rem; }
475
- .rollup-list div {
476
- display: flex; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--line); padding-bottom: 0.4rem;
477
- }
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
- }
487
- .distribution-track {
488
- height: 0.7rem;
489
- min-width: 6rem;
490
- border-radius: 999px;
491
- background: var(--bg);
492
- border: 1px solid var(--line);
493
- overflow: hidden;
494
- }
495
- .distribution-fill {
496
- display: block;
497
- height: 100%;
498
- border-radius: inherit;
499
- background: linear-gradient(90deg, var(--accent-dim), var(--accent-strong));
500
- min-width: 0;
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
- }
533
- .graph-root canvas {
534
- display: block;
535
- width: 100%;
536
- min-height: 620px;
537
- background: var(--bg);
538
- border-radius: 10px;
539
- border: 1px solid var(--line);
540
- touch-action: none;
541
- cursor: grab;
542
- }
543
- .graph-root canvas.is-panning { cursor: grabbing; }
544
- .graph-root canvas.is-dragging-node { cursor: pointer; }
545
- .search-results { display: grid; gap: 0.7rem; margin-top: 1rem; }
546
- .search-result:hover { border-color: var(--accent-dim); }
547
- .hidden { display: none !important; }
548
- .breadcrumbs a { color: var(--muted); }
549
- .breadcrumbs a:hover { color: var(--accent-strong); }
550
- .section-link { font-size: 0.92rem; }
551
- .prose p { line-height: 1.55; }
552
- .idea-summary { white-space: normal; }
553
- .idea-ref {
554
- display: inline;
555
- padding: 0.05em 0.35em;
556
- margin: 0 0.05em;
557
- border-radius: 0.3em;
558
- border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
559
- background: color-mix(in srgb, var(--accent) 14%, var(--bg-raised));
560
- color: var(--accent-strong);
561
- font-weight: 600;
562
- text-decoration: none;
563
- white-space: normal;
564
- overflow-wrap: anywhere;
565
- }
566
- a.idea-ref:hover {
567
- color: var(--fg);
568
- border-color: var(--accent);
569
- background: color-mix(in srgb, var(--accent) 22%, var(--bg-raised));
570
- }
571
- .idea-ref--file {
572
- border-color: color-mix(in srgb, var(--rust) 40%, var(--line));
573
- background: color-mix(in srgb, var(--rust) 16%, var(--bg-raised));
574
- color: #e8a07a;
575
- }
576
- a.idea-ref--file:hover {
577
- color: var(--fg);
578
- border-color: var(--rust);
579
- }
580
- .idea-ref--unresolved {
581
- border-style: dashed;
582
- border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
583
- background: color-mix(in srgb, var(--warn) 10%, var(--bg-raised));
584
- color: var(--warn);
585
- font-weight: 500;
586
- }
587
- pre.code-like {
588
- white-space: pre-wrap; padding: 0.9rem; border-radius: 8px;
589
- background: var(--bg); border: 1px solid var(--line);
590
- }
591
- @media (max-width: 900px) {
592
- .hero, .topbar-inner { grid-template-columns: 1fr; }
593
- }
594
- @media (max-width: 520px) {
595
- .print-attrs div { grid-template-columns: 1fr; }
596
- }
597
- @media print {
598
- :root { color-scheme: light; }
599
- body { background: white; color: black; }
600
- .topbar, .searchbar, .toolbar .actions, .graph-shell .toolbar, .column-filter-row, .table-filter-toggle, .table-filter-actions, .hide-on-print { display: none !important; }
601
- .sort-button { cursor: default; }
602
- .panel, .metric, .card, .table-shell, .graph-shell, .print-card, .search-result, .entity-card {
603
- background: white; color: black; border-color: #cbd5e1; box-shadow: none;
604
- }
605
- .scroll-window {
606
- max-height: none !important;
607
- overflow: visible !important;
608
- border: none !important;
609
- background: transparent !important;
610
- }
611
- .scroll-window thead th {
612
- position: static !important;
613
- box-shadow: none !important;
614
- }
615
- a { color: black; text-decoration: underline; }
616
- .print-break-avoid { break-inside: avoid; }
617
- }
618
- `;
619
- export const APP_JS = `
620
- ${embedPhysicsCoreSource()}
621
-
622
- const searchIndexByRoot = new WeakMap();
623
-
624
- function exportRootPrefix(root) {
625
- const searchUrl = root.dataset.searchIndex || '';
626
- const dataIdx = searchUrl.lastIndexOf('data/');
627
- return dataIdx >= 0 ? searchUrl.slice(0, dataIdx) : '';
628
- }
629
-
630
- function resolveExportUrl(root, url) {
631
- const cleaned = String(url || '').replace(/^\\.\\//, '');
632
- return \`\${exportRootPrefix(root)}\${cleaned}\`;
633
- }
634
-
635
- async function loadSearchIndex(root) {
636
- if (searchIndexByRoot.has(root)) return searchIndexByRoot.get(root);
637
- const embedded = typeof globalThis.__REQLAN_SEARCH_INDEX__ !== 'undefined'
638
- ? globalThis.__REQLAN_SEARCH_INDEX__
639
- : null;
640
- if (Array.isArray(embedded)) {
641
- const ready = Promise.resolve(embedded);
642
- searchIndexByRoot.set(root, ready);
643
- return ready;
644
- }
645
- const url = root.dataset.searchIndex;
646
- if (!url) return [];
647
- const loaded = fetch(url).then(response => {
648
- if (!response.ok) throw new Error('search index fetch failed');
649
- return response.json();
650
- }).catch(() => []);
651
- searchIndexByRoot.set(root, loaded);
652
- return loaded;
653
- }
654
-
655
- function lower(value) {
656
- return String(value || '').toLowerCase();
657
- }
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
-
920
- function scoreDocument(doc, query) {
921
- const haystack = [
922
- doc.title,
923
- doc.summary,
924
- ...(doc.tags || []),
925
- ...(doc.pathTokens || []),
926
- ...(doc.keywords || [])
927
- ].map(lower).join(' ');
928
- return haystack.includes(query) ? haystack.indexOf(query) : -1;
929
- }
930
-
931
- async function wireGlobalSearch(root) {
932
- const input = root.querySelector('[data-global-search]');
933
- const results = root.querySelector('[data-search-results]');
934
- if (!input || !results) return;
935
- const docs = await loadSearchIndex(root);
936
- function render() {
937
- const query = lower(input.value.trim());
938
- if (!query) {
939
- results.innerHTML = '';
940
- results.classList.add('hidden');
941
- return;
942
- }
943
- const matches = docs
944
- .map(doc => ({ doc, score: scoreDocument(doc, query) }))
945
- .filter(item => item.score >= 0)
946
- .sort((a, b) => a.score - b.score || a.doc.title.localeCompare(b.doc.title))
947
- .slice(0, 18);
948
- results.classList.remove('hidden');
949
- results.innerHTML = matches.length === 0
950
- ? '<div class="search-result"><strong>No matches</strong><p class="subtle">Try idea names, tags, statuses, file paths, or cluster labels.</p></div>'
951
- : matches.map(({ doc }) => \`
952
- <a class="search-result" href="\${resolveExportUrl(root, doc.url)}">
953
- <div class="pill-row">
954
- <span class="pill">\${doc.kind}</span>
955
- \${doc.status ? \`<span class="pill">\${doc.status}</span>\` : ''}
956
- </div>
957
- <strong>\${doc.title}</strong>
958
- <p class="subtle">\${doc.summary || ''}</p>
959
- </a>
960
- \`).join('');
961
- }
962
- input.addEventListener('input', render);
963
- }
964
-
965
- function cellSortValue(cell) {
966
- if (!cell) return '';
967
- const explicit = cell.getAttribute('data-sort-value');
968
- if (explicit != null) return explicit.trim();
969
- return String(cell.textContent || '').replace(/\\s+/g, ' ').trim();
970
- }
971
-
972
- function compareSortValues(left, right) {
973
- const leftNumber = Number(String(left).replace(/,/g, ''));
974
- const rightNumber = Number(String(right).replace(/,/g, ''));
975
- const bothNumeric = left !== '' && right !== ''
976
- && Number.isFinite(leftNumber)
977
- && Number.isFinite(rightNumber);
978
- if (bothNumeric) {
979
- return leftNumber - rightNumber;
980
- }
981
- return String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });
982
- }
983
-
984
- function isPlaceholderRow(row) {
985
- if (!row || row.cells.length === 0) return true;
986
- return [...row.cells].some(cell => Number(cell.colSpan || 1) > 1);
987
- }
988
-
989
- const FILTER_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>';
990
-
991
- function ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState) {
992
- const shell = table.closest('.table-shell') || table.parentElement;
993
- const toolbar = shell ? shell.querySelector(':scope > .toolbar') : null;
994
- let actions = toolbar ? toolbar.querySelector('.actions') : null;
995
- if (toolbar && !actions) {
996
- actions = document.createElement('div');
997
- actions.className = 'actions';
998
- toolbar.appendChild(actions);
999
- }
1000
- if (!actions) {
1001
- actions = document.createElement('div');
1002
- actions.className = 'actions table-filter-actions';
1003
- table.parentElement?.insertBefore(actions, table);
1004
- }
1005
-
1006
- const toggle = document.createElement('button');
1007
- toggle.type = 'button';
1008
- toggle.className = 'table-filter-toggle';
1009
- toggle.setAttribute('aria-label', 'Toggle column filters');
1010
- toggle.setAttribute('aria-expanded', 'false');
1011
- toggle.setAttribute('title', 'Column filters');
1012
- toggle.innerHTML = FILTER_ICON_SVG;
1013
- toggle.addEventListener('click', () => {
1014
- const open = !filterRow.classList.contains('is-open');
1015
- filterRow.classList.toggle('is-open', open);
1016
- toggle.classList.toggle('is-active', open);
1017
- toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
1018
- syncToggleState();
1019
- if (open) {
1020
- filterInputs[0]?.focus();
1021
- }
1022
- });
1023
- actions.appendChild(toggle);
1024
- return toggle;
1025
- }
1026
-
1027
- function wireTables(root) {
1028
- const tables = root.querySelectorAll('table');
1029
- const tableControllers = [];
1030
- const params = new URLSearchParams(typeof location !== 'undefined' ? location.search : '');
1031
-
1032
- for (const table of tables) {
1033
- const thead = table.tHead;
1034
- const tbody = table.tBodies[0];
1035
- if (!thead || !tbody || thead.rows.length === 0) continue;
1036
- const headerRow = thead.rows[0];
1037
- if (headerRow.dataset.enhanced === '1') continue;
1038
- headerRow.dataset.enhanced = '1';
1039
-
1040
- const headers = [...headerRow.cells];
1041
- if (headers.length === 0) continue;
1042
-
1043
- let sortIndex = -1;
1044
- let sortDir = 'asc';
1045
- const filterInputs = [];
1046
- let filterToggle = null;
1047
- const syncToggleState = () => {
1048
- if (!filterToggle) return;
1049
- const hasFilters = filterInputs.some(input => input.value.trim());
1050
- filterToggle.classList.toggle('has-filters', hasFilters);
1051
- };
1052
- const controller = {
1053
- table,
1054
- tbody,
1055
- filterInputs,
1056
- applyFilters() {
1057
- const sample = [...tbody.rows].find(row => row.hasAttribute('data-filter-row'));
1058
- const filterId = sample?.getAttribute('data-filter-row');
1059
- const globalInput = filterId
1060
- ? root.querySelector(\`[data-filter-input="\${filterId}"]\`)
1061
- : null;
1062
- const globalQuery = lower(globalInput?.value.trim());
1063
- const columnQueries = filterInputs.map(input => lower(input.value.trim()));
1064
- for (const row of tbody.rows) {
1065
- if (isPlaceholderRow(row)) continue;
1066
- const globalText = lower(row.getAttribute('data-filter-text') || row.textContent);
1067
- const matchesGlobal = !globalQuery || globalText.includes(globalQuery);
1068
- const matchesColumns = columnQueries.every((query, index) => {
1069
- if (!query) return true;
1070
- return lower(cellSortValue(row.cells[index])).includes(query);
1071
- });
1072
- row.classList.toggle('hidden', !(matchesGlobal && matchesColumns));
1073
- }
1074
- syncToggleState();
1075
- }
1076
- };
1077
-
1078
- headers.forEach((th, index) => {
1079
- const label = String(th.textContent || '').trim() || \`Column \${index + 1}\`;
1080
- const filterKey = String(th.getAttribute('data-filter-key') || '').trim();
1081
- th.classList.add('sortable-th');
1082
- th.replaceChildren();
1083
- if (filterKey) {
1084
- th.setAttribute('data-filter-key', filterKey);
1085
- }
1086
- const button = document.createElement('button');
1087
- button.type = 'button';
1088
- button.className = 'sort-button';
1089
- button.dataset.sortCol = String(index);
1090
- button.setAttribute('aria-label', \`Sort by \${label}\`);
1091
- const labelNode = document.createElement('span');
1092
- labelNode.textContent = label;
1093
- const indicator = document.createElement('span');
1094
- indicator.className = 'sort-indicator';
1095
- indicator.setAttribute('aria-hidden', 'true');
1096
- button.append(labelNode, indicator);
1097
- button.addEventListener('click', () => {
1098
- if (sortIndex === index) {
1099
- sortDir = sortDir === 'asc' ? 'desc' : 'asc';
1100
- } else {
1101
- sortIndex = index;
1102
- sortDir = 'asc';
1103
- }
1104
- const dataRows = [...tbody.rows].filter(row => !isPlaceholderRow(row));
1105
- dataRows.sort((left, right) => {
1106
- const cmp = compareSortValues(
1107
- cellSortValue(left.cells[index]),
1108
- cellSortValue(right.cells[index])
1109
- );
1110
- return sortDir === 'asc' ? cmp : -cmp;
1111
- });
1112
- for (const row of dataRows) {
1113
- tbody.appendChild(row);
1114
- }
1115
- for (const header of headers) {
1116
- const sortButton = header.querySelector('.sort-button');
1117
- const mark = header.querySelector('.sort-indicator');
1118
- if (!sortButton || !mark) continue;
1119
- const active = Number(sortButton.dataset.sortCol) === sortIndex;
1120
- sortButton.classList.toggle('sort-active', active);
1121
- mark.textContent = active ? (sortDir === 'asc' ? '▲' : '▼') : '';
1122
- }
1123
- });
1124
- th.appendChild(button);
1125
- });
1126
-
1127
- const filterRow = document.createElement('tr');
1128
- filterRow.className = 'column-filter-row';
1129
- headers.forEach((th, index) => {
1130
- const label = String(th.querySelector('.sort-button span')?.textContent || \`Column \${index + 1}\`);
1131
- const filterKey = String(th.getAttribute('data-filter-key') || '').trim().toLowerCase();
1132
- const cell = document.createElement('th');
1133
- const input = document.createElement('input');
1134
- input.type = 'search';
1135
- input.className = 'column-filter';
1136
- input.placeholder = 'Filter…';
1137
- input.dataset.colFilter = String(index);
1138
- if (filterKey) {
1139
- input.dataset.filterKey = filterKey;
1140
- }
1141
- input.setAttribute('aria-label', \`Filter \${label}\`);
1142
- input.addEventListener('input', () => controller.applyFilters());
1143
- filterInputs.push(input);
1144
- cell.appendChild(input);
1145
- filterRow.appendChild(cell);
1146
- });
1147
- thead.appendChild(filterRow);
1148
- filterToggle = ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState);
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
- }
1176
- }
1177
-
1178
- const inputs = root.querySelectorAll('[data-filter-input]');
1179
- for (const input of inputs) {
1180
- input.addEventListener('input', () => {
1181
- const targetId = input.dataset.filterInput;
1182
- for (const controller of tableControllers) {
1183
- const ownsRows = controller.tbody.querySelector(\`[data-filter-row="\${targetId}"]\`);
1184
- if (ownsRows) {
1185
- controller.applyFilters();
1186
- }
1187
- }
1188
- });
1189
- }
1190
- }
1191
-
1192
- const EXPORT_PHYSICS_SETTINGS = ReqlanGraphPhysics.DEFAULT_PHYSICS_SETTINGS;
1193
-
1194
- // Tight visual radii for denser canvas packing (labels still show full names).
1195
- const GRAPH_NODE_RADIUS = 7;
1196
- const GRAPH_SUBJECT_RADIUS = 10;
1197
- const GRAPH_LABEL_MAX_WIDTH = 132;
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'];
1203
-
1204
- function wireGraph(root) {
1205
- const graphRoots = root.querySelectorAll('[data-graph-json]');
1206
- for (const element of graphRoots) {
1207
- const scriptId = element.dataset.graphJson;
1208
- const dataElement = root.querySelector(\`#\${scriptId}\`);
1209
- if (!dataElement) continue;
1210
- let graph;
1211
- try {
1212
- graph = JSON.parse(dataElement.textContent || '{}');
1213
- } catch {
1214
- continue;
1215
- }
1216
- let controls = root.querySelector(\`[data-graph-controls="\${scriptId}"]\`);
1217
- if (!controls) {
1218
- controls = document.createElement('div');
1219
- controls.className = 'graph-controls-bar';
1220
- controls.dataset.graphControls = scriptId;
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>';
1222
- element.parentElement?.insertBefore(controls, element);
1223
- }
1224
- const searchInput = controls.querySelector('[data-graph-search]');
1225
- const pathInput = controls.querySelector('[data-graph-path]');
1226
- const statusHost = controls.querySelector('[data-graph-status-scd]');
1227
- const tagHost = controls.querySelector('[data-graph-tag-scd]');
1228
- const toggleExternal = controls.querySelector('[data-graph-toggle-external]');
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]');
1232
- const togglePhysics = controls.querySelector('[data-graph-toggle-physics]');
1233
- const fitButton = controls.querySelector('[data-graph-fit]');
1234
- const resetButton = controls.querySelector('[data-graph-reset]');
1235
- const statusText = controls.querySelector('[data-graph-status-text]');
1236
- if (statusText) statusText.textContent = 'Initialising graph…';
1237
- element.classList.add('is-booting');
1238
- let hideExternal = false;
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();
1254
- let livePhysics = false;
1255
- let animationFrame = 0;
1256
- let calmTicks = 0;
1257
- let simNodes = [];
1258
- let simEdges = [];
1259
- const positions = new Map();
1260
- const velocities = new Map();
1261
- let canvas;
1262
- let ctx;
1263
- let cssWidth = 1200;
1264
- let cssHeight = 640;
1265
- const pinnedIds = new Set();
1266
- let view = { x: 0, y: 0, w: 1200, h: 640 };
1267
- let userViewport = false;
1268
- let dragNodeId = null;
1269
- let dragMoved = false;
1270
- let panMode = false;
1271
- let panOrigin = null;
1272
- let suppressClickUntil = 0;
1273
- let hoverNodeId = null;
1274
- let pulsePhase = 0;
1275
-
1276
- function resolveGraphNodeUrl(pageUrl) {
1277
- let cleaned = String(pageUrl || '').replace(/^\\.\\//, '');
1278
- if (cleaned && !cleaned.includes('/') && cleaned.endsWith('.html')) {
1279
- cleaned = \`ideas/\${cleaned}\`;
1280
- }
1281
- return resolveExportUrl(root, cleaned);
1282
- }
1283
-
1284
- function formatNodeMeta(node) {
1285
- const bits = [];
1286
- if (node.status) bits.push(String(node.status));
1287
- if (Array.isArray(node.tags) && node.tags.length) bits.push(node.tags.join(', '));
1288
- return bits.join(' · ');
1289
- }
1290
-
1291
- function formatNodeAttrs(node) {
1292
- const keys = Array.isArray(node.attributeKeys)
1293
- ? node.attributeKeys
1294
- : Object.keys(node.attributes || {});
1295
- return keys.length ? keys.map(key => '@' + key).join(' ') : '';
1296
- }
1297
-
1298
- function nodeRadius(node) {
1299
- return node.isSubject || node.id === graph.centerId ? GRAPH_SUBJECT_RADIUS : GRAPH_NODE_RADIUS;
1300
- }
1301
-
1302
- function seedMissingPositions(nodes, width, height, centerId) {
1303
- const cx = width / 2;
1304
- const cy = height / 2;
1305
- const subject = nodes.find(node => node.isSubject || node.id === centerId);
1306
- const radius = Math.min(width, height) * 0.22;
1307
- nodes.forEach((node, index) => {
1308
- if (positions.has(node.id)) return;
1309
- if (subject && node.id === subject.id) {
1310
- positions.set(node.id, { x: cx, y: cy });
1311
- return;
1312
- }
1313
- const angle = (Math.PI * 2 * index) / Math.max(nodes.length, 1) - Math.PI / 2;
1314
- const ring = radius * (0.7 + (index % 3) * 0.12);
1315
- positions.set(node.id, {
1316
- x: cx + Math.cos(angle) * ring,
1317
- y: cy + Math.sin(angle) * ring
1318
- });
1319
- });
1320
- }
1321
-
1322
- function pruneState(validIds) {
1323
- for (const id of [...positions.keys()]) {
1324
- if (!validIds.has(id)) positions.delete(id);
1325
- }
1326
- for (const id of [...velocities.keys()]) {
1327
- if (!validIds.has(id)) velocities.delete(id);
1328
- }
1329
- }
1330
-
1331
- function physicsStep(nodes, edges) {
1332
- const count = nodes.length;
1333
- if (count === 0) return 0;
1334
- const ids = new Array(count);
1335
- const xs = new Float64Array(count);
1336
- const ys = new Float64Array(count);
1337
- const fxs = new Float64Array(count);
1338
- const fys = new Float64Array(count);
1339
- for (let i = 0; i < count; i += 1) {
1340
- const node = nodes[i];
1341
- const pos = positions.get(node.id) || { x: 0, y: 0 };
1342
- ids[i] = node.id;
1343
- xs[i] = pos.x;
1344
- ys[i] = pos.y;
1345
- }
1346
- const averageSpeed = ReqlanGraphPhysics.stepPhysics({
1347
- ids,
1348
- xs,
1349
- ys,
1350
- fxs,
1351
- fys,
1352
- velocities,
1353
- pinnedIds,
1354
- edges
1355
- }, EXPORT_PHYSICS_SETTINGS);
1356
- for (let i = 0; i < count; i += 1) {
1357
- positions.set(ids[i], { x: xs[i], y: ys[i] });
1358
- }
1359
- return averageSpeed;
1360
- }
1361
-
1362
- function contentBounds() {
1363
- let minX = Infinity;
1364
- let minY = Infinity;
1365
- let maxX = -Infinity;
1366
- let maxY = -Infinity;
1367
- for (const node of simNodes) {
1368
- const pos = positions.get(node.id);
1369
- if (!pos) continue;
1370
- const r = nodeRadius(node);
1371
- minX = Math.min(minX, pos.x - r);
1372
- minY = Math.min(minY, pos.y - r);
1373
- maxX = Math.max(maxX, pos.x + r);
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));
1376
- }
1377
- if (!Number.isFinite(minX)) {
1378
- return { minX: 0, minY: 0, maxX: 1200, maxY: 640 };
1379
- }
1380
- return { minX, minY, maxX, maxY };
1381
- }
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
-
1400
- function fitView(force = false) {
1401
- if (userViewport && !force) return;
1402
- const bounds = contentBounds();
1403
- const pad = 56;
1404
- let x = bounds.minX - pad;
1405
- let y = bounds.minY - pad;
1406
- let w = Math.max(360, bounds.maxX - bounds.minX + pad * 2);
1407
- let h = Math.max(360, bounds.maxY - bounds.minY + pad * 2);
1408
- // Match canvas aspect so uniform scale fills without squashing (SVG viewBox meet).
1409
- const canvasAspect = cssWidth / Math.max(1, cssHeight);
1410
- const viewAspect = w / h;
1411
- if (viewAspect < canvasAspect) {
1412
- const widened = h * canvasAspect;
1413
- x -= (widened - w) / 2;
1414
- w = widened;
1415
- } else if (viewAspect > canvasAspect) {
1416
- const heightened = w / canvasAspect;
1417
- y -= (heightened - h) / 2;
1418
- h = heightened;
1419
- }
1420
- view = { x, y, w, h };
1421
- userViewport = false;
1422
- }
1423
-
1424
- function viewportTransform() {
1425
- const scale = Math.min(cssWidth / Math.max(1, view.w), cssHeight / Math.max(1, view.h));
1426
- return {
1427
- scale,
1428
- offsetX: (cssWidth - view.w * scale) / 2,
1429
- offsetY: (cssHeight - view.h * scale) / 2
1430
- };
1431
- }
1432
-
1433
- function wrapLines(text, maxWidth, font) {
1434
- if (!text) return [];
1435
- ctx.font = font;
1436
- const words = String(text).split(/\\s+/);
1437
- const lines = [];
1438
- let current = '';
1439
- for (const word of words) {
1440
- const next = current ? \`\${current} \${word}\` : word;
1441
- if (ctx.measureText(next).width <= maxWidth) {
1442
- current = next;
1443
- } else {
1444
- if (current) lines.push(current);
1445
- if (ctx.measureText(word).width <= maxWidth) {
1446
- current = word;
1447
- } else {
1448
- let chunk = '';
1449
- for (const ch of word) {
1450
- const trial = chunk + ch;
1451
- if (ctx.measureText(trial).width <= maxWidth) chunk = trial;
1452
- else {
1453
- if (chunk) lines.push(chunk);
1454
- chunk = ch;
1455
- }
1456
- }
1457
- current = chunk;
1458
- }
1459
- }
1460
- }
1461
- if (current) lines.push(current);
1462
- return lines;
1463
- }
1464
-
1465
- function nodeFill(node) {
1466
- if (node.isSubject || node.id === graph.centerId) return '#b85c38';
1467
- if (node.isExternal) return '#8a4530';
1468
- if (node.kind === 'ideaset') return '#e0a24a';
1469
- return '#07a0e5';
1470
- }
1471
-
1472
- function nodeStroke(node, hover) {
1473
- if (hover) return '#0bbefb';
1474
- if (node.isSubject || node.id === graph.centerId) return '#0bbefb';
1475
- if (node.isExternal) return 'rgba(184,92,56,0.55)';
1476
- if (node.kind === 'ideaset') return '#d18616';
1477
- return 'rgba(235,228,222,0.35)';
1478
- }
1479
-
1480
- function paint() {
1481
- if (!canvas || !ctx) return;
1482
- if (!userViewport) fitView(false);
1483
- const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
1484
- const nextW = Math.max(1, Math.floor(cssWidth * dpr));
1485
- const nextH = Math.max(1, Math.floor(cssHeight * dpr));
1486
- if (canvas.width !== nextW || canvas.height !== nextH) {
1487
- canvas.width = nextW;
1488
- canvas.height = nextH;
1489
- }
1490
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1491
- ctx.clearRect(0, 0, cssWidth, cssHeight);
1492
- ctx.fillStyle = '#14100e';
1493
- ctx.fillRect(0, 0, cssWidth, cssHeight);
1494
-
1495
- const { scale, offsetX, offsetY } = viewportTransform();
1496
- const ambientLabelOpacity = labelOpacityAtScale(scale);
1497
- ctx.save();
1498
- ctx.translate(offsetX, offsetY);
1499
- ctx.scale(scale, scale);
1500
- ctx.translate(-view.x, -view.y);
1501
-
1502
- ctx.lineWidth = 1.1 / scale;
1503
- ctx.strokeStyle = 'rgba(61,47,40,0.85)';
1504
- ctx.globalAlpha = 0.85;
1505
- for (const edge of simEdges) {
1506
- const source = positions.get(edge.sourceId);
1507
- const target = positions.get(edge.targetId);
1508
- if (!source || !target) continue;
1509
- const isWildcard = edge.kind === 'wildcard_reference';
1510
- ctx.setLineDash(isWildcard ? [2 / scale, 3.5 / scale] : []);
1511
- ctx.beginPath();
1512
- ctx.moveTo(source.x, source.y);
1513
- ctx.lineTo(target.x, target.y);
1514
- ctx.stroke();
1515
- }
1516
- ctx.setLineDash([]);
1517
- ctx.globalAlpha = 1;
1518
-
1519
- pulsePhase = (pulsePhase + 0.035) % (Math.PI * 2);
1520
- for (const node of simNodes) {
1521
- const pos = positions.get(node.id);
1522
- if (!pos) continue;
1523
- const r = nodeRadius(node);
1524
- const hover = node.id === hoverNodeId || node.id === dragNodeId;
1525
- const isSubject = node.isSubject || node.id === graph.centerId;
1526
- if (isSubject) {
1527
- const glow = 4 + Math.sin(pulsePhase) * 3;
1528
- ctx.beginPath();
1529
- ctx.arc(pos.x, pos.y, r + glow, 0, Math.PI * 2);
1530
- ctx.fillStyle = 'rgba(184,92,56,0.22)';
1531
- ctx.fill();
1532
- }
1533
- ctx.beginPath();
1534
- ctx.arc(pos.x, pos.y, r, 0, Math.PI * 2);
1535
- ctx.fillStyle = nodeFill(node);
1536
- ctx.fill();
1537
- ctx.lineWidth = (isSubject ? 2.2 : hover ? 2 : 1.15) / scale;
1538
- ctx.strokeStyle = nodeStroke(node, hover);
1539
- ctx.stroke();
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;
1544
- const meta = formatNodeMeta(node);
1545
- const attrs = formatNodeAttrs(node);
1546
- const nameFont = isSubject ? '600 11px Inter, system-ui, sans-serif' : '600 10px Inter, system-ui, sans-serif';
1547
- const metaFont = '9.5px Inter, system-ui, sans-serif';
1548
- const nameLines = wrapLines(node.name, GRAPH_LABEL_MAX_WIDTH, nameFont);
1549
- const metaLines = wrapLines(meta, GRAPH_LABEL_MAX_WIDTH, metaFont);
1550
- const attrLines = wrapLines(attrs, GRAPH_LABEL_MAX_WIDTH, metaFont);
1551
- let ty = pos.y + r + 12;
1552
- ctx.save();
1553
- ctx.globalAlpha = labelOpacity;
1554
- ctx.textAlign = 'center';
1555
- ctx.textBaseline = 'top';
1556
- ctx.fillStyle = isSubject ? '#0bbefb' : '#ebe4de';
1557
- ctx.font = nameFont;
1558
- for (const line of nameLines) {
1559
- ctx.lineWidth = 3 / scale;
1560
- ctx.strokeStyle = '#14100e';
1561
- ctx.strokeText(line, pos.x, ty);
1562
- ctx.fillText(line, pos.x, ty);
1563
- ty += 12;
1564
- }
1565
- ctx.fillStyle = '#a89488';
1566
- ctx.font = metaFont;
1567
- for (const line of metaLines) {
1568
- ctx.fillText(line, pos.x, ty);
1569
- ty += 11;
1570
- }
1571
- ctx.fillStyle = '#b85c38';
1572
- for (const line of attrLines) {
1573
- ctx.fillText(line, pos.x, ty);
1574
- ty += 11;
1575
- }
1576
- ctx.restore();
1577
- }
1578
- ctx.restore();
1579
- }
1580
-
1581
- function stopLoop() {
1582
- if (animationFrame) {
1583
- cancelAnimationFrame(animationFrame);
1584
- animationFrame = 0;
1585
- }
1586
- }
1587
-
1588
- function scheduleTick() {
1589
- if (animationFrame) return;
1590
- animationFrame = requestAnimationFrame(() => {
1591
- animationFrame = 0;
1592
- if (!livePhysics) {
1593
- paint();
1594
- return;
1595
- }
1596
- const averageSpeed = physicsStep(simNodes, simEdges);
1597
- paint();
1598
- if (averageSpeed < EXPORT_PHYSICS_SETTINGS.restSpeed) {
1599
- calmTicks += 1;
1600
- if (calmTicks >= EXPORT_PHYSICS_SETTINGS.restTicks) {
1601
- return;
1602
- }
1603
- } else {
1604
- calmTicks = 0;
1605
- }
1606
- scheduleTick();
1607
- });
1608
- }
1609
-
1610
- function wake() {
1611
- calmTicks = 0;
1612
- if (livePhysics) scheduleTick();
1613
- }
1614
-
1615
- function batchSettle(nodes, edges) {
1616
- const iterations = Math.min(96, 36 + Math.floor(nodes.length * 0.45));
1617
- for (let i = 0; i < iterations; i += 1) {
1618
- physicsStep(nodes, edges);
1619
- }
1620
- for (const velocity of velocities.values()) {
1621
- velocity.vx = 0;
1622
- velocity.vy = 0;
1623
- }
1624
- calmTicks = EXPORT_PHYSICS_SETTINGS.restTicks;
1625
- }
1626
-
1627
- function hitTest(point) {
1628
- let best = null;
1629
- let bestDist = Infinity;
1630
- for (const node of simNodes) {
1631
- const pos = positions.get(node.id);
1632
- if (!pos) continue;
1633
- const dx = point.x - pos.x;
1634
- const dy = point.y - pos.y;
1635
- const dist = Math.sqrt(dx * dx + dy * dy);
1636
- const threshold = nodeRadius(node) + GRAPH_HIT_PAD;
1637
- if (dist <= threshold && dist < bestDist) {
1638
- best = node;
1639
- bestDist = dist;
1640
- }
1641
- }
1642
- return best;
1643
- }
1644
-
1645
- function resizeCanvas() {
1646
- const rect = element.getBoundingClientRect();
1647
- cssWidth = Math.max(320, Math.floor(rect.width || 1200));
1648
- cssHeight = Math.max(620, Math.min(1400, 220 + simNodes.length * 18));
1649
- if (canvas) {
1650
- canvas.style.width = '100%';
1651
- canvas.style.height = \`\${cssHeight}px\`;
1652
- }
1653
- }
1654
-
1655
- function mountGraph(filteredNodes, filteredEdges) {
1656
- stopLoop();
1657
- simNodes = filteredNodes;
1658
- simEdges = filteredEdges;
1659
- const validIds = new Set(filteredNodes.map(node => node.id));
1660
- pruneState(validIds);
1661
- const width = 1200;
1662
- const height = Math.max(640, Math.min(1400, 220 + filteredNodes.length * 18));
1663
- seedMissingPositions(filteredNodes, width, height, graph.centerId);
1664
- canvas = document.createElement('canvas');
1665
- canvas.setAttribute('role', 'img');
1666
- canvas.setAttribute('aria-label', \`Requirement graph with \${filteredNodes.length} nodes\`);
1667
- ctx = canvas.getContext('2d');
1668
- element.innerHTML = '';
1669
- element.classList.remove('is-booting');
1670
- element.appendChild(canvas);
1671
- resizeCanvas();
1672
- wireViewport(canvas);
1673
- if (statusText) {
1674
- statusText.textContent = \`\${filteredNodes.length} nodes, \${filteredEdges.length} edges\`;
1675
- }
1676
- userViewport = false;
1677
- if (!livePhysics) {
1678
- batchSettle(filteredNodes, filteredEdges);
1679
- paint();
1680
- } else {
1681
- paint();
1682
- wake();
1683
- }
1684
- }
1685
-
1686
- function clientToGraph(event) {
1687
- const rect = canvas.getBoundingClientRect();
1688
- const { scale, offsetX, offsetY } = viewportTransform();
1689
- const cssX = ((event.clientX - rect.left) / rect.width) * cssWidth;
1690
- const cssY = ((event.clientY - rect.top) / rect.height) * cssHeight;
1691
- return {
1692
- x: view.x + (cssX - offsetX) / scale,
1693
- y: view.y + (cssY - offsetY) / scale
1694
- };
1695
- }
1696
-
1697
- function wireViewport(target) {
1698
- target.addEventListener('wheel', (event) => {
1699
- event.preventDefault();
1700
- const factor = event.deltaY < 0 ? 0.9 : 1.1;
1701
- const before = clientToGraph(event);
1702
- view.w = Math.min(8000, Math.max(180, view.w * factor));
1703
- view.h = Math.min(8000, Math.max(180, view.h * factor));
1704
- const after = clientToGraph(event);
1705
- view.x += before.x - after.x;
1706
- view.y += before.y - after.y;
1707
- userViewport = true;
1708
- paint();
1709
- }, { passive: false });
1710
-
1711
- target.addEventListener('pointerdown', (event) => {
1712
- const point = clientToGraph(event);
1713
- const hit = hitTest(point);
1714
- if (hit) {
1715
- dragNodeId = hit.id;
1716
- dragMoved = false;
1717
- pinnedIds.add(hit.id);
1718
- velocities.set(hit.id, { vx: 0, vy: 0 });
1719
- target.classList.add('is-dragging-node');
1720
- target.setPointerCapture(event.pointerId);
1721
- event.preventDefault();
1722
- paint();
1723
- return;
1724
- }
1725
- panMode = true;
1726
- panOrigin = { x: event.clientX, y: event.clientY, viewX: view.x, viewY: view.y };
1727
- target.classList.add('is-panning');
1728
- target.setPointerCapture(event.pointerId);
1729
- event.preventDefault();
1730
- });
1731
-
1732
- target.addEventListener('pointermove', (event) => {
1733
- if (dragNodeId) {
1734
- const point = clientToGraph(event);
1735
- positions.set(dragNodeId, { x: point.x, y: point.y });
1736
- dragMoved = true;
1737
- wake();
1738
- paint();
1739
- return;
1740
- }
1741
- if (panMode && panOrigin) {
1742
- const rect = target.getBoundingClientRect();
1743
- const { scale } = viewportTransform();
1744
- const dx = ((event.clientX - panOrigin.x) / rect.width) * cssWidth / scale;
1745
- const dy = ((event.clientY - panOrigin.y) / rect.height) * cssHeight / scale;
1746
- view.x = panOrigin.viewX - dx;
1747
- view.y = panOrigin.viewY - dy;
1748
- userViewport = true;
1749
- paint();
1750
- return;
1751
- }
1752
- const hit = hitTest(clientToGraph(event));
1753
- const nextHover = hit?.id || null;
1754
- if (nextHover !== hoverNodeId) {
1755
- hoverNodeId = nextHover;
1756
- paint();
1757
- }
1758
- });
1759
-
1760
- target.addEventListener('pointerup', (event) => {
1761
- target.classList.remove('is-dragging-node');
1762
- if (dragNodeId) {
1763
- const node = simNodes.find(item => item.id === dragNodeId);
1764
- const moved = dragMoved;
1765
- const nodeId = dragNodeId;
1766
- dragNodeId = null;
1767
- pinnedIds.delete(nodeId);
1768
- wake();
1769
- paint();
1770
- if (!moved && node?.pageUrl && Date.now() > suppressClickUntil) {
1771
- window.location.href = resolveGraphNodeUrl(node.pageUrl);
1772
- } else if (moved) {
1773
- suppressClickUntil = Date.now() + 250;
1774
- }
1775
- return;
1776
- }
1777
- if (panMode) {
1778
- panMode = false;
1779
- panOrigin = null;
1780
- target.classList.remove('is-panning');
1781
- }
1782
- });
1783
-
1784
- target.addEventListener('pointercancel', () => {
1785
- target.classList.remove('is-dragging-node');
1786
- if (dragNodeId) {
1787
- pinnedIds.delete(dragNodeId);
1788
- dragNodeId = null;
1789
- wake();
1790
- }
1791
- panMode = false;
1792
- panOrigin = null;
1793
- target.classList.remove('is-panning');
1794
- paint();
1795
- });
1796
-
1797
- window.addEventListener('resize', () => {
1798
- resizeCanvas();
1799
- paint();
1800
- });
1801
- }
1802
-
1803
- function matches(node) {
1804
- const FILTER_NOT_PRESENT = '__not_present__';
1805
- const query = lower(searchInput?.value?.trim());
1806
- const pathQuery = lower(pathInput?.value?.trim());
1807
- if (hideExternal && node.isExternal) return false;
1808
- if (hideIdeasets && node.kind === 'ideaset') return false;
1809
- const haystack = [node.name, node.kind, node.fileUri, node.status || '', ...(node.tags || [])].map(lower).join(' ');
1810
- if (query && !haystack.includes(query)) return false;
1811
- if (pathQuery && !lower(node.fileUri).includes(pathQuery)) 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
- }
1822
- return true;
1823
- }
1824
-
1825
- function refresh() {
1826
- const visibleNodes = (graph.nodes || []).filter(matches);
1827
- const visibleIds = new Set(visibleNodes.map(node => node.id));
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
- });
1833
- mountGraph(visibleNodes, visibleEdges);
1834
- }
1835
-
1836
- for (const control of [searchInput, pathInput]) {
1837
- control?.addEventListener('input', refresh);
1838
- control?.addEventListener('change', refresh);
1839
- }
1840
- toggleExternal?.addEventListener('click', () => {
1841
- hideExternal = !hideExternal;
1842
- toggleExternal.classList.toggle('is-active', hideExternal);
1843
- toggleExternal.textContent = hideExternal ? 'Show external' : 'Hide external';
1844
- refresh();
1845
- });
1846
- toggleIdeasets?.addEventListener('click', () => {
1847
- hideIdeasets = !hideIdeasets;
1848
- toggleIdeasets.classList.toggle('is-active', hideIdeasets);
1849
- toggleIdeasets.textContent = hideIdeasets ? 'Show ideasets' : 'Hide ideasets';
1850
- refresh();
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
- });
1864
- togglePhysics?.addEventListener('click', () => {
1865
- livePhysics = !livePhysics;
1866
- togglePhysics.classList.toggle('is-active', livePhysics);
1867
- togglePhysics.setAttribute('aria-pressed', livePhysics ? 'true' : 'false');
1868
- if (livePhysics) {
1869
- wake();
1870
- } else {
1871
- stopLoop();
1872
- paint();
1873
- }
1874
- });
1875
- fitButton?.addEventListener('click', () => {
1876
- fitView(true);
1877
- paint();
1878
- });
1879
- resetButton?.addEventListener('click', () => {
1880
- if (searchInput) searchInput.value = '';
1881
- if (pathInput) pathInput.value = '';
1882
- statusSelected = [];
1883
- tagSelected = [];
1884
- statusFilter.clear();
1885
- tagFilter.clear();
1886
- hideExternal = false;
1887
- hideIdeasets = false;
1888
- includeWildcardRefs = true;
1889
- labelMode = 'auto';
1890
- toggleExternal?.classList.remove('is-active');
1891
- toggleIdeasets?.classList.remove('is-active');
1892
- toggleWildcard?.classList.add('is-active');
1893
- toggleWildcard?.setAttribute('aria-pressed', 'true');
1894
- syncLabelsButton();
1895
- if (toggleExternal) toggleExternal.textContent = 'Hide external';
1896
- if (toggleIdeasets) toggleIdeasets.textContent = 'Hide ideasets';
1897
- positions.clear();
1898
- velocities.clear();
1899
- pinnedIds.clear();
1900
- userViewport = false;
1901
- refresh();
1902
- });
1903
- syncLabelsButton();
1904
- refresh();
1905
- }
1906
- }
1907
-
1908
- function boot() {
1909
- const root = document.body;
1910
- // Interactive Status/Tags triggers before heavy table/graph init.
1911
- enhanceScdPlaceholders(root);
1912
- wireTables(root);
1913
- wireGraph(root);
1914
- void wireGlobalSearch(root);
1915
- }
1916
-
1917
- if (document.readyState === 'loading') {
1918
- document.addEventListener('DOMContentLoaded', boot);
1919
- } else {
1920
- boot();
1921
- }
1922
- `;
1923
- export function buildSearchIndexScript(documents) {
1924
- return `globalThis.__REQLAN_SEARCH_INDEX__ = ${JSON.stringify(documents).replace(/</g, '\\u003c')};\n`;
1925
- }
1926
- //# sourceMappingURL=html-export-assets.js.map