@reqlan/analytical 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +19 -2
  2. package/README.template.md +1 -1
  3. package/out/core/barrel-page.d.ts +2 -1
  4. package/out/core/barrel-page.js +18 -83
  5. package/out/core/barrel-page.js.map +1 -1
  6. package/out/core/base-discovery.d.ts +6 -0
  7. package/out/core/base-discovery.js +47 -2
  8. package/out/core/base-discovery.js.map +1 -1
  9. package/out/core/create-base.js +10 -14
  10. package/out/core/create-base.js.map +1 -1
  11. package/out/core/file-reference-resolve.js +3 -1
  12. package/out/core/file-reference-resolve.js.map +1 -1
  13. package/out/core/index-error.d.ts +3 -3
  14. package/out/core/index-error.js.map +1 -1
  15. package/out/core/index-state-types.d.ts +47 -0
  16. package/out/core/index-state-types.js +11 -0
  17. package/out/core/index-state-types.js.map +1 -0
  18. package/out/core/path-relative.d.ts +15 -0
  19. package/out/core/path-relative.js +84 -0
  20. package/out/core/path-relative.js.map +1 -0
  21. package/out/core/rqignore.js +62 -62
  22. package/out/core/rqignore.js.map +1 -1
  23. package/out/core/types.d.ts +4 -3
  24. package/out/core/types.js +4 -1
  25. package/out/core/types.js.map +1 -1
  26. package/out/core/workspace-paths.d.ts +1 -2
  27. package/out/core/workspace-paths.js +6 -46
  28. package/out/core/workspace-paths.js.map +1 -1
  29. package/out/export/build-export-snapshot.js +1 -1
  30. package/out/export/build-export-snapshot.js.map +1 -1
  31. package/out/export/export-utils.d.ts +5 -0
  32. package/out/export/export-utils.js +38 -0
  33. package/out/export/export-utils.js.map +1 -0
  34. package/out/export/write-csv-export.js +1 -1
  35. package/out/export/write-csv-export.js.map +1 -1
  36. package/out/export/write-markdown-export.js +1 -1
  37. package/out/export/write-markdown-export.js.map +1 -1
  38. package/out/index-store/base-registry.d.ts +2 -3
  39. package/out/index-store/base-registry.js +8 -4
  40. package/out/index-store/base-registry.js.map +1 -1
  41. package/out/index-store/fuzzy-search-hit.d.ts +10 -0
  42. package/out/index-store/fuzzy-search-hit.js +2 -0
  43. package/out/index-store/fuzzy-search-hit.js.map +1 -0
  44. package/out/index-store/index-diagnostics-store.d.ts +4 -0
  45. package/out/index-store/index-diagnostics-store.js +44 -88
  46. package/out/index-store/index-diagnostics-store.js.map +1 -1
  47. package/out/index-store/index-file-error.d.ts +1 -1
  48. package/out/index-store/index-parse-issues.d.ts +4 -3
  49. package/out/index-store/index-parse-issues.js +0 -38
  50. package/out/index-store/index-parse-issues.js.map +1 -1
  51. package/out/index-store/index-status.d.ts +1 -1
  52. package/out/index-store/sqlite-store.d.ts +6 -2
  53. package/out/index-store/sqlite-store.js +70 -613
  54. package/out/index-store/sqlite-store.js.map +1 -1
  55. package/out/index-store/webview-graph-queries.d.ts +5 -0
  56. package/out/index-store/webview-graph-queries.js.map +1 -1
  57. package/out/index-store/workspace-index-file.d.ts +3 -14
  58. package/out/index-store/workspace-index-file.js +1 -114
  59. package/out/index-store/workspace-index-file.js.map +1 -1
  60. package/out/index-store/workspace-index-sync.d.ts +4 -17
  61. package/out/index-store/workspace-index-sync.js +29 -11
  62. package/out/index-store/workspace-index-sync.js.map +1 -1
  63. package/out/index-store/workspace-index.d.ts +36 -9
  64. package/out/index-store/workspace-index.js +223 -120
  65. package/out/index-store/workspace-index.js.map +1 -1
  66. package/out/index.d.ts +16 -43
  67. package/out/index.js +9 -25
  68. package/out/index.js.map +1 -1
  69. package/out/native/generated.d.ts +102 -0
  70. package/out/native/generated.js +4 -0
  71. package/out/native/generated.js.map +1 -0
  72. package/out/native/index.d.ts +17 -0
  73. package/out/native/index.js +12 -0
  74. package/out/native/index.js.map +1 -0
  75. package/out/native/load-native.d.ts +50 -0
  76. package/out/native/load-native.js +186 -0
  77. package/out/native/load-native.js.map +1 -0
  78. package/out/{analysis-api.d.ts → native/native-analysis-api.d.ts} +19 -19
  79. package/out/native/native-analysis-api.js +212 -0
  80. package/out/native/native-analysis-api.js.map +1 -0
  81. package/out/native/native-index-store.d.ts +5 -0
  82. package/out/native/native-index-store.js +6 -0
  83. package/out/native/native-index-store.js.map +1 -0
  84. package/out/native/native-sql-db.d.ts +135 -0
  85. package/out/native/native-sql-db.js +217 -0
  86. package/out/native/native-sql-db.js.map +1 -0
  87. package/out/native/native-workspace-index.d.ts +164 -0
  88. package/out/native/native-workspace-index.js +283 -0
  89. package/out/native/native-workspace-index.js.map +1 -0
  90. package/out/native/open-analysis-api.d.ts +16 -0
  91. package/out/native/open-analysis-api.js +21 -0
  92. package/out/native/open-analysis-api.js.map +1 -0
  93. package/out/native/parse-source.d.ts +24 -0
  94. package/out/native/parse-source.js +26 -0
  95. package/out/native/parse-source.js.map +1 -0
  96. package/package.json +18 -30
  97. package/out/analysis/analyser-registry.d.ts +0 -21
  98. package/out/analysis/analyser-registry.js +0 -33
  99. package/out/analysis/analyser-registry.js.map +0 -1
  100. package/out/analysis/completion-tracking-analyser.d.ts +0 -3
  101. package/out/analysis/completion-tracking-analyser.js +0 -37
  102. package/out/analysis/completion-tracking-analyser.js.map +0 -1
  103. package/out/analysis/contextual-search.d.ts +0 -26
  104. package/out/analysis/contextual-search.js +0 -94
  105. package/out/analysis/contextual-search.js.map +0 -1
  106. package/out/analysis/deprecation-impact-analyser.d.ts +0 -3
  107. package/out/analysis/deprecation-impact-analyser.js +0 -21
  108. package/out/analysis/deprecation-impact-analyser.js.map +0 -1
  109. package/out/analysis/file-related-analyser.d.ts +0 -5
  110. package/out/analysis/file-related-analyser.js +0 -41
  111. package/out/analysis/file-related-analyser.js.map +0 -1
  112. package/out/analysis/fuzzy-search-analyser.d.ts +0 -17
  113. package/out/analysis/fuzzy-search-analyser.js +0 -25
  114. package/out/analysis/fuzzy-search-analyser.js.map +0 -1
  115. package/out/analysis/fuzzy-search-worker-client.d.ts +0 -39
  116. package/out/analysis/fuzzy-search-worker-client.js +0 -178
  117. package/out/analysis/fuzzy-search-worker-client.js.map +0 -1
  118. package/out/analysis/fuzzy-search-worker.d.ts +0 -32
  119. package/out/analysis/fuzzy-search-worker.js +0 -81
  120. package/out/analysis/fuzzy-search-worker.js.map +0 -1
  121. package/out/analysis/fuzzy-search.d.ts +0 -62
  122. package/out/analysis/fuzzy-search.js +0 -471
  123. package/out/analysis/fuzzy-search.js.map +0 -1
  124. package/out/analysis/git-dates-analyser.d.ts +0 -12
  125. package/out/analysis/git-dates-analyser.js +0 -96
  126. package/out/analysis/git-dates-analyser.js.map +0 -1
  127. package/out/analysis/list-ideas-analyser.d.ts +0 -3
  128. package/out/analysis/list-ideas-analyser.js +0 -7
  129. package/out/analysis/list-ideas-analyser.js.map +0 -1
  130. package/out/analysis/local-graph-analyser.d.ts +0 -6
  131. package/out/analysis/local-graph-analyser.js +0 -42
  132. package/out/analysis/local-graph-analyser.js.map +0 -1
  133. package/out/analysis/semantic-search-analyser.d.ts +0 -8
  134. package/out/analysis/semantic-search-analyser.js +0 -51
  135. package/out/analysis/semantic-search-analyser.js.map +0 -1
  136. package/out/analysis-api.js +0 -199
  137. package/out/analysis-api.js.map +0 -1
  138. package/out/core/analytical-store.d.ts +0 -85
  139. package/out/core/analytical-store.js +0 -143
  140. package/out/core/analytical-store.js.map +0 -1
  141. package/out/core/context-model.d.ts +0 -204
  142. package/out/core/context-model.js +0 -36
  143. package/out/core/context-model.js.map +0 -1
  144. package/out/core/context-signals.d.ts +0 -154
  145. package/out/core/context-signals.js +0 -406
  146. package/out/core/context-signals.js.map +0 -1
  147. package/out/create-runtime.d.ts +0 -47
  148. package/out/create-runtime.js +0 -79
  149. package/out/create-runtime.js.map +0 -1
  150. package/out/export/export-html.d.ts +0 -4
  151. package/out/export/export-html.js +0 -12
  152. package/out/export/export-html.js.map +0 -1
  153. package/out/export/html-export-assets.d.ts +0 -3
  154. package/out/export/html-export-assets.js +0 -2294
  155. package/out/export/html-export-assets.js.map +0 -1
  156. package/out/export/html-export-template.d.ts +0 -18
  157. package/out/export/html-export-template.js +0 -1154
  158. package/out/export/html-export-template.js.map +0 -1
  159. package/out/export/html-export-utils.d.ts +0 -86
  160. package/out/export/html-export-utils.js +0 -485
  161. package/out/export/html-export-utils.js.map +0 -1
  162. package/out/export/write-html-export.d.ts +0 -2
  163. package/out/export/write-html-export.js +0 -181
  164. package/out/export/write-html-export.js.map +0 -1
  165. package/out/graph/physics-core-source.d.ts +0 -5
  166. package/out/graph/physics-core-source.js +0 -8
  167. package/out/graph/physics-core-source.js.map +0 -1
  168. package/out/graph/physics-core.d.ts +0 -141
  169. package/out/graph/physics-core.js +0 -237
  170. package/out/graph/physics-core.js.map +0 -1
  171. package/out/index-store/file-treatment.d.ts +0 -40
  172. package/out/index-store/file-treatment.js +0 -144
  173. package/out/index-store/file-treatment.js.map +0 -1
  174. package/out/index-store/idea-extractor.d.ts +0 -11
  175. package/out/index-store/idea-extractor.js +0 -351
  176. package/out/index-store/idea-extractor.js.map +0 -1
  177. package/out/index-store/overview-coverage.d.ts +0 -23
  178. package/out/index-store/overview-coverage.js +0 -254
  179. package/out/index-store/overview-coverage.js.map +0 -1
@@ -1,1154 +0,0 @@
1
- import { escapeHtml, exportIdeaAnchorId, fileByIdea, filePageEnabled, formatAttributeValue, formatDate, hrefFor, ideaStatus, ideaTags, pageHref, relatedClusters, renderAttributeValueHtml, listFilterHref, renderDefinitionList, renderFilePathCell, renderIdeaSummaryHtml, renderLinkedStatusCell, renderLinkedTagsCell, renderMetric, renderOptionalStatusCell, renderOptionalTagsCell, renderPrintIdeaAttributesHtml, renderTextWithRefsHtml, resolveExportFilePage, slugAttributeKey, statusOrTagFacetHref, stringifyJson } from './html-export-utils.js';
2
- import { FILTER_EMPTY, FILTER_NOT_PRESENT, filterDisplayLabel, isFilterEmpty, isFilterNotPresent, isFilterUnspecified } from '../core/filter-specials.js';
3
- function renderMultiFilterSelect(attr, countsByValue, emptyLabel) {
4
- const valueSet = new Set(Object.keys(countsByValue));
5
- valueSet.add(FILTER_NOT_PRESENT);
6
- valueSet.add(FILTER_EMPTY);
7
- const options = [...valueSet].map(value => ({
8
- value,
9
- label: filterDisplayLabel(value),
10
- special: isFilterNotPresent(value) || isFilterEmpty(value) || isFilterUnspecified(value),
11
- kind: isFilterNotPresent(value)
12
- ? 'not-present'
13
- : isFilterEmpty(value)
14
- ? 'empty'
15
- : isFilterUnspecified(value)
16
- ? 'unspecified'
17
- : 'concrete',
18
- count: countsByValue[value] ?? 0
19
- }));
20
- options.sort((left, right) => {
21
- const rank = (kind) => kind === 'not-present' ? 0 : kind === 'empty' ? 1 : kind === 'unspecified' ? 2 : 3;
22
- return rank(left.kind) - rank(right.kind) || left.label.localeCompare(right.label);
23
- });
24
- return `
25
- <div
26
- class="scd is-loading"
27
- data-graph-${attr}-scd
28
- data-placeholder="${escapeHtml(emptyLabel)}"
29
- data-label="${attr === 'status' ? 'Status' : 'Tags'}"
30
- data-options="${escapeHtml(JSON.stringify(options))}"
31
- >
32
- <button type="button" class="scd-trigger" aria-haspopup="listbox" aria-expanded="false" aria-busy="true">
33
- <span class="scd-trigger-label">${attr === 'status' ? 'Status…' : 'Tags…'}</span>
34
- <span class="scd-chevron" aria-hidden="true"></span>
35
- </button>
36
- </div>
37
- `;
38
- }
39
- function exportHref(snapshot, currentPath, targetPath) {
40
- return hrefFor(currentPath, targetPath, snapshot.urlBase);
41
- }
42
- function exportPageHref(snapshot, currentPath, pageInfo) {
43
- return pageHref(currentPath, pageInfo, snapshot.urlBase);
44
- }
45
- export function renderHomePage(snapshot) {
46
- const currentPath = snapshot.manifest.home.path;
47
- const clustersIndexHref = exportPageHref(snapshot, currentPath, snapshot.manifest.clustersIndex);
48
- const highlightClusters = snapshot.clusters.slice(0, 8).map(cluster => {
49
- const href = snapshot.pageOptions.includeClusterPages
50
- ? exportPageHref(snapshot, currentPath, cluster.page)
51
- : listFilterHref(snapshot, currentPath, snapshot.manifest.clustersIndex, { kind: cluster.kind, cluster: cluster.label });
52
- return `
53
- <a class="entity-card" href="${escapeHtml(href)}">
54
- <div class="pill-row">
55
- <span class="pill">${escapeHtml(cluster.kind)}</span>
56
- <span class="pill">${cluster.counts.ideas} ideas</span>
57
- </div>
58
- <h3>${escapeHtml(cluster.label)}</h3>
59
- <p class="subtle">${escapeHtml(cluster.description)}</p>
60
- </a>
61
- `;
62
- }).join('');
63
- const body = `
64
- <header class="hero">
65
- <div class="panel">
66
- <p class="eyebrow">Reqlan export</p>
67
- <h1>${escapeHtml(snapshot.title)}</h1>
68
- <p class="subtle">Template: ${escapeHtml(snapshot.templateId)} | Scope: ${escapeHtml(snapshot.scope)} | Mode: ${escapeHtml(snapshot.runtimeMode)}</p>
69
- <p class="prose">Documentation-grade export with idea pages, clusters, links, search, and printable views.</p>
70
- </div>
71
- <div class="card">
72
- <p><strong>Generated</strong></p>
73
- <p>${escapeHtml(formatDate(snapshot.generatedAt))}</p>
74
- <p><strong>Workspace</strong></p>
75
- <p>${escapeHtml(snapshot.workspaceRoot)}</p>
76
- </div>
77
- </header>
78
- <section class="grid">
79
- ${renderMetric('Ideas', String(snapshot.counts.ideas), exportPageHref(snapshot, currentPath, snapshot.manifest.ideasIndex))}
80
- ${renderMetric('References', String(snapshot.counts.edges), exportPageHref(snapshot, currentPath, snapshot.manifest.graph))}
81
- ${renderMetric('Files', String(snapshot.counts.files), exportPageHref(snapshot, currentPath, snapshot.manifest.filesIndex))}
82
- ${renderMetric('Code files', String(snapshot.codeFiles.length), exportPageHref(snapshot, currentPath, snapshot.manifest.codeFilesIndex))}
83
- ${renderMetric('Clusters', String(snapshot.counts.clusters), clustersIndexHref)}
84
- ${renderMetric('Attributes', String(snapshot.attributes.length), exportPageHref(snapshot, currentPath, snapshot.manifest.attributesIndex))}
85
- </section>
86
- <section class="split">
87
- <div class="panel">
88
- <h2>Status Rollup</h2>
89
- <div class="scroll-window">${renderDefinitionList(snapshot.byStatus, key => statusOrTagFacetHref(snapshot, currentPath, 'status', key))}</div>
90
- </div>
91
- <div class="panel">
92
- <h2>Tags</h2>
93
- <div class="scroll-window">${renderDefinitionList(snapshot.byTag, key => statusOrTagFacetHref(snapshot, currentPath, 'tag', key))}</div>
94
- </div>
95
- </section>
96
- <section class="panel">
97
- <div class="toolbar">
98
- <h2>Highlighted Clusters</h2>
99
- <div class="actions">
100
- <a class="pill" href="${escapeHtml(exportPageHref(snapshot, currentPath, snapshot.manifest.printHome))}">Printable report</a>
101
- <a class="pill" href="${escapeHtml(exportPageHref(snapshot, currentPath, snapshot.manifest.dataExport))}">Snapshot JSON</a>
102
- </div>
103
- </div>
104
- <div class="entity-list">${highlightClusters}</div>
105
- </section>
106
- `;
107
- return renderShell({
108
- currentPath,
109
- activeNav: 'overview',
110
- title: snapshot.title,
111
- body,
112
- snapshot,
113
- includeGlobalSearch: true
114
- });
115
- }
116
- export function renderIdeasIndexPage(snapshot) {
117
- const currentPath = snapshot.manifest.ideasIndex.path;
118
- const rows = snapshot.ideas.map(idea => `
119
- <tr data-filter-row="ideas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' '), idea.fileUri].join(' '))}">
120
- <td><strong>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</strong></td>
121
- <td>${renderFilePathCell(snapshot, currentPath, idea.fileUri)}</td>
122
- <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
123
- <td>${renderLinkedTagsCell(snapshot, currentPath, idea)}</td>
124
- <td>${renderIdeaSummaryHtml(snapshot, currentPath, idea, '—')}</td>
125
- <td>${idea.references.inbound.length}/${idea.references.outbound.length}</td>
126
- </tr>
127
- `).join('');
128
- return renderShell({
129
- currentPath,
130
- activeNav: 'ideas',
131
- title: `${snapshot.title} - Ideas`,
132
- snapshot,
133
- includeGlobalSearch: true,
134
- body: `
135
- <header class="page-header">
136
- <p class="eyebrow">Ideas index</p>
137
- <h1>${escapeHtml(snapshot.title)}</h1>
138
- <p class="subtle">Searchable list view for all exported ideas.</p>
139
- </header>
140
- <section class="table-shell">
141
- <div class="toolbar">
142
- <h2>Ideas</h2>
143
- <div class="actions">
144
- <input class="searchbar" type="search" placeholder="Filter ideas by name, path, status, tags, or summary" data-filter-input="ideas" />
145
- </div>
146
- </div>
147
- <table>
148
- <thead>
149
- <tr>
150
- <th data-filter-key="name">Name</th>
151
- <th data-filter-key="path">Path</th>
152
- <th data-filter-key="status">Status</th>
153
- <th data-filter-key="tags">Tags</th>
154
- <th data-filter-key="summary">Summary</th>
155
- <th data-filter-key="refs">Refs in/out</th>
156
- </tr>
157
- </thead>
158
- <tbody>${rows}</tbody>
159
- </table>
160
- </section>
161
- `
162
- });
163
- }
164
- export function renderFilesIndexPage(snapshot) {
165
- const currentPath = snapshot.manifest.filesIndex.path;
166
- const rows = snapshot.files.map(file => `
167
- <tr data-filter-row="files" data-filter-text="${escapeHtml([file.name, file.fileUri, Object.keys(file.tags).join(' ')].join(' '))}">
168
- <td><strong>${snapshot.pageOptions.includeFilePages ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, file.page))}">${escapeHtml(file.name)}</a>` : escapeHtml(file.name)}</strong></td>
169
- <td>${escapeHtml(file.directory || '.')}</td>
170
- <td>${file.ideas.length}</td>
171
- <td>${file.edgeCount}</td>
172
- <td>${Object.keys(file.tags).map(tag => {
173
- const href = statusOrTagFacetHref(snapshot, currentPath, 'tag', tag);
174
- return `<a href="${escapeHtml(href)}">${escapeHtml(tag)}</a>`;
175
- }).join(', ') || '—'}</td>
176
- </tr>
177
- `).join('');
178
- return renderShell({
179
- currentPath,
180
- activeNav: 'files',
181
- title: `${snapshot.title} - Files`,
182
- snapshot,
183
- includeGlobalSearch: true,
184
- body: `
185
- <header class="page-header">
186
- <p class="eyebrow">Files index</p>
187
- <h1>${escapeHtml(snapshot.title)}</h1>
188
- <p class="subtle">List view by source file with local counts and drill-down pages.</p>
189
- </header>
190
- <section class="table-shell">
191
- <div class="toolbar">
192
- <h2>Files</h2>
193
- <div class="actions">
194
- <input class="searchbar" type="search" placeholder="Filter files by path or tags" data-filter-input="files" />
195
- </div>
196
- </div>
197
- <table>
198
- <thead>
199
- <tr>
200
- <th data-filter-key="file">File</th>
201
- <th data-filter-key="directory">Directory</th>
202
- <th data-filter-key="ideas">Ideas</th>
203
- <th data-filter-key="references">References</th>
204
- <th data-filter-key="tags">Tags</th>
205
- </tr>
206
- </thead>
207
- <tbody>${rows}</tbody>
208
- </table>
209
- </section>
210
- `
211
- });
212
- }
213
- export function renderCodeFilesIndexPage(snapshot) {
214
- const currentPath = snapshot.manifest.codeFilesIndex.path;
215
- const rows = snapshot.codeFiles.map(file => `
216
- <tr data-filter-row="codeFiles" data-filter-text="${escapeHtml([file.name, file.fileUri, file.labels.join(' ')].join(' '))}">
217
- <td><strong>${snapshot.pageOptions.includeCodeFilePages ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, file.page))}">${escapeHtml(file.name)}</a>` : escapeHtml(file.name)}</strong></td>
218
- <td>${escapeHtml(file.directory || '.')}</td>
219
- <td>${file.referencingIdeaIds.length}</td>
220
- <td>${escapeHtml(file.labels.join(', ') || '—')}</td>
221
- </tr>
222
- `).join('');
223
- return renderShell({
224
- currentPath,
225
- activeNav: 'code-files',
226
- title: `${snapshot.title} - Code files`,
227
- snapshot,
228
- includeGlobalSearch: true,
229
- body: `
230
- <header class="page-header">
231
- <p class="eyebrow">Code reference index</p>
232
- <h1>${escapeHtml(snapshot.title)}</h1>
233
- <p class="subtle">Outbound file_reference targets that are not idea-hosting reqlan files.</p>
234
- </header>
235
- <section class="table-shell">
236
- <div class="toolbar">
237
- <h2>Code files</h2>
238
- <div class="actions">
239
- <input class="searchbar" type="search" placeholder="Filter code files by path or label" data-filter-input="codeFiles" />
240
- </div>
241
- </div>
242
- <table>
243
- <thead>
244
- <tr>
245
- <th data-filter-key="file">File</th>
246
- <th data-filter-key="directory">Directory</th>
247
- <th data-filter-key="referenced-by">Referenced by</th>
248
- <th data-filter-key="labels">Labels</th>
249
- </tr>
250
- </thead>
251
- <tbody>${rows || '<tr><td colspan="4" class="subtle">No outbound code file references.</td></tr>'}</tbody>
252
- </table>
253
- </section>
254
- `
255
- });
256
- }
257
- export function renderClustersIndexPage(snapshot) {
258
- const currentPath = snapshot.manifest.clustersIndex.path;
259
- const rows = snapshot.clusters.map(cluster => `
260
- <tr data-filter-row="clusters" data-filter-text="${escapeHtml([cluster.label, cluster.kind, cluster.description].join(' '))}">
261
- <td><strong>${snapshot.pageOptions.includeClusterPages ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, cluster.page))}">${escapeHtml(cluster.label)}</a>` : escapeHtml(cluster.label)}</strong></td>
262
- <td data-sort-value="${escapeHtml(cluster.kind)}"><a href="${escapeHtml(listFilterHref(snapshot, currentPath, snapshot.manifest.clustersIndex, { kind: cluster.kind }))}">${escapeHtml(cluster.kind)}</a></td>
263
- <td>${cluster.counts.ideas}</td>
264
- <td>${cluster.counts.files}</td>
265
- <td>${escapeHtml(cluster.description)}</td>
266
- </tr>
267
- `).join('');
268
- return renderShell({
269
- currentPath,
270
- activeNav: 'clusters',
271
- title: `${snapshot.title} - Clusters`,
272
- snapshot,
273
- includeGlobalSearch: true,
274
- body: `
275
- <header class="page-header">
276
- <p class="eyebrow">Clusters</p>
277
- <h1>${escapeHtml(snapshot.title)}</h1>
278
- <p class="subtle">Deterministic and computed groupings for easier exploration.</p>
279
- </header>
280
- <section class="table-shell">
281
- <div class="toolbar">
282
- <h2>Clusters</h2>
283
- <div class="actions">
284
- <input class="searchbar" type="search" placeholder="Filter clusters by label, kind, or description" data-filter-input="clusters" />
285
- </div>
286
- </div>
287
- <table>
288
- <thead>
289
- <tr>
290
- <th data-filter-key="cluster">Cluster</th>
291
- <th data-filter-key="kind">Kind</th>
292
- <th data-filter-key="ideas">Ideas</th>
293
- <th data-filter-key="files">Files</th>
294
- <th data-filter-key="description">Description</th>
295
- </tr>
296
- </thead>
297
- <tbody>${rows}</tbody>
298
- </table>
299
- </section>
300
- `
301
- });
302
- }
303
- export function renderAttributesIndexPage(snapshot) {
304
- const currentPath = snapshot.manifest.attributesIndex.path;
305
- const rows = snapshot.attributes.map(attribute => {
306
- const anchor = `attr-${slugAttributeKey(attribute.key)}`;
307
- const valueSummary = attribute.values
308
- .slice(0, 6)
309
- .map(entry => `${entry.value} (${entry.count})`)
310
- .join('; ');
311
- const keyCell = snapshot.pageOptions.includeAttributePages
312
- ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, attribute.page))}">${escapeHtml(attribute.key)}</a>`
313
- : escapeHtml(attribute.key);
314
- return `
315
- <tr id="${escapeHtml(anchor)}" data-filter-row="attributes" data-filter-text="${escapeHtml([attribute.key, valueSummary, attribute.ideaIds.join(' ')].join(' '))}">
316
- <td><strong>${keyCell}</strong></td>
317
- <td>${attribute.ideaCount}</td>
318
- <td>${attribute.values.length}</td>
319
- <td>${escapeHtml(valueSummary || '—')}</td>
320
- </tr>`;
321
- }).join('');
322
- return renderShell({
323
- currentPath,
324
- activeNav: 'attributes',
325
- title: `${snapshot.title} - Attributes`,
326
- snapshot,
327
- includeGlobalSearch: true,
328
- body: `
329
- <header class="page-header">
330
- <p class="eyebrow">Attributes</p>
331
- <h1>${escapeHtml(snapshot.title)}</h1>
332
- <p class="subtle">Every attribute key in this export with values and idea counts.</p>
333
- </header>
334
- <section class="table-shell">
335
- <div class="toolbar">
336
- <h2>Attributes</h2>
337
- <div class="actions">
338
- <input class="searchbar" type="search" placeholder="Filter attributes by key or value" data-filter-input="attributes" />
339
- </div>
340
- </div>
341
- <table>
342
- <thead>
343
- <tr><th data-filter-key="attribute">Attribute</th><th data-filter-key="ideas">Ideas</th><th data-filter-key="values">Values</th><th data-filter-key="value-summary">Value summary</th></tr>
344
- </thead>
345
- <tbody>${rows || '<tr><td colspan="4" class="subtle">No attributes declared.</td></tr>'}</tbody>
346
- </table>
347
- </section>
348
- `
349
- });
350
- }
351
- export function renderAttributeDetailPage(snapshot, attribute) {
352
- const currentPath = attribute.page.path;
353
- const ideas = attribute.ideaIds
354
- .map(ideaId => snapshot.ideasById[ideaId])
355
- .filter((idea) => Boolean(idea));
356
- const totalIdeas = attribute.ideaCount || 1;
357
- const valueRows = attribute.values.map(entry => {
358
- const percent = Math.round((entry.count / totalIdeas) * 1000) / 10;
359
- const width = Math.max(entry.count > 0 ? 1.5 : 0, Math.min(100, (entry.count / totalIdeas) * 100));
360
- const filterText = [entry.value, String(entry.count), `${percent}%`, ...entry.ideaIds].join(' ');
361
- const valueHref = listFilterHref(snapshot, currentPath, attribute.page, { value: entry.value });
362
- return `
363
- <tr data-filter-row="attributeValues" data-filter-text="${escapeHtml(filterText)}">
364
- <td data-sort-value="${escapeHtml(entry.value)}"><a href="${escapeHtml(valueHref)}">${escapeHtml(entry.value)}</a></td>
365
- <td>
366
- <div class="distribution-track" title="${escapeHtml(`${entry.count} of ${attribute.ideaCount} ideas (${percent}%)`)}" role="img" aria-label="${escapeHtml(`${entry.value}: ${entry.count} ideas, ${percent}%`)}">
367
- <span class="distribution-fill" style="width: ${width}%"></span>
368
- </div>
369
- </td>
370
- <td>${entry.count}</td>
371
- <td>${percent}%</td>
372
- </tr>`;
373
- }).join('');
374
- const ideaRows = ideas.map(idea => {
375
- const value = formatAttributeValue(idea.attributes[attribute.key]) || '(empty)';
376
- const filterText = [idea.name, value, ideaStatus(idea) ?? '', ideaTags(idea).join(' '), idea.summary].join(' ');
377
- return `
378
- <tr data-filter-row="attributeIdeas" data-filter-text="${escapeHtml(filterText)}">
379
- <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
380
- <td data-sort-value="${escapeHtml(value)}"><a href="${escapeHtml(listFilterHref(snapshot, currentPath, attribute.page, { value }))}">${escapeHtml(value)}</a></td>
381
- <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
382
- <td>${renderLinkedTagsCell(snapshot, currentPath, idea)}</td>
383
- <td>${escapeHtml(idea.summary)}</td>
384
- </tr>`;
385
- }).join('');
386
- return renderShell({
387
- currentPath,
388
- activeNav: 'attributes',
389
- title: `${snapshot.title} - ${attribute.key}`,
390
- snapshot,
391
- includeGlobalSearch: snapshot.runtimeMode === 'interactive',
392
- breadcrumbs: [
393
- { label: 'Attributes', href: exportPageHref(snapshot, currentPath, snapshot.manifest.attributesIndex) },
394
- { label: attribute.key }
395
- ],
396
- body: `
397
- <header class="page-header">
398
- <p class="eyebrow">Attribute detail</p>
399
- <h1>${escapeHtml(attribute.key)}</h1>
400
- <p class="subtle">${attribute.ideaCount} ideas · ${attribute.values.length} distinct values</p>
401
- </header>
402
- <section class="grid">
403
- ${renderMetric('Ideas', String(attribute.ideaCount))}
404
- ${renderMetric('Values', String(attribute.values.length))}
405
- </section>
406
- <section class="table-shell">
407
- <div class="toolbar">
408
- <h2>Value distribution</h2>
409
- <div class="actions">
410
- <input class="searchbar" type="search" placeholder="Filter values" data-filter-input="attributeValues" />
411
- </div>
412
- </div>
413
- <div class="scroll-window">
414
- <table>
415
- <thead><tr><th data-filter-key="value">Value</th><th data-filter-key="distribution">Distribution</th><th data-filter-key="ideas">Ideas</th><th data-filter-key="share">Share</th></tr></thead>
416
- <tbody>${valueRows || '<tr><td colspan="4" class="subtle">No values.</td></tr>'}</tbody>
417
- </table>
418
- </div>
419
- </section>
420
- <section class="table-shell">
421
- <div class="toolbar">
422
- <h2>Ideas with this attribute</h2>
423
- <div class="actions">
424
- <input class="searchbar" type="search" placeholder="Filter ideas by name, value, status, or tags" data-filter-input="attributeIdeas" />
425
- </div>
426
- </div>
427
- <table>
428
- <thead><tr><th data-filter-key="idea">Idea</th><th data-filter-key="value">Value</th><th data-filter-key="status">Status</th><th data-filter-key="tags">Tags</th><th data-filter-key="summary">Summary</th></tr></thead>
429
- <tbody>${ideaRows || '<tr><td colspan="5" class="subtle">No ideas declare this attribute.</td></tr>'}</tbody>
430
- </table>
431
- </section>
432
- `
433
- });
434
- }
435
- export function renderGraphPage(snapshot) {
436
- const currentPath = snapshot.manifest.graph.path;
437
- const graph = enrichGraphUrls(snapshot, snapshot.graphs.workspace, currentPath);
438
- const statuses = snapshot.byStatus;
439
- const tags = snapshot.byTag;
440
- const clusters = snapshot.clusters.slice(0, 16).map(cluster => snapshot.pageOptions.includeClusterPages
441
- ? `<a class="pill" href="${escapeHtml(exportPageHref(snapshot, currentPath, cluster.page))}">${escapeHtml(cluster.label)}</a>`
442
- : `<span class="pill">${escapeHtml(cluster.label)}</span>`).join('');
443
- return renderShell({
444
- currentPath,
445
- activeNav: 'graph',
446
- title: `${snapshot.title} - Graph`,
447
- snapshot,
448
- includeGlobalSearch: snapshot.runtimeMode === 'interactive',
449
- body: `
450
- <header class="page-header">
451
- <p class="eyebrow">Graph</p>
452
- <h1>${escapeHtml(snapshot.title)}</h1>
453
- <p class="subtle">Workspace graph with links into idea detail pages.</p>
454
- </header>
455
- <section class="graph-shell">
456
- <div class="toolbar">
457
- <h2>Reference graph</h2>
458
- <div class="actions">
459
- <a class="pill" href="${escapeHtml(exportPageHref(snapshot, currentPath, snapshot.manifest.dataGraph))}">Graph JSON</a>
460
- </div>
461
- </div>
462
- <div class="graph-controls-bar" data-graph-controls="graph-data">
463
- <input class="searchbar graph-filter" type="search" placeholder="Search graph nodes" data-graph-search />
464
- <input class="searchbar graph-filter" type="search" placeholder="Path filter" data-graph-path />
465
- ${renderMultiFilterSelect('status', statuses, 'Filter by status')}
466
- ${renderMultiFilterSelect('tag', tags, 'Filter by tag')}
467
- <button type="button" class="graph-action" data-graph-toggle-external="true">Hide external</button>
468
- <button type="button" class="graph-action" data-graph-toggle-ideasets="true">Hide ideasets</button>
469
- <label class="graph-file-treatment">
470
- <span class="visually-hidden">File treatment</span>
471
- <select class="graph-action graph-select" data-graph-file-treatment aria-label="Hosting .rq file treatment" title="How hosting .rq files appear in the graph">
472
- <option value="invisible" title="Do not show hosting .rq files — ideas float freely.">Files: hidden</option>
473
- <option value="compound" title="Draw each hosting .rq file as a container. Drag the box; click the title to open the file.">Files: compound</option>
474
- <option value="linked" selected title="Show each hosting .rq file as a linked ideaset node. Click the node to open the file.">Files: linked</option>
475
- </select>
476
- </label>
477
- <button type="button" class="graph-action is-active" data-graph-toggle-wildcard aria-pressed="true" title="Show edges expanded from path+idea wildcard references">Wildcard refs</button>
478
- <button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button>
479
- <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
480
- <button type="button" class="graph-action" data-graph-fit>Fit</button>
481
- <button type="button" class="graph-action" data-graph-reset>Reset</button>
482
- <span class="graph-status" data-graph-status-text></span>
483
- </div>
484
- <div class="pill-row">${clusters}</div>
485
- <div class="graph-root is-booting" data-graph-json="graph-data">
486
- <div class="graph-boot" data-graph-boot role="status" aria-live="polite" aria-busy="true">
487
- <span class="graph-boot-spinner" aria-hidden="true"></span>
488
- <p>Initialising graph…</p>
489
- </div>
490
- </div>
491
- <script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
492
- </section>
493
- `
494
- });
495
- }
496
- export function renderIdeaDetailPage(snapshot, idea) {
497
- const currentPath = idea.page.path;
498
- const clusters = relatedClusters(snapshot, idea);
499
- const parentFile = fileByIdea(snapshot, idea);
500
- const graph = enrichGraphUrls(snapshot, snapshot.graphs.byIdeaId[idea.id], currentPath);
501
- return renderShell({
502
- currentPath,
503
- activeNav: 'ideas',
504
- title: `${snapshot.title} - ${idea.name}`,
505
- snapshot,
506
- includeGlobalSearch: snapshot.runtimeMode === 'interactive',
507
- breadcrumbs: [
508
- { label: 'Ideas', href: exportPageHref(snapshot, currentPath, snapshot.manifest.ideasIndex) },
509
- { label: idea.name }
510
- ],
511
- body: `
512
- <header class="page-header">
513
- <p class="eyebrow">Idea detail</p>
514
- <h1 id="summary">${escapeHtml(idea.name)}</h1>
515
- <p class="subtle">${escapeHtml(idea.fileUri)}:${idea.lineStart + 1}</p>
516
- <div class="pill-row">
517
- <span class="pill">${escapeHtml(idea.kind)}</span>
518
- ${ideaStatus(idea) ? `<span class="pill">${escapeHtml(ideaStatus(idea))}</span>` : ''}
519
- ${ideaTags(idea).map(tag => `<span class="pill">${escapeHtml(tag)}</span>`).join('')}
520
- </div>
521
- </header>
522
- <section class="detail-grid">
523
- <div class="panel prose print-break-avoid">
524
- <h2>Summary</h2>
525
- <p class="idea-summary">${renderIdeaSummaryHtml(snapshot, currentPath, idea)}</p>
526
- <p><a class="section-link" href="#references-out">Jump to outbound references</a></p>
527
- <p><a class="section-link" href="#references-in">Jump to inbound references</a></p>
528
- </div>
529
- <div class="panel print-break-avoid">
530
- <h2>Navigation</h2>
531
- <div class="entity-list">
532
- ${parentFile ? renderMaybeLinkedCard(snapshot.pageOptions.includeFilePages, exportPageHref(snapshot, currentPath, parentFile.page), 'Source file', parentFile.fileUri) : ''}
533
- ${clusters.map(cluster => renderMaybeLinkedCard(snapshot.pageOptions.includeClusterPages, exportPageHref(snapshot, currentPath, cluster.page), cluster.label, `${cluster.kind} cluster`)).join('')}
534
- ${idea.page.printablePath && snapshot.pageOptions.includePrintPages ? `<a class="entity-card" href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.printablePath))}"><strong>Printable page</strong><p class="subtle">Static print-friendly idea sheet.</p></a>` : ''}
535
- </div>
536
- </div>
537
- </section>
538
- <section class="split">
539
- <div class="panel print-break-avoid" id="attributes">
540
- <h2>Attributes</h2>
541
- ${renderIdeaAttributes(snapshot, currentPath, idea)}
542
- </div>
543
- <div class="panel print-break-avoid">
544
- <h2>Ancestor context</h2>
545
- <div class="scroll-window">
546
- <div class="entity-list">
547
- ${idea.ancestors.ancestors.map(ancestor => {
548
- const linked = snapshot.ideasById[ancestor.id];
549
- return linked
550
- ? renderMaybeLinkedCard(snapshot.pageOptions.includeIdeaPages, exportPageHref(snapshot, currentPath, linked.page), ancestor.name, ancestor.summary)
551
- : '';
552
- }).join('') || '<p class="subtle">No ancestor chain recorded.</p>'}
553
- </div>
554
- </div>
555
- </div>
556
- </section>
557
- ${renderReferenceSection(snapshot, currentPath, 'references-out', 'Outbound references', idea.references.outbound)}
558
- ${renderReferenceSection(snapshot, currentPath, 'references-in', 'Inbound references', idea.references.inbound)}
559
- ${renderReferenceSection(snapshot, currentPath, 'references-unresolved', 'Unresolved references', idea.references.unresolved)}
560
- <section class="graph-shell print-break-avoid" id="graph">
561
- <div class="toolbar"><h2>Local graph</h2></div>
562
- <div class="graph-controls-bar" data-graph-controls="graph-data">
563
- <button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button>
564
- <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
565
- <button type="button" class="graph-action" data-graph-fit>Fit</button>
566
- <span class="graph-status" data-graph-status-text></span>
567
- </div>
568
- <div class="graph-root is-booting" data-graph-json="graph-data">
569
- <div class="graph-boot" data-graph-boot role="status" aria-live="polite" aria-busy="true">
570
- <span class="graph-boot-spinner" aria-hidden="true"></span>
571
- <p>Initialising graph…</p>
572
- </div>
573
- </div>
574
- <script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
575
- </section>
576
- `
577
- });
578
- }
579
- export function renderFileDetailPage(snapshot, file) {
580
- const currentPath = file.page.path;
581
- const graph = enrichGraphUrls(snapshot, snapshot.graphs.byFileId[file.id], currentPath);
582
- const clusterLinks = snapshot.clusters.filter(cluster => cluster.fileUris.includes(file.fileUri)).slice(0, 12);
583
- return renderShell({
584
- currentPath,
585
- activeNav: 'files',
586
- title: `${snapshot.title} - ${file.name}`,
587
- snapshot,
588
- includeGlobalSearch: snapshot.runtimeMode === 'interactive',
589
- breadcrumbs: [
590
- { label: 'Files', href: exportPageHref(snapshot, currentPath, snapshot.manifest.filesIndex) },
591
- { label: file.name }
592
- ],
593
- body: `
594
- <header class="page-header">
595
- <p class="eyebrow">File detail</p>
596
- <h1>${escapeHtml(file.name)}</h1>
597
- <p class="subtle">${escapeHtml(file.fileUri)}</p>
598
- </header>
599
- <section class="grid">
600
- ${renderMetric('Ideas', String(file.ideas.length))}
601
- ${renderMetric('References', String(file.edgeCount))}
602
- ${renderMetric('Statuses', String(Object.keys(file.statuses).length))}
603
- ${renderMetric('Tags', String(Object.keys(file.tags).length))}
604
- </section>
605
- <section class="split">
606
- <div class="table-shell">
607
- <div class="toolbar">
608
- <h2>Ideas in file</h2>
609
- <div class="actions">
610
- <input class="searchbar" type="search" placeholder="Filter file ideas" data-filter-input="fileIdeas" />
611
- </div>
612
- </div>
613
- <div class="scroll-window">
614
- <table>
615
- <thead><tr><th data-filter-key="idea">Idea</th><th data-filter-key="status">Status</th><th data-filter-key="tags">Tags</th><th data-filter-key="summary">Summary</th></tr></thead>
616
- <tbody>
617
- ${file.ideas.map(idea => `
618
- <tr data-filter-row="fileIdeas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' ')].join(' '))}">
619
- <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
620
- <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
621
- <td>${renderLinkedTagsCell(snapshot, currentPath, idea)}</td>
622
- <td>${escapeHtml(idea.summary)}</td>
623
- </tr>
624
- `).join('')}
625
- </tbody>
626
- </table>
627
- </div>
628
- </div>
629
- <div class="panel">
630
- <h2>Related clusters</h2>
631
- <div class="scroll-window">
632
- <div class="entity-list">
633
- ${clusterLinks.map(cluster => renderMaybeLinkedCard(snapshot.pageOptions.includeClusterPages, exportPageHref(snapshot, currentPath, cluster.page), cluster.label, cluster.kind)).join('') || '<p class="subtle">No related clusters.</p>'}
634
- </div>
635
- </div>
636
- </div>
637
- </section>
638
- <section class="graph-shell print-break-avoid">
639
- <div class="toolbar"><h2>Local graph</h2></div>
640
- <div class="graph-controls-bar" data-graph-controls="graph-data">
641
- <button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button>
642
- <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
643
- <button type="button" class="graph-action" data-graph-fit>Fit</button>
644
- <span class="graph-status" data-graph-status-text></span>
645
- </div>
646
- <div class="graph-root is-booting" data-graph-json="graph-data">
647
- <div class="graph-boot" data-graph-boot role="status" aria-live="polite" aria-busy="true">
648
- <span class="graph-boot-spinner" aria-hidden="true"></span>
649
- <p>Initialising graph…</p>
650
- </div>
651
- </div>
652
- <script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
653
- </section>
654
- `
655
- });
656
- }
657
- export function renderCodeFileDetailPage(snapshot, file) {
658
- const currentPath = file.page.path;
659
- const referencingIdeas = file.referencingIdeaIds
660
- .map(ideaId => snapshot.ideasById[ideaId])
661
- .filter((idea) => Boolean(idea));
662
- return renderShell({
663
- currentPath,
664
- activeNav: 'code-files',
665
- title: `${snapshot.title} - ${file.name}`,
666
- snapshot,
667
- includeGlobalSearch: snapshot.runtimeMode === 'interactive',
668
- breadcrumbs: [
669
- { label: 'Code files', href: exportPageHref(snapshot, currentPath, snapshot.manifest.codeFilesIndex) },
670
- { label: file.name }
671
- ],
672
- body: `
673
- <header class="page-header">
674
- <p class="eyebrow">Code reference detail</p>
675
- <h1>${escapeHtml(file.name)}</h1>
676
- <p class="subtle">${escapeHtml(file.fileUri)}</p>
677
- </header>
678
- <section class="grid">
679
- ${renderMetric('Referenced by', String(referencingIdeas.length))}
680
- ${renderMetric('Labels', String(file.labels.length))}
681
- </section>
682
- <section class="table-shell">
683
- <div class="toolbar">
684
- <h2>Referencing ideas</h2>
685
- <div class="actions">
686
- <input class="searchbar" type="search" placeholder="Filter referencing ideas" data-filter-input="codeFileIdeas" />
687
- </div>
688
- </div>
689
- <div class="scroll-window">
690
- <table>
691
- <thead><tr><th data-filter-key="idea">Idea</th><th data-filter-key="status">Status</th><th data-filter-key="tags">Tags</th><th data-filter-key="summary">Summary</th></tr></thead>
692
- <tbody>
693
- ${referencingIdeas.map(idea => `
694
- <tr data-filter-row="codeFileIdeas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' ')].join(' '))}">
695
- <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
696
- <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
697
- <td>${renderLinkedTagsCell(snapshot, currentPath, idea)}</td>
698
- <td>${escapeHtml(idea.summary)}</td>
699
- </tr>
700
- `).join('') || '<tr><td colspan="4" class="subtle">No referencing ideas.</td></tr>'}
701
- </tbody>
702
- </table>
703
- </div>
704
- </section>
705
- ${file.labels.length > 0 ? `
706
- <section class="panel">
707
- <h2>Reference labels</h2>
708
- <p>${escapeHtml(file.labels.join(', '))}</p>
709
- </section>
710
- ` : ''}
711
- `
712
- });
713
- }
714
- export function renderClusterDetailPage(snapshot, cluster) {
715
- const currentPath = cluster.page.path;
716
- const graph = enrichGraphUrls(snapshot, snapshot.graphs.byClusterId[cluster.id], currentPath);
717
- const members = cluster.ideaIds
718
- .map(ideaId => snapshot.ideasById[ideaId])
719
- .filter((idea) => Boolean(idea));
720
- return renderShell({
721
- currentPath,
722
- activeNav: 'clusters',
723
- title: `${snapshot.title} - ${cluster.label}`,
724
- snapshot,
725
- includeGlobalSearch: snapshot.runtimeMode === 'interactive',
726
- breadcrumbs: [
727
- { label: 'Clusters', href: exportPageHref(snapshot, currentPath, snapshot.manifest.clustersIndex) },
728
- { label: cluster.label }
729
- ],
730
- body: `
731
- <header class="page-header">
732
- <p class="eyebrow">Cluster detail</p>
733
- <h1>${escapeHtml(cluster.label)}</h1>
734
- <p class="subtle">${escapeHtml(cluster.description)}</p>
735
- <div class="pill-row">
736
- <span class="pill">${escapeHtml(cluster.kind)}</span>
737
- <span class="pill">${cluster.counts.ideas} ideas</span>
738
- <span class="pill">${cluster.counts.files} files</span>
739
- </div>
740
- </header>
741
- <section class="table-shell">
742
- <div class="toolbar">
743
- <h2>Members</h2>
744
- <div class="actions">
745
- <input class="searchbar" type="search" placeholder="Filter cluster members" data-filter-input="clusterIdeas" />
746
- </div>
747
- </div>
748
- <div class="scroll-window">
749
- <table>
750
- <thead><tr><th data-filter-key="idea">Idea</th><th data-filter-key="path">File</th><th data-filter-key="status">Status</th><th data-filter-key="summary">Summary</th></tr></thead>
751
- <tbody>
752
- ${members.map(idea => `
753
- <tr data-filter-row="clusterIdeas" data-filter-text="${escapeHtml([idea.name, idea.fileUri, ideaStatus(idea) ?? '', idea.summary].join(' '))}">
754
- <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
755
- <td>${renderFilePathCell(snapshot, currentPath, idea.fileUri)}</td>
756
- <td>${renderLinkedStatusCell(snapshot, currentPath, idea)}</td>
757
- <td>${escapeHtml(idea.summary)}</td>
758
- </tr>
759
- `).join('')}
760
- </tbody>
761
- </table>
762
- </div>
763
- </section>
764
- <section class="graph-shell print-break-avoid">
765
- <div class="toolbar"><h2>Cluster graph</h2></div>
766
- <div class="graph-controls-bar" data-graph-controls="graph-data">
767
- <button type="button" class="graph-action is-active" data-graph-toggle-labels data-label-mode="auto" aria-pressed="mixed">Labels: auto</button>
768
- <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
769
- <button type="button" class="graph-action" data-graph-fit>Fit</button>
770
- <span class="graph-status" data-graph-status-text></span>
771
- </div>
772
- <div class="graph-root is-booting" data-graph-json="graph-data">
773
- <div class="graph-boot" data-graph-boot role="status" aria-live="polite" aria-busy="true">
774
- <span class="graph-boot-spinner" aria-hidden="true"></span>
775
- <p>Initialising graph…</p>
776
- </div>
777
- </div>
778
- <script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
779
- </section>
780
- `
781
- });
782
- }
783
- export function renderPrintHomePage(snapshot) {
784
- const currentPath = snapshot.manifest.printHome.path;
785
- const samePageAnchors = { samePageIdeaAnchors: true };
786
- const cards = snapshot.ideas.map(idea => {
787
- const anchorId = exportIdeaAnchorId(idea);
788
- return `
789
- <article class="print-card print-break-avoid" id="${escapeHtml(anchorId)}">
790
- <h3><a href="#${escapeHtml(anchorId)}">${escapeHtml(idea.name)}</a></h3>
791
- <p class="subtle">${escapeHtml(idea.fileUri)}</p>
792
- <p>${renderIdeaSummaryHtml(snapshot, currentPath, idea, '—', samePageAnchors)}</p>
793
- ${renderPrintIdeaAttributesHtml(snapshot, currentPath, idea, samePageAnchors)}
794
- </article>
795
- `;
796
- }).join('');
797
- return renderShell({
798
- currentPath,
799
- activeNav: 'print',
800
- title: `${snapshot.title} - Print`,
801
- snapshot,
802
- body: `
803
- <header class="page-header">
804
- <p class="eyebrow">Printable report</p>
805
- <h1>${escapeHtml(snapshot.title)}</h1>
806
- <p class="subtle">Generated ${escapeHtml(formatDate(snapshot.generatedAt))}</p>
807
- <div class="toolbar">
808
- <p class="subtle">${snapshot.ideas.length} ideas</p>
809
- <div class="actions">
810
- <button type="button" class="print-button hide-on-print" onclick="window.print()">Print</button>
811
- </div>
812
- </div>
813
- </header>
814
- <section class="grid">
815
- ${renderMetric('Ideas', String(snapshot.counts.ideas))}
816
- ${renderMetric('References', String(snapshot.counts.edges))}
817
- ${renderMetric('Files', String(snapshot.counts.files))}
818
- ${renderMetric('Clusters', String(snapshot.counts.clusters))}
819
- </section>
820
- <section class="panel print-break-avoid">
821
- <h2>Status rollup</h2>
822
- <div class="scroll-window">${renderDefinitionList(snapshot.byStatus)}</div>
823
- </section>
824
- <section class="entity-list">${cards}</section>
825
- `
826
- });
827
- }
828
- export function renderPrintIdeaPage(snapshot, idea) {
829
- const currentPath = idea.page.printablePath ?? snapshot.manifest.printHome.path;
830
- return renderShell({
831
- currentPath,
832
- activeNav: 'print',
833
- title: `${snapshot.title} - ${idea.name} print`,
834
- snapshot,
835
- body: `
836
- <header class="page-header">
837
- <p class="eyebrow">Printable idea sheet</p>
838
- <h1 id="${escapeHtml(exportIdeaAnchorId(idea))}">${escapeHtml(idea.name)}</h1>
839
- <p class="subtle">${escapeHtml(idea.fileUri)}:${idea.lineStart + 1}</p>
840
- <div class="toolbar">
841
- <div></div>
842
- <div class="actions">
843
- <button type="button" class="print-button hide-on-print" onclick="window.print()">Print</button>
844
- </div>
845
- </div>
846
- </header>
847
- <section class="print-card print-break-avoid">
848
- <p class="idea-summary">${renderIdeaSummaryHtml(snapshot, currentPath, idea)}</p>
849
- ${renderPrintIdeaAttributesHtml(snapshot, currentPath, idea)}
850
- ${snapshot.pageOptions.includeIdeaPages ? `<p><strong>Interactive page:</strong> <a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a></p>` : ''}
851
- </section>
852
- `
853
- });
854
- }
855
- export function renderPrintFilePage(snapshot, file) {
856
- const currentPath = file.printPage.path;
857
- return renderShell({
858
- currentPath,
859
- activeNav: 'print',
860
- title: `${snapshot.title} - ${file.name} print`,
861
- snapshot,
862
- body: `
863
- <header class="page-header">
864
- <p class="eyebrow">Printable file sheet</p>
865
- <h1>${escapeHtml(file.name)}</h1>
866
- <p class="subtle">${escapeHtml(file.fileUri)}</p>
867
- </header>
868
- <section class="table-shell">
869
- <table>
870
- <thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
871
- <tbody>
872
- ${file.ideas.map(idea => `
873
- <tr>
874
- <td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
875
- <td>${renderOptionalStatusCell(idea)}</td>
876
- <td>${renderOptionalTagsCell(idea)}</td>
877
- <td>${escapeHtml(idea.summary)}</td>
878
- </tr>
879
- `).join('')}
880
- </tbody>
881
- </table>
882
- </section>
883
- `
884
- });
885
- }
886
- export function renderPrintCodeFilePage(snapshot, file) {
887
- const currentPath = file.printPage.path;
888
- const referencingIdeas = file.referencingIdeaIds
889
- .map(ideaId => snapshot.ideasById[ideaId])
890
- .filter((idea) => Boolean(idea));
891
- return renderShell({
892
- currentPath,
893
- activeNav: 'print',
894
- title: `${snapshot.title} - ${file.name} print`,
895
- snapshot,
896
- body: `
897
- <header class="page-header">
898
- <p class="eyebrow">Printable code reference sheet</p>
899
- <h1>${escapeHtml(file.name)}</h1>
900
- <p class="subtle">${escapeHtml(file.fileUri)}</p>
901
- </header>
902
- <section class="table-shell">
903
- <table>
904
- <thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
905
- <tbody>
906
- ${referencingIdeas.map(idea => `
907
- <tr>
908
- <td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
909
- <td>${renderOptionalStatusCell(idea)}</td>
910
- <td>${renderOptionalTagsCell(idea)}</td>
911
- <td>${escapeHtml(idea.summary)}</td>
912
- </tr>
913
- `).join('') || '<tr><td colspan="4" class="subtle">No referencing ideas.</td></tr>'}
914
- </tbody>
915
- </table>
916
- </section>
917
- `
918
- });
919
- }
920
- export function renderPrintClusterPage(snapshot, cluster) {
921
- const currentPath = cluster.page.printablePath ?? snapshot.manifest.printHome.path;
922
- const members = cluster.ideaIds
923
- .map(ideaId => snapshot.ideasById[ideaId])
924
- .filter((idea) => Boolean(idea));
925
- return renderShell({
926
- currentPath,
927
- activeNav: 'print',
928
- title: `${snapshot.title} - ${cluster.label} print`,
929
- snapshot,
930
- body: `
931
- <header class="page-header">
932
- <p class="eyebrow">Printable cluster sheet</p>
933
- <h1>${escapeHtml(cluster.label)}</h1>
934
- <p class="subtle">${escapeHtml(cluster.description)}</p>
935
- </header>
936
- <section class="table-shell">
937
- <table>
938
- <thead><tr><th>Idea</th><th>File</th><th>Status</th><th>Summary</th></tr></thead>
939
- <tbody>
940
- ${members.map(idea => `
941
- <tr>
942
- <td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
943
- <td>${escapeHtml(idea.fileUri)}</td>
944
- <td>${renderOptionalStatusCell(idea)}</td>
945
- <td>${escapeHtml(idea.summary)}</td>
946
- </tr>
947
- `).join('')}
948
- </tbody>
949
- </table>
950
- </section>
951
- `
952
- });
953
- }
954
- function renderReferenceSection(snapshot, currentPath, id, title, rows) {
955
- return `
956
- <section class="table-shell print-break-avoid" id="${escapeHtml(id)}">
957
- <div class="toolbar"><h2>${escapeHtml(title)}</h2></div>
958
- <div class="scroll-window">
959
- <table>
960
- <thead><tr><th>Kind</th><th>Idea</th><th>Path</th><th>Snippet</th></tr></thead>
961
- <tbody>
962
- ${rows.map(row => {
963
- const linked = row.direction === 'inbound'
964
- ? snapshot.ideasById[row.sourceIdeaId]
965
- : (row.targetIdeaId ? snapshot.ideasById[row.targetIdeaId] : undefined);
966
- const label = row.direction === 'inbound' ? row.label || row.targetName : row.targetName;
967
- const fileTarget = row.direction === 'outbound' && !linked
968
- ? resolveExportFilePage(snapshot, { fileUri: row.targetPath })
969
- : undefined;
970
- const pathCell = fileTarget && filePageEnabled(snapshot, fileTarget.kind)
971
- ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, fileTarget.page))}">${escapeHtml(row.targetPath)}</a>`
972
- : escapeHtml(row.targetPath);
973
- const ideaCell = linked
974
- ? renderIdeaAnchor(snapshot, currentPath, linked, linked.name)
975
- : (fileTarget && filePageEnabled(snapshot, fileTarget.kind)
976
- ? `<a class="idea-ref idea-ref--file" href="${escapeHtml(exportPageHref(snapshot, currentPath, fileTarget.page))}">${escapeHtml(label)}</a>`
977
- : escapeHtml(label));
978
- const snippetCell = row.snippet
979
- ? renderTextWithRefsHtml(snapshot, currentPath, row.snippet, { idea: snapshot.ideasById[row.sourceIdeaId] })
980
- : '—';
981
- return `
982
- <tr>
983
- <td>${escapeHtml(row.kind)}</td>
984
- <td>${ideaCell}</td>
985
- <td>${pathCell}</td>
986
- <td>${snippetCell}</td>
987
- </tr>`;
988
- }).join('') || '<tr><td colspan="4" class="subtle">None</td></tr>'}
989
- </tbody>
990
- </table>
991
- </div>
992
- </section>
993
- `;
994
- }
995
- function enrichGraphUrls(snapshot, graph, _currentPath) {
996
- if (!graph || typeof graph !== 'object') {
997
- return graph;
998
- }
999
- // Use export-root-relative idea.page.url (e.g. ./ideas/...) so nested pages can
1000
- // resolve via resolveExportUrl without dropping path segments like ideas/.
1001
- const copy = structuredClone(graph);
1002
- const centerId = typeof copy.centerId === 'string' ? copy.centerId : undefined;
1003
- for (const node of copy.nodes ?? []) {
1004
- const idea = typeof node.id === 'string' ? snapshot.ideasById[node.id] : undefined;
1005
- if (idea) {
1006
- if (snapshot.pageOptions.includeIdeaPages) {
1007
- node.pageUrl = idea.page.url;
1008
- }
1009
- const tags = ideaTags(idea);
1010
- if (tags.length > 0) {
1011
- node.tags = tags;
1012
- }
1013
- else {
1014
- delete node.tags;
1015
- }
1016
- const status = ideaStatus(idea);
1017
- if (status) {
1018
- node.status = status;
1019
- }
1020
- else {
1021
- delete node.status;
1022
- }
1023
- node.statusKey = idea.statusKey;
1024
- node.tagsKeys = idea.tagsKeys;
1025
- node.attributes = idea.attributes;
1026
- const attributeKeys = Object.keys(idea.attributes).sort();
1027
- if (attributeKeys.length > 0) {
1028
- node.attributeKeys = attributeKeys;
1029
- }
1030
- else {
1031
- delete node.attributeKeys;
1032
- }
1033
- const hostFile = resolveExportFilePage(snapshot, { fileUri: idea.fileUri });
1034
- if (hostFile && filePageEnabled(snapshot, hostFile.kind)) {
1035
- node.hostFilePageUrl = hostFile.page.url;
1036
- }
1037
- else {
1038
- delete node.hostFilePageUrl;
1039
- }
1040
- }
1041
- else {
1042
- const fileTarget = resolveExportFilePage(snapshot, {
1043
- id: typeof node.id === 'string' ? node.id : undefined,
1044
- fileUri: typeof node.fileUri === 'string' ? node.fileUri : undefined
1045
- });
1046
- if (fileTarget && filePageEnabled(snapshot, fileTarget.kind)) {
1047
- node.pageUrl = fileTarget.page.url;
1048
- }
1049
- }
1050
- if (centerId && node.id === centerId) {
1051
- node.isSubject = true;
1052
- }
1053
- }
1054
- return copy;
1055
- }
1056
- function renderIdeaAttributes(snapshot, currentPath, idea) {
1057
- const entries = Object.entries(idea.attributes);
1058
- if (entries.length === 0) {
1059
- return '<p class="subtle">No attributes declared.</p>';
1060
- }
1061
- const rows = entries.map(([key, value]) => {
1062
- const attribute = snapshot.attributesByKey[key];
1063
- const href = attribute && snapshot.pageOptions.includeAttributePages
1064
- ? exportPageHref(snapshot, currentPath, attribute.page)
1065
- : `${exportPageHref(snapshot, currentPath, snapshot.manifest.attributesIndex)}#attr-${slugAttributeKey(key)}`;
1066
- return `
1067
- <tr>
1068
- <td><a href="${escapeHtml(href)}">${escapeHtml(key)}</a></td>
1069
- <td>${renderAttributeValueHtml(snapshot, currentPath, value, idea)}</td>
1070
- </tr>`;
1071
- }).join('');
1072
- return `
1073
- <div class="scroll-window">
1074
- <table>
1075
- <thead><tr><th>Key</th><th>Value</th></tr></thead>
1076
- <tbody>${rows}</tbody>
1077
- </table>
1078
- </div>
1079
- <p class="subtle"><a href="${escapeHtml(exportPageHref(snapshot, currentPath, snapshot.manifest.attributesIndex))}">Browse all attributes</a></p>
1080
- `;
1081
- }
1082
- function renderShell(options) {
1083
- const stylesheetHref = exportHref(options.snapshot, options.currentPath, 'assets/styles.css');
1084
- const appHref = exportHref(options.snapshot, options.currentPath, 'assets/app.js');
1085
- const searchIndexScriptHref = exportHref(options.snapshot, options.currentPath, 'assets/search-index.js');
1086
- const searchHref = exportHref(options.snapshot, options.currentPath, options.snapshot.manifest.dataSearch.path);
1087
- const includeGlobalSearch = options.includeGlobalSearch && options.snapshot.runtimeMode === 'interactive';
1088
- const headerLink = options.snapshot.headerLink
1089
- ? `<a class="brand-link" href="${escapeHtml(options.snapshot.headerLink.href)}">${escapeHtml(options.snapshot.headerLink.label)}</a>`
1090
- : '';
1091
- const navLinks = [
1092
- navLink(options, 'overview', options.snapshot.manifest.home, true),
1093
- navLink(options, 'ideas', options.snapshot.manifest.ideasIndex, true),
1094
- navLink(options, 'files', options.snapshot.manifest.filesIndex, options.snapshot.pageOptions.includeFilePages),
1095
- navLink(options, 'code-files', options.snapshot.manifest.codeFilesIndex, options.snapshot.pageOptions.includeCodeFilePages),
1096
- navLink(options, 'clusters', options.snapshot.manifest.clustersIndex, options.snapshot.pageOptions.includeClusterPages),
1097
- navLink(options, 'attributes', options.snapshot.manifest.attributesIndex, true),
1098
- navLink(options, 'graph', options.snapshot.manifest.graph, options.snapshot.pageOptions.includeGraphPage && options.snapshot.runtimeMode !== 'print'),
1099
- navLink(options, 'print', options.snapshot.manifest.printHome, options.snapshot.pageOptions.includePrintPages)
1100
- ].filter(Boolean).join('');
1101
- const breadcrumbs = options.breadcrumbs?.length
1102
- ? `<nav class="breadcrumbs">${options.breadcrumbs.map(item => item.href ? `<a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a>` : `<span>${escapeHtml(item.label)}</span>`).join('<span>/</span>')}</nav>`
1103
- : '';
1104
- const interactiveScripts = options.snapshot.runtimeMode === 'print'
1105
- ? ''
1106
- : `
1107
- <script src="${escapeHtml(searchIndexScriptHref)}"></script>
1108
- <script src="${escapeHtml(appHref)}"></script>`;
1109
- return `<!DOCTYPE html>
1110
- <html lang="en">
1111
- <head>
1112
- <meta charset="utf-8" />
1113
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1114
- <title>${escapeHtml(options.title)}</title>
1115
- <link rel="stylesheet" href="${escapeHtml(stylesheetHref)}" />
1116
- </head>
1117
- <body data-runtime-mode="${escapeHtml(options.snapshot.runtimeMode)}" data-search-index="${escapeHtml(searchHref)}">
1118
- <main class="layout">
1119
- <header class="topbar">
1120
- <div class="topbar-inner">
1121
- <div class="nav">${headerLink}${navLinks}</div>
1122
- ${includeGlobalSearch ? `
1123
- <div>
1124
- <input class="searchbar" type="search" placeholder="Search ideas, files, clusters, tags, statuses" data-global-search />
1125
- </div>
1126
- ` : ''}
1127
- </div>
1128
- ${includeGlobalSearch ? '<div class="search-results hidden" data-search-results></div>' : ''}
1129
- </header>
1130
- ${breadcrumbs}
1131
- ${options.body}
1132
- </main>${interactiveScripts}
1133
- </body>
1134
- </html>`;
1135
- }
1136
- function navLink(options, navId, page, enabled) {
1137
- if (!enabled) {
1138
- return '';
1139
- }
1140
- const href = exportPageHref(options.snapshot, options.currentPath, page);
1141
- const activeClass = options.activeNav === navId ? 'active' : '';
1142
- return `<a class="${activeClass}" href="${escapeHtml(href)}">${escapeHtml(page.title)}</a>`;
1143
- }
1144
- function renderMaybeLinkedCard(enabled, href, title, subtitle) {
1145
- return enabled
1146
- ? `<a class="entity-card" href="${escapeHtml(href)}"><strong>${escapeHtml(title)}</strong><p class="subtle">${escapeHtml(subtitle)}</p></a>`
1147
- : `<div class="entity-card"><strong>${escapeHtml(title)}</strong><p class="subtle">${escapeHtml(subtitle)}</p></div>`;
1148
- }
1149
- function renderIdeaAnchor(snapshot, currentPath, idea, label) {
1150
- return snapshot.pageOptions.includeIdeaPages
1151
- ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, idea.page))}">${escapeHtml(label)}</a>`
1152
- : escapeHtml(label);
1153
- }
1154
- //# sourceMappingURL=html-export-template.js.map