@commentray/render 0.0.4 → 0.0.5

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.
@@ -3,78 +3,109 @@ import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { MARKER_ID_BODY, buildBlockScrollLinks, } from "@commentray/core";
5
5
  import { tryBuildBlockStretchTableHtml } from "./block-stretch-layout.js";
6
+ import { formatCommentrayBuiltAtLocal } from "./build-stamp.js";
6
7
  import { escapeHtml } from "./html-utils.js";
8
+ import { renderHighlightedCodeLineRows } from "./highlighted-code-lines.js";
7
9
  import { mermaidRuntimeScriptHtml } from "./mermaid-runtime-html.js";
8
- import { renderFencedCode, renderMarkdownToHtml, } from "./markdown-pipeline.js";
10
+ import { renderMarkdownToHtml } from "./markdown-pipeline.js";
11
+ import { commentrayRenderVersion } from "./package-version.js";
9
12
  function renderGeneratorMetaHtml(label) {
10
13
  const t = label?.trim();
11
14
  if (!t)
12
15
  return "";
13
16
  return `<meta name="generator" content="${escapeHtml(t)}" />\n `;
14
17
  }
15
- function extractPreCodeInner(html) {
16
- const m = /<pre(?:\s[^>]*)?>\s*<code(?:\s[^>]*)?>([\s\S]*?)<\/code>\s*<\/pre>/i.exec(html.trim());
17
- return m ? m[1] : escapeHtml(html);
18
- }
19
18
  /** Single capture: marker id (avoid a wrapping group around the whole comment — that shifted indices). */
20
19
  const BLOCK_MARKER_HTML_LINE = new RegExp(`^<!--\\s*commentray:block\\s+id=(${MARKER_ID_BODY})\\s*-->$`, "i");
21
- /** Inserts thin separator anchors after each `<!-- commentray:block … -->` line (optional index attrs for scroll sync). */
22
- function injectCommentrayBlockAnchors(markdown, links) {
23
- const byId = links ? new Map(links.map((l) => [l.id, l])) : undefined;
24
- return markdown
25
- .split("\n")
26
- .map((line) => {
27
- const m = BLOCK_MARKER_HTML_LINE.exec(line);
28
- if (!m?.[1])
29
- return line;
30
- const id = m[1];
31
- const link = byId?.get(id);
32
- const attrs = link !== undefined
33
- ? ` data-source-start="${String(link.sourceStart)}" data-commentray-line="${String(link.commentrayLine)}"`
34
- : "";
35
- return `${line}\n\n<div id="commentray-block-${escapeHtml(id)}" class="commentray-block-anchor" aria-hidden="true"${attrs}></div>`;
36
- })
37
- .join("\n");
20
+ function trimEndSpacesTabs(s) {
21
+ return s.replace(/[ \t]+$/, "");
38
22
  }
39
- /** One highlighted row per source line so in-page search can scroll to a line. */
40
- async function renderCodeLineBlocks(code, language) {
41
- const lines = code.split("\n");
42
- const langAttr = escapeHtml(language);
43
- const parts = [];
44
- for (let i = 0; i < lines.length; i++) {
45
- const line = lines[i] === "" ? " " : lines[i];
46
- const fence = "```" + language + "\n" + line + "\n```\n";
47
- const block = await renderFencedCode(fence);
48
- const inner = extractPreCodeInner(block);
49
- const num = i + 1;
50
- parts.push(`<div class="code-line" id="code-line-${i}" data-line="${i}">` +
51
- `<span class="ln" aria-hidden="true">${num}</span>` +
52
- `<pre><code class="hljs language-${langAttr}">${inner}</code></pre>` +
53
- `</div>`);
54
- }
55
- return parts.join("\n");
23
+ function isSetextUnderlineLine(line) {
24
+ const t = trimEndSpacesTabs(line);
25
+ return /^\s{0,3}=+\s*$/.test(t) || /^\s{0,3}-+\s*$/.test(t);
26
+ }
27
+ function isThematicBreakLine(line) {
28
+ const t = trimEndSpacesTabs(line);
29
+ return (/^\s{0,3}(?:\*[ \t]*){3,}\s*$/.test(t) ||
30
+ /^\s{0,3}(?:-[ \t]*){3,}\s*$/.test(t) ||
31
+ /^\s{0,3}(?:_[ \t]*){3,}\s*$/.test(t));
32
+ }
33
+ function parseFenceDelimiter(line) {
34
+ const t = trimEndSpacesTabs(line);
35
+ const m = /^(\s{0,3})(`{3,}|~{3,})(.*)$/.exec(t);
36
+ if (!m)
37
+ return null;
38
+ const run = m[2];
39
+ const head = run[0];
40
+ if (head !== "`" && head !== "~")
41
+ return null;
42
+ const ch = head === "`" ? "`" : "~";
43
+ return { ch, runLen: run.length, rest: m[3] ?? "" };
44
+ }
45
+ function isClosingFenceLine(info, open) {
46
+ if (info.ch !== open.ch || info.runLen < open.len)
47
+ return false;
48
+ return info.rest.trim() === "";
56
49
  }
57
- /** Split a repo-relative path into its directory prefix (with trailing slash) and basename. */
58
- function splitFilePath(p) {
59
- const normalized = p.replaceAll("\\", "/").replace(/^\/+/, "");
60
- const idx = normalized.lastIndexOf("/");
61
- if (idx < 0)
62
- return { dir: "", base: normalized };
63
- return { dir: normalized.slice(0, idx + 1), base: normalized.slice(idx + 1) };
50
+ function lineAnchorHtml(mdLine0) {
51
+ const mdLine = String(mdLine0);
52
+ return `<span class="commentray-line-anchor" data-commentray-md-line="${mdLine}" id="commentray-md-line-${mdLine}" aria-hidden="true"></span>`;
64
53
  }
65
- function renderFilePathLabel(filePath, fallbackTitle) {
66
- const shown = (filePath ?? "").trim();
67
- if (!shown) {
68
- return `<strong class="file-path file-path--title">${escapeHtml(fallbackTitle)}</strong>`;
54
+ function appendMdLineAnchorWhenAllowed(line, mdLine0) {
55
+ if (isSetextUnderlineLine(line) || isThematicBreakLine(line))
56
+ return line;
57
+ /** Blank lines must stay blank: a line that is only `<span …>` breaks CommonMark HTML / paragraph starts after block markers. */
58
+ if (line === "")
59
+ return "";
60
+ return `${line}${lineAnchorHtml(mdLine0)}`;
61
+ }
62
+ /**
63
+ * Inserts per-line anchors for search / hash jumps and block separator anchors after each
64
+ * `<!-- commentray:block … -->` line (optional index attrs).
65
+ *
66
+ * Anchors are appended to the line when safe. A **leading** `<span>` breaks CommonMark block
67
+ * recognition (`#` headings, lists, thematic breaks, fences). Fenced code lines must not get a
68
+ * trailing anchor either (would corrupt fence delimiters or appear inside code).
69
+ */
70
+ function injectCommentrayDocAnchors(markdown, links) {
71
+ const byId = links ? new Map(links.map((l) => [l.id, l])) : undefined;
72
+ const lines = markdown.split("\n");
73
+ let fence = null;
74
+ const out = [];
75
+ for (let i = 0; i < lines.length; i++) {
76
+ const line = lines[i];
77
+ const delim = parseFenceDelimiter(line);
78
+ if (fence) {
79
+ if (delim && isClosingFenceLine(delim, fence)) {
80
+ fence = null;
81
+ out.push(line);
82
+ continue;
83
+ }
84
+ out.push(line);
85
+ continue;
86
+ }
87
+ if (delim) {
88
+ fence = { ch: delim.ch, len: delim.runLen };
89
+ out.push(line);
90
+ continue;
91
+ }
92
+ const m = BLOCK_MARKER_HTML_LINE.exec(line);
93
+ if (m?.[1]) {
94
+ const id = m[1];
95
+ const link = byId?.get(id);
96
+ const attrs = link !== undefined
97
+ ? ` data-source-start="${String(link.sourceStart)}" data-commentray-line="${String(link.commentrayLine)}"`
98
+ : "";
99
+ /** One `push` with embedded `\n\n` merged poorly with `join("\\n")`; keep real blank lines around raw `<div>`. */
100
+ out.push(`${line}${lineAnchorHtml(i)}`);
101
+ out.push("");
102
+ out.push(`<div id="commentray-block-${escapeHtml(id)}" class="commentray-block-anchor" aria-hidden="true"${attrs}></div>`);
103
+ out.push("");
104
+ continue;
105
+ }
106
+ out.push(appendMdLineAnchorWhenAllowed(line, i));
69
107
  }
70
- const { dir, base } = splitFilePath(shown);
71
- const dirHtml = dir
72
- ? `<span class="file-path__dir">${escapeHtml(dir)}</span>`
73
- : `<span class="file-path__dir file-path__dir--root" title="Repository root">/ </span>`;
74
- return (`<strong class="file-path" title="${escapeHtml(shown)}">` +
75
- dirHtml +
76
- `<span class="file-path__base">${escapeHtml(base)}</span>` +
77
- `</strong>`);
108
+ return out.join("\n");
78
109
  }
79
110
  /** GitHub “mark” glyph (Octicons-style path), MIT-licensed silhouette. */
80
111
  const GITHUB_MARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="20" height="20" fill="currentColor" aria-hidden="true">' +
@@ -88,7 +119,7 @@ function safeExternalHttpUrl(url) {
88
119
  return null;
89
120
  return t;
90
121
  }
91
- function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl) {
122
+ function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl, commentrayRenderSemver) {
92
123
  const gh = safeExternalHttpUrl(githubRepoUrl);
93
124
  const tool = safeExternalHttpUrl(toolHomeUrl);
94
125
  const bits = [];
@@ -98,12 +129,20 @@ function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl) {
98
129
  }
99
130
  if (tool) {
100
131
  const te = escapeHtml(tool);
101
- bits.push(`<span class="toolbar-attribution" role="note">Rendered with <a href="${te}" target="_blank" rel="noopener noreferrer">Commentray</a></span>`);
132
+ const ver = escapeHtml(commentrayRenderSemver);
133
+ bits.push(`<span class="toolbar-attribution" role="note">Rendered with <a href="${te}" target="_blank" rel="noopener noreferrer">Commentray</a> <span class="toolbar-attribution__version" translate="no">v${ver}</span></span>`);
102
134
  }
103
135
  if (bits.length === 0)
104
136
  return "";
105
137
  return `<div class="toolbar__end">${bits.join("")}</div>`;
106
138
  }
139
+ function renderPageFooterHtml(builtAt) {
140
+ const iso = builtAt.toISOString();
141
+ const human = formatCommentrayBuiltAtLocal(builtAt);
142
+ return (`<footer class="app__footer" role="contentinfo">` +
143
+ `<p class="app__footer-line">HTML generated <time datetime="${escapeHtml(iso)}">${escapeHtml(human)}</time></p>` +
144
+ `</footer>`);
145
+ }
107
146
  function renderRelatedGithubNavHtml(links) {
108
147
  if (links.length === 0)
109
148
  return "";
@@ -114,34 +153,50 @@ function renderRelatedGithubNavHtml(links) {
114
153
  `</nav>`);
115
154
  }
116
155
  function renderToolbarDocHubHtml(opts) {
117
- const parts = [];
118
- const src = safeExternalHttpUrl(opts.sourceOnGithubUrl);
119
- const cr = safeExternalHttpUrl(opts.commentrayOnGithubUrl);
120
156
  const nav = opts.documentedNavJsonUrl?.trim();
121
157
  const hasEmbed = (opts.documentedPairsEmbeddedB64?.trim() ?? "").length > 0;
122
158
  const showDocumentedTree = Boolean(nav) || hasEmbed;
123
- if (src) {
124
- parts.push(`<a class="toolbar-blob-link" href="${escapeHtml(src)}" target="_blank" rel="noopener noreferrer">Source on GitHub</a>`);
125
- }
126
- if (cr) {
127
- parts.push(`<a class="toolbar-blob-link" href="${escapeHtml(cr)}" target="_blank" rel="noopener noreferrer">Commentray on GitHub</a>`);
128
- }
129
- if (showDocumentedTree) {
130
- const navAttr = nav ? escapeHtml(nav) : "";
131
- parts.push(`<button type="button" class="toolbar-tree-toggle" id="documented-files-toggle" aria-expanded="false" aria-controls="documented-files-panel" data-nav-json-url="${navAttr}">Documented files</button>`);
132
- }
133
- const toolbarDocHubHtml = parts.length > 0
134
- ? `<div class="toolbar-doc-hub">${parts.join('<span class="toolbar-doc-hub__sep" aria-hidden="true"> · </span>')}</div>`
135
- : "";
136
- const documentedPanelHtml = showDocumentedTree
137
- ? `<div id="documented-files-panel" class="documented-files-panel" hidden>
138
- <div class="documented-files-panel__inner">
139
- <p class="documented-files-panel__hint">Indexed source ↔ commentray pairs (embedded for offline when available). Links open on GitHub.</p>
159
+ const toolbarDocHubHtml = "";
160
+ const navAttr = escapeHtml(nav ?? "");
161
+ const navRailDocumentedHtml = showDocumentedTree
162
+ ? `<details class="nav-rail__doc-hub" id="documented-files-hub" data-nav-json-url="${navAttr}">
163
+ <summary class="nav-rail__doc-hub-summary">Documented files</summary>
164
+ <div class="nav-rail__doc-hub-inner">
140
165
  <div id="documented-files-tree" class="documented-files-tree" role="tree"></div>
141
166
  </div>
142
- </div>`
167
+ </details>`
143
168
  : "";
144
- return { toolbarDocHubHtml, documentedPanelHtml };
169
+ return { toolbarDocHubHtml, navRailDocumentedHtml };
170
+ }
171
+ function renderNavRailContextHtml(filePath, commentrayPath, opts) {
172
+ const fpRaw = (filePath ?? "").trim();
173
+ const crRaw = (commentrayPath ?? "").trim();
174
+ const srcUrl = safeExternalHttpUrl(opts?.sourceOnGithubUrl);
175
+ const crUrl = safeExternalHttpUrl(opts?.commentrayOnGithubUrl);
176
+ if (fpRaw.length === 0 && crRaw.length === 0 && srcUrl === null && crUrl === null) {
177
+ return "";
178
+ }
179
+ const fp = escapeHtml(fpRaw);
180
+ const cr = escapeHtml(crRaw);
181
+ const fpDisp = fpRaw.length > 0 ? fp : "—";
182
+ const crDisp = crRaw.length > 0 ? cr : "—";
183
+ const srcGh = srcUrl !== null
184
+ ? `<a class="nav-rail__pair-gh" id="toolbar-source-github" href="${escapeHtml(srcUrl)}" target="_blank" rel="noopener noreferrer" aria-label="Source file on GitHub" title="Open source on GitHub">${GITHUB_MARK_SVG}</a>`
185
+ : "";
186
+ const crGh = crUrl !== null
187
+ ? `<a class="nav-rail__pair-gh" id="toolbar-commentray-github" href="${escapeHtml(crUrl)}" target="_blank" rel="noopener noreferrer" aria-label="Companion commentray on GitHub" title="Open companion Markdown on GitHub">${GITHUB_MARK_SVG}</a>`
188
+ : "";
189
+ return `<div class="nav-rail__context nav-rail__context--compact" aria-label="Current documentation pair">
190
+ <span class="nav-rail__pair">
191
+ <span class="nav-rail__pair-lab">Src</span>
192
+ <span class="nav-rail__pair-path" title="${fp}">${fpDisp}</span>${srcGh}
193
+ </span>
194
+ <span class="nav-rail__pair-sep" aria-hidden="true">·</span>
195
+ <span class="nav-rail__pair">
196
+ <span class="nav-rail__pair-lab">Doc</span>
197
+ <span class="nav-rail__pair-path nav-rail__pair-path--secondary" title="${cr}">${crDisp}</span>${crGh}
198
+ </span>
199
+ </div>`;
145
200
  }
146
201
  /** IIFE produced by `npm run build -w @commentray/render` (esbuild of `code-browser-client.ts`). */
147
202
  function loadCodeBrowserClientBundle() {
@@ -159,6 +214,220 @@ const CODE_BROWSER_STYLES = `
159
214
  :root { color-scheme: light dark; }
160
215
  * { box-sizing: border-box; }
161
216
  body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
217
+ .app {
218
+ display: flex;
219
+ flex-direction: column;
220
+ align-items: stretch;
221
+ height: 100vh;
222
+ width: 100%;
223
+ overflow: hidden;
224
+ }
225
+ .app__chrome {
226
+ flex: 0 0 auto;
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 8px;
230
+ padding: 8px 12px 10px;
231
+ border-bottom: 1px solid color-mix(in oklab, CanvasText 15%, Canvas);
232
+ background: color-mix(in oklab, CanvasText 4%, Canvas);
233
+ max-height: min(40vh, 420px);
234
+ min-height: 0;
235
+ overflow: auto;
236
+ }
237
+ .chrome__search-row {
238
+ display: flex;
239
+ flex-direction: row;
240
+ align-items: center;
241
+ gap: 10px;
242
+ flex-wrap: nowrap;
243
+ }
244
+ .chrome__search-row input[type="search"] {
245
+ flex: 1 1 auto;
246
+ min-width: 140px;
247
+ padding: 8px 10px;
248
+ font: inherit;
249
+ font-size: 14px;
250
+ border-radius: 8px;
251
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
252
+ background: Canvas;
253
+ color: CanvasText;
254
+ }
255
+ .chrome__search-row #search-clear {
256
+ flex: 0 0 auto;
257
+ font: inherit;
258
+ padding: 6px 14px;
259
+ border-radius: 8px;
260
+ cursor: pointer;
261
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
262
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
263
+ color: CanvasText;
264
+ }
265
+ .chrome__search-label {
266
+ flex: 0 0 auto;
267
+ white-space: nowrap;
268
+ }
269
+ .nav-rail__context--compact {
270
+ display: flex;
271
+ flex-direction: row;
272
+ flex-wrap: wrap;
273
+ align-items: center;
274
+ gap: 6px 10px;
275
+ padding: 5px 10px;
276
+ border-radius: 8px;
277
+ border: 1px solid color-mix(in oklab, CanvasText 14%, Canvas);
278
+ background: Canvas;
279
+ font-size: 12px;
280
+ line-height: 1.3;
281
+ }
282
+ .nav-rail__pair {
283
+ display: inline-flex;
284
+ flex-direction: row;
285
+ align-items: center;
286
+ gap: 6px;
287
+ min-width: 0;
288
+ flex: 1 1 140px;
289
+ max-width: min(48%, 100%);
290
+ }
291
+ .nav-rail__pair-lab {
292
+ flex: 0 0 auto;
293
+ font-size: 9px;
294
+ font-weight: 700;
295
+ letter-spacing: 0.06em;
296
+ text-transform: uppercase;
297
+ opacity: 0.72;
298
+ }
299
+ .nav-rail__pair-path {
300
+ flex: 1 1 auto;
301
+ min-width: 0;
302
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
303
+ font-size: 11px;
304
+ color: CanvasText;
305
+ overflow: hidden;
306
+ text-overflow: ellipsis;
307
+ white-space: nowrap;
308
+ }
309
+ .nav-rail__pair-path--secondary { opacity: 0.88; }
310
+ .nav-rail__pair-sep {
311
+ flex: 0 0 auto;
312
+ opacity: 0.45;
313
+ user-select: none;
314
+ padding: 0 2px;
315
+ }
316
+ .nav-rail__pair-gh {
317
+ flex: 0 0 auto;
318
+ display: inline-flex;
319
+ align-items: center;
320
+ justify-content: center;
321
+ width: 26px;
322
+ height: 26px;
323
+ border-radius: 6px;
324
+ border: 1px solid color-mix(in oklab, CanvasText 20%, Canvas);
325
+ background: color-mix(in oklab, CanvasText 5%, Canvas);
326
+ color: CanvasText;
327
+ }
328
+ .nav-rail__pair-gh:hover {
329
+ background: color-mix(in oklab, CanvasText 10%, Canvas);
330
+ }
331
+ .nav-rail__pair-gh:focus-visible {
332
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
333
+ outline-offset: 2px;
334
+ }
335
+ .nav-rail__pair-gh svg {
336
+ width: 14px;
337
+ height: 14px;
338
+ display: block;
339
+ }
340
+ .toolbar .nav-rail__context--compact {
341
+ border: 0;
342
+ background: transparent;
343
+ padding: 0;
344
+ flex: 1 1 200px;
345
+ min-width: 0;
346
+ max-width: none;
347
+ gap: 6px 10px;
348
+ }
349
+ .toolbar .nav-rail__pair {
350
+ flex: 1 1 auto;
351
+ min-width: 0;
352
+ max-width: min(44vw, 420px);
353
+ }
354
+ .nav-rail__search-label {
355
+ font-size: 11px;
356
+ font-weight: 700;
357
+ letter-spacing: 0.05em;
358
+ text-transform: uppercase;
359
+ opacity: 0.8;
360
+ }
361
+ .nav-rail__search-hint {
362
+ margin: 0;
363
+ font-size: 11px;
364
+ line-height: 1.35;
365
+ opacity: 0.78;
366
+ }
367
+ .nav-rail__code {
368
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
369
+ font-size: 10px;
370
+ }
371
+ .nav-rail__doc-hub {
372
+ position: relative;
373
+ flex: 0 0 auto;
374
+ align-self: center;
375
+ display: block;
376
+ border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
377
+ border-radius: 6px;
378
+ background: Canvas;
379
+ overflow: visible;
380
+ }
381
+ .nav-rail__doc-hub-summary {
382
+ cursor: pointer;
383
+ font-size: 12px;
384
+ font-weight: 600;
385
+ padding: 4px 10px;
386
+ list-style: none;
387
+ user-select: none;
388
+ line-height: 1.35;
389
+ }
390
+ .nav-rail__doc-hub-summary::-webkit-details-marker { display: none; }
391
+ .nav-rail__doc-hub-inner {
392
+ position: absolute;
393
+ left: 0;
394
+ top: calc(100% + 4px);
395
+ z-index: 60;
396
+ min-width: min(280px, 78vw);
397
+ max-width: min(440px, 94vw);
398
+ max-height: min(52vh, 400px);
399
+ overflow: auto;
400
+ padding: 8px 10px;
401
+ font-size: 12px;
402
+ border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
403
+ border-radius: 8px;
404
+ background: Canvas;
405
+ box-shadow: 0 8px 28px color-mix(in oklab, CanvasText 12%, transparent);
406
+ }
407
+ .nav-rail__doc-hub-hint {
408
+ margin: 0 0 8px;
409
+ opacity: 0.78;
410
+ line-height: 1.4;
411
+ font-size: 12px;
412
+ }
413
+ .app__main {
414
+ flex: 1 1 auto;
415
+ min-width: 0;
416
+ min-height: 0;
417
+ display: flex;
418
+ flex-direction: column;
419
+ }
420
+ .app__footer {
421
+ flex: 0 0 auto;
422
+ padding: 6px 12px 10px;
423
+ border-top: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
424
+ background: color-mix(in oklab, CanvasText 3%, Canvas);
425
+ font-size: 11px;
426
+ line-height: 1.4;
427
+ color: color-mix(in oklab, CanvasText 72%, Canvas);
428
+ }
429
+ .app__footer-line { margin: 0; }
430
+ .app__footer time { font-variant-numeric: tabular-nums; }
162
431
  .toolbar {
163
432
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; padding: 8px 12px;
164
433
  border-bottom: 1px solid color-mix(in oklab, CanvasText 18%, Canvas);
@@ -166,7 +435,7 @@ const CODE_BROWSER_STYLES = `
166
435
  }
167
436
  .toolbar__main {
168
437
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
169
- flex: 1 1 280px;
438
+ flex: 0 1 auto;
170
439
  min-width: 0;
171
440
  }
172
441
  .toolbar__end {
@@ -215,45 +484,24 @@ const CODE_BROWSER_STYLES = `
215
484
  word-break: break-word;
216
485
  }
217
486
  .toolbar-related__sep { opacity: 0.55; user-select: none; }
218
- .toolbar-doc-hub {
219
- display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 8px;
220
- font-size: 12px; line-height: 1.35;
221
- color: color-mix(in oklab, CanvasText 88%, Canvas);
222
- }
223
- .toolbar-doc-hub__sep { opacity: 0.45; user-select: none; }
224
- .toolbar-blob-link {
225
- color: inherit; font-weight: 500; text-decoration: underline; text-underline-offset: 2px;
226
- white-space: nowrap;
227
- }
228
- .toolbar-tree-toggle {
229
- font: inherit; font-size: 12px; font-weight: 600; padding: 3px 10px; border-radius: 6px; cursor: pointer;
230
- border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
231
- background: color-mix(in oklab, CanvasText 6%, Canvas); color: CanvasText;
487
+ .documented-files-tree ul { list-style: none; margin: 0; padding-left: 12px; }
488
+ .documented-files-tree > ul { padding-left: 0; }
489
+ .documented-files-tree li { margin: 2px 0; line-height: 1.35; }
490
+ .documented-files-tree .tree-dir { font-weight: 600; margin-top: 4px; font-size: 12px; }
491
+ .documented-files-tree .tree-file {
492
+ margin: 3px 0;
493
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
494
+ font-size: 11px;
232
495
  }
233
- .toolbar-tree-toggle:hover { background: color-mix(in oklab, CanvasText 11%, Canvas); }
234
- .documented-files-panel {
235
- flex: 0 0 auto; max-height: min(42vh, 360px); overflow: auto;
236
- border-bottom: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
237
- background: color-mix(in oklab, CanvasText 4%, Canvas);
496
+ .documented-files-tree .tree-file-link {
497
+ color: inherit;
498
+ font-weight: 500;
499
+ text-decoration: underline;
500
+ text-underline-offset: 2px;
501
+ word-break: break-word;
238
502
  }
239
- .documented-files-panel__inner { padding: 10px 14px 14px; font-size: 13px; }
240
- .documented-files-panel__hint { margin: 0 0 10px; opacity: 0.82; line-height: 1.4; }
241
- .documented-files-panel__code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; font-size: 12px; }
242
- .documented-files-tree ul { list-style: none; margin: 0; padding-left: 14px; }
243
- .documented-files-tree > ul { padding-left: 0; }
244
- .documented-files-tree li { margin: 2px 0; line-height: 1.4; }
245
- .documented-files-tree .tree-dir { font-weight: 600; margin-top: 6px; }
246
- .documented-files-tree .tree-file { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; margin: 4px 0; }
247
- .documented-files-tree .tree-file-name { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; font-size: 12px; }
248
- .documented-files-tree .tree-file-links { display: inline-flex; flex-wrap: wrap; gap: 6px 10px; font-size: 11px; }
249
- .documented-files-tree .tree-file-links a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
250
- .toolbar .search-field {
251
- display: inline-flex; align-items: center; gap: 6px; flex: 1 1 220px; min-width: 160px;
252
- }
253
- .toolbar .search-field input[type="search"] {
254
- flex: 1; min-width: 0; padding: 4px 8px; font: inherit; border-radius: 6px;
255
- border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas); background: Canvas;
256
- color: CanvasText;
503
+ .documented-files-tree .tree-file-link:hover {
504
+ opacity: 0.92;
257
505
  }
258
506
  .toolbar button {
259
507
  font: inherit; padding: 4px 10px; border-radius: 6px; cursor: pointer;
@@ -261,23 +509,45 @@ const CODE_BROWSER_STYLES = `
261
509
  color: CanvasText;
262
510
  }
263
511
  .search-results {
264
- flex: 0 0 auto; max-height: 160px; overflow: auto; padding: 6px 12px 8px;
265
- border-bottom: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
266
- font-size: 12px;
512
+ flex: 0 1 auto;
513
+ min-height: 0;
514
+ max-height: min(320px, 38vh);
515
+ overflow: auto;
516
+ padding: 8px 8px 10px;
517
+ border-radius: 8px;
518
+ border: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
519
+ background: Canvas;
520
+ font-size: 13px;
267
521
  }
268
522
  .search-results[hidden] { display: none !important; }
269
- .search-results .hint { opacity: 0.75; margin-bottom: 6px; }
523
+ .search-results .hint { opacity: 0.75; margin-bottom: 8px; line-height: 1.45; }
270
524
  .search-results button.hit {
271
- display: block; width: 100%; text-align: left; margin: 2px 0; padding: 6px 8px;
525
+ display: block; width: 100%; text-align: left; margin: 4px 0; padding: 8px 10px;
272
526
  border-radius: 6px; border: 1px solid color-mix(in oklab, CanvasText 14%, Canvas);
273
527
  background: color-mix(in oklab, CanvasText 5%, Canvas); color: CanvasText; cursor: pointer;
274
528
  font: inherit;
275
529
  }
276
530
  .search-results button.hit:hover { background: color-mix(in oklab, CanvasText 10%, Canvas); }
277
- .search-results button.hit .meta { opacity: 0.8; font-size: 11px; }
278
- .search-results button.hit .src-tag { opacity: 0.75; font-weight: 500; font-size: 10px; }
279
- .search-results button.hit .snippet { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; font-size: 11px; white-space: pre-wrap; word-break: break-word; margin-top: 2px; }
531
+ .search-results button.hit .meta { opacity: 0.8; font-size: 12px; }
532
+ .search-results button.hit .src-tag { opacity: 0.75; font-weight: 500; font-size: 11px; }
533
+ .search-results button.hit .snippet {
534
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; font-size: 13px;
535
+ line-height: 1.45; white-space: pre-wrap; word-break: break-word; margin-top: 4px;
536
+ }
537
+ .search-results mark.search-hit {
538
+ padding: 0 2px; border-radius: 3px; font: inherit;
539
+ background: color-mix(in oklab, #f5a623 70%, Canvas);
540
+ color: CanvasText;
541
+ box-decoration-break: clone;
542
+ -webkit-box-decoration-break: clone;
543
+ }
544
+ @media (prefers-color-scheme: dark) {
545
+ .search-results mark.search-hit {
546
+ background: color-mix(in oklab, #c9a227 55%, Canvas);
547
+ }
548
+ }
280
549
  .shell { display: flex; flex-direction: row; flex: 1; min-height: 0; }
550
+ .app__main .shell { flex: 1 1 auto; }
281
551
  .pane--code {
282
552
  flex: 0 0 50%;
283
553
  min-width: 120px; overflow: auto; padding: 12px 16px;
@@ -285,12 +555,12 @@ const CODE_BROWSER_STYLES = `
285
555
  --code-line-font-size: 13px;
286
556
  --code-line-height: 1.5;
287
557
  }
558
+ .pane--code .code-line-stack { --code-ln-min-ch: 3; }
288
559
  .pane--code .code-line {
289
560
  display: grid;
290
- /* max-content: column wide enough for the longest line number (avoids 100+ bleeding into code). */
291
561
  grid-template-columns: max-content 1fr;
292
- column-gap: 12px;
293
- align-items: baseline;
562
+ column-gap: 10px;
563
+ align-items: start;
294
564
  }
295
565
  .pane--code .code-line pre {
296
566
  margin: 0;
@@ -316,6 +586,8 @@ const CODE_BROWSER_STYLES = `
316
586
  white-space: nowrap;
317
587
  font-size: var(--code-line-font-size);
318
588
  line-height: var(--code-line-height);
589
+ min-width: calc(var(--code-ln-min-ch, 3) * 1ch + 0.6ch);
590
+ box-sizing: content-box;
319
591
  }
320
592
  .pane--code .code-line:target .ln,
321
593
  .pane--code .code-line:hover .ln {
@@ -336,10 +608,27 @@ const CODE_BROWSER_STYLES = `
336
608
  content: ""; position: absolute; top: 0; bottom: 0; left: -4px; right: -4px;
337
609
  }
338
610
  .pane--doc {
339
- flex: 1 1 auto; min-width: 120px; overflow: auto; padding: 12px 16px;
611
+ flex: 1 1 auto; min-width: 0; min-height: 0;
612
+ display: flex; flex-direction: column; overflow: hidden; padding: 12px 16px;
613
+ }
614
+ .doc-pane-body {
615
+ flex: 1 1 auto; min-height: 0; overflow: auto;
616
+ }
617
+ .toolbar-angle-picker {
618
+ display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
619
+ font-size: 12px; color: color-mix(in oklab, CanvasText 88%, Canvas);
620
+ }
621
+ .toolbar-angle-picker select {
622
+ font: inherit; font-size: 12px; padding: 3px 8px; border-radius: 6px;
623
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas); background: Canvas; color: CanvasText;
340
624
  }
341
625
  .pane--doc { font-size: 15px; line-height: 1.45; }
342
626
  .pane--doc img { max-width: 100%; height: auto; }
627
+ .pane--doc .commentray-line-anchor {
628
+ display: inline;
629
+ vertical-align: baseline;
630
+ scroll-margin-top: 10px;
631
+ }
343
632
  .pane--doc .commentray-block-anchor {
344
633
  display: block;
345
634
  height: 0;
@@ -349,7 +638,6 @@ const CODE_BROWSER_STYLES = `
349
638
  pointer-events: none;
350
639
  }
351
640
  .pane h2.pane-title { margin: 0 0 10px; font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.75; }
352
- .app { display: flex; flex-direction: column; height: 100vh; }
353
641
  .shell--stretch-rows {
354
642
  flex: 1;
355
643
  min-height: 0;
@@ -413,6 +701,7 @@ const CODE_BROWSER_STYLES = `
413
701
  font-size: var(--code-line-font-size, 13px);
414
702
  line-height: var(--code-line-height, 1.5);
415
703
  }
704
+ .block-stretch .code-line-stack { --code-ln-min-ch: 3; }
416
705
  .block-stretch .code-line .ln {
417
706
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
418
707
  font-variant-numeric: tabular-nums;
@@ -425,6 +714,8 @@ const CODE_BROWSER_STYLES = `
425
714
  white-space: nowrap;
426
715
  font-size: var(--code-line-font-size, 13px);
427
716
  line-height: var(--code-line-height, 1.5);
717
+ min-width: calc(var(--code-ln-min-ch, 3) * 1ch + 0.6ch);
718
+ box-sizing: content-box;
428
719
  }
429
720
  .block-stretch.wrap .code-line pre,
430
721
  .block-stretch.wrap .code-line pre code { white-space: pre-wrap; word-break: break-word; }
@@ -439,6 +730,8 @@ const CODE_BROWSER_STYLES = `
439
730
  }
440
731
  .block-stretch-headings .pane-title { margin: 0; }
441
732
  `;
733
+ /** Native tooltip on #search-q (short hint is visible under the search row). */
734
+ const CODE_BROWSER_SEARCH_INPUT_TITLE = "Filename, path, or words. Matches this pair (source + commentray lines) first; merges commentray-nav-search.json when the export includes it (indexed paths + commentray lines).";
442
735
  function buildCodeBrowserPageHtml(p) {
443
736
  const shellClass = p.layout === "stretch" ? "shell shell--stretch-rows" : "shell";
444
737
  return `<!doctype html>
@@ -455,26 +748,34 @@ ${CODE_BROWSER_STYLES}
455
748
  </head>
456
749
  <body>
457
750
  <div class="app">
458
- <header class="toolbar" aria-label="View options">
459
- <div class="toolbar__main">
460
- ${p.filePathHtml}
461
- ${p.toolbarDocHubHtml}
462
- <span class="search-field">
463
- <label for="search-q">Search</label>
464
- <input type="search" id="search-q" placeholder="${escapeHtml(p.searchPlaceholder)}" autocomplete="off" spellcheck="false" />
465
- <button type="button" id="search-clear" title="Clear search">Clear</button>
466
- </span>
467
- ${p.relatedNavHtml}
468
- <label><input type="checkbox" id="wrap-lines" /> Wrap code lines</label>
751
+ <header class="app__chrome" role="region" aria-label="Search and navigation">
752
+ <div class="chrome__search-row">
753
+ <label class="chrome__search-label nav-rail__search-label" for="search-q">Search</label>
754
+ <input type="search" id="search-q" placeholder="${escapeHtml(p.searchPlaceholder)}" title="${escapeHtml(CODE_BROWSER_SEARCH_INPUT_TITLE)}" autocomplete="off" spellcheck="false" />
755
+ <button type="button" id="search-clear" title="Clear search">Clear</button>
469
756
  </div>
470
- ${p.toolbarEndHtml}
757
+ <div class="search-results" id="search-results" hidden aria-live="polite"></div>
758
+ <p class="nav-rail__search-hint chrome__search-hint">This pair + merged <code class="nav-rail__code">commentray-nav-search.json</code> when the export ships it.</p>
471
759
  </header>
472
- ${p.documentedPanelHtml}
473
- <div class="search-results" id="search-results" hidden aria-live="polite"></div>
474
- <div class="${shellClass}" id="shell" data-layout="${p.layout}" data-raw-code-b64="${escapeHtml(p.rawCodeB64)}" data-raw-md-b64="${escapeHtml(p.rawMdB64)}" data-scroll-block-links-b64="${escapeHtml(p.scrollBlockLinksB64)}"${p.shellDocumentedPairsAttr}${p.shellSearchAttrs}>
760
+ <div class="app__main">
761
+ <header class="toolbar" aria-label="View options">
762
+ <div class="toolbar__main">
763
+ ${p.navRailContextHtml}
764
+ ${p.navRailDocumentedHtml}
765
+ ${p.angleSelectHtml}
766
+ ${p.toolbarDocHubHtml}
767
+ ${p.relatedNavHtml}
768
+ <label><input type="checkbox" id="wrap-lines" /> Wrap code lines</label>
769
+ </div>
770
+ ${p.toolbarEndHtml}
771
+ </header>
772
+ <div class="${shellClass}" id="shell" data-layout="${p.layout}" data-raw-code-b64="${escapeHtml(p.rawCodeB64)}" data-raw-md-b64="${escapeHtml(p.rawMdB64)}" data-scroll-block-links-b64="${escapeHtml(p.scrollBlockLinksB64)}"${p.shellDocumentedPairsAttr}${p.shellSearchAttrs}>
475
773
  ${p.shellInner}
774
+ </div>
476
775
  </div>
776
+ ${p.pageFooterHtml}
477
777
  </div>
778
+ <script type="text/plain" id="commentray-multi-angle-b64">${p.multiAngleScriptBlock}</script>
478
779
  <script>
479
780
  ${loadCodeBrowserClientBundle()}
480
781
  </script>
@@ -482,10 +783,94 @@ ${loadCodeBrowserClientBundle()}
482
783
  </body>
483
784
  </html>`;
484
785
  }
786
+ async function buildMultiAngleDualPaneShell(opts, multi) {
787
+ const defaultId = multi.angles.some((a) => a.id === multi.defaultAngleId)
788
+ ? multi.defaultAngleId
789
+ : (multi.angles[0]?.id ?? "main");
790
+ const jsonAngles = [];
791
+ let defaultMarkdown = opts.commentrayMarkdown;
792
+ let defaultScrollB64 = "";
793
+ let defaultPathSearch = (opts.commentrayPathForSearch ?? "").trim();
794
+ let defaultGh = opts.commentrayOnGithubUrl;
795
+ let defaultPaneHtml = "";
796
+ const codeHtml = await renderHighlightedCodeLineRows(opts.code, opts.language);
797
+ for (const spec of multi.angles) {
798
+ const rows = spec.blockStretchRows;
799
+ const links = rows !== undefined
800
+ ? buildBlockScrollLinks(rows.index, rows.sourceRelative, rows.commentrayPathRel, spec.markdown, opts.code)
801
+ : [];
802
+ const mdForDoc = injectCommentrayDocAnchors(spec.markdown, links.length > 0 ? links : undefined);
803
+ const scrollB64 = links.length > 0 ? Buffer.from(JSON.stringify(links), "utf8").toString("base64") : "";
804
+ const commentrayHtml = await renderMarkdownToHtml(mdForDoc, {
805
+ commentrayOutputUrls: opts.commentrayOutputUrls,
806
+ });
807
+ if (spec.id === defaultId) {
808
+ defaultMarkdown = spec.markdown;
809
+ defaultScrollB64 = scrollB64;
810
+ defaultPathSearch = spec.commentrayPathRel.trim();
811
+ defaultGh = spec.commentrayOnGithubUrl;
812
+ defaultPaneHtml = commentrayHtml;
813
+ }
814
+ jsonAngles.push({
815
+ id: spec.id,
816
+ title: spec.title?.trim() || spec.id,
817
+ docInnerHtmlB64: Buffer.from(commentrayHtml, "utf8").toString("base64"),
818
+ rawMdB64: Buffer.from(spec.markdown, "utf8").toString("base64"),
819
+ scrollBlockLinksB64: scrollB64,
820
+ commentrayPathForSearch: spec.commentrayPathRel.trim(),
821
+ commentrayOnGithubUrl: spec.commentrayOnGithubUrl,
822
+ });
823
+ }
824
+ const selOpts = multi.angles
825
+ .map((a) => {
826
+ const lab = escapeHtml(a.title?.trim() || a.id);
827
+ return `<option value="${escapeHtml(a.id)}"${a.id === defaultId ? " selected" : ""}>${lab}</option>`;
828
+ })
829
+ .join("");
830
+ const angleSelectHtml = `<span class="toolbar-angle-picker"><label for="angle-select">Angle</label><select id="angle-select" aria-label="Commentray angle">${selOpts}</select></span>`;
831
+ const shellInner = ` <section class="pane--code" id="code-pane" aria-label="Source code">` +
832
+ `<h2 class="pane-title">Code</h2>\n` +
833
+ ` ${codeHtml}\n` +
834
+ ` </section>\n` +
835
+ ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
836
+ ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
837
+ ` <h2 class="pane-title">Commentray</h2>\n` +
838
+ ` <div id="doc-pane-body" class="doc-pane-body">\n` +
839
+ ` ${defaultPaneHtml}\n` +
840
+ ` </div>\n` +
841
+ ` </section>\n`;
842
+ const payloadObj = { defaultAngleId: defaultId, angles: jsonAngles };
843
+ const multiAnglePayloadB64 = Buffer.from(JSON.stringify(payloadObj), "utf8").toString("base64");
844
+ return {
845
+ shellInner,
846
+ multiShell: {
847
+ rawMdB64: Buffer.from(defaultMarkdown, "utf8").toString("base64"),
848
+ scrollBlockLinksB64: defaultScrollB64,
849
+ commentrayPathForSearch: defaultPathSearch,
850
+ commentrayOnGithubUrl: defaultGh,
851
+ },
852
+ angleSelectHtml,
853
+ multiAnglePayloadB64,
854
+ };
855
+ }
485
856
  async function buildCodeBrowserShell(opts, layoutPref) {
486
857
  let layout = "dual";
487
858
  let shellInner = "";
488
859
  let scrollBlockLinksB64 = "";
860
+ const multi = opts.multiAngleBrowsing;
861
+ const multiActive = Boolean(multi && multi.angles.length >= 2);
862
+ if (multiActive && multi) {
863
+ const built = await buildMultiAngleDualPaneShell(opts, multi);
864
+ const ms = built.multiShell;
865
+ return {
866
+ layout: "dual",
867
+ shellInner: built.shellInner,
868
+ scrollBlockLinksB64: ms.scrollBlockLinksB64,
869
+ angleSelectHtml: built.angleSelectHtml,
870
+ multiAnglePayloadB64: built.multiAnglePayloadB64,
871
+ multiShell: ms,
872
+ };
873
+ }
489
874
  if (opts.blockStretchRows && layoutPref !== "dual") {
490
875
  const stretched = await tryBuildBlockStretchTableHtml({
491
876
  code: opts.code,
@@ -511,12 +896,12 @@ async function buildCodeBrowserShell(opts, layoutPref) {
511
896
  const links = opts.blockStretchRows !== undefined
512
897
  ? buildBlockScrollLinks(opts.blockStretchRows.index, opts.blockStretchRows.sourceRelative, opts.blockStretchRows.commentrayPathRel, opts.commentrayMarkdown, opts.code)
513
898
  : [];
514
- const mdForDoc = injectCommentrayBlockAnchors(opts.commentrayMarkdown, links.length > 0 ? links : undefined);
899
+ const mdForDoc = injectCommentrayDocAnchors(opts.commentrayMarkdown, links.length > 0 ? links : undefined);
515
900
  if (links.length > 0) {
516
901
  scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
517
902
  }
518
903
  const [codeHtml, commentrayHtml] = await Promise.all([
519
- renderCodeLineBlocks(opts.code, opts.language),
904
+ renderHighlightedCodeLineRows(opts.code, opts.language),
520
905
  renderMarkdownToHtml(mdForDoc, {
521
906
  commentrayOutputUrls: opts.commentrayOutputUrls,
522
907
  }),
@@ -529,20 +914,29 @@ async function buildCodeBrowserShell(opts, layoutPref) {
529
914
  ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
530
915
  ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
531
916
  ` <h2 class="pane-title">Commentray</h2>\n` +
917
+ ` <div id="doc-pane-body" class="doc-pane-body">\n` +
532
918
  ` ${commentrayHtml}\n` +
919
+ ` </div>\n` +
533
920
  ` </section>\n`;
534
921
  }
535
- return { layout, shellInner, scrollBlockLinksB64 };
922
+ return {
923
+ layout,
924
+ shellInner,
925
+ scrollBlockLinksB64,
926
+ angleSelectHtml: "",
927
+ multiAnglePayloadB64: "",
928
+ };
536
929
  }
537
- function searchChromeFromOptions(opts) {
930
+ function searchChromeFromOptions(opts, commentrayPathOverride) {
931
+ const crPath = (commentrayPathOverride ?? opts.commentrayPathForSearch ?? "").trim();
538
932
  if (opts.staticSearchScope === "commentray-and-paths") {
539
933
  return {
540
- searchPlaceholder: "Commentray + file paths (ordered tokens + fuzzy lines)…",
541
- shellSearchAttrs: ` data-search-scope="commentray-and-paths" data-search-file-path="${escapeHtml(opts.filePath ?? "")}" data-search-commentray-path="${escapeHtml((opts.commentrayPathForSearch ?? "").trim())}"`,
934
+ searchPlaceholder: "Filename, path, or keywords…",
935
+ shellSearchAttrs: ` data-search-scope="commentray-and-paths" data-search-file-path="${escapeHtml(opts.filePath ?? "")}" data-search-commentray-path="${escapeHtml(crPath)}"`,
542
936
  };
543
937
  }
544
938
  return {
545
- searchPlaceholder: "Whole source (ordered tokens + fuzzy lines)…",
939
+ searchPlaceholder: "Filename, path, or keywords…",
546
940
  shellSearchAttrs: "",
547
941
  };
548
942
  }
@@ -552,6 +946,32 @@ function shellDocumentedPairsAttrFromOptions(opts) {
552
946
  return "";
553
947
  return ` data-documented-pairs-b64="${escapeHtml(emb)}"`;
554
948
  }
949
+ function codeBrowserPageTitle(opts) {
950
+ return opts.title ?? opts.filePath ?? "Commentray";
951
+ }
952
+ function codeBrowserHljsThemes(opts) {
953
+ const hljs = opts.hljsTheme ?? "github";
954
+ const hljsDark = opts.hljsTheme?.includes("dark") ? opts.hljsTheme : "github-dark";
955
+ return { hljs, hljsDark };
956
+ }
957
+ function toolbarCommentrayGithubFromShell(shell, opts) {
958
+ return shell.multiShell?.commentrayOnGithubUrl ?? opts.commentrayOnGithubUrl;
959
+ }
960
+ function rawMdB64FromShell(shell, opts) {
961
+ return (shell.multiShell?.rawMdB64 ?? Buffer.from(opts.commentrayMarkdown, "utf8").toString("base64"));
962
+ }
963
+ function navRailCommentrayPathFromShell(shell, opts) {
964
+ const trimmed = (shell.multiShell?.commentrayPathForSearch ??
965
+ opts.commentrayPathForSearch ??
966
+ "").trim();
967
+ return trimmed.length > 0 ? trimmed : undefined;
968
+ }
969
+ function shellSearchAttrsWithNavJson(shellSearchAttrsBase, documentedNavJsonUrl) {
970
+ const navJson = documentedNavJsonUrl?.trim() ?? "";
971
+ if (navJson.length === 0)
972
+ return shellSearchAttrsBase;
973
+ return `${shellSearchAttrsBase} data-nav-search-json-url="${escapeHtml(navJson)}"`;
974
+ }
555
975
  /**
556
976
  * Static HTML shell for a minimal “code browser”: code + rendered commentray,
557
977
  * draggable vertical splitter, togglable line wrap for the code pane, and
@@ -559,35 +979,42 @@ function shellDocumentedPairsAttrFromOptions(opts) {
559
979
  */
560
980
  export async function renderCodeBrowserHtml(opts) {
561
981
  const rawCodeB64 = Buffer.from(opts.code, "utf8").toString("base64");
562
- const rawMdB64 = Buffer.from(opts.commentrayMarkdown, "utf8").toString("base64");
563
- const title = opts.title ?? opts.filePath ?? "Commentray";
564
- const filePathHtml = renderFilePathLabel(opts.filePath, title);
565
- const toolbarEndHtml = buildToolbarEndHtml(opts.githubRepoUrl, opts.toolHomeUrl);
566
- const hljs = opts.hljsTheme ?? "github";
567
- const hljsDark = opts.hljsTheme?.includes("dark") ? opts.hljsTheme : "github-dark";
982
+ const title = codeBrowserPageTitle(opts);
983
+ const builtAt = opts.builtAt ?? new Date();
984
+ const renderSemver = commentrayRenderVersion();
985
+ const toolbarEndHtml = buildToolbarEndHtml(opts.githubRepoUrl, opts.toolHomeUrl, renderSemver);
986
+ const pageFooterHtml = renderPageFooterHtml(builtAt);
987
+ const { hljs, hljsDark } = codeBrowserHljsThemes(opts);
568
988
  const mermaidScript = mermaidRuntimeScriptHtml(opts.includeMermaidRuntime);
569
989
  const relatedNavHtml = renderRelatedGithubNavHtml(opts.relatedGithubNav ?? []);
570
990
  const generatorMetaHtml = renderGeneratorMetaHtml(opts.generatorLabel);
571
- const { toolbarDocHubHtml, documentedPanelHtml } = renderToolbarDocHubHtml({
572
- sourceOnGithubUrl: opts.sourceOnGithubUrl,
573
- commentrayOnGithubUrl: opts.commentrayOnGithubUrl,
991
+ const layoutPref = opts.codeBrowserLayout ?? "auto";
992
+ const shell = await buildCodeBrowserShell(opts, layoutPref);
993
+ const { toolbarDocHubHtml, navRailDocumentedHtml } = renderToolbarDocHubHtml({
574
994
  documentedNavJsonUrl: opts.documentedNavJsonUrl,
575
995
  documentedPairsEmbeddedB64: opts.documentedPairsEmbeddedB64,
576
996
  });
577
- const layoutPref = opts.codeBrowserLayout ?? "auto";
578
- const { layout, shellInner, scrollBlockLinksB64 } = await buildCodeBrowserShell(opts, layoutPref);
579
- const { searchPlaceholder, shellSearchAttrs } = searchChromeFromOptions(opts);
997
+ const rawMdB64 = rawMdB64FromShell(shell, opts);
998
+ const scrollBlockLinksB64 = shell.scrollBlockLinksB64;
999
+ const { searchPlaceholder, shellSearchAttrs: shellSearchAttrsBase } = searchChromeFromOptions(opts, shell.multiShell?.commentrayPathForSearch);
580
1000
  const shellDocumentedPairsAttr = shellDocumentedPairsAttrFromOptions(opts);
1001
+ const shellSearchAttrs = shellSearchAttrsWithNavJson(shellSearchAttrsBase, opts.documentedNavJsonUrl);
1002
+ const navRailContextHtml = renderNavRailContextHtml(opts.filePath, navRailCommentrayPathFromShell(shell, opts), {
1003
+ sourceOnGithubUrl: opts.sourceOnGithubUrl,
1004
+ commentrayOnGithubUrl: toolbarCommentrayGithubFromShell(shell, opts),
1005
+ });
581
1006
  return buildCodeBrowserPageHtml({
582
1007
  title,
583
1008
  generatorMetaHtml,
584
- filePathHtml,
1009
+ navRailContextHtml,
1010
+ angleSelectHtml: shell.angleSelectHtml,
585
1011
  toolbarDocHubHtml,
586
- documentedPanelHtml,
1012
+ navRailDocumentedHtml,
587
1013
  relatedNavHtml,
588
1014
  toolbarEndHtml,
589
- layout,
590
- shellInner,
1015
+ pageFooterHtml,
1016
+ layout: shell.layout,
1017
+ shellInner: shell.shellInner,
591
1018
  rawCodeB64,
592
1019
  rawMdB64,
593
1020
  scrollBlockLinksB64,
@@ -597,6 +1024,7 @@ export async function renderCodeBrowserHtml(opts) {
597
1024
  mermaidScript,
598
1025
  searchPlaceholder,
599
1026
  shellSearchAttrs,
1027
+ multiAngleScriptBlock: shell.multiAnglePayloadB64,
600
1028
  });
601
1029
  }
602
1030
  //# sourceMappingURL=code-browser.js.map