@reqlan/analytical 0.4.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/out/analysis/analyser-registry.d.ts +21 -0
- package/out/analysis/analyser-registry.js +33 -0
- package/out/analysis/analyser-registry.js.map +1 -0
- package/out/analysis/completion-tracking-analyser.d.ts +3 -0
- package/out/analysis/completion-tracking-analyser.js +37 -0
- package/out/analysis/completion-tracking-analyser.js.map +1 -0
- package/out/analysis/deprecation-impact-analyser.d.ts +3 -0
- package/out/analysis/deprecation-impact-analyser.js +21 -0
- package/out/analysis/deprecation-impact-analyser.js.map +1 -0
- package/out/analysis/file-related-analyser.d.ts +5 -0
- package/out/analysis/file-related-analyser.js +41 -0
- package/out/analysis/file-related-analyser.js.map +1 -0
- package/out/analysis/git-dates-analyser.d.ts +9 -0
- package/out/analysis/git-dates-analyser.js +39 -0
- package/out/analysis/git-dates-analyser.js.map +1 -0
- package/out/analysis/list-ideas-analyser.d.ts +3 -0
- package/out/analysis/list-ideas-analyser.js +7 -0
- package/out/analysis/list-ideas-analyser.js.map +1 -0
- package/out/analysis/local-graph-analyser.d.ts +6 -0
- package/out/analysis/local-graph-analyser.js +42 -0
- package/out/analysis/local-graph-analyser.js.map +1 -0
- package/out/analysis/semantic-search-analyser.d.ts +8 -0
- package/out/analysis/semantic-search-analyser.js +51 -0
- package/out/analysis/semantic-search-analyser.js.map +1 -0
- package/out/analysis-api.d.ts +31 -0
- package/out/analysis-api.js +136 -0
- package/out/analysis-api.js.map +1 -0
- package/out/core/analytical-store.d.ts +78 -0
- package/out/core/analytical-store.js +141 -0
- package/out/core/analytical-store.js.map +1 -0
- package/out/core/context-model.d.ts +187 -0
- package/out/core/context-model.js +36 -0
- package/out/core/context-model.js.map +1 -0
- package/out/core/context-signals.d.ts +149 -0
- package/out/core/context-signals.js +397 -0
- package/out/core/context-signals.js.map +1 -0
- package/out/core/file-path-match.d.ts +7 -0
- package/out/core/file-path-match.js +39 -0
- package/out/core/file-path-match.js.map +1 -0
- package/out/core/file-reference-resolve.d.ts +8 -0
- package/out/core/file-reference-resolve.js +28 -0
- package/out/core/file-reference-resolve.js.map +1 -0
- package/out/core/idea-references.d.ts +13 -0
- package/out/core/idea-references.js +25 -0
- package/out/core/idea-references.js.map +1 -0
- package/out/core/index-error.d.ts +23 -0
- package/out/core/index-error.js +33 -0
- package/out/core/index-error.js.map +1 -0
- package/out/core/types.d.ts +172 -0
- package/out/core/types.js +48 -0
- package/out/core/types.js.map +1 -0
- package/out/core/workspace-paths.d.ts +4 -0
- package/out/core/workspace-paths.js +92 -0
- package/out/core/workspace-paths.js.map +1 -0
- package/out/create-runtime.d.ts +21 -0
- package/out/create-runtime.js +39 -0
- package/out/create-runtime.js.map +1 -0
- package/out/export/build-export-snapshot.d.ts +3 -0
- package/out/export/build-export-snapshot.js +573 -0
- package/out/export/build-export-snapshot.js.map +1 -0
- package/out/export/html-export-assets.d.ts +3 -0
- package/out/export/html-export-assets.js +1066 -0
- package/out/export/html-export-assets.js.map +1 -0
- package/out/export/html-export-template.d.ts +17 -0
- package/out/export/html-export-template.js +930 -0
- package/out/export/html-export-template.js.map +1 -0
- package/out/export/html-export-utils.d.ts +45 -0
- package/out/export/html-export-utils.js +262 -0
- package/out/export/html-export-utils.js.map +1 -0
- package/out/export/types.d.ts +187 -0
- package/out/export/types.js +2 -0
- package/out/export/types.js.map +1 -0
- package/out/export/write-html-export.d.ts +2 -0
- package/out/export/write-html-export.js +143 -0
- package/out/export/write-html-export.js.map +1 -0
- package/out/headless-index-service.d.ts +23 -0
- package/out/headless-index-service.js +171 -0
- package/out/headless-index-service.js.map +1 -0
- package/out/index-store/idea-extractor.d.ts +6 -0
- package/out/index-store/idea-extractor.js +271 -0
- package/out/index-store/idea-extractor.js.map +1 -0
- package/out/index-store/schema.d.ts +10 -0
- package/out/index-store/schema.js +54 -0
- package/out/index-store/schema.js.map +1 -0
- package/out/index-store/sqlite-store.d.ts +66 -0
- package/out/index-store/sqlite-store.js +964 -0
- package/out/index-store/sqlite-store.js.map +1 -0
- package/out/index-store/webview-graph-queries.d.ts +62 -0
- package/out/index-store/webview-graph-queries.js +256 -0
- package/out/index-store/webview-graph-queries.js.map +1 -0
- package/out/index-store/webview-table-queries.d.ts +57 -0
- package/out/index-store/webview-table-queries.js +199 -0
- package/out/index-store/webview-table-queries.js.map +1 -0
- package/out/index.d.ts +40 -0
- package/out/index.js +27 -0
- package/out/index.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
import { escapeHtml, fileByIdea, filePageEnabled, formatAttributeValue, formatDate, hrefFor, ideaStatus, ideaTags, pageHref, relatedClusters, renderDefinitionList, renderIdeaSummaryHtml, renderMetric, renderOptionalStatusCell, renderOptionalStatusParagraph, renderOptionalTagsCell, renderOptionalTagsParagraph, resolveExportFilePage, slugAttributeKey, stringifyJson } from './html-export-utils.js';
|
|
2
|
+
export function renderHomePage(snapshot) {
|
|
3
|
+
const currentPath = snapshot.manifest.home.path;
|
|
4
|
+
const highlightClusters = snapshot.clusters.slice(0, 8).map(cluster => `
|
|
5
|
+
<a class="entity-card" href="${escapeHtml(pageHref(currentPath, cluster.page))}">
|
|
6
|
+
<div class="pill-row">
|
|
7
|
+
<span class="pill">${escapeHtml(cluster.kind)}</span>
|
|
8
|
+
<span class="pill">${cluster.counts.ideas} ideas</span>
|
|
9
|
+
</div>
|
|
10
|
+
<h3>${escapeHtml(cluster.label)}</h3>
|
|
11
|
+
<p class="subtle">${escapeHtml(cluster.description)}</p>
|
|
12
|
+
</a>
|
|
13
|
+
`).join('');
|
|
14
|
+
const body = `
|
|
15
|
+
<header class="hero">
|
|
16
|
+
<div class="panel">
|
|
17
|
+
<p class="eyebrow">Reqlan export</p>
|
|
18
|
+
<h1>${escapeHtml(snapshot.title)}</h1>
|
|
19
|
+
<p class="subtle">Template: ${escapeHtml(snapshot.templateId)} | Scope: ${escapeHtml(snapshot.scope)} | Mode: ${escapeHtml(snapshot.runtimeMode)}</p>
|
|
20
|
+
<p class="prose">Documentation-grade export with idea pages, clusters, links, search, and printable views.</p>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="card">
|
|
23
|
+
<p><strong>Generated</strong></p>
|
|
24
|
+
<p>${escapeHtml(formatDate(snapshot.generatedAt))}</p>
|
|
25
|
+
<p><strong>Workspace</strong></p>
|
|
26
|
+
<p>${escapeHtml(snapshot.workspaceRoot)}</p>
|
|
27
|
+
</div>
|
|
28
|
+
</header>
|
|
29
|
+
<section class="grid">
|
|
30
|
+
${renderMetric('Ideas', String(snapshot.counts.ideas), pageHref(currentPath, snapshot.manifest.ideasIndex))}
|
|
31
|
+
${renderMetric('References', String(snapshot.counts.edges), pageHref(currentPath, snapshot.manifest.graph))}
|
|
32
|
+
${renderMetric('Files', String(snapshot.counts.files), pageHref(currentPath, snapshot.manifest.filesIndex))}
|
|
33
|
+
${renderMetric('Code files', String(snapshot.codeFiles.length), pageHref(currentPath, snapshot.manifest.codeFilesIndex))}
|
|
34
|
+
${renderMetric('Clusters', String(snapshot.counts.clusters), pageHref(currentPath, snapshot.manifest.clustersIndex))}
|
|
35
|
+
${renderMetric('Attributes', String(snapshot.attributes.length), pageHref(currentPath, snapshot.manifest.attributesIndex))}
|
|
36
|
+
</section>
|
|
37
|
+
<section class="split">
|
|
38
|
+
<div class="panel">
|
|
39
|
+
<h2>Status Rollup</h2>
|
|
40
|
+
${renderDefinitionList(snapshot.byStatus)}
|
|
41
|
+
</div>
|
|
42
|
+
<div class="panel">
|
|
43
|
+
<h2>Tags</h2>
|
|
44
|
+
${renderDefinitionList(snapshot.byTag)}
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
<section class="panel">
|
|
48
|
+
<div class="toolbar">
|
|
49
|
+
<h2>Highlighted Clusters</h2>
|
|
50
|
+
<div class="actions">
|
|
51
|
+
<a class="pill" href="${escapeHtml(pageHref(currentPath, snapshot.manifest.printHome))}">Printable report</a>
|
|
52
|
+
<a class="pill" href="${escapeHtml(pageHref(currentPath, snapshot.manifest.dataExport))}">Snapshot JSON</a>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="entity-list">${highlightClusters}</div>
|
|
56
|
+
</section>
|
|
57
|
+
`;
|
|
58
|
+
return renderShell({
|
|
59
|
+
currentPath,
|
|
60
|
+
activeNav: 'overview',
|
|
61
|
+
title: snapshot.title,
|
|
62
|
+
body,
|
|
63
|
+
snapshot,
|
|
64
|
+
includeGlobalSearch: true
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function renderIdeasIndexPage(snapshot) {
|
|
68
|
+
const currentPath = snapshot.manifest.ideasIndex.path;
|
|
69
|
+
const rows = snapshot.ideas.map(idea => `
|
|
70
|
+
<tr data-filter-row="ideas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' '), idea.fileUri].join(' '))}">
|
|
71
|
+
<td><strong>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</strong></td>
|
|
72
|
+
<td>${escapeHtml(idea.fileUri)}</td>
|
|
73
|
+
<td>${renderOptionalStatusCell(idea)}</td>
|
|
74
|
+
<td>${renderOptionalTagsCell(idea)}</td>
|
|
75
|
+
<td>${escapeHtml(idea.summary || '—')}</td>
|
|
76
|
+
<td>${idea.references.inbound.length}/${idea.references.outbound.length}</td>
|
|
77
|
+
</tr>
|
|
78
|
+
`).join('');
|
|
79
|
+
return renderShell({
|
|
80
|
+
currentPath,
|
|
81
|
+
activeNav: 'ideas',
|
|
82
|
+
title: `${snapshot.title} - Ideas`,
|
|
83
|
+
snapshot,
|
|
84
|
+
includeGlobalSearch: true,
|
|
85
|
+
body: `
|
|
86
|
+
<header class="page-header">
|
|
87
|
+
<p class="eyebrow">Ideas index</p>
|
|
88
|
+
<h1>${escapeHtml(snapshot.title)}</h1>
|
|
89
|
+
<p class="subtle">Searchable list view for all exported ideas.</p>
|
|
90
|
+
</header>
|
|
91
|
+
<section class="table-shell">
|
|
92
|
+
<div class="toolbar">
|
|
93
|
+
<h2>Ideas</h2>
|
|
94
|
+
<div class="actions">
|
|
95
|
+
<input class="searchbar" type="search" placeholder="Filter ideas by name, path, status, tags, or summary" data-filter-input="ideas" />
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<table>
|
|
99
|
+
<thead>
|
|
100
|
+
<tr>
|
|
101
|
+
<th>Name</th>
|
|
102
|
+
<th>Path</th>
|
|
103
|
+
<th>Status</th>
|
|
104
|
+
<th>Tags</th>
|
|
105
|
+
<th>Summary</th>
|
|
106
|
+
<th>Refs in/out</th>
|
|
107
|
+
</tr>
|
|
108
|
+
</thead>
|
|
109
|
+
<tbody>${rows}</tbody>
|
|
110
|
+
</table>
|
|
111
|
+
</section>
|
|
112
|
+
`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export function renderFilesIndexPage(snapshot) {
|
|
116
|
+
const currentPath = snapshot.manifest.filesIndex.path;
|
|
117
|
+
const rows = snapshot.files.map(file => `
|
|
118
|
+
<tr data-filter-row="files" data-filter-text="${escapeHtml([file.name, file.fileUri, Object.keys(file.tags).join(' ')].join(' '))}">
|
|
119
|
+
<td><strong>${snapshot.pageOptions.includeFilePages ? `<a href="${escapeHtml(pageHref(currentPath, file.page))}">${escapeHtml(file.name)}</a>` : escapeHtml(file.name)}</strong></td>
|
|
120
|
+
<td>${escapeHtml(file.directory || '.')}</td>
|
|
121
|
+
<td>${file.ideas.length}</td>
|
|
122
|
+
<td>${file.edgeCount}</td>
|
|
123
|
+
<td>${escapeHtml(Object.keys(file.tags).join(', ') || '—')}</td>
|
|
124
|
+
</tr>
|
|
125
|
+
`).join('');
|
|
126
|
+
return renderShell({
|
|
127
|
+
currentPath,
|
|
128
|
+
activeNav: 'files',
|
|
129
|
+
title: `${snapshot.title} - Files`,
|
|
130
|
+
snapshot,
|
|
131
|
+
includeGlobalSearch: true,
|
|
132
|
+
body: `
|
|
133
|
+
<header class="page-header">
|
|
134
|
+
<p class="eyebrow">Files index</p>
|
|
135
|
+
<h1>${escapeHtml(snapshot.title)}</h1>
|
|
136
|
+
<p class="subtle">List view by source file with local counts and drill-down pages.</p>
|
|
137
|
+
</header>
|
|
138
|
+
<section class="table-shell">
|
|
139
|
+
<div class="toolbar">
|
|
140
|
+
<h2>Files</h2>
|
|
141
|
+
<div class="actions">
|
|
142
|
+
<input class="searchbar" type="search" placeholder="Filter files by path or tags" data-filter-input="files" />
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<table>
|
|
146
|
+
<thead>
|
|
147
|
+
<tr><th>File</th><th>Directory</th><th>Ideas</th><th>References</th><th>Tags</th></tr>
|
|
148
|
+
</thead>
|
|
149
|
+
<tbody>${rows}</tbody>
|
|
150
|
+
</table>
|
|
151
|
+
</section>
|
|
152
|
+
`
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
export function renderCodeFilesIndexPage(snapshot) {
|
|
156
|
+
const currentPath = snapshot.manifest.codeFilesIndex.path;
|
|
157
|
+
const rows = snapshot.codeFiles.map(file => `
|
|
158
|
+
<tr data-filter-row="codeFiles" data-filter-text="${escapeHtml([file.name, file.fileUri, file.labels.join(' ')].join(' '))}">
|
|
159
|
+
<td><strong>${snapshot.pageOptions.includeCodeFilePages ? `<a href="${escapeHtml(pageHref(currentPath, file.page))}">${escapeHtml(file.name)}</a>` : escapeHtml(file.name)}</strong></td>
|
|
160
|
+
<td>${escapeHtml(file.directory || '.')}</td>
|
|
161
|
+
<td>${file.referencingIdeaIds.length}</td>
|
|
162
|
+
<td>${escapeHtml(file.labels.join(', ') || '—')}</td>
|
|
163
|
+
</tr>
|
|
164
|
+
`).join('');
|
|
165
|
+
return renderShell({
|
|
166
|
+
currentPath,
|
|
167
|
+
activeNav: 'code-files',
|
|
168
|
+
title: `${snapshot.title} - Code files`,
|
|
169
|
+
snapshot,
|
|
170
|
+
includeGlobalSearch: true,
|
|
171
|
+
body: `
|
|
172
|
+
<header class="page-header">
|
|
173
|
+
<p class="eyebrow">Code reference index</p>
|
|
174
|
+
<h1>${escapeHtml(snapshot.title)}</h1>
|
|
175
|
+
<p class="subtle">Outbound file_reference targets that are not idea-hosting reqlan files.</p>
|
|
176
|
+
</header>
|
|
177
|
+
<section class="table-shell">
|
|
178
|
+
<div class="toolbar">
|
|
179
|
+
<h2>Code files</h2>
|
|
180
|
+
<div class="actions">
|
|
181
|
+
<input class="searchbar" type="search" placeholder="Filter code files by path or label" data-filter-input="codeFiles" />
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<table>
|
|
185
|
+
<thead>
|
|
186
|
+
<tr><th>File</th><th>Directory</th><th>Referenced by</th><th>Labels</th></tr>
|
|
187
|
+
</thead>
|
|
188
|
+
<tbody>${rows || '<tr><td colspan="4" class="subtle">No outbound code file references.</td></tr>'}</tbody>
|
|
189
|
+
</table>
|
|
190
|
+
</section>
|
|
191
|
+
`
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
export function renderClustersIndexPage(snapshot) {
|
|
195
|
+
const currentPath = snapshot.manifest.clustersIndex.path;
|
|
196
|
+
const rows = snapshot.clusters.map(cluster => `
|
|
197
|
+
<tr data-filter-row="clusters" data-filter-text="${escapeHtml([cluster.label, cluster.kind, cluster.description].join(' '))}">
|
|
198
|
+
<td><strong>${snapshot.pageOptions.includeClusterPages ? `<a href="${escapeHtml(pageHref(currentPath, cluster.page))}">${escapeHtml(cluster.label)}</a>` : escapeHtml(cluster.label)}</strong></td>
|
|
199
|
+
<td>${escapeHtml(cluster.kind)}</td>
|
|
200
|
+
<td>${cluster.counts.ideas}</td>
|
|
201
|
+
<td>${cluster.counts.files}</td>
|
|
202
|
+
<td>${escapeHtml(cluster.description)}</td>
|
|
203
|
+
</tr>
|
|
204
|
+
`).join('');
|
|
205
|
+
return renderShell({
|
|
206
|
+
currentPath,
|
|
207
|
+
activeNav: 'clusters',
|
|
208
|
+
title: `${snapshot.title} - Clusters`,
|
|
209
|
+
snapshot,
|
|
210
|
+
includeGlobalSearch: true,
|
|
211
|
+
body: `
|
|
212
|
+
<header class="page-header">
|
|
213
|
+
<p class="eyebrow">Clusters</p>
|
|
214
|
+
<h1>${escapeHtml(snapshot.title)}</h1>
|
|
215
|
+
<p class="subtle">Deterministic and computed groupings for easier exploration.</p>
|
|
216
|
+
</header>
|
|
217
|
+
<section class="table-shell">
|
|
218
|
+
<div class="toolbar">
|
|
219
|
+
<h2>Clusters</h2>
|
|
220
|
+
<div class="actions">
|
|
221
|
+
<input class="searchbar" type="search" placeholder="Filter clusters by label, kind, or description" data-filter-input="clusters" />
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
<table>
|
|
225
|
+
<thead>
|
|
226
|
+
<tr><th>Cluster</th><th>Kind</th><th>Ideas</th><th>Files</th><th>Description</th></tr>
|
|
227
|
+
</thead>
|
|
228
|
+
<tbody>${rows}</tbody>
|
|
229
|
+
</table>
|
|
230
|
+
</section>
|
|
231
|
+
`
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
export function renderAttributesIndexPage(snapshot) {
|
|
235
|
+
const currentPath = snapshot.manifest.attributesIndex.path;
|
|
236
|
+
const rows = snapshot.attributes.map(attribute => {
|
|
237
|
+
const anchor = `attr-${slugAttributeKey(attribute.key)}`;
|
|
238
|
+
const valueSummary = attribute.values
|
|
239
|
+
.slice(0, 6)
|
|
240
|
+
.map(entry => `${entry.value} (${entry.count})`)
|
|
241
|
+
.join('; ');
|
|
242
|
+
const ideaLinks = attribute.ideaIds.slice(0, 8).map(ideaId => {
|
|
243
|
+
const idea = snapshot.ideasById[ideaId];
|
|
244
|
+
if (!idea) {
|
|
245
|
+
return '';
|
|
246
|
+
}
|
|
247
|
+
return snapshot.pageOptions.includeIdeaPages
|
|
248
|
+
? `<a href="${escapeHtml(pageHref(currentPath, idea.page))}">${escapeHtml(idea.name)}</a>`
|
|
249
|
+
: escapeHtml(idea.name);
|
|
250
|
+
}).filter(Boolean).join(', ');
|
|
251
|
+
return `
|
|
252
|
+
<tr id="${escapeHtml(anchor)}" data-filter-row="attributes" data-filter-text="${escapeHtml([attribute.key, valueSummary, attribute.ideaIds.join(' ')].join(' '))}">
|
|
253
|
+
<td><strong>${escapeHtml(attribute.key)}</strong></td>
|
|
254
|
+
<td>${attribute.ideaCount}</td>
|
|
255
|
+
<td>${attribute.values.length}</td>
|
|
256
|
+
<td>${escapeHtml(valueSummary || '—')}</td>
|
|
257
|
+
<td>${ideaLinks || '—'}${attribute.ideaIds.length > 8 ? ` <span class="subtle">+${attribute.ideaIds.length - 8} more</span>` : ''}</td>
|
|
258
|
+
</tr>`;
|
|
259
|
+
}).join('');
|
|
260
|
+
return renderShell({
|
|
261
|
+
currentPath,
|
|
262
|
+
activeNav: 'attributes',
|
|
263
|
+
title: `${snapshot.title} - Attributes`,
|
|
264
|
+
snapshot,
|
|
265
|
+
includeGlobalSearch: true,
|
|
266
|
+
body: `
|
|
267
|
+
<header class="page-header">
|
|
268
|
+
<p class="eyebrow">Attributes</p>
|
|
269
|
+
<h1>${escapeHtml(snapshot.title)}</h1>
|
|
270
|
+
<p class="subtle">Every attribute key in this export with values and linked ideas.</p>
|
|
271
|
+
</header>
|
|
272
|
+
<section class="table-shell">
|
|
273
|
+
<div class="toolbar">
|
|
274
|
+
<h2>Attributes</h2>
|
|
275
|
+
<div class="actions">
|
|
276
|
+
<input class="searchbar" type="search" placeholder="Filter attributes by key, value, or idea" data-filter-input="attributes" />
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<table>
|
|
280
|
+
<thead>
|
|
281
|
+
<tr><th>Attribute</th><th>Ideas</th><th>Values</th><th>Value summary</th><th>Ideas</th></tr>
|
|
282
|
+
</thead>
|
|
283
|
+
<tbody>${rows || '<tr><td colspan="5" class="subtle">No attributes declared.</td></tr>'}</tbody>
|
|
284
|
+
</table>
|
|
285
|
+
</section>
|
|
286
|
+
`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
export function renderGraphPage(snapshot) {
|
|
290
|
+
const currentPath = snapshot.manifest.graph.path;
|
|
291
|
+
const graph = enrichGraphUrls(snapshot, snapshot.graphs.workspace, currentPath);
|
|
292
|
+
const statuses = Object.keys(snapshot.byStatus).sort();
|
|
293
|
+
const tags = Object.keys(snapshot.byTag).sort();
|
|
294
|
+
const clusters = snapshot.clusters.slice(0, 16).map(cluster => snapshot.pageOptions.includeClusterPages
|
|
295
|
+
? `<a class="pill" href="${escapeHtml(pageHref(currentPath, cluster.page))}">${escapeHtml(cluster.label)}</a>`
|
|
296
|
+
: `<span class="pill">${escapeHtml(cluster.label)}</span>`).join('');
|
|
297
|
+
return renderShell({
|
|
298
|
+
currentPath,
|
|
299
|
+
activeNav: 'graph',
|
|
300
|
+
title: `${snapshot.title} - Graph`,
|
|
301
|
+
snapshot,
|
|
302
|
+
includeGlobalSearch: snapshot.runtimeMode === 'interactive',
|
|
303
|
+
body: `
|
|
304
|
+
<header class="page-header">
|
|
305
|
+
<p class="eyebrow">Graph</p>
|
|
306
|
+
<h1>${escapeHtml(snapshot.title)}</h1>
|
|
307
|
+
<p class="subtle">Workspace graph with links into idea detail pages.</p>
|
|
308
|
+
</header>
|
|
309
|
+
<section class="graph-shell">
|
|
310
|
+
<div class="toolbar">
|
|
311
|
+
<h2>Reference graph</h2>
|
|
312
|
+
<div class="actions">
|
|
313
|
+
<a class="pill" href="${escapeHtml(pageHref(currentPath, snapshot.manifest.dataGraph))}">Graph JSON</a>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="graph-controls-bar" data-graph-controls="graph-data">
|
|
317
|
+
<input class="searchbar graph-filter" type="search" placeholder="Search graph nodes" data-graph-search />
|
|
318
|
+
<input class="searchbar graph-filter" type="search" placeholder="Path filter" data-graph-path />
|
|
319
|
+
<select class="graph-filter" data-graph-status>
|
|
320
|
+
<option value="">All statuses</option>
|
|
321
|
+
${statuses.map(status => `<option value="${escapeHtml(status)}">${escapeHtml(status)}</option>`).join('')}
|
|
322
|
+
</select>
|
|
323
|
+
<select class="graph-filter" data-graph-tag>
|
|
324
|
+
<option value="">All tags</option>
|
|
325
|
+
${tags.map(tag => `<option value="${escapeHtml(tag)}">${escapeHtml(tag)}</option>`).join('')}
|
|
326
|
+
</select>
|
|
327
|
+
<button type="button" class="graph-action" data-graph-toggle-external="true">Hide external</button>
|
|
328
|
+
<button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
|
|
329
|
+
<button type="button" class="graph-action" data-graph-fit>Fit</button>
|
|
330
|
+
<button type="button" class="graph-action" data-graph-reset>Reset</button>
|
|
331
|
+
<span class="graph-status" data-graph-status-text></span>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="pill-row">${clusters}</div>
|
|
334
|
+
<div class="graph-root" data-graph-json="graph-data"></div>
|
|
335
|
+
<script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
|
|
336
|
+
</section>
|
|
337
|
+
`
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
export function renderIdeaDetailPage(snapshot, idea) {
|
|
341
|
+
const currentPath = idea.page.path;
|
|
342
|
+
const clusters = relatedClusters(snapshot, idea);
|
|
343
|
+
const parentFile = fileByIdea(snapshot, idea);
|
|
344
|
+
const graph = enrichGraphUrls(snapshot, snapshot.graphs.byIdeaId[idea.id], currentPath);
|
|
345
|
+
return renderShell({
|
|
346
|
+
currentPath,
|
|
347
|
+
activeNav: 'ideas',
|
|
348
|
+
title: `${snapshot.title} - ${idea.name}`,
|
|
349
|
+
snapshot,
|
|
350
|
+
includeGlobalSearch: snapshot.runtimeMode === 'interactive',
|
|
351
|
+
breadcrumbs: [
|
|
352
|
+
{ label: 'Ideas', href: pageHref(currentPath, snapshot.manifest.ideasIndex) },
|
|
353
|
+
{ label: idea.name }
|
|
354
|
+
],
|
|
355
|
+
body: `
|
|
356
|
+
<header class="page-header">
|
|
357
|
+
<p class="eyebrow">Idea detail</p>
|
|
358
|
+
<h1 id="summary">${escapeHtml(idea.name)}</h1>
|
|
359
|
+
<p class="subtle">${escapeHtml(idea.fileUri)}:${idea.lineStart + 1}</p>
|
|
360
|
+
<div class="pill-row">
|
|
361
|
+
<span class="pill">${escapeHtml(idea.kind)}</span>
|
|
362
|
+
${ideaStatus(idea) ? `<span class="pill">${escapeHtml(ideaStatus(idea))}</span>` : ''}
|
|
363
|
+
${ideaTags(idea).map(tag => `<span class="pill">${escapeHtml(tag)}</span>`).join('')}
|
|
364
|
+
</div>
|
|
365
|
+
</header>
|
|
366
|
+
<section class="detail-grid">
|
|
367
|
+
<div class="panel prose print-break-avoid">
|
|
368
|
+
<h2>Summary</h2>
|
|
369
|
+
<p class="idea-summary">${renderIdeaSummaryHtml(snapshot, currentPath, idea)}</p>
|
|
370
|
+
<p><a class="section-link" href="#references-out">Jump to outbound references</a></p>
|
|
371
|
+
<p><a class="section-link" href="#references-in">Jump to inbound references</a></p>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="panel print-break-avoid">
|
|
374
|
+
<h2>Navigation</h2>
|
|
375
|
+
<div class="entity-list">
|
|
376
|
+
${parentFile ? renderMaybeLinkedCard(snapshot.pageOptions.includeFilePages, pageHref(currentPath, parentFile.page), 'Source file', parentFile.fileUri) : ''}
|
|
377
|
+
${clusters.map(cluster => renderMaybeLinkedCard(snapshot.pageOptions.includeClusterPages, pageHref(currentPath, cluster.page), cluster.label, `${cluster.kind} cluster`)).join('')}
|
|
378
|
+
${idea.page.printablePath && snapshot.pageOptions.includePrintPages ? `<a class="entity-card" href="${escapeHtml(hrefFor(currentPath, idea.page.printablePath))}"><strong>Printable page</strong><p class="subtle">Static print-friendly idea sheet.</p></a>` : ''}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</section>
|
|
382
|
+
<section class="split">
|
|
383
|
+
<div class="panel print-break-avoid" id="attributes">
|
|
384
|
+
<h2>Attributes</h2>
|
|
385
|
+
${renderIdeaAttributes(snapshot, currentPath, idea)}
|
|
386
|
+
</div>
|
|
387
|
+
<div class="panel print-break-avoid">
|
|
388
|
+
<h2>Ancestor context</h2>
|
|
389
|
+
<div class="entity-list">
|
|
390
|
+
${idea.ancestors.ancestors.map(ancestor => {
|
|
391
|
+
const linked = snapshot.ideasById[ancestor.id];
|
|
392
|
+
return linked
|
|
393
|
+
? renderMaybeLinkedCard(snapshot.pageOptions.includeIdeaPages, pageHref(currentPath, linked.page), ancestor.name, ancestor.summary)
|
|
394
|
+
: '';
|
|
395
|
+
}).join('') || '<p class="subtle">No ancestor chain recorded.</p>'}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</section>
|
|
399
|
+
${renderReferenceSection(snapshot, currentPath, 'references-out', 'Outbound references', idea.references.outbound)}
|
|
400
|
+
${renderReferenceSection(snapshot, currentPath, 'references-in', 'Inbound references', idea.references.inbound)}
|
|
401
|
+
${renderReferenceSection(snapshot, currentPath, 'references-unresolved', 'Unresolved references', idea.references.unresolved)}
|
|
402
|
+
<section class="graph-shell print-break-avoid" id="graph">
|
|
403
|
+
<div class="toolbar"><h2>Local graph</h2></div>
|
|
404
|
+
<div class="graph-controls-bar" data-graph-controls="graph-data">
|
|
405
|
+
<button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
|
|
406
|
+
<button type="button" class="graph-action" data-graph-fit>Fit</button>
|
|
407
|
+
<span class="graph-status" data-graph-status-text></span>
|
|
408
|
+
</div>
|
|
409
|
+
<div class="graph-root" data-graph-json="graph-data"></div>
|
|
410
|
+
<script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
|
|
411
|
+
</section>
|
|
412
|
+
`
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
export function renderFileDetailPage(snapshot, file) {
|
|
416
|
+
const currentPath = file.page.path;
|
|
417
|
+
const graph = enrichGraphUrls(snapshot, snapshot.graphs.byFileId[file.id], currentPath);
|
|
418
|
+
const clusterLinks = snapshot.clusters.filter(cluster => cluster.fileUris.includes(file.fileUri)).slice(0, 12);
|
|
419
|
+
return renderShell({
|
|
420
|
+
currentPath,
|
|
421
|
+
activeNav: 'files',
|
|
422
|
+
title: `${snapshot.title} - ${file.name}`,
|
|
423
|
+
snapshot,
|
|
424
|
+
includeGlobalSearch: snapshot.runtimeMode === 'interactive',
|
|
425
|
+
breadcrumbs: [
|
|
426
|
+
{ label: 'Files', href: pageHref(currentPath, snapshot.manifest.filesIndex) },
|
|
427
|
+
{ label: file.name }
|
|
428
|
+
],
|
|
429
|
+
body: `
|
|
430
|
+
<header class="page-header">
|
|
431
|
+
<p class="eyebrow">File detail</p>
|
|
432
|
+
<h1>${escapeHtml(file.name)}</h1>
|
|
433
|
+
<p class="subtle">${escapeHtml(file.fileUri)}</p>
|
|
434
|
+
</header>
|
|
435
|
+
<section class="grid">
|
|
436
|
+
${renderMetric('Ideas', String(file.ideas.length))}
|
|
437
|
+
${renderMetric('References', String(file.edgeCount))}
|
|
438
|
+
${renderMetric('Statuses', String(Object.keys(file.statuses).length))}
|
|
439
|
+
${renderMetric('Tags', String(Object.keys(file.tags).length))}
|
|
440
|
+
</section>
|
|
441
|
+
<section class="split">
|
|
442
|
+
<div class="table-shell">
|
|
443
|
+
<div class="toolbar">
|
|
444
|
+
<h2>Ideas in file</h2>
|
|
445
|
+
<div class="actions">
|
|
446
|
+
<input class="searchbar" type="search" placeholder="Filter file ideas" data-filter-input="fileIdeas" />
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
<table>
|
|
450
|
+
<thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
|
|
451
|
+
<tbody>
|
|
452
|
+
${file.ideas.map(idea => `
|
|
453
|
+
<tr data-filter-row="fileIdeas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' ')].join(' '))}">
|
|
454
|
+
<td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
|
|
455
|
+
<td>${renderOptionalStatusCell(idea)}</td>
|
|
456
|
+
<td>${renderOptionalTagsCell(idea)}</td>
|
|
457
|
+
<td>${escapeHtml(idea.summary)}</td>
|
|
458
|
+
</tr>
|
|
459
|
+
`).join('')}
|
|
460
|
+
</tbody>
|
|
461
|
+
</table>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="panel">
|
|
464
|
+
<h2>Related clusters</h2>
|
|
465
|
+
<div class="entity-list">
|
|
466
|
+
${clusterLinks.map(cluster => renderMaybeLinkedCard(snapshot.pageOptions.includeClusterPages, pageHref(currentPath, cluster.page), cluster.label, cluster.kind)).join('') || '<p class="subtle">No related clusters.</p>'}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</section>
|
|
470
|
+
<section class="graph-shell print-break-avoid">
|
|
471
|
+
<div class="toolbar"><h2>Local graph</h2></div>
|
|
472
|
+
<div class="graph-controls-bar" data-graph-controls="graph-data">
|
|
473
|
+
<button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
|
|
474
|
+
<button type="button" class="graph-action" data-graph-fit>Fit</button>
|
|
475
|
+
<span class="graph-status" data-graph-status-text></span>
|
|
476
|
+
</div>
|
|
477
|
+
<div class="graph-root" data-graph-json="graph-data"></div>
|
|
478
|
+
<script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
|
|
479
|
+
</section>
|
|
480
|
+
`
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
export function renderCodeFileDetailPage(snapshot, file) {
|
|
484
|
+
const currentPath = file.page.path;
|
|
485
|
+
const referencingIdeas = file.referencingIdeaIds
|
|
486
|
+
.map(ideaId => snapshot.ideasById[ideaId])
|
|
487
|
+
.filter((idea) => Boolean(idea));
|
|
488
|
+
return renderShell({
|
|
489
|
+
currentPath,
|
|
490
|
+
activeNav: 'code-files',
|
|
491
|
+
title: `${snapshot.title} - ${file.name}`,
|
|
492
|
+
snapshot,
|
|
493
|
+
includeGlobalSearch: snapshot.runtimeMode === 'interactive',
|
|
494
|
+
breadcrumbs: [
|
|
495
|
+
{ label: 'Code files', href: pageHref(currentPath, snapshot.manifest.codeFilesIndex) },
|
|
496
|
+
{ label: file.name }
|
|
497
|
+
],
|
|
498
|
+
body: `
|
|
499
|
+
<header class="page-header">
|
|
500
|
+
<p class="eyebrow">Code reference detail</p>
|
|
501
|
+
<h1>${escapeHtml(file.name)}</h1>
|
|
502
|
+
<p class="subtle">${escapeHtml(file.fileUri)}</p>
|
|
503
|
+
</header>
|
|
504
|
+
<section class="grid">
|
|
505
|
+
${renderMetric('Referenced by', String(referencingIdeas.length))}
|
|
506
|
+
${renderMetric('Labels', String(file.labels.length))}
|
|
507
|
+
</section>
|
|
508
|
+
<section class="table-shell">
|
|
509
|
+
<div class="toolbar">
|
|
510
|
+
<h2>Referencing ideas</h2>
|
|
511
|
+
<div class="actions">
|
|
512
|
+
<input class="searchbar" type="search" placeholder="Filter referencing ideas" data-filter-input="codeFileIdeas" />
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
<table>
|
|
516
|
+
<thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
|
|
517
|
+
<tbody>
|
|
518
|
+
${referencingIdeas.map(idea => `
|
|
519
|
+
<tr data-filter-row="codeFileIdeas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' ')].join(' '))}">
|
|
520
|
+
<td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
|
|
521
|
+
<td>${renderOptionalStatusCell(idea)}</td>
|
|
522
|
+
<td>${renderOptionalTagsCell(idea)}</td>
|
|
523
|
+
<td>${escapeHtml(idea.summary)}</td>
|
|
524
|
+
</tr>
|
|
525
|
+
`).join('') || '<tr><td colspan="4" class="subtle">No referencing ideas.</td></tr>'}
|
|
526
|
+
</tbody>
|
|
527
|
+
</table>
|
|
528
|
+
</section>
|
|
529
|
+
${file.labels.length > 0 ? `
|
|
530
|
+
<section class="panel">
|
|
531
|
+
<h2>Reference labels</h2>
|
|
532
|
+
<p>${escapeHtml(file.labels.join(', '))}</p>
|
|
533
|
+
</section>
|
|
534
|
+
` : ''}
|
|
535
|
+
`
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
export function renderClusterDetailPage(snapshot, cluster) {
|
|
539
|
+
const currentPath = cluster.page.path;
|
|
540
|
+
const graph = enrichGraphUrls(snapshot, snapshot.graphs.byClusterId[cluster.id], currentPath);
|
|
541
|
+
const members = cluster.ideaIds
|
|
542
|
+
.map(ideaId => snapshot.ideasById[ideaId])
|
|
543
|
+
.filter((idea) => Boolean(idea));
|
|
544
|
+
return renderShell({
|
|
545
|
+
currentPath,
|
|
546
|
+
activeNav: 'clusters',
|
|
547
|
+
title: `${snapshot.title} - ${cluster.label}`,
|
|
548
|
+
snapshot,
|
|
549
|
+
includeGlobalSearch: snapshot.runtimeMode === 'interactive',
|
|
550
|
+
breadcrumbs: [
|
|
551
|
+
{ label: 'Clusters', href: pageHref(currentPath, snapshot.manifest.clustersIndex) },
|
|
552
|
+
{ label: cluster.label }
|
|
553
|
+
],
|
|
554
|
+
body: `
|
|
555
|
+
<header class="page-header">
|
|
556
|
+
<p class="eyebrow">Cluster detail</p>
|
|
557
|
+
<h1>${escapeHtml(cluster.label)}</h1>
|
|
558
|
+
<p class="subtle">${escapeHtml(cluster.description)}</p>
|
|
559
|
+
<div class="pill-row">
|
|
560
|
+
<span class="pill">${escapeHtml(cluster.kind)}</span>
|
|
561
|
+
<span class="pill">${cluster.counts.ideas} ideas</span>
|
|
562
|
+
<span class="pill">${cluster.counts.files} files</span>
|
|
563
|
+
</div>
|
|
564
|
+
</header>
|
|
565
|
+
<section class="table-shell">
|
|
566
|
+
<div class="toolbar">
|
|
567
|
+
<h2>Members</h2>
|
|
568
|
+
<div class="actions">
|
|
569
|
+
<input class="searchbar" type="search" placeholder="Filter cluster members" data-filter-input="clusterIdeas" />
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
<table>
|
|
573
|
+
<thead><tr><th>Idea</th><th>File</th><th>Status</th><th>Summary</th></tr></thead>
|
|
574
|
+
<tbody>
|
|
575
|
+
${members.map(idea => `
|
|
576
|
+
<tr data-filter-row="clusterIdeas" data-filter-text="${escapeHtml([idea.name, idea.fileUri, ideaStatus(idea) ?? '', idea.summary].join(' '))}">
|
|
577
|
+
<td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
|
|
578
|
+
<td>${escapeHtml(idea.fileUri)}</td>
|
|
579
|
+
<td>${renderOptionalStatusCell(idea)}</td>
|
|
580
|
+
<td>${escapeHtml(idea.summary)}</td>
|
|
581
|
+
</tr>
|
|
582
|
+
`).join('')}
|
|
583
|
+
</tbody>
|
|
584
|
+
</table>
|
|
585
|
+
</section>
|
|
586
|
+
<section class="graph-shell print-break-avoid">
|
|
587
|
+
<div class="toolbar"><h2>Cluster graph</h2></div>
|
|
588
|
+
<div class="graph-controls-bar" data-graph-controls="graph-data">
|
|
589
|
+
<button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
|
|
590
|
+
<button type="button" class="graph-action" data-graph-fit>Fit</button>
|
|
591
|
+
<span class="graph-status" data-graph-status-text></span>
|
|
592
|
+
</div>
|
|
593
|
+
<div class="graph-root" data-graph-json="graph-data"></div>
|
|
594
|
+
<script id="graph-data" type="application/json">${stringifyJson(graph)}</script>
|
|
595
|
+
</section>
|
|
596
|
+
`
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
export function renderPrintHomePage(snapshot) {
|
|
600
|
+
const currentPath = snapshot.manifest.printHome.path;
|
|
601
|
+
const cards = snapshot.ideas.map(idea => `
|
|
602
|
+
<article class="print-card print-break-avoid">
|
|
603
|
+
<h3>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(hrefFor(currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</h3>
|
|
604
|
+
<p class="subtle">${escapeHtml(idea.fileUri)}</p>
|
|
605
|
+
<p>${renderIdeaSummaryHtml(snapshot, currentPath, idea, '—')}</p>
|
|
606
|
+
${renderOptionalStatusParagraph(idea)}
|
|
607
|
+
${renderOptionalTagsParagraph(idea)}
|
|
608
|
+
</article>
|
|
609
|
+
`).join('');
|
|
610
|
+
return renderShell({
|
|
611
|
+
currentPath,
|
|
612
|
+
activeNav: 'print',
|
|
613
|
+
title: `${snapshot.title} - Print`,
|
|
614
|
+
snapshot,
|
|
615
|
+
body: `
|
|
616
|
+
<header class="page-header">
|
|
617
|
+
<p class="eyebrow">Printable report</p>
|
|
618
|
+
<h1>${escapeHtml(snapshot.title)}</h1>
|
|
619
|
+
<p class="subtle">Generated ${escapeHtml(formatDate(snapshot.generatedAt))}</p>
|
|
620
|
+
</header>
|
|
621
|
+
<section class="grid">
|
|
622
|
+
${renderMetric('Ideas', String(snapshot.counts.ideas))}
|
|
623
|
+
${renderMetric('References', String(snapshot.counts.edges))}
|
|
624
|
+
${renderMetric('Files', String(snapshot.counts.files))}
|
|
625
|
+
${renderMetric('Clusters', String(snapshot.counts.clusters))}
|
|
626
|
+
</section>
|
|
627
|
+
<section class="panel print-break-avoid">
|
|
628
|
+
<h2>Status rollup</h2>
|
|
629
|
+
${renderDefinitionList(snapshot.byStatus)}
|
|
630
|
+
</section>
|
|
631
|
+
<section class="entity-list">${cards}</section>
|
|
632
|
+
`
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
export function renderPrintIdeaPage(snapshot, idea) {
|
|
636
|
+
const currentPath = idea.page.printablePath ?? snapshot.manifest.printHome.path;
|
|
637
|
+
return renderShell({
|
|
638
|
+
currentPath,
|
|
639
|
+
activeNav: 'print',
|
|
640
|
+
title: `${snapshot.title} - ${idea.name} print`,
|
|
641
|
+
snapshot,
|
|
642
|
+
body: `
|
|
643
|
+
<header class="page-header">
|
|
644
|
+
<p class="eyebrow">Printable idea sheet</p>
|
|
645
|
+
<h1>${escapeHtml(idea.name)}</h1>
|
|
646
|
+
<p class="subtle">${escapeHtml(idea.fileUri)}:${idea.lineStart + 1}</p>
|
|
647
|
+
</header>
|
|
648
|
+
<section class="print-card print-break-avoid">
|
|
649
|
+
<p class="idea-summary">${renderIdeaSummaryHtml(snapshot, currentPath, idea)}</p>
|
|
650
|
+
${renderOptionalStatusParagraph(idea)}
|
|
651
|
+
${renderOptionalTagsParagraph(idea)}
|
|
652
|
+
${snapshot.pageOptions.includeIdeaPages ? `<p><strong>Interactive page:</strong> <a href="${escapeHtml(hrefFor(currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a></p>` : ''}
|
|
653
|
+
</section>
|
|
654
|
+
`
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
export function renderPrintFilePage(snapshot, file) {
|
|
658
|
+
const currentPath = file.printPage.path;
|
|
659
|
+
return renderShell({
|
|
660
|
+
currentPath,
|
|
661
|
+
activeNav: 'print',
|
|
662
|
+
title: `${snapshot.title} - ${file.name} print`,
|
|
663
|
+
snapshot,
|
|
664
|
+
body: `
|
|
665
|
+
<header class="page-header">
|
|
666
|
+
<p class="eyebrow">Printable file sheet</p>
|
|
667
|
+
<h1>${escapeHtml(file.name)}</h1>
|
|
668
|
+
<p class="subtle">${escapeHtml(file.fileUri)}</p>
|
|
669
|
+
</header>
|
|
670
|
+
<section class="table-shell">
|
|
671
|
+
<table>
|
|
672
|
+
<thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
|
|
673
|
+
<tbody>
|
|
674
|
+
${file.ideas.map(idea => `
|
|
675
|
+
<tr>
|
|
676
|
+
<td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(hrefFor(currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
|
|
677
|
+
<td>${renderOptionalStatusCell(idea)}</td>
|
|
678
|
+
<td>${renderOptionalTagsCell(idea)}</td>
|
|
679
|
+
<td>${escapeHtml(idea.summary)}</td>
|
|
680
|
+
</tr>
|
|
681
|
+
`).join('')}
|
|
682
|
+
</tbody>
|
|
683
|
+
</table>
|
|
684
|
+
</section>
|
|
685
|
+
`
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
export function renderPrintCodeFilePage(snapshot, file) {
|
|
689
|
+
const currentPath = file.printPage.path;
|
|
690
|
+
const referencingIdeas = file.referencingIdeaIds
|
|
691
|
+
.map(ideaId => snapshot.ideasById[ideaId])
|
|
692
|
+
.filter((idea) => Boolean(idea));
|
|
693
|
+
return renderShell({
|
|
694
|
+
currentPath,
|
|
695
|
+
activeNav: 'print',
|
|
696
|
+
title: `${snapshot.title} - ${file.name} print`,
|
|
697
|
+
snapshot,
|
|
698
|
+
body: `
|
|
699
|
+
<header class="page-header">
|
|
700
|
+
<p class="eyebrow">Printable code reference sheet</p>
|
|
701
|
+
<h1>${escapeHtml(file.name)}</h1>
|
|
702
|
+
<p class="subtle">${escapeHtml(file.fileUri)}</p>
|
|
703
|
+
</header>
|
|
704
|
+
<section class="table-shell">
|
|
705
|
+
<table>
|
|
706
|
+
<thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
|
|
707
|
+
<tbody>
|
|
708
|
+
${referencingIdeas.map(idea => `
|
|
709
|
+
<tr>
|
|
710
|
+
<td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(hrefFor(currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
|
|
711
|
+
<td>${renderOptionalStatusCell(idea)}</td>
|
|
712
|
+
<td>${renderOptionalTagsCell(idea)}</td>
|
|
713
|
+
<td>${escapeHtml(idea.summary)}</td>
|
|
714
|
+
</tr>
|
|
715
|
+
`).join('') || '<tr><td colspan="4" class="subtle">No referencing ideas.</td></tr>'}
|
|
716
|
+
</tbody>
|
|
717
|
+
</table>
|
|
718
|
+
</section>
|
|
719
|
+
`
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
export function renderPrintClusterPage(snapshot, cluster) {
|
|
723
|
+
const currentPath = cluster.page.printablePath ?? snapshot.manifest.printHome.path;
|
|
724
|
+
const members = cluster.ideaIds
|
|
725
|
+
.map(ideaId => snapshot.ideasById[ideaId])
|
|
726
|
+
.filter((idea) => Boolean(idea));
|
|
727
|
+
return renderShell({
|
|
728
|
+
currentPath,
|
|
729
|
+
activeNav: 'print',
|
|
730
|
+
title: `${snapshot.title} - ${cluster.label} print`,
|
|
731
|
+
snapshot,
|
|
732
|
+
body: `
|
|
733
|
+
<header class="page-header">
|
|
734
|
+
<p class="eyebrow">Printable cluster sheet</p>
|
|
735
|
+
<h1>${escapeHtml(cluster.label)}</h1>
|
|
736
|
+
<p class="subtle">${escapeHtml(cluster.description)}</p>
|
|
737
|
+
</header>
|
|
738
|
+
<section class="table-shell">
|
|
739
|
+
<table>
|
|
740
|
+
<thead><tr><th>Idea</th><th>File</th><th>Status</th><th>Summary</th></tr></thead>
|
|
741
|
+
<tbody>
|
|
742
|
+
${members.map(idea => `
|
|
743
|
+
<tr>
|
|
744
|
+
<td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(hrefFor(currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
|
|
745
|
+
<td>${escapeHtml(idea.fileUri)}</td>
|
|
746
|
+
<td>${renderOptionalStatusCell(idea)}</td>
|
|
747
|
+
<td>${escapeHtml(idea.summary)}</td>
|
|
748
|
+
</tr>
|
|
749
|
+
`).join('')}
|
|
750
|
+
</tbody>
|
|
751
|
+
</table>
|
|
752
|
+
</section>
|
|
753
|
+
`
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
function renderReferenceSection(snapshot, currentPath, id, title, rows) {
|
|
757
|
+
return `
|
|
758
|
+
<section class="table-shell print-break-avoid" id="${escapeHtml(id)}">
|
|
759
|
+
<div class="toolbar"><h2>${escapeHtml(title)}</h2></div>
|
|
760
|
+
<table>
|
|
761
|
+
<thead><tr><th>Kind</th><th>Idea</th><th>Path</th><th>Snippet</th></tr></thead>
|
|
762
|
+
<tbody>
|
|
763
|
+
${rows.map(row => {
|
|
764
|
+
const linked = row.direction === 'inbound'
|
|
765
|
+
? snapshot.ideasById[row.sourceIdeaId]
|
|
766
|
+
: (row.targetIdeaId ? snapshot.ideasById[row.targetIdeaId] : undefined);
|
|
767
|
+
const label = row.direction === 'inbound' ? row.label || row.targetName : row.targetName;
|
|
768
|
+
const fileTarget = row.direction === 'outbound' && !linked
|
|
769
|
+
? resolveExportFilePage(snapshot, { fileUri: row.targetPath })
|
|
770
|
+
: undefined;
|
|
771
|
+
const pathCell = fileTarget && filePageEnabled(snapshot, fileTarget.kind)
|
|
772
|
+
? `<a href="${escapeHtml(pageHref(currentPath, fileTarget.page))}">${escapeHtml(row.targetPath)}</a>`
|
|
773
|
+
: escapeHtml(row.targetPath);
|
|
774
|
+
return `
|
|
775
|
+
<tr>
|
|
776
|
+
<td>${escapeHtml(row.kind)}</td>
|
|
777
|
+
<td>${linked ? renderIdeaAnchor(snapshot, currentPath, linked, linked.name) : escapeHtml(label)}</td>
|
|
778
|
+
<td>${pathCell}</td>
|
|
779
|
+
<td>${escapeHtml(row.snippet || '—')}</td>
|
|
780
|
+
</tr>
|
|
781
|
+
`;
|
|
782
|
+
}).join('') || '<tr><td colspan="4" class="subtle">No references.</td></tr>'}
|
|
783
|
+
</tbody>
|
|
784
|
+
</table>
|
|
785
|
+
</section>
|
|
786
|
+
`;
|
|
787
|
+
}
|
|
788
|
+
function enrichGraphUrls(snapshot, graph, _currentPath) {
|
|
789
|
+
if (!graph || typeof graph !== 'object') {
|
|
790
|
+
return graph;
|
|
791
|
+
}
|
|
792
|
+
// Use export-root-relative idea.page.url (e.g. ./ideas/...) so nested pages can
|
|
793
|
+
// resolve via resolveExportUrl without dropping path segments like ideas/.
|
|
794
|
+
const copy = structuredClone(graph);
|
|
795
|
+
const centerId = typeof copy.centerId === 'string' ? copy.centerId : undefined;
|
|
796
|
+
for (const node of copy.nodes ?? []) {
|
|
797
|
+
const idea = typeof node.id === 'string' ? snapshot.ideasById[node.id] : undefined;
|
|
798
|
+
if (idea) {
|
|
799
|
+
if (snapshot.pageOptions.includeIdeaPages) {
|
|
800
|
+
node.pageUrl = idea.page.url;
|
|
801
|
+
}
|
|
802
|
+
const tags = ideaTags(idea);
|
|
803
|
+
if (tags.length > 0) {
|
|
804
|
+
node.tags = tags;
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
delete node.tags;
|
|
808
|
+
}
|
|
809
|
+
const status = ideaStatus(idea);
|
|
810
|
+
if (status) {
|
|
811
|
+
node.status = status;
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
delete node.status;
|
|
815
|
+
}
|
|
816
|
+
node.attributes = idea.attributes;
|
|
817
|
+
const attributeKeys = Object.keys(idea.attributes).sort();
|
|
818
|
+
if (attributeKeys.length > 0) {
|
|
819
|
+
node.attributeKeys = attributeKeys;
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
delete node.attributeKeys;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
const fileTarget = resolveExportFilePage(snapshot, {
|
|
827
|
+
id: typeof node.id === 'string' ? node.id : undefined,
|
|
828
|
+
fileUri: typeof node.fileUri === 'string' ? node.fileUri : undefined
|
|
829
|
+
});
|
|
830
|
+
if (fileTarget && filePageEnabled(snapshot, fileTarget.kind)) {
|
|
831
|
+
node.pageUrl = fileTarget.page.url;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (centerId && node.id === centerId) {
|
|
835
|
+
node.isSubject = true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return copy;
|
|
839
|
+
}
|
|
840
|
+
function renderIdeaAttributes(snapshot, currentPath, idea) {
|
|
841
|
+
const entries = Object.entries(idea.attributes);
|
|
842
|
+
if (entries.length === 0) {
|
|
843
|
+
return '<p class="subtle">No attributes declared.</p>';
|
|
844
|
+
}
|
|
845
|
+
const rows = entries.map(([key, value]) => {
|
|
846
|
+
const href = `${pageHref(currentPath, snapshot.manifest.attributesIndex)}#attr-${slugAttributeKey(key)}`;
|
|
847
|
+
return `
|
|
848
|
+
<tr>
|
|
849
|
+
<td><a href="${escapeHtml(href)}">${escapeHtml(key)}</a></td>
|
|
850
|
+
<td>${escapeHtml(formatAttributeValue(value) || '—')}</td>
|
|
851
|
+
</tr>`;
|
|
852
|
+
}).join('');
|
|
853
|
+
return `
|
|
854
|
+
<table>
|
|
855
|
+
<thead><tr><th>Key</th><th>Value</th></tr></thead>
|
|
856
|
+
<tbody>${rows}</tbody>
|
|
857
|
+
</table>
|
|
858
|
+
<p class="subtle"><a href="${escapeHtml(pageHref(currentPath, snapshot.manifest.attributesIndex))}">Browse all attributes</a></p>
|
|
859
|
+
`;
|
|
860
|
+
}
|
|
861
|
+
function renderShell(options) {
|
|
862
|
+
const stylesheetHref = hrefFor(options.currentPath, 'assets/styles.css');
|
|
863
|
+
const appHref = hrefFor(options.currentPath, 'assets/app.js');
|
|
864
|
+
const searchIndexScriptHref = hrefFor(options.currentPath, 'assets/search-index.js');
|
|
865
|
+
const searchHref = hrefFor(options.currentPath, options.snapshot.manifest.dataSearch.path);
|
|
866
|
+
const includeGlobalSearch = options.includeGlobalSearch && options.snapshot.runtimeMode === 'interactive';
|
|
867
|
+
const navLinks = [
|
|
868
|
+
navLink(options, 'overview', options.snapshot.manifest.home, true),
|
|
869
|
+
navLink(options, 'ideas', options.snapshot.manifest.ideasIndex, true),
|
|
870
|
+
navLink(options, 'files', options.snapshot.manifest.filesIndex, options.snapshot.pageOptions.includeFilePages),
|
|
871
|
+
navLink(options, 'code-files', options.snapshot.manifest.codeFilesIndex, options.snapshot.pageOptions.includeCodeFilePages),
|
|
872
|
+
navLink(options, 'clusters', options.snapshot.manifest.clustersIndex, options.snapshot.pageOptions.includeClusterPages),
|
|
873
|
+
navLink(options, 'attributes', options.snapshot.manifest.attributesIndex, true),
|
|
874
|
+
navLink(options, 'graph', options.snapshot.manifest.graph, options.snapshot.pageOptions.includeGraphPage && options.snapshot.runtimeMode !== 'print'),
|
|
875
|
+
navLink(options, 'print', options.snapshot.manifest.printHome, options.snapshot.pageOptions.includePrintPages)
|
|
876
|
+
].filter(Boolean).join('');
|
|
877
|
+
const breadcrumbs = options.breadcrumbs?.length
|
|
878
|
+
? `<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>`
|
|
879
|
+
: '';
|
|
880
|
+
const interactiveScripts = options.snapshot.runtimeMode === 'print'
|
|
881
|
+
? ''
|
|
882
|
+
: `
|
|
883
|
+
<script src="${escapeHtml(searchIndexScriptHref)}"></script>
|
|
884
|
+
<script src="${escapeHtml(appHref)}"></script>`;
|
|
885
|
+
return `<!DOCTYPE html>
|
|
886
|
+
<html lang="en">
|
|
887
|
+
<head>
|
|
888
|
+
<meta charset="utf-8" />
|
|
889
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
890
|
+
<title>${escapeHtml(options.title)}</title>
|
|
891
|
+
<link rel="stylesheet" href="${escapeHtml(stylesheetHref)}" />
|
|
892
|
+
</head>
|
|
893
|
+
<body data-search-index="${escapeHtml(searchHref)}">
|
|
894
|
+
<main class="layout">
|
|
895
|
+
<header class="topbar">
|
|
896
|
+
<div class="topbar-inner">
|
|
897
|
+
<div class="nav">${navLinks}</div>
|
|
898
|
+
${includeGlobalSearch ? `
|
|
899
|
+
<div>
|
|
900
|
+
<input class="searchbar" type="search" placeholder="Search ideas, files, clusters, tags, statuses" data-global-search />
|
|
901
|
+
</div>
|
|
902
|
+
` : ''}
|
|
903
|
+
</div>
|
|
904
|
+
${includeGlobalSearch ? '<div class="search-results hidden" data-search-results></div>' : ''}
|
|
905
|
+
</header>
|
|
906
|
+
${breadcrumbs}
|
|
907
|
+
${options.body}
|
|
908
|
+
</main>${interactiveScripts}
|
|
909
|
+
</body>
|
|
910
|
+
</html>`;
|
|
911
|
+
}
|
|
912
|
+
function navLink(options, navId, page, enabled) {
|
|
913
|
+
if (!enabled) {
|
|
914
|
+
return '';
|
|
915
|
+
}
|
|
916
|
+
const href = pageHref(options.currentPath, page);
|
|
917
|
+
const activeClass = options.activeNav === navId ? 'active' : '';
|
|
918
|
+
return `<a class="${activeClass}" href="${escapeHtml(href)}">${escapeHtml(page.title)}</a>`;
|
|
919
|
+
}
|
|
920
|
+
function renderMaybeLinkedCard(enabled, href, title, subtitle) {
|
|
921
|
+
return enabled
|
|
922
|
+
? `<a class="entity-card" href="${escapeHtml(href)}"><strong>${escapeHtml(title)}</strong><p class="subtle">${escapeHtml(subtitle)}</p></a>`
|
|
923
|
+
: `<div class="entity-card"><strong>${escapeHtml(title)}</strong><p class="subtle">${escapeHtml(subtitle)}</p></div>`;
|
|
924
|
+
}
|
|
925
|
+
function renderIdeaAnchor(snapshot, currentPath, idea, label) {
|
|
926
|
+
return snapshot.pageOptions.includeIdeaPages
|
|
927
|
+
? `<a href="${escapeHtml(pageHref(currentPath, idea.page))}">${escapeHtml(label)}</a>`
|
|
928
|
+
: escapeHtml(label);
|
|
929
|
+
}
|
|
930
|
+
//# sourceMappingURL=html-export-template.js.map
|