@reqlan/analytical 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +164 -0
  2. package/README.template.md +33 -0
  3. package/out/analysis-api.d.ts +8 -0
  4. package/out/analysis-api.js +21 -0
  5. package/out/analysis-api.js.map +1 -1
  6. package/out/core/application-memory.d.ts +11 -0
  7. package/out/core/application-memory.js +26 -0
  8. package/out/core/application-memory.js.map +1 -0
  9. package/out/create-runtime.d.ts +1 -0
  10. package/out/create-runtime.js +2 -3
  11. package/out/create-runtime.js.map +1 -1
  12. package/out/export/build-export-snapshot.js +52 -30
  13. package/out/export/build-export-snapshot.js.map +1 -1
  14. package/out/export/export-html.d.ts +4 -0
  15. package/out/export/export-html.js +8 -0
  16. package/out/export/export-html.js.map +1 -0
  17. package/out/export/html-export-assets.d.ts +2 -2
  18. package/out/export/html-export-assets.js +187 -17
  19. package/out/export/html-export-assets.js.map +1 -1
  20. package/out/export/html-export-template.d.ts +2 -1
  21. package/out/export/html-export-template.js +230 -120
  22. package/out/export/html-export-template.js.map +1 -1
  23. package/out/export/html-export-utils.d.ts +13 -2
  24. package/out/export/html-export-utils.js +167 -52
  25. package/out/export/html-export-utils.js.map +1 -1
  26. package/out/export/secret-rq.d.ts +2 -0
  27. package/out/export/secret-rq.js +9 -0
  28. package/out/export/secret-rq.js.map +1 -0
  29. package/out/export/types.d.ts +25 -0
  30. package/out/export/write-html-export.js +7 -1
  31. package/out/export/write-html-export.js.map +1 -1
  32. package/out/headless-index-service.js +2 -1
  33. package/out/headless-index-service.js.map +1 -1
  34. package/out/index-store/idea-extractor.js +42 -7
  35. package/out/index-store/idea-extractor.js.map +1 -1
  36. package/out/index-store/webview-graph-queries.d.ts +12 -0
  37. package/out/index-store/webview-graph-queries.js +37 -3
  38. package/out/index-store/webview-graph-queries.js.map +1 -1
  39. package/out/index.d.ts +5 -2
  40. package/out/index.js +4 -1
  41. package/out/index.js.map +1 -1
  42. package/package.json +2 -2
@@ -1,8 +1,14 @@
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';
1
+ import { escapeHtml, fileByIdea, filePageEnabled, formatAttributeValue, formatDate, hrefFor, ideaStatus, ideaTags, pageHref, relatedClusters, renderAttributeValueHtml, renderDefinitionList, renderIdeaSummaryHtml, renderMetric, renderOptionalStatusCell, renderOptionalStatusParagraph, renderOptionalTagsCell, renderOptionalTagsParagraph, renderTextWithRefsHtml, resolveExportFilePage, slugAttributeKey, stringifyJson } from './html-export-utils.js';
2
+ function exportHref(snapshot, currentPath, targetPath) {
3
+ return hrefFor(currentPath, targetPath, snapshot.urlBase);
4
+ }
5
+ function exportPageHref(snapshot, currentPath, pageInfo) {
6
+ return pageHref(currentPath, pageInfo, snapshot.urlBase);
7
+ }
2
8
  export function renderHomePage(snapshot) {
3
9
  const currentPath = snapshot.manifest.home.path;
4
10
  const highlightClusters = snapshot.clusters.slice(0, 8).map(cluster => `
5
- <a class="entity-card" href="${escapeHtml(pageHref(currentPath, cluster.page))}">
11
+ <a class="entity-card" href="${escapeHtml(exportPageHref(snapshot, currentPath, cluster.page))}">
6
12
  <div class="pill-row">
7
13
  <span class="pill">${escapeHtml(cluster.kind)}</span>
8
14
  <span class="pill">${cluster.counts.ideas} ideas</span>
@@ -27,29 +33,29 @@ export function renderHomePage(snapshot) {
27
33
  </div>
28
34
  </header>
29
35
  <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
+ ${renderMetric('Ideas', String(snapshot.counts.ideas), exportPageHref(snapshot, currentPath, snapshot.manifest.ideasIndex))}
37
+ ${renderMetric('References', String(snapshot.counts.edges), exportPageHref(snapshot, currentPath, snapshot.manifest.graph))}
38
+ ${renderMetric('Files', String(snapshot.counts.files), exportPageHref(snapshot, currentPath, snapshot.manifest.filesIndex))}
39
+ ${renderMetric('Code files', String(snapshot.codeFiles.length), exportPageHref(snapshot, currentPath, snapshot.manifest.codeFilesIndex))}
40
+ ${renderMetric('Clusters', String(snapshot.counts.clusters), exportPageHref(snapshot, currentPath, snapshot.manifest.clustersIndex))}
41
+ ${renderMetric('Attributes', String(snapshot.attributes.length), exportPageHref(snapshot, currentPath, snapshot.manifest.attributesIndex))}
36
42
  </section>
37
43
  <section class="split">
38
44
  <div class="panel">
39
45
  <h2>Status Rollup</h2>
40
- ${renderDefinitionList(snapshot.byStatus)}
46
+ <div class="scroll-window">${renderDefinitionList(snapshot.byStatus)}</div>
41
47
  </div>
42
48
  <div class="panel">
43
49
  <h2>Tags</h2>
44
- ${renderDefinitionList(snapshot.byTag)}
50
+ <div class="scroll-window">${renderDefinitionList(snapshot.byTag)}</div>
45
51
  </div>
46
52
  </section>
47
53
  <section class="panel">
48
54
  <div class="toolbar">
49
55
  <h2>Highlighted Clusters</h2>
50
56
  <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>
57
+ <a class="pill" href="${escapeHtml(exportPageHref(snapshot, currentPath, snapshot.manifest.printHome))}">Printable report</a>
58
+ <a class="pill" href="${escapeHtml(exportPageHref(snapshot, currentPath, snapshot.manifest.dataExport))}">Snapshot JSON</a>
53
59
  </div>
54
60
  </div>
55
61
  <div class="entity-list">${highlightClusters}</div>
@@ -72,7 +78,7 @@ export function renderIdeasIndexPage(snapshot) {
72
78
  <td>${escapeHtml(idea.fileUri)}</td>
73
79
  <td>${renderOptionalStatusCell(idea)}</td>
74
80
  <td>${renderOptionalTagsCell(idea)}</td>
75
- <td>${escapeHtml(idea.summary || '—')}</td>
81
+ <td>${renderIdeaSummaryHtml(snapshot, currentPath, idea, '—')}</td>
76
82
  <td>${idea.references.inbound.length}/${idea.references.outbound.length}</td>
77
83
  </tr>
78
84
  `).join('');
@@ -116,7 +122,7 @@ export function renderFilesIndexPage(snapshot) {
116
122
  const currentPath = snapshot.manifest.filesIndex.path;
117
123
  const rows = snapshot.files.map(file => `
118
124
  <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>
125
+ <td><strong>${snapshot.pageOptions.includeFilePages ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, file.page))}">${escapeHtml(file.name)}</a>` : escapeHtml(file.name)}</strong></td>
120
126
  <td>${escapeHtml(file.directory || '.')}</td>
121
127
  <td>${file.ideas.length}</td>
122
128
  <td>${file.edgeCount}</td>
@@ -156,7 +162,7 @@ export function renderCodeFilesIndexPage(snapshot) {
156
162
  const currentPath = snapshot.manifest.codeFilesIndex.path;
157
163
  const rows = snapshot.codeFiles.map(file => `
158
164
  <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>
165
+ <td><strong>${snapshot.pageOptions.includeCodeFilePages ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, file.page))}">${escapeHtml(file.name)}</a>` : escapeHtml(file.name)}</strong></td>
160
166
  <td>${escapeHtml(file.directory || '.')}</td>
161
167
  <td>${file.referencingIdeaIds.length}</td>
162
168
  <td>${escapeHtml(file.labels.join(', ') || '—')}</td>
@@ -195,7 +201,7 @@ export function renderClustersIndexPage(snapshot) {
195
201
  const currentPath = snapshot.manifest.clustersIndex.path;
196
202
  const rows = snapshot.clusters.map(cluster => `
197
203
  <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>
204
+ <td><strong>${snapshot.pageOptions.includeClusterPages ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, cluster.page))}">${escapeHtml(cluster.label)}</a>` : escapeHtml(cluster.label)}</strong></td>
199
205
  <td>${escapeHtml(cluster.kind)}</td>
200
206
  <td>${cluster.counts.ideas}</td>
201
207
  <td>${cluster.counts.files}</td>
@@ -239,22 +245,15 @@ export function renderAttributesIndexPage(snapshot) {
239
245
  .slice(0, 6)
240
246
  .map(entry => `${entry.value} (${entry.count})`)
241
247
  .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(', ');
248
+ const keyCell = snapshot.pageOptions.includeAttributePages
249
+ ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, attribute.page))}">${escapeHtml(attribute.key)}</a>`
250
+ : escapeHtml(attribute.key);
251
251
  return `
252
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>
253
+ <td><strong>${keyCell}</strong></td>
254
254
  <td>${attribute.ideaCount}</td>
255
255
  <td>${attribute.values.length}</td>
256
256
  <td>${escapeHtml(valueSummary || '—')}</td>
257
- <td>${ideaLinks || '—'}${attribute.ideaIds.length > 8 ? ` <span class="subtle">+${attribute.ideaIds.length - 8} more</span>` : ''}</td>
258
257
  </tr>`;
259
258
  }).join('');
260
259
  return renderShell({
@@ -267,20 +266,103 @@ export function renderAttributesIndexPage(snapshot) {
267
266
  <header class="page-header">
268
267
  <p class="eyebrow">Attributes</p>
269
268
  <h1>${escapeHtml(snapshot.title)}</h1>
270
- <p class="subtle">Every attribute key in this export with values and linked ideas.</p>
269
+ <p class="subtle">Every attribute key in this export with values and idea counts.</p>
271
270
  </header>
272
271
  <section class="table-shell">
273
272
  <div class="toolbar">
274
273
  <h2>Attributes</h2>
275
274
  <div class="actions">
276
- <input class="searchbar" type="search" placeholder="Filter attributes by key, value, or idea" data-filter-input="attributes" />
275
+ <input class="searchbar" type="search" placeholder="Filter attributes by key or value" data-filter-input="attributes" />
277
276
  </div>
278
277
  </div>
279
278
  <table>
280
279
  <thead>
281
- <tr><th>Attribute</th><th>Ideas</th><th>Values</th><th>Value summary</th><th>Ideas</th></tr>
280
+ <tr><th>Attribute</th><th>Ideas</th><th>Values</th><th>Value summary</th></tr>
282
281
  </thead>
283
- <tbody>${rows || '<tr><td colspan="5" class="subtle">No attributes declared.</td></tr>'}</tbody>
282
+ <tbody>${rows || '<tr><td colspan="4" class="subtle">No attributes declared.</td></tr>'}</tbody>
283
+ </table>
284
+ </section>
285
+ `
286
+ });
287
+ }
288
+ export function renderAttributeDetailPage(snapshot, attribute) {
289
+ const currentPath = attribute.page.path;
290
+ const ideas = attribute.ideaIds
291
+ .map(ideaId => snapshot.ideasById[ideaId])
292
+ .filter((idea) => Boolean(idea));
293
+ const totalIdeas = attribute.ideaCount || 1;
294
+ const valueRows = attribute.values.map(entry => {
295
+ const percent = Math.round((entry.count / totalIdeas) * 1000) / 10;
296
+ const width = Math.max(entry.count > 0 ? 1.5 : 0, Math.min(100, (entry.count / totalIdeas) * 100));
297
+ const filterText = [entry.value, String(entry.count), `${percent}%`, ...entry.ideaIds].join(' ');
298
+ return `
299
+ <tr data-filter-row="attributeValues" data-filter-text="${escapeHtml(filterText)}">
300
+ <td>${escapeHtml(entry.value)}</td>
301
+ <td>
302
+ <div class="distribution-track" title="${escapeHtml(`${entry.count} of ${attribute.ideaCount} ideas (${percent}%)`)}" role="img" aria-label="${escapeHtml(`${entry.value}: ${entry.count} ideas, ${percent}%`)}">
303
+ <span class="distribution-fill" style="width: ${width}%"></span>
304
+ </div>
305
+ </td>
306
+ <td>${entry.count}</td>
307
+ <td>${percent}%</td>
308
+ </tr>`;
309
+ }).join('');
310
+ const ideaRows = ideas.map(idea => {
311
+ const value = formatAttributeValue(idea.attributes[attribute.key]) || '(empty)';
312
+ const filterText = [idea.name, value, ideaStatus(idea) ?? '', ideaTags(idea).join(' '), idea.summary].join(' ');
313
+ return `
314
+ <tr data-filter-row="attributeIdeas" data-filter-text="${escapeHtml(filterText)}">
315
+ <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
316
+ <td>${escapeHtml(value)}</td>
317
+ <td>${renderOptionalStatusCell(idea)}</td>
318
+ <td>${renderOptionalTagsCell(idea)}</td>
319
+ <td>${escapeHtml(idea.summary)}</td>
320
+ </tr>`;
321
+ }).join('');
322
+ return renderShell({
323
+ currentPath,
324
+ activeNav: 'attributes',
325
+ title: `${snapshot.title} - ${attribute.key}`,
326
+ snapshot,
327
+ includeGlobalSearch: snapshot.runtimeMode === 'interactive',
328
+ breadcrumbs: [
329
+ { label: 'Attributes', href: exportPageHref(snapshot, currentPath, snapshot.manifest.attributesIndex) },
330
+ { label: attribute.key }
331
+ ],
332
+ body: `
333
+ <header class="page-header">
334
+ <p class="eyebrow">Attribute detail</p>
335
+ <h1>${escapeHtml(attribute.key)}</h1>
336
+ <p class="subtle">${attribute.ideaCount} ideas · ${attribute.values.length} distinct values</p>
337
+ </header>
338
+ <section class="grid">
339
+ ${renderMetric('Ideas', String(attribute.ideaCount))}
340
+ ${renderMetric('Values', String(attribute.values.length))}
341
+ </section>
342
+ <section class="table-shell">
343
+ <div class="toolbar">
344
+ <h2>Value distribution</h2>
345
+ <div class="actions">
346
+ <input class="searchbar" type="search" placeholder="Filter values" data-filter-input="attributeValues" />
347
+ </div>
348
+ </div>
349
+ <div class="scroll-window">
350
+ <table>
351
+ <thead><tr><th>Value</th><th>Distribution</th><th>Ideas</th><th>Share</th></tr></thead>
352
+ <tbody>${valueRows || '<tr><td colspan="4" class="subtle">No values.</td></tr>'}</tbody>
353
+ </table>
354
+ </div>
355
+ </section>
356
+ <section class="table-shell">
357
+ <div class="toolbar">
358
+ <h2>Ideas with this attribute</h2>
359
+ <div class="actions">
360
+ <input class="searchbar" type="search" placeholder="Filter ideas by name, value, status, or tags" data-filter-input="attributeIdeas" />
361
+ </div>
362
+ </div>
363
+ <table>
364
+ <thead><tr><th>Idea</th><th>Value</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
365
+ <tbody>${ideaRows || '<tr><td colspan="5" class="subtle">No ideas declare this attribute.</td></tr>'}</tbody>
284
366
  </table>
285
367
  </section>
286
368
  `
@@ -292,7 +374,7 @@ export function renderGraphPage(snapshot) {
292
374
  const statuses = Object.keys(snapshot.byStatus).sort();
293
375
  const tags = Object.keys(snapshot.byTag).sort();
294
376
  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>`
377
+ ? `<a class="pill" href="${escapeHtml(exportPageHref(snapshot, currentPath, cluster.page))}">${escapeHtml(cluster.label)}</a>`
296
378
  : `<span class="pill">${escapeHtml(cluster.label)}</span>`).join('');
297
379
  return renderShell({
298
380
  currentPath,
@@ -310,7 +392,7 @@ export function renderGraphPage(snapshot) {
310
392
  <div class="toolbar">
311
393
  <h2>Reference graph</h2>
312
394
  <div class="actions">
313
- <a class="pill" href="${escapeHtml(pageHref(currentPath, snapshot.manifest.dataGraph))}">Graph JSON</a>
395
+ <a class="pill" href="${escapeHtml(exportPageHref(snapshot, currentPath, snapshot.manifest.dataGraph))}">Graph JSON</a>
314
396
  </div>
315
397
  </div>
316
398
  <div class="graph-controls-bar" data-graph-controls="graph-data">
@@ -325,6 +407,7 @@ export function renderGraphPage(snapshot) {
325
407
  ${tags.map(tag => `<option value="${escapeHtml(tag)}">${escapeHtml(tag)}</option>`).join('')}
326
408
  </select>
327
409
  <button type="button" class="graph-action" data-graph-toggle-external="true">Hide external</button>
410
+ <button type="button" class="graph-action" data-graph-toggle-ideasets="true">Hide ideasets</button>
328
411
  <button type="button" class="graph-action" data-graph-toggle-physics aria-pressed="false">Live physics</button>
329
412
  <button type="button" class="graph-action" data-graph-fit>Fit</button>
330
413
  <button type="button" class="graph-action" data-graph-reset>Reset</button>
@@ -349,7 +432,7 @@ export function renderIdeaDetailPage(snapshot, idea) {
349
432
  snapshot,
350
433
  includeGlobalSearch: snapshot.runtimeMode === 'interactive',
351
434
  breadcrumbs: [
352
- { label: 'Ideas', href: pageHref(currentPath, snapshot.manifest.ideasIndex) },
435
+ { label: 'Ideas', href: exportPageHref(snapshot, currentPath, snapshot.manifest.ideasIndex) },
353
436
  { label: idea.name }
354
437
  ],
355
438
  body: `
@@ -373,9 +456,9 @@ export function renderIdeaDetailPage(snapshot, idea) {
373
456
  <div class="panel print-break-avoid">
374
457
  <h2>Navigation</h2>
375
458
  <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>` : ''}
459
+ ${parentFile ? renderMaybeLinkedCard(snapshot.pageOptions.includeFilePages, exportPageHref(snapshot, currentPath, parentFile.page), 'Source file', parentFile.fileUri) : ''}
460
+ ${clusters.map(cluster => renderMaybeLinkedCard(snapshot.pageOptions.includeClusterPages, exportPageHref(snapshot, currentPath, cluster.page), cluster.label, `${cluster.kind} cluster`)).join('')}
461
+ ${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>` : ''}
379
462
  </div>
380
463
  </div>
381
464
  </section>
@@ -386,13 +469,15 @@ export function renderIdeaDetailPage(snapshot, idea) {
386
469
  </div>
387
470
  <div class="panel print-break-avoid">
388
471
  <h2>Ancestor context</h2>
389
- <div class="entity-list">
390
- ${idea.ancestors.ancestors.map(ancestor => {
472
+ <div class="scroll-window">
473
+ <div class="entity-list">
474
+ ${idea.ancestors.ancestors.map(ancestor => {
391
475
  const linked = snapshot.ideasById[ancestor.id];
392
476
  return linked
393
- ? renderMaybeLinkedCard(snapshot.pageOptions.includeIdeaPages, pageHref(currentPath, linked.page), ancestor.name, ancestor.summary)
477
+ ? renderMaybeLinkedCard(snapshot.pageOptions.includeIdeaPages, exportPageHref(snapshot, currentPath, linked.page), ancestor.name, ancestor.summary)
394
478
  : '';
395
479
  }).join('') || '<p class="subtle">No ancestor chain recorded.</p>'}
480
+ </div>
396
481
  </div>
397
482
  </div>
398
483
  </section>
@@ -423,7 +508,7 @@ export function renderFileDetailPage(snapshot, file) {
423
508
  snapshot,
424
509
  includeGlobalSearch: snapshot.runtimeMode === 'interactive',
425
510
  breadcrumbs: [
426
- { label: 'Files', href: pageHref(currentPath, snapshot.manifest.filesIndex) },
511
+ { label: 'Files', href: exportPageHref(snapshot, currentPath, snapshot.manifest.filesIndex) },
427
512
  { label: file.name }
428
513
  ],
429
514
  body: `
@@ -446,24 +531,28 @@ export function renderFileDetailPage(snapshot, file) {
446
531
  <input class="searchbar" type="search" placeholder="Filter file ideas" data-filter-input="fileIdeas" />
447
532
  </div>
448
533
  </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>
534
+ <div class="scroll-window">
535
+ <table>
536
+ <thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
537
+ <tbody>
538
+ ${file.ideas.map(idea => `
539
+ <tr data-filter-row="fileIdeas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' ')].join(' '))}">
540
+ <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
541
+ <td>${renderOptionalStatusCell(idea)}</td>
542
+ <td>${renderOptionalTagsCell(idea)}</td>
543
+ <td>${escapeHtml(idea.summary)}</td>
544
+ </tr>
545
+ `).join('')}
546
+ </tbody>
547
+ </table>
548
+ </div>
462
549
  </div>
463
550
  <div class="panel">
464
551
  <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>'}
552
+ <div class="scroll-window">
553
+ <div class="entity-list">
554
+ ${clusterLinks.map(cluster => renderMaybeLinkedCard(snapshot.pageOptions.includeClusterPages, exportPageHref(snapshot, currentPath, cluster.page), cluster.label, cluster.kind)).join('') || '<p class="subtle">No related clusters.</p>'}
555
+ </div>
467
556
  </div>
468
557
  </div>
469
558
  </section>
@@ -492,7 +581,7 @@ export function renderCodeFileDetailPage(snapshot, file) {
492
581
  snapshot,
493
582
  includeGlobalSearch: snapshot.runtimeMode === 'interactive',
494
583
  breadcrumbs: [
495
- { label: 'Code files', href: pageHref(currentPath, snapshot.manifest.codeFilesIndex) },
584
+ { label: 'Code files', href: exportPageHref(snapshot, currentPath, snapshot.manifest.codeFilesIndex) },
496
585
  { label: file.name }
497
586
  ],
498
587
  body: `
@@ -512,19 +601,21 @@ export function renderCodeFileDetailPage(snapshot, file) {
512
601
  <input class="searchbar" type="search" placeholder="Filter referencing ideas" data-filter-input="codeFileIdeas" />
513
602
  </div>
514
603
  </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>
604
+ <div class="scroll-window">
605
+ <table>
606
+ <thead><tr><th>Idea</th><th>Status</th><th>Tags</th><th>Summary</th></tr></thead>
607
+ <tbody>
608
+ ${referencingIdeas.map(idea => `
609
+ <tr data-filter-row="codeFileIdeas" data-filter-text="${escapeHtml([idea.name, idea.summary, ideaStatus(idea) ?? '', ideaTags(idea).join(' ')].join(' '))}">
610
+ <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
611
+ <td>${renderOptionalStatusCell(idea)}</td>
612
+ <td>${renderOptionalTagsCell(idea)}</td>
613
+ <td>${escapeHtml(idea.summary)}</td>
614
+ </tr>
615
+ `).join('') || '<tr><td colspan="4" class="subtle">No referencing ideas.</td></tr>'}
616
+ </tbody>
617
+ </table>
618
+ </div>
528
619
  </section>
529
620
  ${file.labels.length > 0 ? `
530
621
  <section class="panel">
@@ -548,7 +639,7 @@ export function renderClusterDetailPage(snapshot, cluster) {
548
639
  snapshot,
549
640
  includeGlobalSearch: snapshot.runtimeMode === 'interactive',
550
641
  breadcrumbs: [
551
- { label: 'Clusters', href: pageHref(currentPath, snapshot.manifest.clustersIndex) },
642
+ { label: 'Clusters', href: exportPageHref(snapshot, currentPath, snapshot.manifest.clustersIndex) },
552
643
  { label: cluster.label }
553
644
  ],
554
645
  body: `
@@ -569,19 +660,21 @@ export function renderClusterDetailPage(snapshot, cluster) {
569
660
  <input class="searchbar" type="search" placeholder="Filter cluster members" data-filter-input="clusterIdeas" />
570
661
  </div>
571
662
  </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>
663
+ <div class="scroll-window">
664
+ <table>
665
+ <thead><tr><th>Idea</th><th>File</th><th>Status</th><th>Summary</th></tr></thead>
666
+ <tbody>
667
+ ${members.map(idea => `
668
+ <tr data-filter-row="clusterIdeas" data-filter-text="${escapeHtml([idea.name, idea.fileUri, ideaStatus(idea) ?? '', idea.summary].join(' '))}">
669
+ <td>${renderIdeaAnchor(snapshot, currentPath, idea, idea.name)}</td>
670
+ <td>${escapeHtml(idea.fileUri)}</td>
671
+ <td>${renderOptionalStatusCell(idea)}</td>
672
+ <td>${escapeHtml(idea.summary)}</td>
673
+ </tr>
674
+ `).join('')}
675
+ </tbody>
676
+ </table>
677
+ </div>
585
678
  </section>
586
679
  <section class="graph-shell print-break-avoid">
587
680
  <div class="toolbar"><h2>Cluster graph</h2></div>
@@ -600,7 +693,7 @@ export function renderPrintHomePage(snapshot) {
600
693
  const currentPath = snapshot.manifest.printHome.path;
601
694
  const cards = snapshot.ideas.map(idea => `
602
695
  <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>
696
+ <h3>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</h3>
604
697
  <p class="subtle">${escapeHtml(idea.fileUri)}</p>
605
698
  <p>${renderIdeaSummaryHtml(snapshot, currentPath, idea, '—')}</p>
606
699
  ${renderOptionalStatusParagraph(idea)}
@@ -626,7 +719,7 @@ export function renderPrintHomePage(snapshot) {
626
719
  </section>
627
720
  <section class="panel print-break-avoid">
628
721
  <h2>Status rollup</h2>
629
- ${renderDefinitionList(snapshot.byStatus)}
722
+ <div class="scroll-window">${renderDefinitionList(snapshot.byStatus)}</div>
630
723
  </section>
631
724
  <section class="entity-list">${cards}</section>
632
725
  `
@@ -649,7 +742,7 @@ export function renderPrintIdeaPage(snapshot, idea) {
649
742
  <p class="idea-summary">${renderIdeaSummaryHtml(snapshot, currentPath, idea)}</p>
650
743
  ${renderOptionalStatusParagraph(idea)}
651
744
  ${renderOptionalTagsParagraph(idea)}
652
- ${snapshot.pageOptions.includeIdeaPages ? `<p><strong>Interactive page:</strong> <a href="${escapeHtml(hrefFor(currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a></p>` : ''}
745
+ ${snapshot.pageOptions.includeIdeaPages ? `<p><strong>Interactive page:</strong> <a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a></p>` : ''}
653
746
  </section>
654
747
  `
655
748
  });
@@ -673,7 +766,7 @@ export function renderPrintFilePage(snapshot, file) {
673
766
  <tbody>
674
767
  ${file.ideas.map(idea => `
675
768
  <tr>
676
- <td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(hrefFor(currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
769
+ <td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
677
770
  <td>${renderOptionalStatusCell(idea)}</td>
678
771
  <td>${renderOptionalTagsCell(idea)}</td>
679
772
  <td>${escapeHtml(idea.summary)}</td>
@@ -707,7 +800,7 @@ export function renderPrintCodeFilePage(snapshot, file) {
707
800
  <tbody>
708
801
  ${referencingIdeas.map(idea => `
709
802
  <tr>
710
- <td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(hrefFor(currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
803
+ <td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
711
804
  <td>${renderOptionalStatusCell(idea)}</td>
712
805
  <td>${renderOptionalTagsCell(idea)}</td>
713
806
  <td>${escapeHtml(idea.summary)}</td>
@@ -741,7 +834,7 @@ export function renderPrintClusterPage(snapshot, cluster) {
741
834
  <tbody>
742
835
  ${members.map(idea => `
743
836
  <tr>
744
- <td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(hrefFor(currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
837
+ <td>${snapshot.pageOptions.includeIdeaPages ? `<a href="${escapeHtml(exportHref(snapshot, currentPath, idea.page.path))}">${escapeHtml(idea.name)}</a>` : escapeHtml(idea.name)}</td>
745
838
  <td>${escapeHtml(idea.fileUri)}</td>
746
839
  <td>${renderOptionalStatusCell(idea)}</td>
747
840
  <td>${escapeHtml(idea.summary)}</td>
@@ -757,10 +850,11 @@ function renderReferenceSection(snapshot, currentPath, id, title, rows) {
757
850
  return `
758
851
  <section class="table-shell print-break-avoid" id="${escapeHtml(id)}">
759
852
  <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 => {
853
+ <div class="scroll-window">
854
+ <table>
855
+ <thead><tr><th>Kind</th><th>Idea</th><th>Path</th><th>Snippet</th></tr></thead>
856
+ <tbody>
857
+ ${rows.map(row => {
764
858
  const linked = row.direction === 'inbound'
765
859
  ? snapshot.ideasById[row.sourceIdeaId]
766
860
  : (row.targetIdeaId ? snapshot.ideasById[row.targetIdeaId] : undefined);
@@ -769,19 +863,27 @@ function renderReferenceSection(snapshot, currentPath, id, title, rows) {
769
863
  ? resolveExportFilePage(snapshot, { fileUri: row.targetPath })
770
864
  : undefined;
771
865
  const pathCell = fileTarget && filePageEnabled(snapshot, fileTarget.kind)
772
- ? `<a href="${escapeHtml(pageHref(currentPath, fileTarget.page))}">${escapeHtml(row.targetPath)}</a>`
866
+ ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, fileTarget.page))}">${escapeHtml(row.targetPath)}</a>`
773
867
  : escapeHtml(row.targetPath);
868
+ const ideaCell = linked
869
+ ? renderIdeaAnchor(snapshot, currentPath, linked, linked.name)
870
+ : (fileTarget && filePageEnabled(snapshot, fileTarget.kind)
871
+ ? `<a class="idea-ref idea-ref--file" href="${escapeHtml(exportPageHref(snapshot, currentPath, fileTarget.page))}">${escapeHtml(label)}</a>`
872
+ : escapeHtml(label));
873
+ const snippetCell = row.snippet
874
+ ? renderTextWithRefsHtml(snapshot, currentPath, row.snippet, { idea: snapshot.ideasById[row.sourceIdeaId] })
875
+ : '—';
774
876
  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>
877
+ <tr>
878
+ <td>${escapeHtml(row.kind)}</td>
879
+ <td>${ideaCell}</td>
880
+ <td>${pathCell}</td>
881
+ <td>${snippetCell}</td>
882
+ </tr>`;
883
+ }).join('') || '<tr><td colspan="4" class="subtle">None</td></tr>'}
884
+ </tbody>
885
+ </table>
886
+ </div>
785
887
  </section>
786
888
  `;
787
889
  }
@@ -843,27 +945,35 @@ function renderIdeaAttributes(snapshot, currentPath, idea) {
843
945
  return '<p class="subtle">No attributes declared.</p>';
844
946
  }
845
947
  const rows = entries.map(([key, value]) => {
846
- const href = `${pageHref(currentPath, snapshot.manifest.attributesIndex)}#attr-${slugAttributeKey(key)}`;
948
+ const attribute = snapshot.attributesByKey[key];
949
+ const href = attribute && snapshot.pageOptions.includeAttributePages
950
+ ? exportPageHref(snapshot, currentPath, attribute.page)
951
+ : `${exportPageHref(snapshot, currentPath, snapshot.manifest.attributesIndex)}#attr-${slugAttributeKey(key)}`;
847
952
  return `
848
953
  <tr>
849
954
  <td><a href="${escapeHtml(href)}">${escapeHtml(key)}</a></td>
850
- <td>${escapeHtml(formatAttributeValue(value) || '—')}</td>
955
+ <td>${renderAttributeValueHtml(snapshot, currentPath, value, idea)}</td>
851
956
  </tr>`;
852
957
  }).join('');
853
958
  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>
959
+ <div class="scroll-window">
960
+ <table>
961
+ <thead><tr><th>Key</th><th>Value</th></tr></thead>
962
+ <tbody>${rows}</tbody>
963
+ </table>
964
+ </div>
965
+ <p class="subtle"><a href="${escapeHtml(exportPageHref(snapshot, currentPath, snapshot.manifest.attributesIndex))}">Browse all attributes</a></p>
859
966
  `;
860
967
  }
861
968
  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);
969
+ const stylesheetHref = exportHref(options.snapshot, options.currentPath, 'assets/styles.css');
970
+ const appHref = exportHref(options.snapshot, options.currentPath, 'assets/app.js');
971
+ const searchIndexScriptHref = exportHref(options.snapshot, options.currentPath, 'assets/search-index.js');
972
+ const searchHref = exportHref(options.snapshot, options.currentPath, options.snapshot.manifest.dataSearch.path);
866
973
  const includeGlobalSearch = options.includeGlobalSearch && options.snapshot.runtimeMode === 'interactive';
974
+ const headerLink = options.snapshot.headerLink
975
+ ? `<a class="brand-link" href="${escapeHtml(options.snapshot.headerLink.href)}">${escapeHtml(options.snapshot.headerLink.label)}</a>`
976
+ : '';
867
977
  const navLinks = [
868
978
  navLink(options, 'overview', options.snapshot.manifest.home, true),
869
979
  navLink(options, 'ideas', options.snapshot.manifest.ideasIndex, true),
@@ -890,11 +1000,11 @@ function renderShell(options) {
890
1000
  <title>${escapeHtml(options.title)}</title>
891
1001
  <link rel="stylesheet" href="${escapeHtml(stylesheetHref)}" />
892
1002
  </head>
893
- <body data-search-index="${escapeHtml(searchHref)}">
1003
+ <body data-runtime-mode="${escapeHtml(options.snapshot.runtimeMode)}" data-search-index="${escapeHtml(searchHref)}">
894
1004
  <main class="layout">
895
1005
  <header class="topbar">
896
1006
  <div class="topbar-inner">
897
- <div class="nav">${navLinks}</div>
1007
+ <div class="nav">${headerLink}${navLinks}</div>
898
1008
  ${includeGlobalSearch ? `
899
1009
  <div>
900
1010
  <input class="searchbar" type="search" placeholder="Search ideas, files, clusters, tags, statuses" data-global-search />
@@ -913,7 +1023,7 @@ function navLink(options, navId, page, enabled) {
913
1023
  if (!enabled) {
914
1024
  return '';
915
1025
  }
916
- const href = pageHref(options.currentPath, page);
1026
+ const href = exportPageHref(options.snapshot, options.currentPath, page);
917
1027
  const activeClass = options.activeNav === navId ? 'active' : '';
918
1028
  return `<a class="${activeClass}" href="${escapeHtml(href)}">${escapeHtml(page.title)}</a>`;
919
1029
  }
@@ -924,7 +1034,7 @@ function renderMaybeLinkedCard(enabled, href, title, subtitle) {
924
1034
  }
925
1035
  function renderIdeaAnchor(snapshot, currentPath, idea, label) {
926
1036
  return snapshot.pageOptions.includeIdeaPages
927
- ? `<a href="${escapeHtml(pageHref(currentPath, idea.page))}">${escapeHtml(label)}</a>`
1037
+ ? `<a href="${escapeHtml(exportPageHref(snapshot, currentPath, idea.page))}">${escapeHtml(label)}</a>`
928
1038
  : escapeHtml(label);
929
1039
  }
930
1040
  //# sourceMappingURL=html-export-template.js.map