@reqlan/analytical 0.9.1 → 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.
- package/README.md +13 -2
- package/README.template.md +1 -1
- package/out/core/barrel-page.d.ts +2 -1
- package/out/core/barrel-page.js +18 -83
- package/out/core/barrel-page.js.map +1 -1
- package/out/core/base-discovery.d.ts +6 -0
- package/out/core/base-discovery.js +47 -2
- package/out/core/base-discovery.js.map +1 -1
- package/out/core/create-base.js +10 -14
- package/out/core/create-base.js.map +1 -1
- package/out/core/file-reference-resolve.js +3 -1
- package/out/core/file-reference-resolve.js.map +1 -1
- package/out/core/index-error.d.ts +3 -3
- package/out/core/index-error.js.map +1 -1
- package/out/core/index-state-types.d.ts +47 -0
- package/out/core/index-state-types.js +11 -0
- package/out/core/index-state-types.js.map +1 -0
- package/out/core/path-relative.d.ts +15 -0
- package/out/core/path-relative.js +84 -0
- package/out/core/path-relative.js.map +1 -0
- package/out/core/rqignore.js +62 -62
- package/out/core/rqignore.js.map +1 -1
- package/out/core/types.d.ts +4 -3
- package/out/core/types.js +4 -1
- package/out/core/types.js.map +1 -1
- package/out/core/workspace-paths.d.ts +1 -2
- package/out/core/workspace-paths.js +6 -46
- package/out/core/workspace-paths.js.map +1 -1
- package/out/export/build-export-snapshot.js +1 -1
- package/out/export/build-export-snapshot.js.map +1 -1
- package/out/export/export-utils.d.ts +5 -0
- package/out/export/export-utils.js +38 -0
- package/out/export/export-utils.js.map +1 -0
- package/out/export/write-csv-export.js +1 -1
- package/out/export/write-csv-export.js.map +1 -1
- package/out/export/write-markdown-export.js +1 -1
- package/out/export/write-markdown-export.js.map +1 -1
- package/out/index-store/base-registry.d.ts +2 -3
- package/out/index-store/base-registry.js +8 -4
- package/out/index-store/base-registry.js.map +1 -1
- package/out/index-store/fuzzy-search-hit.d.ts +10 -0
- package/out/index-store/fuzzy-search-hit.js +2 -0
- package/out/index-store/fuzzy-search-hit.js.map +1 -0
- package/out/index-store/index-diagnostics-store.d.ts +4 -0
- package/out/index-store/index-diagnostics-store.js +44 -88
- package/out/index-store/index-diagnostics-store.js.map +1 -1
- package/out/index-store/index-file-error.d.ts +1 -1
- package/out/index-store/index-parse-issues.d.ts +4 -3
- package/out/index-store/index-parse-issues.js +0 -38
- package/out/index-store/index-parse-issues.js.map +1 -1
- package/out/index-store/index-status.d.ts +1 -1
- package/out/index-store/sqlite-store.d.ts +6 -2
- package/out/index-store/sqlite-store.js +70 -613
- package/out/index-store/sqlite-store.js.map +1 -1
- package/out/index-store/webview-graph-queries.d.ts +5 -0
- package/out/index-store/webview-graph-queries.js.map +1 -1
- package/out/index-store/workspace-index-file.d.ts +3 -14
- package/out/index-store/workspace-index-file.js +1 -114
- package/out/index-store/workspace-index-file.js.map +1 -1
- package/out/index-store/workspace-index-sync.d.ts +4 -17
- package/out/index-store/workspace-index-sync.js +29 -11
- package/out/index-store/workspace-index-sync.js.map +1 -1
- package/out/index-store/workspace-index.d.ts +36 -9
- package/out/index-store/workspace-index.js +223 -120
- package/out/index-store/workspace-index.js.map +1 -1
- package/out/index.d.ts +16 -43
- package/out/index.js +9 -25
- package/out/index.js.map +1 -1
- package/out/native/generated.d.ts +102 -0
- package/out/native/generated.js +4 -0
- package/out/native/generated.js.map +1 -0
- package/out/native/index.d.ts +17 -0
- package/out/native/index.js +12 -0
- package/out/native/index.js.map +1 -0
- package/out/native/load-native.d.ts +50 -0
- package/out/native/load-native.js +152 -0
- package/out/native/load-native.js.map +1 -0
- package/out/{analysis-api.d.ts → native/native-analysis-api.d.ts} +19 -19
- package/out/native/native-analysis-api.js +212 -0
- package/out/native/native-analysis-api.js.map +1 -0
- package/out/native/native-index-store.d.ts +5 -0
- package/out/native/native-index-store.js +6 -0
- package/out/native/native-index-store.js.map +1 -0
- package/out/native/native-sql-db.d.ts +135 -0
- package/out/native/native-sql-db.js +217 -0
- package/out/native/native-sql-db.js.map +1 -0
- package/out/native/native-workspace-index.d.ts +164 -0
- package/out/native/native-workspace-index.js +283 -0
- package/out/native/native-workspace-index.js.map +1 -0
- package/out/native/open-analysis-api.d.ts +16 -0
- package/out/native/open-analysis-api.js +21 -0
- package/out/native/open-analysis-api.js.map +1 -0
- package/out/native/parse-source.d.ts +24 -0
- package/out/native/parse-source.js +26 -0
- package/out/native/parse-source.js.map +1 -0
- package/package.json +18 -30
- package/out/analysis/analyser-registry.d.ts +0 -21
- package/out/analysis/analyser-registry.js +0 -33
- package/out/analysis/analyser-registry.js.map +0 -1
- package/out/analysis/completion-tracking-analyser.d.ts +0 -3
- package/out/analysis/completion-tracking-analyser.js +0 -37
- package/out/analysis/completion-tracking-analyser.js.map +0 -1
- package/out/analysis/contextual-search.d.ts +0 -26
- package/out/analysis/contextual-search.js +0 -94
- package/out/analysis/contextual-search.js.map +0 -1
- package/out/analysis/deprecation-impact-analyser.d.ts +0 -3
- package/out/analysis/deprecation-impact-analyser.js +0 -21
- package/out/analysis/deprecation-impact-analyser.js.map +0 -1
- package/out/analysis/file-related-analyser.d.ts +0 -5
- package/out/analysis/file-related-analyser.js +0 -41
- package/out/analysis/file-related-analyser.js.map +0 -1
- package/out/analysis/fuzzy-search-analyser.d.ts +0 -17
- package/out/analysis/fuzzy-search-analyser.js +0 -25
- package/out/analysis/fuzzy-search-analyser.js.map +0 -1
- package/out/analysis/fuzzy-search-worker-client.d.ts +0 -39
- package/out/analysis/fuzzy-search-worker-client.js +0 -178
- package/out/analysis/fuzzy-search-worker-client.js.map +0 -1
- package/out/analysis/fuzzy-search-worker.d.ts +0 -32
- package/out/analysis/fuzzy-search-worker.js +0 -81
- package/out/analysis/fuzzy-search-worker.js.map +0 -1
- package/out/analysis/fuzzy-search.d.ts +0 -62
- package/out/analysis/fuzzy-search.js +0 -471
- package/out/analysis/fuzzy-search.js.map +0 -1
- package/out/analysis/git-dates-analyser.d.ts +0 -12
- package/out/analysis/git-dates-analyser.js +0 -96
- package/out/analysis/git-dates-analyser.js.map +0 -1
- package/out/analysis/list-ideas-analyser.d.ts +0 -3
- package/out/analysis/list-ideas-analyser.js +0 -7
- package/out/analysis/list-ideas-analyser.js.map +0 -1
- package/out/analysis/local-graph-analyser.d.ts +0 -6
- package/out/analysis/local-graph-analyser.js +0 -42
- package/out/analysis/local-graph-analyser.js.map +0 -1
- package/out/analysis/semantic-search-analyser.d.ts +0 -8
- package/out/analysis/semantic-search-analyser.js +0 -51
- package/out/analysis/semantic-search-analyser.js.map +0 -1
- package/out/analysis-api.js +0 -199
- package/out/analysis-api.js.map +0 -1
- package/out/core/analytical-store.d.ts +0 -85
- package/out/core/analytical-store.js +0 -143
- package/out/core/analytical-store.js.map +0 -1
- package/out/core/context-model.d.ts +0 -204
- package/out/core/context-model.js +0 -36
- package/out/core/context-model.js.map +0 -1
- package/out/core/context-signals.d.ts +0 -154
- package/out/core/context-signals.js +0 -406
- package/out/core/context-signals.js.map +0 -1
- package/out/create-runtime.d.ts +0 -47
- package/out/create-runtime.js +0 -79
- package/out/create-runtime.js.map +0 -1
- package/out/export/export-html.d.ts +0 -4
- package/out/export/export-html.js +0 -12
- package/out/export/export-html.js.map +0 -1
- package/out/export/html-export-assets.d.ts +0 -3
- package/out/export/html-export-assets.js +0 -2294
- package/out/export/html-export-assets.js.map +0 -1
- package/out/export/html-export-template.d.ts +0 -18
- package/out/export/html-export-template.js +0 -1154
- package/out/export/html-export-template.js.map +0 -1
- package/out/export/html-export-utils.d.ts +0 -86
- package/out/export/html-export-utils.js +0 -485
- package/out/export/html-export-utils.js.map +0 -1
- package/out/export/write-html-export.d.ts +0 -2
- package/out/export/write-html-export.js +0 -181
- package/out/export/write-html-export.js.map +0 -1
- package/out/graph/physics-core-source.d.ts +0 -5
- package/out/graph/physics-core-source.js +0 -8
- package/out/graph/physics-core-source.js.map +0 -1
- package/out/graph/physics-core.d.ts +0 -141
- package/out/graph/physics-core.js +0 -237
- package/out/graph/physics-core.js.map +0 -1
- package/out/index-store/file-treatment.d.ts +0 -40
- package/out/index-store/file-treatment.js +0 -144
- package/out/index-store/file-treatment.js.map +0 -1
- package/out/index-store/idea-extractor.d.ts +0 -11
- package/out/index-store/idea-extractor.js +0 -351
- package/out/index-store/idea-extractor.js.map +0 -1
- package/out/index-store/overview-coverage.d.ts +0 -23
- package/out/index-store/overview-coverage.js +0 -254
- package/out/index-store/overview-coverage.js.map +0 -1
|
@@ -1,2294 +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-file-treatment {
|
|
313
|
-
display: inline-flex;
|
|
314
|
-
align-items: center;
|
|
315
|
-
}
|
|
316
|
-
.graph-file-treatment .graph-select,
|
|
317
|
-
select.graph-action {
|
|
318
|
-
border-radius: 8px;
|
|
319
|
-
padding-right: 1.6rem;
|
|
320
|
-
cursor: pointer;
|
|
321
|
-
}
|
|
322
|
-
.visually-hidden {
|
|
323
|
-
position: absolute;
|
|
324
|
-
width: 1px;
|
|
325
|
-
height: 1px;
|
|
326
|
-
padding: 0;
|
|
327
|
-
margin: -1px;
|
|
328
|
-
overflow: hidden;
|
|
329
|
-
clip: rect(0, 0, 0, 0);
|
|
330
|
-
white-space: nowrap;
|
|
331
|
-
border: 0;
|
|
332
|
-
}
|
|
333
|
-
.graph-status {
|
|
334
|
-
color: var(--muted);
|
|
335
|
-
font-size: 0.92rem;
|
|
336
|
-
margin-left: auto;
|
|
337
|
-
}
|
|
338
|
-
.searchbar, select, input[type="search"] {
|
|
339
|
-
width: min(100%, 440px); background: var(--bg); color: var(--fg);
|
|
340
|
-
border: 1px solid var(--line); border-radius: 8px; padding: 0.75rem 0.9rem;
|
|
341
|
-
}
|
|
342
|
-
.searchbar:focus, select:focus, input[type="search"]:focus {
|
|
343
|
-
outline: none;
|
|
344
|
-
border-color: var(--accent);
|
|
345
|
-
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 25%, transparent);
|
|
346
|
-
}
|
|
347
|
-
table { width: 100%; border-collapse: collapse; }
|
|
348
|
-
th, td {
|
|
349
|
-
padding: 0.75rem 0.65rem; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top;
|
|
350
|
-
}
|
|
351
|
-
th { color: var(--muted); font-size: 0.94rem; }
|
|
352
|
-
tbody tr:hover { background: var(--bg-raised); }
|
|
353
|
-
.table-shell { overflow-x: auto; }
|
|
354
|
-
.scroll-window {
|
|
355
|
-
--scroll-window-max: min(28rem, 50vh);
|
|
356
|
-
}
|
|
357
|
-
body[data-runtime-mode="interactive"] .scroll-window {
|
|
358
|
-
max-height: var(--scroll-window-max);
|
|
359
|
-
overflow: auto;
|
|
360
|
-
border: 1px solid var(--line);
|
|
361
|
-
border-radius: 8px;
|
|
362
|
-
background: var(--bg);
|
|
363
|
-
}
|
|
364
|
-
body[data-runtime-mode="interactive"] .scroll-window > table {
|
|
365
|
-
margin: 0;
|
|
366
|
-
}
|
|
367
|
-
body[data-runtime-mode="interactive"] .scroll-window .entity-list,
|
|
368
|
-
body[data-runtime-mode="interactive"] .scroll-window .rollup-list {
|
|
369
|
-
padding: 0.75rem;
|
|
370
|
-
}
|
|
371
|
-
.sortable-th { padding: 0.45rem 0.4rem; }
|
|
372
|
-
.sort-button {
|
|
373
|
-
display: inline-flex; align-items: center; gap: 0.35rem;
|
|
374
|
-
width: 100%; margin: 0; padding: 0.3rem 0.25rem;
|
|
375
|
-
border: 0; background: transparent; color: inherit;
|
|
376
|
-
font: inherit; text-align: left; cursor: pointer;
|
|
377
|
-
}
|
|
378
|
-
.sort-button:hover { color: var(--fg); }
|
|
379
|
-
.sort-button.sort-active { color: var(--accent-strong); }
|
|
380
|
-
.sort-indicator { color: var(--faint); font-size: 0.8em; min-width: 1em; }
|
|
381
|
-
.sort-button.sort-active .sort-indicator { color: var(--accent-strong); }
|
|
382
|
-
.table-filter-toggle {
|
|
383
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
384
|
-
width: 2.25rem; height: 2.25rem; padding: 0;
|
|
385
|
-
border: 1px solid var(--line); border-radius: 8px;
|
|
386
|
-
background: var(--bg-raised); color: var(--muted); cursor: pointer;
|
|
387
|
-
}
|
|
388
|
-
.table-filter-toggle:hover { color: var(--fg); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
|
|
389
|
-
.table-filter-toggle.is-active {
|
|
390
|
-
color: var(--fg);
|
|
391
|
-
border-color: var(--accent);
|
|
392
|
-
background: color-mix(in srgb, var(--accent) 18%, var(--bg-raised));
|
|
393
|
-
}
|
|
394
|
-
.table-filter-toggle.has-filters:not(.is-active) {
|
|
395
|
-
color: var(--accent-strong);
|
|
396
|
-
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
|
397
|
-
}
|
|
398
|
-
.table-filter-toggle svg { width: 1rem; height: 1rem; display: block; }
|
|
399
|
-
.table-filter-actions {
|
|
400
|
-
display: flex; justify-content: flex-end; gap: 0.55rem;
|
|
401
|
-
margin-bottom: 0.65rem;
|
|
402
|
-
}
|
|
403
|
-
.column-filter-row { display: none; }
|
|
404
|
-
.column-filter-row.is-open { display: table-row; }
|
|
405
|
-
.column-filter-row th {
|
|
406
|
-
padding: 0.35rem 0.4rem 0.75rem; border-bottom: 1px solid var(--line); font-weight: 400;
|
|
407
|
-
}
|
|
408
|
-
.column-filter {
|
|
409
|
-
width: 100%; min-width: 4.5rem; background: var(--bg); color: var(--fg);
|
|
410
|
-
border: 1px solid var(--line); border-radius: 6px; padding: 0.4rem 0.55rem; font-size: 0.85rem;
|
|
411
|
-
}
|
|
412
|
-
.column-filter:focus {
|
|
413
|
-
outline: none;
|
|
414
|
-
border-color: var(--accent);
|
|
415
|
-
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 25%, transparent);
|
|
416
|
-
}
|
|
417
|
-
body[data-runtime-mode="interactive"] .scroll-window thead tr:first-child th {
|
|
418
|
-
position: sticky;
|
|
419
|
-
top: 0;
|
|
420
|
-
z-index: 2;
|
|
421
|
-
background: var(--bg-raised);
|
|
422
|
-
box-shadow: 0 1px 0 var(--line);
|
|
423
|
-
}
|
|
424
|
-
body[data-runtime-mode="interactive"] .scroll-window thead .column-filter-row.is-open th {
|
|
425
|
-
position: sticky;
|
|
426
|
-
top: 2.65rem;
|
|
427
|
-
z-index: 1;
|
|
428
|
-
background: var(--bg-raised);
|
|
429
|
-
box-shadow: 0 1px 0 var(--line);
|
|
430
|
-
}
|
|
431
|
-
.entity-list {
|
|
432
|
-
display: grid;
|
|
433
|
-
grid-template-columns: minmax(0, 1fr);
|
|
434
|
-
gap: 1.5rem;
|
|
435
|
-
}
|
|
436
|
-
.entity-card {
|
|
437
|
-
border: 1px solid var(--line); border-radius: 10px; padding: 0.9rem 1rem; background: var(--bg);
|
|
438
|
-
color: inherit;
|
|
439
|
-
min-width: 0;
|
|
440
|
-
max-width: 100%;
|
|
441
|
-
overflow-wrap: anywhere;
|
|
442
|
-
word-break: break-word;
|
|
443
|
-
}
|
|
444
|
-
.entity-card:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
|
|
445
|
-
.entity-card h3, .entity-card p { margin: 0; }
|
|
446
|
-
.entity-card p + p, .entity-card strong + p, .entity-card h3 + p { margin-top: 0.35rem; }
|
|
447
|
-
.print-card {
|
|
448
|
-
min-width: 0;
|
|
449
|
-
max-width: 100%;
|
|
450
|
-
overflow-wrap: anywhere;
|
|
451
|
-
word-break: break-word;
|
|
452
|
-
scroll-margin-top: 5.5rem;
|
|
453
|
-
}
|
|
454
|
-
.print-card > h3 { overflow-wrap: anywhere; }
|
|
455
|
-
.print-attrs {
|
|
456
|
-
display: grid;
|
|
457
|
-
gap: 0.45rem;
|
|
458
|
-
margin: 0.75rem 0 0;
|
|
459
|
-
}
|
|
460
|
-
.print-attrs div {
|
|
461
|
-
display: grid;
|
|
462
|
-
grid-template-columns: minmax(0, 8rem) minmax(0, 1fr);
|
|
463
|
-
gap: 0.65rem;
|
|
464
|
-
border-bottom: 1px solid var(--line);
|
|
465
|
-
padding-bottom: 0.35rem;
|
|
466
|
-
}
|
|
467
|
-
.print-attrs dt {
|
|
468
|
-
margin: 0;
|
|
469
|
-
color: var(--muted);
|
|
470
|
-
font-weight: 600;
|
|
471
|
-
overflow-wrap: anywhere;
|
|
472
|
-
}
|
|
473
|
-
.print-attrs dd {
|
|
474
|
-
margin: 0;
|
|
475
|
-
overflow-wrap: anywhere;
|
|
476
|
-
word-break: break-word;
|
|
477
|
-
}
|
|
478
|
-
.print-button {
|
|
479
|
-
appearance: none;
|
|
480
|
-
border: 1px solid var(--line);
|
|
481
|
-
border-radius: 8px;
|
|
482
|
-
background: color-mix(in srgb, var(--accent) 18%, var(--bg-raised));
|
|
483
|
-
color: var(--fg);
|
|
484
|
-
font: inherit;
|
|
485
|
-
font-weight: 600;
|
|
486
|
-
padding: 0.45rem 0.9rem;
|
|
487
|
-
cursor: pointer;
|
|
488
|
-
}
|
|
489
|
-
.print-button:hover {
|
|
490
|
-
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
|
491
|
-
background: color-mix(in srgb, var(--accent) 28%, var(--bg-raised));
|
|
492
|
-
}
|
|
493
|
-
.panel > h2, .table-shell > h2, .graph-shell > h2, .print-card > h3 { margin-top: 0; margin-bottom: 0.75rem; }
|
|
494
|
-
.toolbar h2 { margin: 0; }
|
|
495
|
-
.rollup-list { display: grid; gap: 0.5rem; }
|
|
496
|
-
.rollup-list div {
|
|
497
|
-
display: flex; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--line); padding-bottom: 0.4rem;
|
|
498
|
-
}
|
|
499
|
-
.rollup-list dd { margin: 0; }
|
|
500
|
-
.rollup-list a.rollup-link {
|
|
501
|
-
color: inherit;
|
|
502
|
-
text-decoration: none;
|
|
503
|
-
}
|
|
504
|
-
.rollup-list a.rollup-link:hover {
|
|
505
|
-
color: var(--accent);
|
|
506
|
-
text-decoration: underline;
|
|
507
|
-
}
|
|
508
|
-
.distribution-track {
|
|
509
|
-
height: 0.7rem;
|
|
510
|
-
min-width: 6rem;
|
|
511
|
-
border-radius: 999px;
|
|
512
|
-
background: var(--bg);
|
|
513
|
-
border: 1px solid var(--line);
|
|
514
|
-
overflow: hidden;
|
|
515
|
-
}
|
|
516
|
-
.distribution-fill {
|
|
517
|
-
display: block;
|
|
518
|
-
height: 100%;
|
|
519
|
-
border-radius: inherit;
|
|
520
|
-
background: linear-gradient(90deg, var(--accent-dim), var(--accent-strong));
|
|
521
|
-
min-width: 0;
|
|
522
|
-
}
|
|
523
|
-
.graph-root {
|
|
524
|
-
position: relative;
|
|
525
|
-
min-height: 620px;
|
|
526
|
-
}
|
|
527
|
-
.graph-root.is-booting {
|
|
528
|
-
display: grid;
|
|
529
|
-
place-items: center;
|
|
530
|
-
border: 1px solid var(--line);
|
|
531
|
-
border-radius: 10px;
|
|
532
|
-
background: var(--bg);
|
|
533
|
-
}
|
|
534
|
-
.graph-boot {
|
|
535
|
-
display: grid;
|
|
536
|
-
justify-items: center;
|
|
537
|
-
gap: 0.75rem;
|
|
538
|
-
padding: 1.5rem;
|
|
539
|
-
text-align: center;
|
|
540
|
-
color: var(--muted);
|
|
541
|
-
}
|
|
542
|
-
.graph-boot p { margin: 0; }
|
|
543
|
-
.graph-boot-spinner {
|
|
544
|
-
width: 1.35rem;
|
|
545
|
-
height: 1.35rem;
|
|
546
|
-
border: 2px solid color-mix(in srgb, var(--muted) 35%, transparent);
|
|
547
|
-
border-top-color: var(--accent-strong);
|
|
548
|
-
border-radius: 50%;
|
|
549
|
-
animation: graph-boot-spin 0.7s linear infinite;
|
|
550
|
-
}
|
|
551
|
-
@keyframes graph-boot-spin {
|
|
552
|
-
to { transform: rotate(360deg); }
|
|
553
|
-
}
|
|
554
|
-
.graph-root canvas {
|
|
555
|
-
display: block;
|
|
556
|
-
width: 100%;
|
|
557
|
-
min-height: 620px;
|
|
558
|
-
background: var(--bg);
|
|
559
|
-
border-radius: 10px;
|
|
560
|
-
border: 1px solid var(--line);
|
|
561
|
-
touch-action: none;
|
|
562
|
-
cursor: grab;
|
|
563
|
-
}
|
|
564
|
-
.graph-root canvas.is-panning { cursor: grabbing; }
|
|
565
|
-
.graph-root canvas.is-dragging-node { cursor: pointer; }
|
|
566
|
-
.search-results { display: grid; gap: 0.7rem; margin-top: 1rem; }
|
|
567
|
-
.search-result:hover { border-color: var(--accent-dim); }
|
|
568
|
-
.hidden { display: none !important; }
|
|
569
|
-
.breadcrumbs a { color: var(--muted); }
|
|
570
|
-
.breadcrumbs a:hover { color: var(--accent-strong); }
|
|
571
|
-
.section-link { font-size: 0.92rem; }
|
|
572
|
-
.prose p { line-height: 1.55; }
|
|
573
|
-
.idea-summary { white-space: normal; }
|
|
574
|
-
.idea-ref {
|
|
575
|
-
display: inline;
|
|
576
|
-
padding: 0.05em 0.35em;
|
|
577
|
-
margin: 0 0.05em;
|
|
578
|
-
border-radius: 0.3em;
|
|
579
|
-
border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
|
|
580
|
-
background: color-mix(in srgb, var(--accent) 14%, var(--bg-raised));
|
|
581
|
-
color: var(--accent-strong);
|
|
582
|
-
font-weight: 600;
|
|
583
|
-
text-decoration: none;
|
|
584
|
-
white-space: normal;
|
|
585
|
-
overflow-wrap: anywhere;
|
|
586
|
-
}
|
|
587
|
-
a.idea-ref:hover {
|
|
588
|
-
color: var(--fg);
|
|
589
|
-
border-color: var(--accent);
|
|
590
|
-
background: color-mix(in srgb, var(--accent) 22%, var(--bg-raised));
|
|
591
|
-
}
|
|
592
|
-
.idea-ref--file {
|
|
593
|
-
border-color: color-mix(in srgb, var(--rust) 40%, var(--line));
|
|
594
|
-
background: color-mix(in srgb, var(--rust) 16%, var(--bg-raised));
|
|
595
|
-
color: #e8a07a;
|
|
596
|
-
}
|
|
597
|
-
a.idea-ref--file:hover {
|
|
598
|
-
color: var(--fg);
|
|
599
|
-
border-color: var(--rust);
|
|
600
|
-
}
|
|
601
|
-
.idea-ref--unresolved {
|
|
602
|
-
border-style: dashed;
|
|
603
|
-
border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
|
|
604
|
-
background: color-mix(in srgb, var(--warn) 10%, var(--bg-raised));
|
|
605
|
-
color: var(--warn);
|
|
606
|
-
font-weight: 500;
|
|
607
|
-
}
|
|
608
|
-
pre.code-like {
|
|
609
|
-
white-space: pre-wrap; padding: 0.9rem; border-radius: 8px;
|
|
610
|
-
background: var(--bg); border: 1px solid var(--line);
|
|
611
|
-
}
|
|
612
|
-
@media (max-width: 900px) {
|
|
613
|
-
.hero, .topbar-inner { grid-template-columns: 1fr; }
|
|
614
|
-
}
|
|
615
|
-
@media (max-width: 520px) {
|
|
616
|
-
.print-attrs div { grid-template-columns: 1fr; }
|
|
617
|
-
}
|
|
618
|
-
@media print {
|
|
619
|
-
:root { color-scheme: light; }
|
|
620
|
-
body { background: white; color: black; }
|
|
621
|
-
.topbar, .searchbar, .toolbar .actions, .graph-shell .toolbar, .column-filter-row, .table-filter-toggle, .table-filter-actions, .hide-on-print { display: none !important; }
|
|
622
|
-
.sort-button { cursor: default; }
|
|
623
|
-
.panel, .metric, .card, .table-shell, .graph-shell, .print-card, .search-result, .entity-card {
|
|
624
|
-
background: white; color: black; border-color: #cbd5e1; box-shadow: none;
|
|
625
|
-
}
|
|
626
|
-
.scroll-window {
|
|
627
|
-
max-height: none !important;
|
|
628
|
-
overflow: visible !important;
|
|
629
|
-
border: none !important;
|
|
630
|
-
background: transparent !important;
|
|
631
|
-
}
|
|
632
|
-
.scroll-window thead th {
|
|
633
|
-
position: static !important;
|
|
634
|
-
box-shadow: none !important;
|
|
635
|
-
}
|
|
636
|
-
a { color: black; text-decoration: underline; }
|
|
637
|
-
.print-break-avoid { break-inside: avoid; }
|
|
638
|
-
}
|
|
639
|
-
`;
|
|
640
|
-
export const APP_JS = `
|
|
641
|
-
${embedPhysicsCoreSource()}
|
|
642
|
-
|
|
643
|
-
const searchIndexByRoot = new WeakMap();
|
|
644
|
-
|
|
645
|
-
function exportRootPrefix(root) {
|
|
646
|
-
const searchUrl = root.dataset.searchIndex || '';
|
|
647
|
-
const dataIdx = searchUrl.lastIndexOf('data/');
|
|
648
|
-
return dataIdx >= 0 ? searchUrl.slice(0, dataIdx) : '';
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function resolveExportUrl(root, url) {
|
|
652
|
-
const cleaned = String(url || '').replace(/^\\.\\//, '');
|
|
653
|
-
return \`\${exportRootPrefix(root)}\${cleaned}\`;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
async function loadSearchIndex(root) {
|
|
657
|
-
if (searchIndexByRoot.has(root)) return searchIndexByRoot.get(root);
|
|
658
|
-
const embedded = typeof globalThis.__REQLAN_SEARCH_INDEX__ !== 'undefined'
|
|
659
|
-
? globalThis.__REQLAN_SEARCH_INDEX__
|
|
660
|
-
: null;
|
|
661
|
-
if (Array.isArray(embedded)) {
|
|
662
|
-
const ready = Promise.resolve(embedded);
|
|
663
|
-
searchIndexByRoot.set(root, ready);
|
|
664
|
-
return ready;
|
|
665
|
-
}
|
|
666
|
-
const url = root.dataset.searchIndex;
|
|
667
|
-
if (!url) return [];
|
|
668
|
-
const loaded = fetch(url).then(response => {
|
|
669
|
-
if (!response.ok) throw new Error('search index fetch failed');
|
|
670
|
-
return response.json();
|
|
671
|
-
}).catch(() => []);
|
|
672
|
-
searchIndexByRoot.set(root, loaded);
|
|
673
|
-
return loaded;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function lower(value) {
|
|
677
|
-
return String(value || '').toLowerCase();
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function mountSearchableCheckboxDropdown(host, onChange) {
|
|
681
|
-
if (!host) {
|
|
682
|
-
return {
|
|
683
|
-
getSelected: () => [],
|
|
684
|
-
clear: () => undefined,
|
|
685
|
-
setOnChange: () => undefined
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
if (host.__scdApi) {
|
|
689
|
-
host.__scdApi.setOnChange(onChange);
|
|
690
|
-
return host.__scdApi;
|
|
691
|
-
}
|
|
692
|
-
let options = [];
|
|
693
|
-
try {
|
|
694
|
-
options = JSON.parse(host.dataset.options || '[]');
|
|
695
|
-
} catch {
|
|
696
|
-
options = [];
|
|
697
|
-
}
|
|
698
|
-
const label = host.dataset.label || 'Filter';
|
|
699
|
-
const placeholder = host.dataset.placeholder || 'Search…';
|
|
700
|
-
let selected = new Set();
|
|
701
|
-
let open = false;
|
|
702
|
-
let searching = false;
|
|
703
|
-
let query = '';
|
|
704
|
-
let panel = null;
|
|
705
|
-
let search = null;
|
|
706
|
-
let searchRow = null;
|
|
707
|
-
let clearBtn = null;
|
|
708
|
-
let list = null;
|
|
709
|
-
let listenersBound = false;
|
|
710
|
-
let changeHandler = onChange;
|
|
711
|
-
|
|
712
|
-
let trigger = host.querySelector('.scd-trigger');
|
|
713
|
-
let triggerLabel = host.querySelector('.scd-trigger-label');
|
|
714
|
-
if (!trigger) {
|
|
715
|
-
trigger = document.createElement('button');
|
|
716
|
-
trigger.type = 'button';
|
|
717
|
-
trigger.className = 'scd-trigger';
|
|
718
|
-
trigger.setAttribute('aria-haspopup', 'listbox');
|
|
719
|
-
triggerLabel = document.createElement('span');
|
|
720
|
-
triggerLabel.className = 'scd-trigger-label';
|
|
721
|
-
const chevron = document.createElement('span');
|
|
722
|
-
chevron.className = 'scd-chevron';
|
|
723
|
-
chevron.setAttribute('aria-hidden', 'true');
|
|
724
|
-
trigger.append(triggerLabel, chevron);
|
|
725
|
-
host.appendChild(trigger);
|
|
726
|
-
}
|
|
727
|
-
if (!trigger.querySelector('.scd-chevron')) {
|
|
728
|
-
const chevron = document.createElement('span');
|
|
729
|
-
chevron.className = 'scd-chevron';
|
|
730
|
-
chevron.setAttribute('aria-hidden', 'true');
|
|
731
|
-
trigger.appendChild(chevron);
|
|
732
|
-
}
|
|
733
|
-
trigger.setAttribute('aria-expanded', 'false');
|
|
734
|
-
trigger.setAttribute('aria-busy', 'false');
|
|
735
|
-
host.classList.remove('is-loading');
|
|
736
|
-
host.dataset.scdReady = '1';
|
|
737
|
-
|
|
738
|
-
function emitChange() {
|
|
739
|
-
changeHandler?.([...selected]);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function syncTrigger() {
|
|
743
|
-
const values = [...selected];
|
|
744
|
-
host.dataset.selected = JSON.stringify(values);
|
|
745
|
-
host.classList.toggle('has-selection', values.length > 0);
|
|
746
|
-
if (!triggerLabel) return;
|
|
747
|
-
if (values.length === 0) triggerLabel.textContent = label;
|
|
748
|
-
else if (values.length === 1) {
|
|
749
|
-
const match = options.find(option => option.value === values[0]);
|
|
750
|
-
triggerLabel.textContent = match?.label || values[0];
|
|
751
|
-
} else triggerLabel.textContent = \`\${values.length} selected\`;
|
|
752
|
-
if (clearBtn) clearBtn.hidden = values.length === 0;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function setSearching(next) {
|
|
756
|
-
searching = next;
|
|
757
|
-
host.classList.toggle('is-searching', searching);
|
|
758
|
-
if (searchRow) searchRow.classList.toggle('is-active', searching);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function endSearching() {
|
|
762
|
-
query = '';
|
|
763
|
-
if (search) search.value = '';
|
|
764
|
-
setSearching(false);
|
|
765
|
-
if (search && document.activeElement === search) search.blur();
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function ensurePanel() {
|
|
769
|
-
if (panel) return;
|
|
770
|
-
panel = document.createElement('div');
|
|
771
|
-
panel.className = 'scd-panel';
|
|
772
|
-
panel.hidden = true;
|
|
773
|
-
panel.setAttribute('role', 'listbox');
|
|
774
|
-
panel.setAttribute('aria-multiselectable', 'true');
|
|
775
|
-
panel.setAttribute('aria-label', label);
|
|
776
|
-
|
|
777
|
-
searchRow = document.createElement('div');
|
|
778
|
-
searchRow.className = 'scd-search-row';
|
|
779
|
-
search = document.createElement('input');
|
|
780
|
-
search.type = 'search';
|
|
781
|
-
search.className = 'scd-search';
|
|
782
|
-
search.placeholder = placeholder;
|
|
783
|
-
search.setAttribute('aria-label', \`Search \${label}\`);
|
|
784
|
-
clearBtn = document.createElement('button');
|
|
785
|
-
clearBtn.type = 'button';
|
|
786
|
-
clearBtn.className = 'scd-clear';
|
|
787
|
-
clearBtn.textContent = 'Clear';
|
|
788
|
-
clearBtn.hidden = true;
|
|
789
|
-
searchRow.append(search, clearBtn);
|
|
790
|
-
|
|
791
|
-
list = document.createElement('div');
|
|
792
|
-
list.className = 'scd-list';
|
|
793
|
-
panel.append(searchRow, list);
|
|
794
|
-
host.appendChild(panel);
|
|
795
|
-
|
|
796
|
-
search.addEventListener('focus', () => setSearching(true));
|
|
797
|
-
search.addEventListener('blur', () => {
|
|
798
|
-
if (!query.trim()) setSearching(false);
|
|
799
|
-
});
|
|
800
|
-
search.addEventListener('input', () => {
|
|
801
|
-
query = search.value;
|
|
802
|
-
if (query.trim()) setSearching(true);
|
|
803
|
-
renderList();
|
|
804
|
-
});
|
|
805
|
-
clearBtn.addEventListener('click', () => {
|
|
806
|
-
selected.clear();
|
|
807
|
-
syncTrigger();
|
|
808
|
-
renderList();
|
|
809
|
-
emitChange();
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function renderList() {
|
|
814
|
-
if (!list) return;
|
|
815
|
-
const needle = lower(query.trim());
|
|
816
|
-
const visible = options.filter(option => !needle || lower(option.label).includes(needle));
|
|
817
|
-
list.innerHTML = '';
|
|
818
|
-
if (visible.length === 0) {
|
|
819
|
-
const empty = document.createElement('p');
|
|
820
|
-
empty.className = 'scd-empty';
|
|
821
|
-
empty.textContent = 'No matches';
|
|
822
|
-
list.appendChild(empty);
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
let lastGroup = '';
|
|
826
|
-
for (const option of visible) {
|
|
827
|
-
const group = option.special ? 'Special' : label;
|
|
828
|
-
if (group !== lastGroup) {
|
|
829
|
-
const groupLabel = document.createElement('div');
|
|
830
|
-
groupLabel.className = 'scd-group-label';
|
|
831
|
-
groupLabel.textContent = group;
|
|
832
|
-
list.appendChild(groupLabel);
|
|
833
|
-
lastGroup = group;
|
|
834
|
-
}
|
|
835
|
-
const row = document.createElement('label');
|
|
836
|
-
row.className = 'scd-option';
|
|
837
|
-
if (option.kind) row.classList.add(\`is-\${option.kind}\`);
|
|
838
|
-
if (option.special) row.classList.add('is-special');
|
|
839
|
-
const box = document.createElement('input');
|
|
840
|
-
box.type = 'checkbox';
|
|
841
|
-
box.checked = selected.has(option.value);
|
|
842
|
-
box.addEventListener('change', () => {
|
|
843
|
-
if (box.checked) selected.add(option.value);
|
|
844
|
-
else selected.delete(option.value);
|
|
845
|
-
syncTrigger();
|
|
846
|
-
emitChange();
|
|
847
|
-
});
|
|
848
|
-
const text = document.createElement('span');
|
|
849
|
-
text.className = 'scd-option-label';
|
|
850
|
-
text.textContent = option.label;
|
|
851
|
-
row.append(box, text);
|
|
852
|
-
if (typeof option.count === 'number') {
|
|
853
|
-
const count = document.createElement('span');
|
|
854
|
-
count.className = 'scd-option-count';
|
|
855
|
-
count.textContent = String(option.count);
|
|
856
|
-
row.appendChild(count);
|
|
857
|
-
}
|
|
858
|
-
list.appendChild(row);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function onDocPointerDown(event) {
|
|
863
|
-
if (!open) return;
|
|
864
|
-
if (!host.contains(event.target)) setOpen(false);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function onKeyDown(event) {
|
|
868
|
-
if (event.key !== 'Escape' || !open) return;
|
|
869
|
-
if (searching || query.trim()) {
|
|
870
|
-
endSearching();
|
|
871
|
-
renderList();
|
|
872
|
-
event.stopPropagation();
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
setOpen(false);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function onPeerOpen(event) {
|
|
879
|
-
if (!open) return;
|
|
880
|
-
if (event.detail?.source && event.detail.source !== host) setOpen(false);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
function bindListeners() {
|
|
884
|
-
if (listenersBound) return;
|
|
885
|
-
document.addEventListener('pointerdown', onDocPointerDown);
|
|
886
|
-
document.addEventListener('keydown', onKeyDown);
|
|
887
|
-
document.addEventListener('reqlan-scd-open', onPeerOpen);
|
|
888
|
-
listenersBound = true;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function unbindListeners() {
|
|
892
|
-
if (!listenersBound) return;
|
|
893
|
-
document.removeEventListener('pointerdown', onDocPointerDown);
|
|
894
|
-
document.removeEventListener('keydown', onKeyDown);
|
|
895
|
-
document.removeEventListener('reqlan-scd-open', onPeerOpen);
|
|
896
|
-
listenersBound = false;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function setOpen(next) {
|
|
900
|
-
open = next;
|
|
901
|
-
host.classList.toggle('is-open', open);
|
|
902
|
-
if (open) {
|
|
903
|
-
ensurePanel();
|
|
904
|
-
panel.hidden = false;
|
|
905
|
-
trigger.setAttribute('aria-expanded', 'true');
|
|
906
|
-
setSearching(false);
|
|
907
|
-
renderList();
|
|
908
|
-
bindListeners();
|
|
909
|
-
document.dispatchEvent(new CustomEvent('reqlan-scd-open', { detail: { source: host } }));
|
|
910
|
-
} else {
|
|
911
|
-
if (panel) panel.hidden = true;
|
|
912
|
-
trigger.setAttribute('aria-expanded', 'false');
|
|
913
|
-
endSearching();
|
|
914
|
-
unbindListeners();
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
trigger.addEventListener('click', () => setOpen(!open));
|
|
919
|
-
syncTrigger();
|
|
920
|
-
const api = {
|
|
921
|
-
getSelected: () => [...selected],
|
|
922
|
-
clear: () => {
|
|
923
|
-
selected.clear();
|
|
924
|
-
syncTrigger();
|
|
925
|
-
if (open) renderList();
|
|
926
|
-
},
|
|
927
|
-
setOnChange: (fn) => {
|
|
928
|
-
changeHandler = fn;
|
|
929
|
-
}
|
|
930
|
-
};
|
|
931
|
-
host.__scdApi = api;
|
|
932
|
-
return api;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
function enhanceScdPlaceholders(root) {
|
|
936
|
-
for (const host of root.querySelectorAll('[data-graph-status-scd], [data-graph-tag-scd]')) {
|
|
937
|
-
mountSearchableCheckboxDropdown(host, null);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function scoreDocument(doc, query) {
|
|
942
|
-
const haystack = [
|
|
943
|
-
doc.title,
|
|
944
|
-
doc.summary,
|
|
945
|
-
...(doc.tags || []),
|
|
946
|
-
...(doc.pathTokens || []),
|
|
947
|
-
...(doc.keywords || [])
|
|
948
|
-
].map(lower).join(' ');
|
|
949
|
-
return haystack.includes(query) ? haystack.indexOf(query) : -1;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
async function wireGlobalSearch(root) {
|
|
953
|
-
const input = root.querySelector('[data-global-search]');
|
|
954
|
-
const results = root.querySelector('[data-search-results]');
|
|
955
|
-
if (!input || !results) return;
|
|
956
|
-
const docs = await loadSearchIndex(root);
|
|
957
|
-
function render() {
|
|
958
|
-
const query = lower(input.value.trim());
|
|
959
|
-
if (!query) {
|
|
960
|
-
results.innerHTML = '';
|
|
961
|
-
results.classList.add('hidden');
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
const matches = docs
|
|
965
|
-
.map(doc => ({ doc, score: scoreDocument(doc, query) }))
|
|
966
|
-
.filter(item => item.score >= 0)
|
|
967
|
-
.sort((a, b) => a.score - b.score || a.doc.title.localeCompare(b.doc.title))
|
|
968
|
-
.slice(0, 18);
|
|
969
|
-
results.classList.remove('hidden');
|
|
970
|
-
results.innerHTML = matches.length === 0
|
|
971
|
-
? '<div class="search-result"><strong>No matches</strong><p class="subtle">Try idea names, tags, statuses, file paths, or cluster labels.</p></div>'
|
|
972
|
-
: matches.map(({ doc }) => \`
|
|
973
|
-
<a class="search-result" href="\${resolveExportUrl(root, doc.url)}">
|
|
974
|
-
<div class="pill-row">
|
|
975
|
-
<span class="pill">\${doc.kind}</span>
|
|
976
|
-
\${doc.status ? \`<span class="pill">\${doc.status}</span>\` : ''}
|
|
977
|
-
</div>
|
|
978
|
-
<strong>\${doc.title}</strong>
|
|
979
|
-
<p class="subtle">\${doc.summary || ''}</p>
|
|
980
|
-
</a>
|
|
981
|
-
\`).join('');
|
|
982
|
-
}
|
|
983
|
-
input.addEventListener('input', render);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function cellSortValue(cell) {
|
|
987
|
-
if (!cell) return '';
|
|
988
|
-
const explicit = cell.getAttribute('data-sort-value');
|
|
989
|
-
if (explicit != null) return explicit.trim();
|
|
990
|
-
return String(cell.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function compareSortValues(left, right) {
|
|
994
|
-
const leftNumber = Number(String(left).replace(/,/g, ''));
|
|
995
|
-
const rightNumber = Number(String(right).replace(/,/g, ''));
|
|
996
|
-
const bothNumeric = left !== '' && right !== ''
|
|
997
|
-
&& Number.isFinite(leftNumber)
|
|
998
|
-
&& Number.isFinite(rightNumber);
|
|
999
|
-
if (bothNumeric) {
|
|
1000
|
-
return leftNumber - rightNumber;
|
|
1001
|
-
}
|
|
1002
|
-
return String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
function isPlaceholderRow(row) {
|
|
1006
|
-
if (!row || row.cells.length === 0) return true;
|
|
1007
|
-
return [...row.cells].some(cell => Number(cell.colSpan || 1) > 1);
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
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>';
|
|
1011
|
-
|
|
1012
|
-
function ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState) {
|
|
1013
|
-
const shell = table.closest('.table-shell') || table.parentElement;
|
|
1014
|
-
const toolbar = shell ? shell.querySelector(':scope > .toolbar') : null;
|
|
1015
|
-
let actions = toolbar ? toolbar.querySelector('.actions') : null;
|
|
1016
|
-
if (toolbar && !actions) {
|
|
1017
|
-
actions = document.createElement('div');
|
|
1018
|
-
actions.className = 'actions';
|
|
1019
|
-
toolbar.appendChild(actions);
|
|
1020
|
-
}
|
|
1021
|
-
if (!actions) {
|
|
1022
|
-
actions = document.createElement('div');
|
|
1023
|
-
actions.className = 'actions table-filter-actions';
|
|
1024
|
-
table.parentElement?.insertBefore(actions, table);
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
const toggle = document.createElement('button');
|
|
1028
|
-
toggle.type = 'button';
|
|
1029
|
-
toggle.className = 'table-filter-toggle';
|
|
1030
|
-
toggle.setAttribute('aria-label', 'Toggle column filters');
|
|
1031
|
-
toggle.setAttribute('aria-expanded', 'false');
|
|
1032
|
-
toggle.setAttribute('title', 'Column filters');
|
|
1033
|
-
toggle.innerHTML = FILTER_ICON_SVG;
|
|
1034
|
-
toggle.addEventListener('click', () => {
|
|
1035
|
-
const open = !filterRow.classList.contains('is-open');
|
|
1036
|
-
filterRow.classList.toggle('is-open', open);
|
|
1037
|
-
toggle.classList.toggle('is-active', open);
|
|
1038
|
-
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
1039
|
-
syncToggleState();
|
|
1040
|
-
if (open) {
|
|
1041
|
-
filterInputs[0]?.focus();
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1044
|
-
actions.appendChild(toggle);
|
|
1045
|
-
return toggle;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function wireTables(root) {
|
|
1049
|
-
const tables = root.querySelectorAll('table');
|
|
1050
|
-
const tableControllers = [];
|
|
1051
|
-
const params = new URLSearchParams(typeof location !== 'undefined' ? location.search : '');
|
|
1052
|
-
|
|
1053
|
-
for (const table of tables) {
|
|
1054
|
-
const thead = table.tHead;
|
|
1055
|
-
const tbody = table.tBodies[0];
|
|
1056
|
-
if (!thead || !tbody || thead.rows.length === 0) continue;
|
|
1057
|
-
const headerRow = thead.rows[0];
|
|
1058
|
-
if (headerRow.dataset.enhanced === '1') continue;
|
|
1059
|
-
headerRow.dataset.enhanced = '1';
|
|
1060
|
-
|
|
1061
|
-
const headers = [...headerRow.cells];
|
|
1062
|
-
if (headers.length === 0) continue;
|
|
1063
|
-
|
|
1064
|
-
let sortIndex = -1;
|
|
1065
|
-
let sortDir = 'asc';
|
|
1066
|
-
const filterInputs = [];
|
|
1067
|
-
let filterToggle = null;
|
|
1068
|
-
const syncToggleState = () => {
|
|
1069
|
-
if (!filterToggle) return;
|
|
1070
|
-
const hasFilters = filterInputs.some(input => input.value.trim());
|
|
1071
|
-
filterToggle.classList.toggle('has-filters', hasFilters);
|
|
1072
|
-
};
|
|
1073
|
-
const controller = {
|
|
1074
|
-
table,
|
|
1075
|
-
tbody,
|
|
1076
|
-
filterInputs,
|
|
1077
|
-
applyFilters() {
|
|
1078
|
-
const sample = [...tbody.rows].find(row => row.hasAttribute('data-filter-row'));
|
|
1079
|
-
const filterId = sample?.getAttribute('data-filter-row');
|
|
1080
|
-
const globalInput = filterId
|
|
1081
|
-
? root.querySelector(\`[data-filter-input="\${filterId}"]\`)
|
|
1082
|
-
: null;
|
|
1083
|
-
const globalQuery = lower(globalInput?.value.trim());
|
|
1084
|
-
const columnQueries = filterInputs.map(input => lower(input.value.trim()));
|
|
1085
|
-
for (const row of tbody.rows) {
|
|
1086
|
-
if (isPlaceholderRow(row)) continue;
|
|
1087
|
-
const globalText = lower(row.getAttribute('data-filter-text') || row.textContent);
|
|
1088
|
-
const matchesGlobal = !globalQuery || globalText.includes(globalQuery);
|
|
1089
|
-
const matchesColumns = columnQueries.every((query, index) => {
|
|
1090
|
-
if (!query) return true;
|
|
1091
|
-
return lower(cellSortValue(row.cells[index])).includes(query);
|
|
1092
|
-
});
|
|
1093
|
-
row.classList.toggle('hidden', !(matchesGlobal && matchesColumns));
|
|
1094
|
-
}
|
|
1095
|
-
syncToggleState();
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
|
|
1099
|
-
headers.forEach((th, index) => {
|
|
1100
|
-
const label = String(th.textContent || '').trim() || \`Column \${index + 1}\`;
|
|
1101
|
-
const filterKey = String(th.getAttribute('data-filter-key') || '').trim();
|
|
1102
|
-
th.classList.add('sortable-th');
|
|
1103
|
-
th.replaceChildren();
|
|
1104
|
-
if (filterKey) {
|
|
1105
|
-
th.setAttribute('data-filter-key', filterKey);
|
|
1106
|
-
}
|
|
1107
|
-
const button = document.createElement('button');
|
|
1108
|
-
button.type = 'button';
|
|
1109
|
-
button.className = 'sort-button';
|
|
1110
|
-
button.dataset.sortCol = String(index);
|
|
1111
|
-
button.setAttribute('aria-label', \`Sort by \${label}\`);
|
|
1112
|
-
const labelNode = document.createElement('span');
|
|
1113
|
-
labelNode.textContent = label;
|
|
1114
|
-
const indicator = document.createElement('span');
|
|
1115
|
-
indicator.className = 'sort-indicator';
|
|
1116
|
-
indicator.setAttribute('aria-hidden', 'true');
|
|
1117
|
-
button.append(labelNode, indicator);
|
|
1118
|
-
button.addEventListener('click', () => {
|
|
1119
|
-
if (sortIndex === index) {
|
|
1120
|
-
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
|
1121
|
-
} else {
|
|
1122
|
-
sortIndex = index;
|
|
1123
|
-
sortDir = 'asc';
|
|
1124
|
-
}
|
|
1125
|
-
const dataRows = [...tbody.rows].filter(row => !isPlaceholderRow(row));
|
|
1126
|
-
dataRows.sort((left, right) => {
|
|
1127
|
-
const cmp = compareSortValues(
|
|
1128
|
-
cellSortValue(left.cells[index]),
|
|
1129
|
-
cellSortValue(right.cells[index])
|
|
1130
|
-
);
|
|
1131
|
-
return sortDir === 'asc' ? cmp : -cmp;
|
|
1132
|
-
});
|
|
1133
|
-
for (const row of dataRows) {
|
|
1134
|
-
tbody.appendChild(row);
|
|
1135
|
-
}
|
|
1136
|
-
for (const header of headers) {
|
|
1137
|
-
const sortButton = header.querySelector('.sort-button');
|
|
1138
|
-
const mark = header.querySelector('.sort-indicator');
|
|
1139
|
-
if (!sortButton || !mark) continue;
|
|
1140
|
-
const active = Number(sortButton.dataset.sortCol) === sortIndex;
|
|
1141
|
-
sortButton.classList.toggle('sort-active', active);
|
|
1142
|
-
mark.textContent = active ? (sortDir === 'asc' ? '▲' : '▼') : '';
|
|
1143
|
-
}
|
|
1144
|
-
});
|
|
1145
|
-
th.appendChild(button);
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
const filterRow = document.createElement('tr');
|
|
1149
|
-
filterRow.className = 'column-filter-row';
|
|
1150
|
-
headers.forEach((th, index) => {
|
|
1151
|
-
const label = String(th.querySelector('.sort-button span')?.textContent || \`Column \${index + 1}\`);
|
|
1152
|
-
const filterKey = String(th.getAttribute('data-filter-key') || '').trim().toLowerCase();
|
|
1153
|
-
const cell = document.createElement('th');
|
|
1154
|
-
const input = document.createElement('input');
|
|
1155
|
-
input.type = 'search';
|
|
1156
|
-
input.className = 'column-filter';
|
|
1157
|
-
input.placeholder = 'Filter…';
|
|
1158
|
-
input.dataset.colFilter = String(index);
|
|
1159
|
-
if (filterKey) {
|
|
1160
|
-
input.dataset.filterKey = filterKey;
|
|
1161
|
-
}
|
|
1162
|
-
input.setAttribute('aria-label', \`Filter \${label}\`);
|
|
1163
|
-
input.addEventListener('input', () => controller.applyFilters());
|
|
1164
|
-
filterInputs.push(input);
|
|
1165
|
-
cell.appendChild(input);
|
|
1166
|
-
filterRow.appendChild(cell);
|
|
1167
|
-
});
|
|
1168
|
-
thead.appendChild(filterRow);
|
|
1169
|
-
filterToggle = ensureTableFilterToggle(table, filterRow, filterInputs, syncToggleState);
|
|
1170
|
-
tableControllers.push(controller);
|
|
1171
|
-
|
|
1172
|
-
let seeded = false;
|
|
1173
|
-
for (const input of filterInputs) {
|
|
1174
|
-
const key = String(input.dataset.filterKey || '').trim().toLowerCase();
|
|
1175
|
-
if (!key) continue;
|
|
1176
|
-
const aliases = [key];
|
|
1177
|
-
if (key === 'tags') aliases.push('tag');
|
|
1178
|
-
if (key === 'status') aliases.push('statuses');
|
|
1179
|
-
if (key === 'cluster') aliases.push('label');
|
|
1180
|
-
let value = '';
|
|
1181
|
-
for (const alias of aliases) {
|
|
1182
|
-
if (params.has(alias)) {
|
|
1183
|
-
value = params.get(alias) || '';
|
|
1184
|
-
break;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
if (!value) continue;
|
|
1188
|
-
input.value = value;
|
|
1189
|
-
seeded = true;
|
|
1190
|
-
}
|
|
1191
|
-
if (seeded) {
|
|
1192
|
-
filterRow.classList.add('is-open');
|
|
1193
|
-
filterToggle.classList.add('is-active', 'has-filters');
|
|
1194
|
-
filterToggle.setAttribute('aria-expanded', 'true');
|
|
1195
|
-
controller.applyFilters();
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
const inputs = root.querySelectorAll('[data-filter-input]');
|
|
1200
|
-
for (const input of inputs) {
|
|
1201
|
-
input.addEventListener('input', () => {
|
|
1202
|
-
const targetId = input.dataset.filterInput;
|
|
1203
|
-
for (const controller of tableControllers) {
|
|
1204
|
-
const ownsRows = controller.tbody.querySelector(\`[data-filter-row="\${targetId}"]\`);
|
|
1205
|
-
if (ownsRows) {
|
|
1206
|
-
controller.applyFilters();
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
});
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
const EXPORT_PHYSICS_SETTINGS = ReqlanGraphPhysics.DEFAULT_PHYSICS_SETTINGS;
|
|
1214
|
-
|
|
1215
|
-
// Tight visual radii for denser canvas packing (labels still show full names).
|
|
1216
|
-
const GRAPH_NODE_RADIUS = 7;
|
|
1217
|
-
const GRAPH_SUBJECT_RADIUS = 10;
|
|
1218
|
-
const GRAPH_LABEL_MAX_WIDTH = 132;
|
|
1219
|
-
const GRAPH_HIT_PAD = 6;
|
|
1220
|
-
// Auto-mode opacity ramp: fully hidden below START, fully opaque at END (brief muted band between).
|
|
1221
|
-
const GRAPH_LABEL_FADE_START = 0.62;
|
|
1222
|
-
const GRAPH_LABEL_FADE_END = 0.82;
|
|
1223
|
-
const GRAPH_LABEL_MODES = ['auto', 'on', 'off'];
|
|
1224
|
-
const FILE_TREATMENT_MODES = ['invisible', 'compound', 'linked'];
|
|
1225
|
-
|
|
1226
|
-
function fileIdeasetNodeId(fileUri) {
|
|
1227
|
-
return 'rq-file:' + fileUri;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
function isFileIdeasetNode(node) {
|
|
1231
|
-
return Boolean(node && (node.isFileIdeaset || String(node.id || '').startsWith('rq-file:')));
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
function fileIdeasetDisplayName(fileUri) {
|
|
1235
|
-
const trimmed = String(fileUri || '').replace(/\\\\/g, '/');
|
|
1236
|
-
const slash = trimmed.lastIndexOf('/');
|
|
1237
|
-
const fileName = slash >= 0 ? trimmed.slice(slash + 1) : trimmed;
|
|
1238
|
-
return fileName.endsWith('.rq') ? fileName.slice(0, -3) : fileName;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
function fileTreatmentLabel(mode) {
|
|
1242
|
-
if (mode === 'compound') return 'Files: compound';
|
|
1243
|
-
if (mode === 'linked') return 'Files: linked';
|
|
1244
|
-
return 'Files: hidden';
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
function applyFileTreatment(graph, treatment) {
|
|
1248
|
-
const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
1249
|
-
const edges = Array.isArray(graph.edges) ? graph.edges : [];
|
|
1250
|
-
const baseNodes = nodes.filter(node => !isFileIdeasetNode(node));
|
|
1251
|
-
const keep = new Set(baseNodes.map(node => node.id));
|
|
1252
|
-
const baseEdges = edges.filter(edge => keep.has(edge.sourceId) && keep.has(edge.targetId));
|
|
1253
|
-
if (treatment !== 'linked') {
|
|
1254
|
-
return { ...graph, nodes: baseNodes, edges: baseEdges };
|
|
1255
|
-
}
|
|
1256
|
-
const membersByFile = new Map();
|
|
1257
|
-
for (const node of baseNodes) {
|
|
1258
|
-
if (node.isExternal || !node.fileUri) continue;
|
|
1259
|
-
const list = membersByFile.get(node.fileUri) || [];
|
|
1260
|
-
list.push(node);
|
|
1261
|
-
membersByFile.set(node.fileUri, list);
|
|
1262
|
-
}
|
|
1263
|
-
const extraNodes = [];
|
|
1264
|
-
const extraEdges = [];
|
|
1265
|
-
for (const fileUri of [...membersByFile.keys()].sort()) {
|
|
1266
|
-
const members = membersByFile.get(fileUri) || [];
|
|
1267
|
-
if (!members.length) continue;
|
|
1268
|
-
const id = fileIdeasetNodeId(fileUri);
|
|
1269
|
-
const name = fileIdeasetDisplayName(fileUri);
|
|
1270
|
-
extraNodes.push({
|
|
1271
|
-
id,
|
|
1272
|
-
name,
|
|
1273
|
-
kind: 'ideaset',
|
|
1274
|
-
fileUri,
|
|
1275
|
-
lineStart: 0,
|
|
1276
|
-
tags: [],
|
|
1277
|
-
isFileIdeaset: true,
|
|
1278
|
-
pageUrl: members.find(member => member.hostFilePageUrl)?.hostFilePageUrl
|
|
1279
|
-
|| members.find(member => member.pageUrl)?.pageUrl
|
|
1280
|
-
|| undefined
|
|
1281
|
-
});
|
|
1282
|
-
for (const member of members) {
|
|
1283
|
-
extraEdges.push({
|
|
1284
|
-
id: id + '->ideaset_member:' + member.id,
|
|
1285
|
-
sourceId: id,
|
|
1286
|
-
targetId: member.id,
|
|
1287
|
-
kind: 'ideaset_member',
|
|
1288
|
-
label: name
|
|
1289
|
-
});
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
return {
|
|
1293
|
-
...graph,
|
|
1294
|
-
nodes: baseNodes.concat(extraNodes),
|
|
1295
|
-
edges: baseEdges.concat(extraEdges)
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
function wireGraph(root) {
|
|
1300
|
-
const graphRoots = root.querySelectorAll('[data-graph-json]');
|
|
1301
|
-
for (const element of graphRoots) {
|
|
1302
|
-
const scriptId = element.dataset.graphJson;
|
|
1303
|
-
const dataElement = root.querySelector(\`#\${scriptId}\`);
|
|
1304
|
-
if (!dataElement) continue;
|
|
1305
|
-
let graph;
|
|
1306
|
-
try {
|
|
1307
|
-
graph = JSON.parse(dataElement.textContent || '{}');
|
|
1308
|
-
} catch {
|
|
1309
|
-
continue;
|
|
1310
|
-
}
|
|
1311
|
-
let controls = root.querySelector(\`[data-graph-controls="\${scriptId}"]\`);
|
|
1312
|
-
if (!controls) {
|
|
1313
|
-
controls = document.createElement('div');
|
|
1314
|
-
controls.className = 'graph-controls-bar';
|
|
1315
|
-
controls.dataset.graphControls = scriptId;
|
|
1316
|
-
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>';
|
|
1317
|
-
element.parentElement?.insertBefore(controls, element);
|
|
1318
|
-
}
|
|
1319
|
-
const searchInput = controls.querySelector('[data-graph-search]');
|
|
1320
|
-
const pathInput = controls.querySelector('[data-graph-path]');
|
|
1321
|
-
const statusHost = controls.querySelector('[data-graph-status-scd]');
|
|
1322
|
-
const tagHost = controls.querySelector('[data-graph-tag-scd]');
|
|
1323
|
-
const toggleExternal = controls.querySelector('[data-graph-toggle-external]');
|
|
1324
|
-
const toggleIdeasets = controls.querySelector('[data-graph-toggle-ideasets]');
|
|
1325
|
-
const fileTreatmentSelect = controls.querySelector('[data-graph-file-treatment]');
|
|
1326
|
-
const toggleWildcard = controls.querySelector('[data-graph-toggle-wildcard]');
|
|
1327
|
-
const toggleLabels = controls.querySelector('[data-graph-toggle-labels]');
|
|
1328
|
-
const togglePhysics = controls.querySelector('[data-graph-toggle-physics]');
|
|
1329
|
-
const fitButton = controls.querySelector('[data-graph-fit]');
|
|
1330
|
-
const resetButton = controls.querySelector('[data-graph-reset]');
|
|
1331
|
-
const statusText = controls.querySelector('[data-graph-status-text]');
|
|
1332
|
-
if (statusText) statusText.textContent = 'Initialising graph…';
|
|
1333
|
-
element.classList.add('is-booting');
|
|
1334
|
-
let hideExternal = false;
|
|
1335
|
-
let hideIdeasets = false;
|
|
1336
|
-
let fileTreatment = fileTreatmentSelect?.value || 'linked';
|
|
1337
|
-
let includeWildcardRefs = true;
|
|
1338
|
-
let labelMode = 'auto';
|
|
1339
|
-
let statusSelected = [];
|
|
1340
|
-
let tagSelected = [];
|
|
1341
|
-
const statusFilter = mountSearchableCheckboxDropdown(statusHost, (values) => {
|
|
1342
|
-
statusSelected = values;
|
|
1343
|
-
refresh();
|
|
1344
|
-
});
|
|
1345
|
-
const tagFilter = mountSearchableCheckboxDropdown(tagHost, (values) => {
|
|
1346
|
-
tagSelected = values;
|
|
1347
|
-
refresh();
|
|
1348
|
-
});
|
|
1349
|
-
statusSelected = statusFilter.getSelected();
|
|
1350
|
-
tagSelected = tagFilter.getSelected();
|
|
1351
|
-
let livePhysics = false;
|
|
1352
|
-
let animationFrame = 0;
|
|
1353
|
-
let settleFrame = 0;
|
|
1354
|
-
let settleGeneration = 0;
|
|
1355
|
-
// While true, auto labels stay at opacity 0 so text paint does not lag settle.
|
|
1356
|
-
let settlingLayout = false;
|
|
1357
|
-
let calmTicks = 0;
|
|
1358
|
-
let simNodes = [];
|
|
1359
|
-
let simEdges = [];
|
|
1360
|
-
const positions = new Map();
|
|
1361
|
-
const velocities = new Map();
|
|
1362
|
-
let canvas;
|
|
1363
|
-
let ctx;
|
|
1364
|
-
let cssWidth = 1200;
|
|
1365
|
-
let cssHeight = 640;
|
|
1366
|
-
const pinnedIds = new Set();
|
|
1367
|
-
let view = { x: 0, y: 0, w: 1200, h: 640 };
|
|
1368
|
-
let userViewport = false;
|
|
1369
|
-
let dragNodeId = null;
|
|
1370
|
-
let dragCompound = null;
|
|
1371
|
-
let dragMoved = false;
|
|
1372
|
-
let panMode = false;
|
|
1373
|
-
let panOrigin = null;
|
|
1374
|
-
let suppressClickUntil = 0;
|
|
1375
|
-
let hoverNodeId = null;
|
|
1376
|
-
let hoverCompound = null;
|
|
1377
|
-
let pulsePhase = 0;
|
|
1378
|
-
let compoundRegions = [];
|
|
1379
|
-
|
|
1380
|
-
function resolveGraphNodeUrl(pageUrl) {
|
|
1381
|
-
let cleaned = String(pageUrl || '').replace(/^\\.\\//, '');
|
|
1382
|
-
if (cleaned && !cleaned.includes('/') && cleaned.endsWith('.html')) {
|
|
1383
|
-
cleaned = \`ideas/\${cleaned}\`;
|
|
1384
|
-
}
|
|
1385
|
-
return resolveExportUrl(root, cleaned);
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
function formatNodeMeta(node) {
|
|
1389
|
-
const bits = [];
|
|
1390
|
-
if (node.status) bits.push(String(node.status));
|
|
1391
|
-
if (Array.isArray(node.tags) && node.tags.length) bits.push(node.tags.join(', '));
|
|
1392
|
-
return bits.join(' · ');
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
function formatNodeAttrs(node) {
|
|
1396
|
-
const keys = Array.isArray(node.attributeKeys)
|
|
1397
|
-
? node.attributeKeys
|
|
1398
|
-
: Object.keys(node.attributes || {});
|
|
1399
|
-
return keys.length ? keys.map(key => '@' + key).join(' ') : '';
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
function nodeRadius(node) {
|
|
1403
|
-
return node.isSubject || node.id === graph.centerId ? GRAPH_SUBJECT_RADIUS : GRAPH_NODE_RADIUS;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
function seedMissingPositions(nodes, width, height, centerId) {
|
|
1407
|
-
const cx = width / 2;
|
|
1408
|
-
const cy = height / 2;
|
|
1409
|
-
const subject = nodes.find(node => node.isSubject || node.id === centerId);
|
|
1410
|
-
const radius = Math.min(width, height) * 0.22;
|
|
1411
|
-
nodes.forEach((node, index) => {
|
|
1412
|
-
if (positions.has(node.id)) return;
|
|
1413
|
-
if (subject && node.id === subject.id) {
|
|
1414
|
-
positions.set(node.id, { x: cx, y: cy });
|
|
1415
|
-
return;
|
|
1416
|
-
}
|
|
1417
|
-
const angle = (Math.PI * 2 * index) / Math.max(nodes.length, 1) - Math.PI / 2;
|
|
1418
|
-
const ring = radius * (0.7 + (index % 3) * 0.12);
|
|
1419
|
-
positions.set(node.id, {
|
|
1420
|
-
x: cx + Math.cos(angle) * ring,
|
|
1421
|
-
y: cy + Math.sin(angle) * ring
|
|
1422
|
-
});
|
|
1423
|
-
});
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function pruneState(validIds) {
|
|
1427
|
-
for (const id of [...positions.keys()]) {
|
|
1428
|
-
if (!validIds.has(id)) positions.delete(id);
|
|
1429
|
-
}
|
|
1430
|
-
for (const id of [...velocities.keys()]) {
|
|
1431
|
-
if (!validIds.has(id)) velocities.delete(id);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function physicsStep(nodes, edges) {
|
|
1436
|
-
const count = nodes.length;
|
|
1437
|
-
if (count === 0) return 0;
|
|
1438
|
-
const ids = new Array(count);
|
|
1439
|
-
const xs = new Float64Array(count);
|
|
1440
|
-
const ys = new Float64Array(count);
|
|
1441
|
-
const fxs = new Float64Array(count);
|
|
1442
|
-
const fys = new Float64Array(count);
|
|
1443
|
-
for (let i = 0; i < count; i += 1) {
|
|
1444
|
-
const node = nodes[i];
|
|
1445
|
-
const pos = positions.get(node.id) || { x: 0, y: 0 };
|
|
1446
|
-
ids[i] = node.id;
|
|
1447
|
-
xs[i] = pos.x;
|
|
1448
|
-
ys[i] = pos.y;
|
|
1449
|
-
}
|
|
1450
|
-
const averageSpeed = ReqlanGraphPhysics.stepPhysics({
|
|
1451
|
-
ids,
|
|
1452
|
-
xs,
|
|
1453
|
-
ys,
|
|
1454
|
-
fxs,
|
|
1455
|
-
fys,
|
|
1456
|
-
velocities,
|
|
1457
|
-
pinnedIds,
|
|
1458
|
-
edges
|
|
1459
|
-
}, EXPORT_PHYSICS_SETTINGS);
|
|
1460
|
-
for (let i = 0; i < count; i += 1) {
|
|
1461
|
-
positions.set(ids[i], { x: xs[i], y: ys[i] });
|
|
1462
|
-
}
|
|
1463
|
-
return averageSpeed;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function contentBounds() {
|
|
1467
|
-
let minX = Infinity;
|
|
1468
|
-
let minY = Infinity;
|
|
1469
|
-
let maxX = -Infinity;
|
|
1470
|
-
let maxY = -Infinity;
|
|
1471
|
-
for (const node of simNodes) {
|
|
1472
|
-
const pos = positions.get(node.id);
|
|
1473
|
-
if (!pos) continue;
|
|
1474
|
-
const r = nodeRadius(node);
|
|
1475
|
-
minX = Math.min(minX, pos.x - r);
|
|
1476
|
-
minY = Math.min(minY, pos.y - r);
|
|
1477
|
-
maxX = Math.max(maxX, pos.x + r);
|
|
1478
|
-
// Reserve label space only when labels are forced on; auto hides when fitted/zoomed out.
|
|
1479
|
-
maxY = Math.max(maxY, pos.y + r + (labelMode === 'on' ? 54 : 0));
|
|
1480
|
-
}
|
|
1481
|
-
if (!Number.isFinite(minX)) {
|
|
1482
|
-
return { minX: 0, minY: 0, maxX: 1200, maxY: 640 };
|
|
1483
|
-
}
|
|
1484
|
-
return { minX, minY, maxX, maxY };
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
function syncLabelsButton() {
|
|
1488
|
-
if (!toggleLabels) return;
|
|
1489
|
-
const label = labelMode === 'auto' ? 'Labels: auto' : labelMode === 'on' ? 'Labels: on' : 'Labels: off';
|
|
1490
|
-
toggleLabels.textContent = label;
|
|
1491
|
-
toggleLabels.dataset.labelMode = labelMode;
|
|
1492
|
-
toggleLabels.classList.toggle('is-active', labelMode !== 'off');
|
|
1493
|
-
toggleLabels.setAttribute('aria-pressed', labelMode === 'auto' ? 'mixed' : labelMode === 'on' ? 'true' : 'false');
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
function labelOpacityAtScale(scale) {
|
|
1497
|
-
if (labelMode === 'on') return 1;
|
|
1498
|
-
if (labelMode === 'off') return 0;
|
|
1499
|
-
if (scale <= GRAPH_LABEL_FADE_START) return 0;
|
|
1500
|
-
if (scale >= GRAPH_LABEL_FADE_END) return 1;
|
|
1501
|
-
return (scale - GRAPH_LABEL_FADE_START) / (GRAPH_LABEL_FADE_END - GRAPH_LABEL_FADE_START);
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
function fitView(force = false) {
|
|
1505
|
-
if (userViewport && !force) return;
|
|
1506
|
-
const bounds = contentBounds();
|
|
1507
|
-
const pad = 56;
|
|
1508
|
-
let x = bounds.minX - pad;
|
|
1509
|
-
let y = bounds.minY - pad;
|
|
1510
|
-
let w = Math.max(360, bounds.maxX - bounds.minX + pad * 2);
|
|
1511
|
-
let h = Math.max(360, bounds.maxY - bounds.minY + pad * 2);
|
|
1512
|
-
// Match canvas aspect so uniform scale fills without squashing (SVG viewBox meet).
|
|
1513
|
-
const canvasAspect = cssWidth / Math.max(1, cssHeight);
|
|
1514
|
-
const viewAspect = w / h;
|
|
1515
|
-
if (viewAspect < canvasAspect) {
|
|
1516
|
-
const widened = h * canvasAspect;
|
|
1517
|
-
x -= (widened - w) / 2;
|
|
1518
|
-
w = widened;
|
|
1519
|
-
} else if (viewAspect > canvasAspect) {
|
|
1520
|
-
const heightened = w / canvasAspect;
|
|
1521
|
-
y -= (heightened - h) / 2;
|
|
1522
|
-
h = heightened;
|
|
1523
|
-
}
|
|
1524
|
-
view = { x, y, w, h };
|
|
1525
|
-
userViewport = false;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
function viewportTransform() {
|
|
1529
|
-
const scale = Math.min(cssWidth / Math.max(1, view.w), cssHeight / Math.max(1, view.h));
|
|
1530
|
-
return {
|
|
1531
|
-
scale,
|
|
1532
|
-
offsetX: (cssWidth - view.w * scale) / 2,
|
|
1533
|
-
offsetY: (cssHeight - view.h * scale) / 2
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function wrapLines(text, maxWidth, font) {
|
|
1538
|
-
if (!text) return [];
|
|
1539
|
-
ctx.font = font;
|
|
1540
|
-
const words = String(text).split(/\\s+/);
|
|
1541
|
-
const lines = [];
|
|
1542
|
-
let current = '';
|
|
1543
|
-
for (const word of words) {
|
|
1544
|
-
const next = current ? \`\${current} \${word}\` : word;
|
|
1545
|
-
if (ctx.measureText(next).width <= maxWidth) {
|
|
1546
|
-
current = next;
|
|
1547
|
-
} else {
|
|
1548
|
-
if (current) lines.push(current);
|
|
1549
|
-
if (ctx.measureText(word).width <= maxWidth) {
|
|
1550
|
-
current = word;
|
|
1551
|
-
} else {
|
|
1552
|
-
let chunk = '';
|
|
1553
|
-
for (const ch of word) {
|
|
1554
|
-
const trial = chunk + ch;
|
|
1555
|
-
if (ctx.measureText(trial).width <= maxWidth) chunk = trial;
|
|
1556
|
-
else {
|
|
1557
|
-
if (chunk) lines.push(chunk);
|
|
1558
|
-
chunk = ch;
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
current = chunk;
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
if (current) lines.push(current);
|
|
1566
|
-
return lines;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
function nodeFill(node) {
|
|
1570
|
-
if (node.isSubject || node.id === graph.centerId) return '#b85c38';
|
|
1571
|
-
if (node.isExternal) return '#8a4530';
|
|
1572
|
-
if (node.kind === 'ideaset') return '#e0a24a';
|
|
1573
|
-
return '#07a0e5';
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
function nodeStroke(node, hover) {
|
|
1577
|
-
if (hover) return '#0bbefb';
|
|
1578
|
-
if (node.isSubject || node.id === graph.centerId) return '#0bbefb';
|
|
1579
|
-
if (node.isExternal) return 'rgba(184,92,56,0.55)';
|
|
1580
|
-
if (node.kind === 'ideaset') return '#d18616';
|
|
1581
|
-
return 'rgba(235,228,222,0.35)';
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
function paint() {
|
|
1585
|
-
if (!canvas || !ctx) return;
|
|
1586
|
-
if (!userViewport) fitView(false);
|
|
1587
|
-
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
|
1588
|
-
const nextW = Math.max(1, Math.floor(cssWidth * dpr));
|
|
1589
|
-
const nextH = Math.max(1, Math.floor(cssHeight * dpr));
|
|
1590
|
-
if (canvas.width !== nextW || canvas.height !== nextH) {
|
|
1591
|
-
canvas.width = nextW;
|
|
1592
|
-
canvas.height = nextH;
|
|
1593
|
-
}
|
|
1594
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1595
|
-
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
1596
|
-
ctx.fillStyle = '#14100e';
|
|
1597
|
-
ctx.fillRect(0, 0, cssWidth, cssHeight);
|
|
1598
|
-
|
|
1599
|
-
const { scale, offsetX, offsetY } = viewportTransform();
|
|
1600
|
-
// Auto labels stay invisible during batch settle ([graph_label_auto] / html_export_graph_label_auto).
|
|
1601
|
-
const ambientLabelOpacity =
|
|
1602
|
-
settlingLayout && labelMode === 'auto' ? 0 : labelOpacityAtScale(scale);
|
|
1603
|
-
ctx.save();
|
|
1604
|
-
ctx.translate(offsetX, offsetY);
|
|
1605
|
-
ctx.scale(scale, scale);
|
|
1606
|
-
ctx.translate(-view.x, -view.y);
|
|
1607
|
-
|
|
1608
|
-
if (fileTreatment === 'compound') {
|
|
1609
|
-
compoundRegions = [];
|
|
1610
|
-
const byFile = new Map();
|
|
1611
|
-
for (const node of simNodes) {
|
|
1612
|
-
if (node.isExternal || isFileIdeasetNode(node) || !node.fileUri) continue;
|
|
1613
|
-
const list = byFile.get(node.fileUri) || [];
|
|
1614
|
-
list.push(node);
|
|
1615
|
-
byFile.set(node.fileUri, list);
|
|
1616
|
-
}
|
|
1617
|
-
for (const [fileUri, members] of byFile) {
|
|
1618
|
-
let minX = Infinity;
|
|
1619
|
-
let minY = Infinity;
|
|
1620
|
-
let maxX = -Infinity;
|
|
1621
|
-
let maxY = -Infinity;
|
|
1622
|
-
let any = false;
|
|
1623
|
-
let pageUrl;
|
|
1624
|
-
for (const node of members) {
|
|
1625
|
-
const pos = positions.get(node.id);
|
|
1626
|
-
if (!pos) continue;
|
|
1627
|
-
const r = nodeRadius(node) + 14;
|
|
1628
|
-
any = true;
|
|
1629
|
-
minX = Math.min(minX, pos.x - r);
|
|
1630
|
-
minY = Math.min(minY, pos.y - r);
|
|
1631
|
-
maxX = Math.max(maxX, pos.x + r);
|
|
1632
|
-
maxY = Math.max(maxY, pos.y + r);
|
|
1633
|
-
pageUrl = pageUrl || node.hostFilePageUrl || node.pageUrl;
|
|
1634
|
-
}
|
|
1635
|
-
if (!any) continue;
|
|
1636
|
-
const pad = 8;
|
|
1637
|
-
const x = minX - pad;
|
|
1638
|
-
const y = minY - pad;
|
|
1639
|
-
const w = (maxX - minX) + pad * 2;
|
|
1640
|
-
const h = (maxY - minY) + pad * 2;
|
|
1641
|
-
const titleH = 18;
|
|
1642
|
-
const label = fileIdeasetDisplayName(fileUri);
|
|
1643
|
-
compoundRegions.push({
|
|
1644
|
-
fileUri,
|
|
1645
|
-
pageUrl,
|
|
1646
|
-
memberIds: members.map(member => member.id),
|
|
1647
|
-
x,
|
|
1648
|
-
y,
|
|
1649
|
-
w,
|
|
1650
|
-
h,
|
|
1651
|
-
titleY: y - titleH,
|
|
1652
|
-
titleH,
|
|
1653
|
-
label
|
|
1654
|
-
});
|
|
1655
|
-
const radius = 10;
|
|
1656
|
-
const hover = hoverCompound && hoverCompound.fileUri === fileUri;
|
|
1657
|
-
ctx.beginPath();
|
|
1658
|
-
ctx.moveTo(x + radius, y);
|
|
1659
|
-
ctx.arcTo(x + w, y, x + w, y + h, radius);
|
|
1660
|
-
ctx.arcTo(x + w, y + h, x, y + h, radius);
|
|
1661
|
-
ctx.arcTo(x, y + h, x, y, radius);
|
|
1662
|
-
ctx.arcTo(x, y, x + w, y, radius);
|
|
1663
|
-
ctx.closePath();
|
|
1664
|
-
ctx.fillStyle = hover ? 'rgba(209, 134, 22, 0.14)' : 'rgba(209, 134, 22, 0.08)';
|
|
1665
|
-
ctx.fill();
|
|
1666
|
-
ctx.strokeStyle = hover ? 'rgba(224, 162, 74, 0.85)' : 'rgba(209, 134, 22, 0.45)';
|
|
1667
|
-
ctx.lineWidth = (hover ? 1.8 : 1.2) / scale;
|
|
1668
|
-
ctx.stroke();
|
|
1669
|
-
ctx.fillStyle = 'rgba(224, 162, 74, 0.95)';
|
|
1670
|
-
ctx.font = \`600 \${11 / scale}px "IBM Plex Sans", system-ui, sans-serif\`;
|
|
1671
|
-
ctx.textAlign = 'left';
|
|
1672
|
-
ctx.textBaseline = 'bottom';
|
|
1673
|
-
ctx.fillText(label, x + 6, y - 4 / scale);
|
|
1674
|
-
// Underline title to signal clickability.
|
|
1675
|
-
const titleWidth = ctx.measureText(label).width;
|
|
1676
|
-
ctx.beginPath();
|
|
1677
|
-
ctx.moveTo(x + 6, y - 2 / scale);
|
|
1678
|
-
ctx.lineTo(x + 6 + titleWidth, y - 2 / scale);
|
|
1679
|
-
ctx.strokeStyle = 'rgba(224, 162, 74, 0.85)';
|
|
1680
|
-
ctx.lineWidth = 1 / scale;
|
|
1681
|
-
ctx.stroke();
|
|
1682
|
-
}
|
|
1683
|
-
} else {
|
|
1684
|
-
compoundRegions = [];
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
ctx.lineWidth = 1.1 / scale;
|
|
1688
|
-
ctx.strokeStyle = 'rgba(61,47,40,0.85)';
|
|
1689
|
-
ctx.globalAlpha = 0.85;
|
|
1690
|
-
for (const edge of simEdges) {
|
|
1691
|
-
const source = positions.get(edge.sourceId);
|
|
1692
|
-
const target = positions.get(edge.targetId);
|
|
1693
|
-
if (!source || !target) continue;
|
|
1694
|
-
const isWildcard = edge.kind === 'wildcard_reference';
|
|
1695
|
-
ctx.setLineDash(isWildcard ? [2 / scale, 3.5 / scale] : []);
|
|
1696
|
-
ctx.beginPath();
|
|
1697
|
-
ctx.moveTo(source.x, source.y);
|
|
1698
|
-
ctx.lineTo(target.x, target.y);
|
|
1699
|
-
ctx.stroke();
|
|
1700
|
-
}
|
|
1701
|
-
ctx.setLineDash([]);
|
|
1702
|
-
ctx.globalAlpha = 1;
|
|
1703
|
-
|
|
1704
|
-
pulsePhase = (pulsePhase + 0.035) % (Math.PI * 2);
|
|
1705
|
-
for (const node of simNodes) {
|
|
1706
|
-
const pos = positions.get(node.id);
|
|
1707
|
-
if (!pos) continue;
|
|
1708
|
-
const r = nodeRadius(node);
|
|
1709
|
-
const hover = node.id === hoverNodeId || node.id === dragNodeId;
|
|
1710
|
-
const isSubject = node.isSubject || node.id === graph.centerId;
|
|
1711
|
-
if (isSubject) {
|
|
1712
|
-
const glow = 4 + Math.sin(pulsePhase) * 3;
|
|
1713
|
-
ctx.beginPath();
|
|
1714
|
-
ctx.arc(pos.x, pos.y, r + glow, 0, Math.PI * 2);
|
|
1715
|
-
ctx.fillStyle = 'rgba(184,92,56,0.22)';
|
|
1716
|
-
ctx.fill();
|
|
1717
|
-
}
|
|
1718
|
-
ctx.beginPath();
|
|
1719
|
-
ctx.arc(pos.x, pos.y, r, 0, Math.PI * 2);
|
|
1720
|
-
ctx.fillStyle = nodeFill(node);
|
|
1721
|
-
ctx.fill();
|
|
1722
|
-
ctx.lineWidth = (isSubject ? 2.2 : hover ? 2 : 1.15) / scale;
|
|
1723
|
-
ctx.strokeStyle = nodeStroke(node, hover);
|
|
1724
|
-
ctx.stroke();
|
|
1725
|
-
|
|
1726
|
-
// Auto: continuous opacity from zoom; hovered/dragged nodes stay fully opaque.
|
|
1727
|
-
const labelOpacity = hover && labelMode === 'auto' ? 1 : ambientLabelOpacity;
|
|
1728
|
-
if (labelOpacity <= 0) continue;
|
|
1729
|
-
const meta = formatNodeMeta(node);
|
|
1730
|
-
const attrs = formatNodeAttrs(node);
|
|
1731
|
-
const nameFont = isSubject ? '600 11px Inter, system-ui, sans-serif' : '600 10px Inter, system-ui, sans-serif';
|
|
1732
|
-
const metaFont = '9.5px Inter, system-ui, sans-serif';
|
|
1733
|
-
const nameLines = wrapLines(node.name, GRAPH_LABEL_MAX_WIDTH, nameFont);
|
|
1734
|
-
const metaLines = wrapLines(meta, GRAPH_LABEL_MAX_WIDTH, metaFont);
|
|
1735
|
-
const attrLines = wrapLines(attrs, GRAPH_LABEL_MAX_WIDTH, metaFont);
|
|
1736
|
-
let ty = pos.y + r + 12;
|
|
1737
|
-
ctx.save();
|
|
1738
|
-
ctx.globalAlpha = labelOpacity;
|
|
1739
|
-
ctx.textAlign = 'center';
|
|
1740
|
-
ctx.textBaseline = 'top';
|
|
1741
|
-
ctx.fillStyle = isSubject ? '#0bbefb' : '#ebe4de';
|
|
1742
|
-
ctx.font = nameFont;
|
|
1743
|
-
for (const line of nameLines) {
|
|
1744
|
-
ctx.lineWidth = 3 / scale;
|
|
1745
|
-
ctx.strokeStyle = '#14100e';
|
|
1746
|
-
ctx.strokeText(line, pos.x, ty);
|
|
1747
|
-
ctx.fillText(line, pos.x, ty);
|
|
1748
|
-
ty += 12;
|
|
1749
|
-
}
|
|
1750
|
-
ctx.fillStyle = '#a89488';
|
|
1751
|
-
ctx.font = metaFont;
|
|
1752
|
-
for (const line of metaLines) {
|
|
1753
|
-
ctx.fillText(line, pos.x, ty);
|
|
1754
|
-
ty += 11;
|
|
1755
|
-
}
|
|
1756
|
-
ctx.fillStyle = '#b85c38';
|
|
1757
|
-
for (const line of attrLines) {
|
|
1758
|
-
ctx.fillText(line, pos.x, ty);
|
|
1759
|
-
ty += 11;
|
|
1760
|
-
}
|
|
1761
|
-
ctx.restore();
|
|
1762
|
-
}
|
|
1763
|
-
ctx.restore();
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
function stopLoop() {
|
|
1767
|
-
if (animationFrame) {
|
|
1768
|
-
cancelAnimationFrame(animationFrame);
|
|
1769
|
-
animationFrame = 0;
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
function cancelSettle() {
|
|
1774
|
-
settleGeneration += 1;
|
|
1775
|
-
if (settleFrame) {
|
|
1776
|
-
cancelAnimationFrame(settleFrame);
|
|
1777
|
-
settleFrame = 0;
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
function scheduleTick() {
|
|
1782
|
-
if (animationFrame) return;
|
|
1783
|
-
animationFrame = requestAnimationFrame(() => {
|
|
1784
|
-
animationFrame = 0;
|
|
1785
|
-
if (!livePhysics) {
|
|
1786
|
-
paint();
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
const averageSpeed = physicsStep(simNodes, simEdges);
|
|
1790
|
-
paint();
|
|
1791
|
-
if (averageSpeed < EXPORT_PHYSICS_SETTINGS.restSpeed) {
|
|
1792
|
-
calmTicks += 1;
|
|
1793
|
-
if (calmTicks >= EXPORT_PHYSICS_SETTINGS.restTicks) {
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
} else {
|
|
1797
|
-
calmTicks = 0;
|
|
1798
|
-
}
|
|
1799
|
-
scheduleTick();
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
function wake() {
|
|
1804
|
-
calmTicks = 0;
|
|
1805
|
-
if (livePhysics) scheduleTick();
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
function settleChunkSize(nodeCount) {
|
|
1809
|
-
if (nodeCount > 400) return 1;
|
|
1810
|
-
if (nodeCount > 200) return 2;
|
|
1811
|
-
if (nodeCount > 80) return 4;
|
|
1812
|
-
return 8;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
function batchSettleAsync(nodes, edges, onDone) {
|
|
1816
|
-
cancelSettle();
|
|
1817
|
-
const generation = settleGeneration;
|
|
1818
|
-
const iterations = Math.min(96, 36 + Math.floor(nodes.length * 0.45));
|
|
1819
|
-
const chunkSize = settleChunkSize(nodes.length);
|
|
1820
|
-
let step = 0;
|
|
1821
|
-
|
|
1822
|
-
function finish() {
|
|
1823
|
-
if (generation !== settleGeneration) return;
|
|
1824
|
-
for (const velocity of velocities.values()) {
|
|
1825
|
-
velocity.vx = 0;
|
|
1826
|
-
velocity.vy = 0;
|
|
1827
|
-
}
|
|
1828
|
-
calmTicks = EXPORT_PHYSICS_SETTINGS.restTicks;
|
|
1829
|
-
settleFrame = 0;
|
|
1830
|
-
onDone();
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
function runChunk() {
|
|
1834
|
-
if (generation !== settleGeneration) return;
|
|
1835
|
-
const end = Math.min(step + chunkSize, iterations);
|
|
1836
|
-
for (; step < end; step += 1) {
|
|
1837
|
-
physicsStep(nodes, edges);
|
|
1838
|
-
}
|
|
1839
|
-
paint();
|
|
1840
|
-
if (statusText && iterations > 0) {
|
|
1841
|
-
const pct = Math.min(99, Math.round((step / iterations) * 100));
|
|
1842
|
-
statusText.textContent = \`Settling layout… \${pct}%\`;
|
|
1843
|
-
}
|
|
1844
|
-
if (step >= iterations) {
|
|
1845
|
-
finish();
|
|
1846
|
-
return;
|
|
1847
|
-
}
|
|
1848
|
-
settleFrame = requestAnimationFrame(runChunk);
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
if (iterations <= 0) {
|
|
1852
|
-
finish();
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
settleFrame = requestAnimationFrame(runChunk);
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
function hitTest(point) {
|
|
1859
|
-
let best = null;
|
|
1860
|
-
let bestDist = Infinity;
|
|
1861
|
-
for (const node of simNodes) {
|
|
1862
|
-
const pos = positions.get(node.id);
|
|
1863
|
-
if (!pos) continue;
|
|
1864
|
-
const dx = point.x - pos.x;
|
|
1865
|
-
const dy = point.y - pos.y;
|
|
1866
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1867
|
-
const threshold = nodeRadius(node) + GRAPH_HIT_PAD;
|
|
1868
|
-
if (dist <= threshold && dist < bestDist) {
|
|
1869
|
-
best = node;
|
|
1870
|
-
bestDist = dist;
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
return best;
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
function hitTestCompound(point) {
|
|
1877
|
-
// Prefer title hits, then body (for drag).
|
|
1878
|
-
let bodyHit = null;
|
|
1879
|
-
for (let i = compoundRegions.length - 1; i >= 0; i -= 1) {
|
|
1880
|
-
const region = compoundRegions[i];
|
|
1881
|
-
const inTitle = point.x >= region.x
|
|
1882
|
-
&& point.x <= region.x + region.w
|
|
1883
|
-
&& point.y >= region.titleY
|
|
1884
|
-
&& point.y <= region.y + 2;
|
|
1885
|
-
if (inTitle) {
|
|
1886
|
-
return { region, titleHit: true };
|
|
1887
|
-
}
|
|
1888
|
-
const inBody = point.x >= region.x
|
|
1889
|
-
&& point.x <= region.x + region.w
|
|
1890
|
-
&& point.y >= region.y
|
|
1891
|
-
&& point.y <= region.y + region.h;
|
|
1892
|
-
if (inBody && !bodyHit) {
|
|
1893
|
-
bodyHit = { region, titleHit: false };
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
return bodyHit;
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
function resizeCanvas() {
|
|
1900
|
-
const rect = element.getBoundingClientRect();
|
|
1901
|
-
cssWidth = Math.max(320, Math.floor(rect.width || 1200));
|
|
1902
|
-
cssHeight = Math.max(620, Math.min(1400, 220 + simNodes.length * 18));
|
|
1903
|
-
if (canvas) {
|
|
1904
|
-
canvas.style.width = '100%';
|
|
1905
|
-
canvas.style.height = \`\${cssHeight}px\`;
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
function mountGraph(filteredNodes, filteredEdges) {
|
|
1910
|
-
stopLoop();
|
|
1911
|
-
cancelSettle();
|
|
1912
|
-
simNodes = filteredNodes;
|
|
1913
|
-
simEdges = filteredEdges;
|
|
1914
|
-
const validIds = new Set(filteredNodes.map(node => node.id));
|
|
1915
|
-
pruneState(validIds);
|
|
1916
|
-
const width = 1200;
|
|
1917
|
-
const height = Math.max(640, Math.min(1400, 220 + filteredNodes.length * 18));
|
|
1918
|
-
seedMissingPositions(filteredNodes, width, height, graph.centerId);
|
|
1919
|
-
canvas = document.createElement('canvas');
|
|
1920
|
-
canvas.setAttribute('role', 'img');
|
|
1921
|
-
canvas.setAttribute('aria-label', \`Requirement graph with \${filteredNodes.length} nodes\`);
|
|
1922
|
-
ctx = canvas.getContext('2d');
|
|
1923
|
-
element.innerHTML = '';
|
|
1924
|
-
element.classList.remove('is-booting');
|
|
1925
|
-
element.appendChild(canvas);
|
|
1926
|
-
resizeCanvas();
|
|
1927
|
-
wireViewport(canvas);
|
|
1928
|
-
userViewport = false;
|
|
1929
|
-
const finishStatus = () => {
|
|
1930
|
-
settlingLayout = false;
|
|
1931
|
-
if (statusText) {
|
|
1932
|
-
statusText.textContent = \`\${filteredNodes.length} nodes, \${filteredEdges.length} edges\`;
|
|
1933
|
-
}
|
|
1934
|
-
paint();
|
|
1935
|
-
};
|
|
1936
|
-
if (!livePhysics) {
|
|
1937
|
-
settlingLayout = true;
|
|
1938
|
-
if (statusText) statusText.textContent = 'Settling layout…';
|
|
1939
|
-
paint();
|
|
1940
|
-
batchSettleAsync(filteredNodes, filteredEdges, finishStatus);
|
|
1941
|
-
} else {
|
|
1942
|
-
settlingLayout = false;
|
|
1943
|
-
finishStatus();
|
|
1944
|
-
wake();
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
function clientToGraph(event) {
|
|
1949
|
-
const rect = canvas.getBoundingClientRect();
|
|
1950
|
-
const { scale, offsetX, offsetY } = viewportTransform();
|
|
1951
|
-
const cssX = ((event.clientX - rect.left) / rect.width) * cssWidth;
|
|
1952
|
-
const cssY = ((event.clientY - rect.top) / rect.height) * cssHeight;
|
|
1953
|
-
return {
|
|
1954
|
-
x: view.x + (cssX - offsetX) / scale,
|
|
1955
|
-
y: view.y + (cssY - offsetY) / scale
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
function wireViewport(target) {
|
|
1960
|
-
target.addEventListener('wheel', (event) => {
|
|
1961
|
-
event.preventDefault();
|
|
1962
|
-
const factor = event.deltaY < 0 ? 0.9 : 1.1;
|
|
1963
|
-
const before = clientToGraph(event);
|
|
1964
|
-
view.w = Math.min(8000, Math.max(180, view.w * factor));
|
|
1965
|
-
view.h = Math.min(8000, Math.max(180, view.h * factor));
|
|
1966
|
-
const after = clientToGraph(event);
|
|
1967
|
-
view.x += before.x - after.x;
|
|
1968
|
-
view.y += before.y - after.y;
|
|
1969
|
-
userViewport = true;
|
|
1970
|
-
paint();
|
|
1971
|
-
}, { passive: false });
|
|
1972
|
-
|
|
1973
|
-
target.addEventListener('pointerdown', (event) => {
|
|
1974
|
-
const point = clientToGraph(event);
|
|
1975
|
-
const hit = hitTest(point);
|
|
1976
|
-
if (hit) {
|
|
1977
|
-
dragNodeId = hit.id;
|
|
1978
|
-
dragCompound = null;
|
|
1979
|
-
dragMoved = false;
|
|
1980
|
-
pinnedIds.add(hit.id);
|
|
1981
|
-
velocities.set(hit.id, { vx: 0, vy: 0 });
|
|
1982
|
-
target.classList.add('is-dragging-node');
|
|
1983
|
-
target.setPointerCapture(event.pointerId);
|
|
1984
|
-
event.preventDefault();
|
|
1985
|
-
paint();
|
|
1986
|
-
return;
|
|
1987
|
-
}
|
|
1988
|
-
const compoundHit = hitTestCompound(point);
|
|
1989
|
-
if (compoundHit) {
|
|
1990
|
-
dragCompound = {
|
|
1991
|
-
...compoundHit.region,
|
|
1992
|
-
titleHit: compoundHit.titleHit,
|
|
1993
|
-
origin: point,
|
|
1994
|
-
memberOrigins: Object.fromEntries(
|
|
1995
|
-
compoundHit.region.memberIds.map(id => {
|
|
1996
|
-
const pos = positions.get(id) || { x: 0, y: 0 };
|
|
1997
|
-
return [id, { x: pos.x, y: pos.y }];
|
|
1998
|
-
})
|
|
1999
|
-
)
|
|
2000
|
-
};
|
|
2001
|
-
dragNodeId = null;
|
|
2002
|
-
dragMoved = false;
|
|
2003
|
-
for (const id of compoundHit.region.memberIds) {
|
|
2004
|
-
pinnedIds.add(id);
|
|
2005
|
-
velocities.set(id, { vx: 0, vy: 0 });
|
|
2006
|
-
}
|
|
2007
|
-
target.classList.add('is-dragging-node');
|
|
2008
|
-
target.setPointerCapture(event.pointerId);
|
|
2009
|
-
event.preventDefault();
|
|
2010
|
-
paint();
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
panMode = true;
|
|
2014
|
-
panOrigin = { x: event.clientX, y: event.clientY, viewX: view.x, viewY: view.y };
|
|
2015
|
-
target.classList.add('is-panning');
|
|
2016
|
-
target.setPointerCapture(event.pointerId);
|
|
2017
|
-
event.preventDefault();
|
|
2018
|
-
});
|
|
2019
|
-
|
|
2020
|
-
target.addEventListener('pointermove', (event) => {
|
|
2021
|
-
if (dragNodeId) {
|
|
2022
|
-
const point = clientToGraph(event);
|
|
2023
|
-
positions.set(dragNodeId, { x: point.x, y: point.y });
|
|
2024
|
-
dragMoved = true;
|
|
2025
|
-
wake();
|
|
2026
|
-
paint();
|
|
2027
|
-
return;
|
|
2028
|
-
}
|
|
2029
|
-
if (dragCompound) {
|
|
2030
|
-
const point = clientToGraph(event);
|
|
2031
|
-
const dx = point.x - dragCompound.origin.x;
|
|
2032
|
-
const dy = point.y - dragCompound.origin.y;
|
|
2033
|
-
if (Math.abs(dx) + Math.abs(dy) > 1) {
|
|
2034
|
-
dragMoved = true;
|
|
2035
|
-
}
|
|
2036
|
-
for (const id of dragCompound.memberIds) {
|
|
2037
|
-
const origin = dragCompound.memberOrigins[id];
|
|
2038
|
-
if (!origin) continue;
|
|
2039
|
-
positions.set(id, { x: origin.x + dx, y: origin.y + dy });
|
|
2040
|
-
}
|
|
2041
|
-
wake();
|
|
2042
|
-
paint();
|
|
2043
|
-
return;
|
|
2044
|
-
}
|
|
2045
|
-
if (panMode && panOrigin) {
|
|
2046
|
-
const rect = target.getBoundingClientRect();
|
|
2047
|
-
const { scale } = viewportTransform();
|
|
2048
|
-
const dx = ((event.clientX - panOrigin.x) / rect.width) * cssWidth / scale;
|
|
2049
|
-
const dy = ((event.clientY - panOrigin.y) / rect.height) * cssHeight / scale;
|
|
2050
|
-
view.x = panOrigin.viewX - dx;
|
|
2051
|
-
view.y = panOrigin.viewY - dy;
|
|
2052
|
-
userViewport = true;
|
|
2053
|
-
paint();
|
|
2054
|
-
return;
|
|
2055
|
-
}
|
|
2056
|
-
const point = clientToGraph(event);
|
|
2057
|
-
const hit = hitTest(point);
|
|
2058
|
-
const nextHover = hit?.id || null;
|
|
2059
|
-
const compoundHit = hit ? null : hitTestCompound(point);
|
|
2060
|
-
const nextCompound = compoundHit?.region || null;
|
|
2061
|
-
if (nextHover !== hoverNodeId || nextCompound?.fileUri !== hoverCompound?.fileUri) {
|
|
2062
|
-
hoverNodeId = nextHover;
|
|
2063
|
-
hoverCompound = nextCompound;
|
|
2064
|
-
paint();
|
|
2065
|
-
}
|
|
2066
|
-
});
|
|
2067
|
-
|
|
2068
|
-
target.addEventListener('pointerup', (event) => {
|
|
2069
|
-
target.classList.remove('is-dragging-node');
|
|
2070
|
-
if (dragNodeId) {
|
|
2071
|
-
const node = simNodes.find(item => item.id === dragNodeId);
|
|
2072
|
-
const moved = dragMoved;
|
|
2073
|
-
const nodeId = dragNodeId;
|
|
2074
|
-
dragNodeId = null;
|
|
2075
|
-
pinnedIds.delete(nodeId);
|
|
2076
|
-
wake();
|
|
2077
|
-
paint();
|
|
2078
|
-
if (!moved && node?.pageUrl && Date.now() > suppressClickUntil) {
|
|
2079
|
-
window.location.href = resolveGraphNodeUrl(node.pageUrl);
|
|
2080
|
-
} else if (moved) {
|
|
2081
|
-
suppressClickUntil = Date.now() + 250;
|
|
2082
|
-
}
|
|
2083
|
-
return;
|
|
2084
|
-
}
|
|
2085
|
-
if (dragCompound) {
|
|
2086
|
-
const compound = dragCompound;
|
|
2087
|
-
const moved = dragMoved;
|
|
2088
|
-
const openTitle = !moved && compound.titleHit;
|
|
2089
|
-
for (const id of compound.memberIds) {
|
|
2090
|
-
pinnedIds.delete(id);
|
|
2091
|
-
}
|
|
2092
|
-
dragCompound = null;
|
|
2093
|
-
wake();
|
|
2094
|
-
paint();
|
|
2095
|
-
if (openTitle && compound.pageUrl && Date.now() > suppressClickUntil) {
|
|
2096
|
-
window.location.href = resolveGraphNodeUrl(compound.pageUrl);
|
|
2097
|
-
} else if (moved) {
|
|
2098
|
-
suppressClickUntil = Date.now() + 250;
|
|
2099
|
-
}
|
|
2100
|
-
return;
|
|
2101
|
-
}
|
|
2102
|
-
if (panMode) {
|
|
2103
|
-
panMode = false;
|
|
2104
|
-
panOrigin = null;
|
|
2105
|
-
target.classList.remove('is-panning');
|
|
2106
|
-
}
|
|
2107
|
-
});
|
|
2108
|
-
|
|
2109
|
-
target.addEventListener('pointercancel', () => {
|
|
2110
|
-
target.classList.remove('is-dragging-node');
|
|
2111
|
-
if (dragNodeId) {
|
|
2112
|
-
pinnedIds.delete(dragNodeId);
|
|
2113
|
-
dragNodeId = null;
|
|
2114
|
-
wake();
|
|
2115
|
-
}
|
|
2116
|
-
if (dragCompound) {
|
|
2117
|
-
for (const id of dragCompound.memberIds) {
|
|
2118
|
-
pinnedIds.delete(id);
|
|
2119
|
-
}
|
|
2120
|
-
dragCompound = null;
|
|
2121
|
-
wake();
|
|
2122
|
-
}
|
|
2123
|
-
panMode = false;
|
|
2124
|
-
panOrigin = null;
|
|
2125
|
-
target.classList.remove('is-panning');
|
|
2126
|
-
paint();
|
|
2127
|
-
});
|
|
2128
|
-
|
|
2129
|
-
window.addEventListener('resize', () => {
|
|
2130
|
-
resizeCanvas();
|
|
2131
|
-
paint();
|
|
2132
|
-
});
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
function matches(node) {
|
|
2136
|
-
const FILTER_NOT_PRESENT = '__not_present__';
|
|
2137
|
-
const query = lower(searchInput?.value?.trim());
|
|
2138
|
-
const pathQuery = lower(pathInput?.value?.trim());
|
|
2139
|
-
if (hideExternal && node.isExternal) return false;
|
|
2140
|
-
if (hideIdeasets && node.kind === 'ideaset' && !isFileIdeasetNode(node)) return false;
|
|
2141
|
-
const haystack = [node.name, node.kind, node.fileUri, node.status || '', ...(node.tags || [])].map(lower).join(' ');
|
|
2142
|
-
if (query && !haystack.includes(query)) return false;
|
|
2143
|
-
if (pathQuery && !lower(node.fileUri).includes(pathQuery)) return false;
|
|
2144
|
-
if (statusSelected.length) {
|
|
2145
|
-
const key = node.statusKey || (String(node.status || '').trim() ? String(node.status).trim() : FILTER_NOT_PRESENT);
|
|
2146
|
-
if (!statusSelected.includes(key)) return false;
|
|
2147
|
-
}
|
|
2148
|
-
if (tagSelected.length) {
|
|
2149
|
-
const keys = Array.isArray(node.tagsKeys) && node.tagsKeys.length
|
|
2150
|
-
? node.tagsKeys
|
|
2151
|
-
: (Array.isArray(node.tags) && node.tags.length ? node.tags : [FILTER_NOT_PRESENT]);
|
|
2152
|
-
if (!tagSelected.some(value => keys.includes(value))) return false;
|
|
2153
|
-
}
|
|
2154
|
-
return true;
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
function syncFilesSelect() {
|
|
2158
|
-
if (!fileTreatmentSelect) return;
|
|
2159
|
-
fileTreatmentSelect.value = fileTreatment;
|
|
2160
|
-
const selected = fileTreatmentSelect.selectedOptions?.[0];
|
|
2161
|
-
if (selected?.title) {
|
|
2162
|
-
fileTreatmentSelect.title = selected.title;
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
function refresh() {
|
|
2167
|
-
const treated = applyFileTreatment(graph, fileTreatment);
|
|
2168
|
-
const visibleNodes = (treated.nodes || []).filter(matches);
|
|
2169
|
-
const visibleIds = new Set(visibleNodes.map(node => node.id));
|
|
2170
|
-
const visibleEdges = (treated.edges || []).filter(edge => {
|
|
2171
|
-
if (!visibleIds.has(edge.sourceId) || !visibleIds.has(edge.targetId)) return false;
|
|
2172
|
-
if (!includeWildcardRefs && edge.kind === 'wildcard_reference') return false;
|
|
2173
|
-
return true;
|
|
2174
|
-
});
|
|
2175
|
-
mountGraph(visibleNodes, visibleEdges);
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
for (const control of [searchInput, pathInput]) {
|
|
2179
|
-
control?.addEventListener('input', refresh);
|
|
2180
|
-
control?.addEventListener('change', refresh);
|
|
2181
|
-
}
|
|
2182
|
-
toggleExternal?.addEventListener('click', () => {
|
|
2183
|
-
hideExternal = !hideExternal;
|
|
2184
|
-
toggleExternal.classList.toggle('is-active', hideExternal);
|
|
2185
|
-
toggleExternal.textContent = hideExternal ? 'Show external' : 'Hide external';
|
|
2186
|
-
refresh();
|
|
2187
|
-
});
|
|
2188
|
-
toggleIdeasets?.addEventListener('click', () => {
|
|
2189
|
-
hideIdeasets = !hideIdeasets;
|
|
2190
|
-
toggleIdeasets.classList.toggle('is-active', hideIdeasets);
|
|
2191
|
-
toggleIdeasets.textContent = hideIdeasets ? 'Show ideasets' : 'Hide ideasets';
|
|
2192
|
-
refresh();
|
|
2193
|
-
});
|
|
2194
|
-
fileTreatmentSelect?.addEventListener('change', () => {
|
|
2195
|
-
fileTreatment = fileTreatmentSelect.value || 'invisible';
|
|
2196
|
-
syncFilesSelect();
|
|
2197
|
-
refresh();
|
|
2198
|
-
});
|
|
2199
|
-
toggleWildcard?.addEventListener('click', () => {
|
|
2200
|
-
includeWildcardRefs = !includeWildcardRefs;
|
|
2201
|
-
toggleWildcard.classList.toggle('is-active', includeWildcardRefs);
|
|
2202
|
-
toggleWildcard.setAttribute('aria-pressed', includeWildcardRefs ? 'true' : 'false');
|
|
2203
|
-
refresh();
|
|
2204
|
-
});
|
|
2205
|
-
toggleLabels?.addEventListener('click', () => {
|
|
2206
|
-
const index = GRAPH_LABEL_MODES.indexOf(labelMode);
|
|
2207
|
-
labelMode = GRAPH_LABEL_MODES[(index + 1) % GRAPH_LABEL_MODES.length];
|
|
2208
|
-
syncLabelsButton();
|
|
2209
|
-
paint();
|
|
2210
|
-
});
|
|
2211
|
-
togglePhysics?.addEventListener('click', () => {
|
|
2212
|
-
livePhysics = !livePhysics;
|
|
2213
|
-
togglePhysics.classList.toggle('is-active', livePhysics);
|
|
2214
|
-
togglePhysics.setAttribute('aria-pressed', livePhysics ? 'true' : 'false');
|
|
2215
|
-
if (livePhysics) {
|
|
2216
|
-
cancelSettle();
|
|
2217
|
-
if (statusText) {
|
|
2218
|
-
statusText.textContent = \`\${simNodes.length} nodes, \${simEdges.length} edges\`;
|
|
2219
|
-
}
|
|
2220
|
-
wake();
|
|
2221
|
-
} else {
|
|
2222
|
-
stopLoop();
|
|
2223
|
-
paint();
|
|
2224
|
-
}
|
|
2225
|
-
});
|
|
2226
|
-
fitButton?.addEventListener('click', () => {
|
|
2227
|
-
fitView(true);
|
|
2228
|
-
paint();
|
|
2229
|
-
});
|
|
2230
|
-
resetButton?.addEventListener('click', () => {
|
|
2231
|
-
if (searchInput) searchInput.value = '';
|
|
2232
|
-
if (pathInput) pathInput.value = '';
|
|
2233
|
-
statusSelected = [];
|
|
2234
|
-
tagSelected = [];
|
|
2235
|
-
statusFilter.clear();
|
|
2236
|
-
tagFilter.clear();
|
|
2237
|
-
hideExternal = false;
|
|
2238
|
-
hideIdeasets = false;
|
|
2239
|
-
fileTreatment = 'linked';
|
|
2240
|
-
includeWildcardRefs = true;
|
|
2241
|
-
labelMode = 'auto';
|
|
2242
|
-
toggleExternal?.classList.remove('is-active');
|
|
2243
|
-
toggleIdeasets?.classList.remove('is-active');
|
|
2244
|
-
toggleWildcard?.classList.add('is-active');
|
|
2245
|
-
toggleWildcard?.setAttribute('aria-pressed', 'true');
|
|
2246
|
-
syncLabelsButton();
|
|
2247
|
-
syncFilesSelect();
|
|
2248
|
-
if (toggleExternal) toggleExternal.textContent = 'Hide external';
|
|
2249
|
-
if (toggleIdeasets) toggleIdeasets.textContent = 'Hide ideasets';
|
|
2250
|
-
positions.clear();
|
|
2251
|
-
velocities.clear();
|
|
2252
|
-
pinnedIds.clear();
|
|
2253
|
-
userViewport = false;
|
|
2254
|
-
refresh();
|
|
2255
|
-
});
|
|
2256
|
-
syncLabelsButton();
|
|
2257
|
-
syncFilesSelect();
|
|
2258
|
-
// Defer first mount so page chrome paints before JSON/layout work.
|
|
2259
|
-
requestAnimationFrame(() => {
|
|
2260
|
-
refresh();
|
|
2261
|
-
});
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
function scheduleBackground(task) {
|
|
2266
|
-
if (typeof requestIdleCallback === 'function') {
|
|
2267
|
-
requestIdleCallback(() => task(), { timeout: 250 });
|
|
2268
|
-
return;
|
|
2269
|
-
}
|
|
2270
|
-
requestAnimationFrame(() => {
|
|
2271
|
-
setTimeout(task, 0);
|
|
2272
|
-
});
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
function boot() {
|
|
2276
|
-
const root = document.body;
|
|
2277
|
-
// Interactive Status/Tags triggers before heavy table/graph init.
|
|
2278
|
-
enhanceScdPlaceholders(root);
|
|
2279
|
-
wireTables(root);
|
|
2280
|
-
void wireGlobalSearch(root);
|
|
2281
|
-
// Graph boot + settle run in the background so large exports stay responsive.
|
|
2282
|
-
scheduleBackground(() => wireGraph(root));
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
if (document.readyState === 'loading') {
|
|
2286
|
-
document.addEventListener('DOMContentLoaded', boot);
|
|
2287
|
-
} else {
|
|
2288
|
-
boot();
|
|
2289
|
-
}
|
|
2290
|
-
`;
|
|
2291
|
-
export function buildSearchIndexScript(documents) {
|
|
2292
|
-
return `globalThis.__REQLAN_SEARCH_INDEX__ = ${JSON.stringify(documents).replace(/</g, '\\u003c')};\n`;
|
|
2293
|
-
}
|
|
2294
|
-
//# sourceMappingURL=html-export-assets.js.map
|