@commentray/render 0.0.6 → 0.0.8

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 (39) hide show
  1. package/dist/code-browser-client.bundle.js +10 -10
  2. package/dist/code-browser-client.js +747 -104
  3. package/dist/code-browser-client.js.map +1 -1
  4. package/dist/code-browser-color-theme.d.ts +15 -0
  5. package/dist/code-browser-color-theme.d.ts.map +1 -0
  6. package/dist/code-browser-color-theme.js +73 -0
  7. package/dist/code-browser-color-theme.js.map +1 -0
  8. package/dist/code-browser-pair-nav.d.ts +8 -0
  9. package/dist/code-browser-pair-nav.d.ts.map +1 -1
  10. package/dist/code-browser-pair-nav.js +20 -2
  11. package/dist/code-browser-pair-nav.js.map +1 -1
  12. package/dist/code-browser-scroll-sync.js +1 -1
  13. package/dist/code-browser-scroll-sync.js.map +1 -1
  14. package/dist/code-browser.d.ts +2 -2
  15. package/dist/code-browser.d.ts.map +1 -1
  16. package/dist/code-browser.js +903 -228
  17. package/dist/code-browser.js.map +1 -1
  18. package/dist/hljs-stylesheet-themes.d.ts +13 -0
  19. package/dist/hljs-stylesheet-themes.d.ts.map +1 -0
  20. package/dist/hljs-stylesheet-themes.js +19 -0
  21. package/dist/hljs-stylesheet-themes.js.map +1 -0
  22. package/dist/inline-favicon.d.ts +2 -0
  23. package/dist/inline-favicon.d.ts.map +1 -0
  24. package/dist/inline-favicon.js +25 -0
  25. package/dist/inline-favicon.js.map +1 -0
  26. package/dist/markdown-pipeline.d.ts.map +1 -1
  27. package/dist/markdown-pipeline.js +37 -2
  28. package/dist/markdown-pipeline.js.map +1 -1
  29. package/dist/mermaid-runtime-html.d.ts.map +1 -1
  30. package/dist/mermaid-runtime-html.js +10 -2
  31. package/dist/mermaid-runtime-html.js.map +1 -1
  32. package/dist/package-version.d.ts.map +1 -1
  33. package/dist/package-version.js +4 -4
  34. package/dist/package-version.js.map +1 -1
  35. package/dist/side-by-side-layout.css +58 -0
  36. package/dist/side-by-side.d.ts.map +1 -1
  37. package/dist/side-by-side.js +10 -12
  38. package/dist/side-by-side.js.map +1 -1
  39. package/package.json +2 -2
@@ -1,11 +1,13 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { MARKER_ID_BODY, buildBlockScrollLinks, } from "@commentray/core";
2
+ import { join } from "node:path";
3
+ import { MARKER_ID_BODY, buildBlockScrollLinks, findMonorepoPackagesDir, monorepoLayoutStartDir, } from "@commentray/core";
5
4
  import { tryBuildBlockStretchTableHtml } from "./block-stretch-layout.js";
6
5
  import { formatCommentrayBuiltAtLocal } from "./build-stamp.js";
7
6
  import { escapeHtml } from "./html-utils.js";
7
+ import { commentrayColorThemeHeadBoot } from "./code-browser-color-theme.js";
8
+ import { hljsStylesheetThemes } from "./hljs-stylesheet-themes.js";
8
9
  import { renderHighlightedCodeLineRows } from "./highlighted-code-lines.js";
10
+ import { COMMENTRAY_FAVICON_LINK_HTML } from "./inline-favicon.js";
9
11
  import { mermaidRuntimeScriptHtml } from "./mermaid-runtime-html.js";
10
12
  import { renderMarkdownToHtml } from "./markdown-pipeline.js";
11
13
  import { commentrayRenderVersion } from "./package-version.js";
@@ -134,6 +136,29 @@ const GITHUB_MARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16
134
136
  const SITE_HOME_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true">' +
135
137
  '<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>' +
136
138
  "</svg>";
139
+ /** Folder-with-list glyph (file tree / documented pairs hub). */
140
+ const TOOLBAR_ICON_TREE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
141
+ '<path d="M4 20h16a1 1 0 0 0 1-1V9a2 2 0 0 0-2-2h-5.5a2 2 0 0 1-1.6-.8L10.5 4.5a2 2 0 0 0-1.6-.8H5a2 2 0 0 0-2 2v14a1 1 0 0 0 1 1Z"/>' +
142
+ '<path d="M8 12h8M8 16h6M8 20h4"/>' +
143
+ "</svg>";
144
+ /**
145
+ * Line wrap — Material "wrap_text" glyph (Apache-2.0), same visual family as
146
+ * https://www.svgrepo.com/svg/376703/text-wrap-line (filled 24dp path scaled to 18px).
147
+ */
148
+ const TOOLBAR_ICON_WRAP_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">' +
149
+ '<path d="M4 19h6v-2H4v2zM20 5H4v2h16V5zm-3 6H4v2h13.25c1.1 0 2 .9 2 2s-.9 2-2 2H15v-2l-3 3 3 3v-2h2c2.21 0 4-1.79 4-4s-1.79-4-4-4z"/>' +
150
+ "</svg>";
151
+ const CHROME_ICON_SEARCH_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
152
+ '<circle cx="11" cy="11" r="7"/>' +
153
+ '<path d="m21 21-4.3-4.3"/>' +
154
+ "</svg>";
155
+ /** Swap / flip: circle split by a diameter, one arrow per half (narrow viewports). */
156
+ const TOOLBAR_ICON_FLIP_PANES_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
157
+ '<circle cx="12" cy="12" r="9"/>' +
158
+ '<path d="M12 4v16"/>' +
159
+ '<path d="M10.5 12H6l2.5-2.5M6 12l2.5 2.5"/>' +
160
+ '<path d="M13.5 12H18l-2.5-2.5M18 12l-2.5 2.5"/>' +
161
+ "</svg>";
137
162
  function safeExternalHttpUrl(url) {
138
163
  const t = url?.trim();
139
164
  if (!t)
@@ -158,27 +183,32 @@ function buildToolbarSiteHubHtml(siteHubUrl) {
158
183
  const se = escapeHtml(site);
159
184
  return `<a class="toolbar-github" href="${se}" aria-label="Documentation home" title="Back to this site (hub)">${SITE_HOME_SVG}</a>`;
160
185
  }
161
- function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl, commentrayRenderSemver, siteHubUrl) {
186
+ /** GitHub Octocat in the toolbar when a repo URL is set and the hub link does not replace it. */
187
+ function buildToolbarEndHtml(githubRepoUrl, siteHubUrl) {
162
188
  const site = safeToolbarNavigationHref(siteHubUrl);
163
189
  const gh = safeExternalHttpUrl(githubRepoUrl);
164
- const tool = safeExternalHttpUrl(toolHomeUrl);
165
- const bits = [];
166
190
  if (!site && gh) {
167
191
  const he = escapeHtml(gh);
168
- bits.push(`<a class="toolbar-github" href="${he}" target="_blank" rel="noopener noreferrer" aria-label="View repository on GitHub" title="View repository on GitHub">${GITHUB_MARK_SVG}</a>`);
192
+ return `<div class="toolbar__end"><a class="toolbar-github" href="${he}" target="_blank" rel="noopener noreferrer" aria-label="View repository on GitHub" title="View repository on GitHub">${GITHUB_MARK_SVG}</a></div>`;
169
193
  }
194
+ return "";
195
+ }
196
+ function renderPageFooterHtml(input) {
197
+ const { builtAt, toolHomeUrl, commentrayRenderSemver } = input;
198
+ const iso = builtAt.toISOString();
199
+ const human = formatCommentrayBuiltAtLocal(builtAt);
200
+ const tool = safeExternalHttpUrl(toolHomeUrl);
170
201
  if (tool) {
171
202
  const te = escapeHtml(tool);
172
203
  const ver = escapeHtml(commentrayRenderSemver);
173
- 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>`);
204
+ return (`<footer class="app__footer" role="contentinfo">` +
205
+ `<p class="app__footer-line app__footer-attribution" role="note">` +
206
+ `Rendered with <a href="${te}" target="_blank" rel="noopener noreferrer">Commentray</a> ` +
207
+ `<span class="app__footer-attribution__version" translate="no">v${ver}</span>: ` +
208
+ `<time datetime="${escapeHtml(iso)}">${escapeHtml(human)}</time>` +
209
+ `</p>` +
210
+ `</footer>`);
174
211
  }
175
- if (bits.length === 0)
176
- return "";
177
- return `<div class="toolbar__end">${bits.join("")}</div>`;
178
- }
179
- function renderPageFooterHtml(builtAt) {
180
- const iso = builtAt.toISOString();
181
- const human = formatCommentrayBuiltAtLocal(builtAt);
182
212
  return (`<footer class="app__footer" role="contentinfo">` +
183
213
  `<p class="app__footer-line">HTML generated <time datetime="${escapeHtml(iso)}">${escapeHtml(human)}</time></p>` +
184
214
  `</footer>`);
@@ -200,7 +230,7 @@ function renderToolbarDocHubHtml(opts) {
200
230
  const navAttr = escapeHtml(nav ?? "");
201
231
  const navRailDocumentedHtml = showDocumentedTree
202
232
  ? `<details class="nav-rail__doc-hub" id="documented-files-hub" data-nav-json-url="${navAttr}">
203
- <summary class="nav-rail__doc-hub-summary">Comment-rayed files</summary>
233
+ <summary class="nav-rail__doc-hub-summary" title="Comment-rayed files" aria-label="Comment-rayed files"><span class="nav-rail__doc-hub-summary__caption">Comment-rayed files</span><span class="nav-rail__doc-hub-summary__glyph" aria-hidden="true">${TOOLBAR_ICON_TREE_SVG}</span></summary>
204
234
  <div class="nav-rail__doc-hub-inner">
205
235
  <div class="nav-rail__doc-hub-filter-row">
206
236
  <label class="nav-rail__doc-hub-filter-label" for="documented-files-filter">Filter</label>
@@ -212,44 +242,49 @@ function renderToolbarDocHubHtml(opts) {
212
242
  : "";
213
243
  return { toolbarDocHubHtml, navRailDocumentedHtml };
214
244
  }
215
- function renderNavRailContextHtml(filePath, commentrayPath, opts) {
245
+ function dualPanePanesInnerHtml(codeHtml, commentrayHtml) {
246
+ return (` <section class="pane--code" id="code-pane" aria-label="Source code">` +
247
+ ` ${codeHtml}\n` +
248
+ ` </section>\n` +
249
+ ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
250
+ ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
251
+ ` <div id="doc-pane-body" class="doc-pane-body">\n` +
252
+ ` ${commentrayHtml}\n` +
253
+ ` </div>\n` +
254
+ ` </section>\n`);
255
+ }
256
+ /** Plain-text Src/Doc labels above the panes; column widths track the resizable split via `--split-pct`. */
257
+ function renderShellPairContextHtml(filePath, commentrayPath) {
216
258
  const fpRaw = (filePath ?? "").trim();
217
259
  const crRaw = (commentrayPath ?? "").trim();
218
- const srcUrl = safeExternalHttpUrl(opts?.sourceOnGithubUrl);
219
- const crUrl = safeExternalHttpUrl(opts?.commentrayOnGithubUrl);
220
- const browseForCr = safeToolbarNavigationHref(opts?.commentrayStaticBrowseUrl);
221
- const srcGh = srcUrl !== null
222
- ? `<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>`
223
- : "";
224
- const crGh = browseForCr !== null
225
- ? `<a class="nav-rail__pair-gh" id="toolbar-commentray-github" href="${escapeHtml(browseForCr)}" rel="noopener" aria-label="Open companion pair in the site viewer" title="Open on site">${GITHUB_MARK_SVG}</a>`
226
- : crUrl !== null
227
- ? `<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>`
228
- : "";
229
- if (fpRaw.length === 0 && crRaw.length === 0 && srcGh === "" && crGh === "") {
260
+ if (fpRaw.length === 0 && crRaw.length === 0)
230
261
  return "";
231
- }
232
262
  const fp = escapeHtml(fpRaw);
233
263
  const cr = escapeHtml(crRaw);
234
264
  const fpDisp = fpRaw.length > 0 ? fp : "—";
235
265
  const crDisp = crRaw.length > 0 ? cr : "—";
236
- return `<div class="nav-rail__context nav-rail__context--compact" aria-label="Current documentation pair">
237
- <span class="nav-rail__pair">
238
- <span class="nav-rail__pair-lab">Src</span>
239
- <span class="nav-rail__pair-path" title="${fp}">${fpDisp}</span>${srcGh}
240
- </span>
241
- <span class="nav-rail__pair-sep" aria-hidden="true">·</span>
242
- <span class="nav-rail__pair">
243
- <span class="nav-rail__pair-lab">Doc</span>
244
- <span class="nav-rail__pair-path nav-rail__pair-path--secondary" id="nav-rail-doc-path" title="${cr}">${crDisp}</span>${crGh}
245
- </span>
266
+ return `<div class="shell__pair-context" aria-label="Current documentation pair">
267
+ <div class="shell__pair-cell shell__pair-cell--src">
268
+ <span class="shell__pair-lab">Src</span>
269
+ <span class="shell__pair-path" title="${fp}">${fpDisp}</span>
270
+ </div>
271
+ <div class="shell__pair-gutter-spacer" aria-hidden="true"></div>
272
+ <div class="shell__pair-cell shell__pair-cell--doc">
273
+ <span class="shell__pair-lab">Doc</span>
274
+ <span class="shell__pair-path shell__pair-path--secondary" id="nav-rail-doc-path" title="${cr}">${crDisp}</span>
275
+ </div>
246
276
  </div>`;
247
277
  }
278
+ function wrapDualShellInner(pairContextHtml, panesHtml) {
279
+ const row = pairContextHtml.trim().length > 0 ? ` ${pairContextHtml.trim()}\n` : "";
280
+ return `${row} <div class="shell__panes">\n${panesHtml} </div>\n`;
281
+ }
248
282
  /** IIFE produced by `npm run build -w @commentray/render` (esbuild of `code-browser-client.ts`). */
249
283
  function loadCodeBrowserClientBundle() {
250
- const here = dirname(fileURLToPath(import.meta.url));
251
- const inDist = join(here, "code-browser-client.bundle.js");
252
- const fromSrc = join(here, "..", "dist", "code-browser-client.bundle.js");
284
+ const packagesDir = findMonorepoPackagesDir(monorepoLayoutStartDir(import.meta.url));
285
+ const renderDistDir = join(packagesDir, "render", "dist");
286
+ const inDist = join(renderDistDir, "code-browser-client.bundle.js");
287
+ const fromSrc = join(packagesDir, "render", "code-browser-client.bundle.js");
253
288
  for (const p of [inDist, fromSrc]) {
254
289
  if (existsSync(p)) {
255
290
  return readFileSync(p, "utf8");
@@ -257,18 +292,51 @@ function loadCodeBrowserClientBundle() {
257
292
  }
258
293
  throw new Error("Missing code-browser-client.bundle.js. Run `npm run build -w @commentray/render` to bundle the browser client.");
259
294
  }
295
+ /**
296
+ * Compact theme control: primary click opens a menu (readme.io–style), secondary click cycles
297
+ * system → light → dark. Paired with {@link ./code-browser-color-theme.ts} and the client bundle.
298
+ */
299
+ const TOOLBAR_COLOR_THEME_HTML = ` <div class="toolbar-theme">
300
+ <button type="button" id="commentray-theme-trigger" class="toolbar-theme__trigger" data-commentray-trigger-mode="system" aria-haspopup="menu" aria-expanded="false" aria-label="Color theme" title="Appearance: left-click opens the theme menu. Right-click cycles System, Light, and Dark.">
301
+ <span class="toolbar-theme__icon toolbar-theme__icon--system" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8m-4-4v4"/></svg></span>
302
+ <span class="toolbar-theme__icon toolbar-theme__icon--light" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 6.34L4.93 4.93m12.02 12.02l1.41 1.41M17.66 6.34l1.41-1.41M6.34 17.66l-1.41 1.41"/></svg></span>
303
+ <span class="toolbar-theme__icon toolbar-theme__icon--dark" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
304
+ </button>
305
+ <div id="commentray-theme-menu" class="toolbar-theme__menu" role="menu" hidden aria-labelledby="commentray-theme-trigger">
306
+ <button type="button" role="menuitemradio" class="toolbar-theme__menuitem" data-commentray-theme-value="system" aria-checked="true">System</button>
307
+ <button type="button" role="menuitemradio" class="toolbar-theme__menuitem" data-commentray-theme-value="light" aria-checked="false">Light</button>
308
+ <button type="button" role="menuitemradio" class="toolbar-theme__menuitem" data-commentray-theme-value="dark" aria-checked="false">Dark</button>
309
+ </div>
310
+ </div>
311
+ `;
260
312
  const CODE_BROWSER_STYLES = `
261
313
  :root {
262
- color-scheme: light dark;
263
314
  --cr-control-h: 32px;
264
315
  --cr-control-radius: 8px;
265
316
  --cr-icon-inner: 18px;
266
317
  --cr-label-caps-fs: 10px;
267
318
  --cr-label-caps-track: 0.06em;
268
319
  --cr-ui-fs: 12px;
320
+ /** Matches code/doc pane horizontal padding so pair-context rows line up with pane content (e.g. line nums). */
321
+ --cr-pane-inline-pad: 12px;
322
+ }
323
+ :root:is(:not([data-commentray-theme]), [data-commentray-theme="system"]) {
324
+ color-scheme: light dark;
325
+ }
326
+ :root[data-commentray-theme="light"] {
327
+ color-scheme: light;
328
+ }
329
+ :root[data-commentray-theme="dark"] {
330
+ color-scheme: dark;
269
331
  }
270
332
  * { box-sizing: border-box; }
271
- body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
333
+ html { background: Canvas; color: CanvasText; }
334
+ body {
335
+ margin: 0;
336
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
337
+ background: Canvas;
338
+ color: CanvasText;
339
+ }
272
340
  .skip-link {
273
341
  position: absolute;
274
342
  left: -9999px;
@@ -354,16 +422,23 @@ const CODE_BROWSER_STYLES = `
354
422
  }
355
423
  .chrome__search-row #search-clear {
356
424
  flex: 0 0 auto;
425
+ display: inline-flex;
426
+ align-items: center;
427
+ justify-content: center;
428
+ min-height: var(--cr-control-h);
429
+ padding: 0 12px;
357
430
  font: inherit;
358
431
  font-size: var(--cr-ui-fs);
359
432
  font-weight: 500;
360
- min-height: var(--cr-control-h);
361
- padding: 0 16px;
362
433
  border-radius: var(--cr-control-radius);
363
434
  cursor: pointer;
364
435
  border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
365
436
  background: color-mix(in oklab, CanvasText 6%, Canvas);
366
437
  color: CanvasText;
438
+ white-space: nowrap;
439
+ }
440
+ .chrome__search-row #search-clear:hover {
441
+ background: color-mix(in oklab, CanvasText 11%, Canvas);
367
442
  }
368
443
  .chrome__search-row input[type="search"]:focus-visible,
369
444
  .chrome__search-row #search-clear:focus-visible {
@@ -372,92 +447,17 @@ const CODE_BROWSER_STYLES = `
372
447
  }
373
448
  .chrome__search-label {
374
449
  flex: 0 0 auto;
375
- white-space: nowrap;
376
- }
377
- .nav-rail__context--compact {
378
- display: flex;
379
- flex-direction: row;
380
- flex-wrap: wrap;
381
- align-items: center;
382
- gap: 6px 10px;
383
- padding: 5px 10px;
384
- border-radius: var(--cr-control-radius);
385
- border: 1px solid color-mix(in oklab, CanvasText 14%, Canvas);
386
- background: Canvas;
387
- font-size: var(--cr-ui-fs);
388
- line-height: 1.3;
389
- }
390
- .nav-rail__pair {
391
450
  display: inline-flex;
392
451
  flex-direction: row;
393
452
  align-items: center;
394
453
  gap: 6px;
395
- min-width: 0;
396
- flex: 1 1 140px;
397
- max-width: min(48%, 100%);
398
- }
399
- .nav-rail__pair-lab {
400
- flex: 0 0 auto;
401
- font-size: var(--cr-label-caps-fs);
402
- font-weight: 700;
403
- letter-spacing: var(--cr-label-caps-track);
404
- text-transform: uppercase;
405
- opacity: 0.72;
406
- }
407
- .nav-rail__pair-path {
408
- flex: 1 1 auto;
409
- min-width: 0;
410
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
411
- font-size: var(--cr-ui-fs);
412
- color: CanvasText;
413
- overflow: hidden;
414
- text-overflow: ellipsis;
415
454
  white-space: nowrap;
416
- }
417
- .nav-rail__pair-path--secondary { opacity: 0.88; }
418
- .nav-rail__pair-sep {
419
- flex: 0 0 auto;
420
- opacity: 0.45;
455
+ cursor: default;
421
456
  user-select: none;
422
- padding: 0 2px;
423
- }
424
- .nav-rail__pair-gh {
425
- flex: 0 0 auto;
426
- display: inline-flex;
427
- align-items: center;
428
- justify-content: center;
429
- width: var(--cr-control-h);
430
- height: var(--cr-control-h);
431
- border-radius: var(--cr-control-radius);
432
- border: 1px solid color-mix(in oklab, CanvasText 20%, Canvas);
433
- background: color-mix(in oklab, CanvasText 5%, Canvas);
434
- color: CanvasText;
435
- }
436
- .nav-rail__pair-gh:hover {
437
- background: color-mix(in oklab, CanvasText 10%, Canvas);
438
457
  }
439
- .nav-rail__pair-gh:focus-visible {
440
- outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
441
- outline-offset: 2px;
442
- }
443
- .nav-rail__pair-gh svg {
444
- width: var(--cr-icon-inner);
445
- height: var(--cr-icon-inner);
446
- display: block;
447
- }
448
- .toolbar .nav-rail__context--compact {
449
- border: 0;
450
- background: transparent;
451
- padding: 0;
452
- flex: 1 1 200px;
453
- min-width: 0;
454
- max-width: none;
455
- gap: 6px 10px;
456
- }
457
- .toolbar .nav-rail__pair {
458
- flex: 1 1 auto;
459
- min-width: 0;
460
- max-width: min(44vw, 420px);
458
+ /* Wide viewports: same legible caps word as historic Pages shell (icon hidden). */
459
+ .chrome__search-label__glyph {
460
+ display: none;
461
461
  }
462
462
  .nav-rail__search-label {
463
463
  font-size: var(--cr-label-caps-fs);
@@ -466,16 +466,6 @@ const CODE_BROWSER_STYLES = `
466
466
  text-transform: uppercase;
467
467
  opacity: 0.78;
468
468
  }
469
- .nav-rail__search-hint {
470
- margin: 0;
471
- font-size: 11px;
472
- line-height: 1.35;
473
- opacity: 0.78;
474
- }
475
- .nav-rail__code {
476
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
477
- font-size: 10px;
478
- }
479
469
  .nav-rail__doc-hub {
480
470
  position: relative;
481
471
  flex: 0 0 auto;
@@ -489,16 +479,33 @@ const CODE_BROWSER_STYLES = `
489
479
  .nav-rail__doc-hub-summary {
490
480
  cursor: pointer;
491
481
  font-size: var(--cr-ui-fs);
492
- font-weight: 600;
482
+ font-weight: 500;
483
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
493
484
  padding: 0 12px;
494
485
  min-height: var(--cr-control-h);
495
486
  display: inline-flex;
487
+ flex-direction: row;
496
488
  align-items: center;
489
+ justify-content: flex-start;
490
+ gap: 8px;
497
491
  box-sizing: border-box;
498
492
  list-style: none;
499
493
  user-select: none;
500
494
  line-height: 1.25;
501
495
  }
496
+ .nav-rail__doc-hub-summary:hover {
497
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
498
+ }
499
+ .nav-rail__doc-hub-summary__caption {
500
+ white-space: nowrap;
501
+ }
502
+ .nav-rail__doc-hub-summary__glyph {
503
+ display: none;
504
+ }
505
+ .nav-rail__doc-hub-summary svg {
506
+ display: block;
507
+ flex: 0 0 auto;
508
+ }
502
509
  .nav-rail__doc-hub-summary::-webkit-details-marker { display: none; }
503
510
  .nav-rail__doc-hub-inner {
504
511
  position: absolute;
@@ -572,20 +579,52 @@ const CODE_BROWSER_STYLES = `
572
579
  .app__footer time { font-variant-numeric: tabular-nums; }
573
580
  .toolbar {
574
581
  position: relative;
575
- display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; padding: 8px 12px;
582
+ display: flex;
583
+ flex-wrap: wrap;
584
+ align-items: center;
585
+ gap: 10px 14px;
586
+ padding: 8px 12px;
576
587
  border-bottom: 1px solid color-mix(in oklab, CanvasText 18%, Canvas);
588
+ background: color-mix(in oklab, CanvasText 4%, Canvas);
577
589
  font-size: var(--cr-ui-fs);
578
590
  flex: 0 0 auto;
591
+ min-width: 0;
579
592
  }
580
- .toolbar__main {
581
- display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
593
+ .toolbar__primary {
594
+ display: flex;
595
+ flex-direction: row;
596
+ flex-wrap: wrap;
597
+ align-items: center;
598
+ gap: 10px 14px;
599
+ flex: 1 1 auto;
600
+ min-width: 0;
601
+ }
602
+ .toolbar__primary-main {
603
+ display: flex;
604
+ flex-direction: row;
605
+ flex-wrap: wrap;
606
+ align-items: center;
607
+ gap: 10px 14px;
582
608
  flex: 0 1 auto;
583
609
  min-width: 0;
584
610
  }
585
- .toolbar__end {
586
- display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
611
+ .toolbar__primary-trail {
612
+ display: flex;
613
+ flex-direction: row;
614
+ flex-wrap: wrap;
615
+ align-items: center;
616
+ justify-content: flex-end;
617
+ gap: 10px 14px;
587
618
  margin-left: auto;
619
+ min-width: 0;
620
+ }
621
+ .toolbar__end {
622
+ display: flex;
623
+ flex-wrap: wrap;
624
+ align-items: center;
588
625
  justify-content: flex-end;
626
+ gap: 10px 14px;
627
+ min-width: 0;
589
628
  }
590
629
  .toolbar-github {
591
630
  display: inline-flex; align-items: center; justify-content: center;
@@ -603,31 +642,227 @@ const CODE_BROWSER_STYLES = `
603
642
  }
604
643
  .toolbar-github:hover { background: color-mix(in oklab, CanvasText 11%, Canvas); }
605
644
  .toolbar-github:focus-visible { outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas); outline-offset: 2px; }
606
- .toolbar-attribution {
607
- font-size: var(--cr-ui-fs);
608
- line-height: 1.35;
609
- opacity: 0.85;
610
- max-width: min(360px, 42vw);
611
- text-align: right;
645
+ .app__footer-attribution {
646
+ margin: 0;
612
647
  color: color-mix(in oklab, CanvasText 88%, Canvas);
613
648
  }
614
- .toolbar-attribution a { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
649
+ .app__footer-attribution a {
650
+ color: inherit;
651
+ font-weight: 600;
652
+ text-decoration: underline;
653
+ text-underline-offset: 2px;
654
+ }
655
+ .app__footer-attribution__version { font-weight: 600; }
615
656
  .toolbar label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
616
- .toolbar__main > label:has(#wrap-lines) {
657
+ .toolbar-wrap-lines {
658
+ position: relative;
617
659
  margin: 0;
618
660
  min-height: var(--cr-control-h);
619
661
  padding: 0 12px 0 10px;
620
662
  border-radius: var(--cr-control-radius);
621
663
  border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
622
- background: color-mix(in oklab, CanvasText 4%, Canvas);
664
+ background: Canvas;
665
+ display: inline-flex;
666
+ flex-direction: row;
667
+ align-items: center;
668
+ justify-content: flex-start;
669
+ gap: 8px;
623
670
  font-size: var(--cr-ui-fs);
624
671
  font-weight: 500;
625
- gap: 8px;
672
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
673
+ cursor: pointer;
674
+ }
675
+ .toolbar-wrap-lines:hover {
676
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
677
+ }
678
+ .toolbar-wrap-lines__input {
679
+ position: absolute;
680
+ width: 1px;
681
+ height: 1px;
682
+ padding: 0;
683
+ margin: -1px;
684
+ overflow: hidden;
685
+ clip: rect(0, 0, 0, 0);
686
+ white-space: nowrap;
687
+ border: 0;
688
+ opacity: 0;
689
+ }
690
+ /** Visible tick box: the real input is visually hidden for a11y; unchecked looked like an empty box with no mark when on. */
691
+ .toolbar-wrap-lines__box {
692
+ flex: 0 0 auto;
693
+ width: 16px;
694
+ height: 16px;
695
+ box-sizing: border-box;
696
+ border: 1.5px solid color-mix(in oklab, CanvasText 38%, Canvas);
697
+ border-radius: 3px;
698
+ background: Canvas;
699
+ display: inline-flex;
700
+ align-items: center;
701
+ justify-content: center;
702
+ color: CanvasText;
703
+ }
704
+ .toolbar-wrap-lines:has(.toolbar-wrap-lines__input:checked) .toolbar-wrap-lines__box {
705
+ border-color: color-mix(in oklab, CanvasText 52%, Canvas);
706
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
707
+ }
708
+ .toolbar-wrap-lines__box::after {
709
+ content: "";
710
+ display: none;
711
+ width: 4px;
712
+ height: 9px;
713
+ margin-top: -2px;
714
+ border: solid currentColor;
715
+ border-width: 0 2px 2px 0;
716
+ transform: rotate(45deg);
717
+ }
718
+ .toolbar-wrap-lines:has(.toolbar-wrap-lines__input:checked) .toolbar-wrap-lines__box::after {
719
+ display: block;
720
+ }
721
+ .toolbar-wrap-lines__face {
722
+ display: none;
723
+ align-items: center;
724
+ justify-content: center;
725
+ min-height: var(--cr-control-h);
726
+ min-width: var(--cr-control-h);
727
+ color: color-mix(in oklab, CanvasText 82%, Canvas);
728
+ }
729
+ .toolbar-wrap-lines__caption {
730
+ white-space: nowrap;
731
+ }
732
+ .toolbar-wrap-lines:has(.toolbar-wrap-lines__input:checked) {
733
+ color: CanvasText;
734
+ background: color-mix(in oklab, CanvasText 10%, Canvas);
735
+ }
736
+ .toolbar-wrap-lines:has(.toolbar-wrap-lines__input:checked) .toolbar-wrap-lines__caption {
737
+ color: CanvasText;
738
+ }
739
+ .toolbar-wrap-lines:has(.toolbar-wrap-lines__input:checked) .toolbar-wrap-lines__face {
740
+ color: CanvasText;
741
+ background: color-mix(in oklab, CanvasText 10%, Canvas);
742
+ border-radius: calc(var(--cr-control-radius) - 1px);
743
+ }
744
+ .toolbar-wrap-lines:has(.toolbar-wrap-lines__input:focus-visible) {
745
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
746
+ outline-offset: 2px;
747
+ }
748
+ .toolbar-icon-btn {
749
+ display: none;
750
+ align-items: center;
751
+ justify-content: center;
752
+ width: var(--cr-control-h);
753
+ height: var(--cr-control-h);
754
+ padding: 0;
755
+ margin: 0;
756
+ border-radius: var(--cr-control-radius);
757
+ border: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
758
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
759
+ color: CanvasText;
760
+ cursor: pointer;
761
+ flex: 0 0 auto;
762
+ }
763
+ .toolbar-icon-btn svg {
764
+ display: block;
765
+ flex: 0 0 auto;
766
+ }
767
+ .toolbar-icon-btn:hover {
768
+ background: color-mix(in oklab, CanvasText 14%, Canvas);
769
+ border-color: color-mix(in oklab, CanvasText 34%, Canvas);
770
+ }
771
+ .toolbar-icon-btn:focus-visible {
772
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
773
+ outline-offset: 2px;
626
774
  }
627
775
  .toolbar label input:focus-visible {
628
776
  outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
629
777
  outline-offset: 2px;
630
778
  }
779
+ .toolbar .toolbar-theme {
780
+ position: relative;
781
+ display: inline-flex;
782
+ align-items: center;
783
+ margin: 0;
784
+ padding: 0;
785
+ min-width: 0;
786
+ border: 0;
787
+ }
788
+ .toolbar-theme__trigger {
789
+ display: inline-flex;
790
+ align-items: center;
791
+ justify-content: center;
792
+ width: var(--cr-control-h);
793
+ height: var(--cr-control-h);
794
+ padding: 0;
795
+ margin: 0;
796
+ border-radius: var(--cr-control-radius);
797
+ border: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
798
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
799
+ color: CanvasText;
800
+ cursor: pointer;
801
+ }
802
+ .toolbar-theme__trigger:hover {
803
+ background: color-mix(in oklab, CanvasText 14%, Canvas);
804
+ border-color: color-mix(in oklab, CanvasText 34%, Canvas);
805
+ }
806
+ .toolbar-theme__trigger:active {
807
+ background: color-mix(in oklab, CanvasText 18%, Canvas);
808
+ }
809
+ .toolbar-theme__trigger:focus-visible {
810
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
811
+ outline-offset: 2px;
812
+ }
813
+ .toolbar-theme__trigger .toolbar-theme__icon {
814
+ display: none;
815
+ flex: 0 0 auto;
816
+ }
817
+ .toolbar-theme__trigger[data-commentray-trigger-mode="system"] .toolbar-theme__icon--system,
818
+ .toolbar-theme__trigger[data-commentray-trigger-mode="light"] .toolbar-theme__icon--light,
819
+ .toolbar-theme__trigger[data-commentray-trigger-mode="dark"] .toolbar-theme__icon--dark {
820
+ display: block;
821
+ }
822
+ .toolbar-theme__menu {
823
+ position: absolute;
824
+ left: 0;
825
+ top: calc(100% + 4px);
826
+ z-index: 80;
827
+ min-width: 148px;
828
+ padding: 4px;
829
+ margin: 0;
830
+ list-style: none;
831
+ border-radius: 8px;
832
+ border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
833
+ background: Canvas;
834
+ color: CanvasText;
835
+ box-shadow: 0 8px 28px color-mix(in oklab, CanvasText 12%, transparent);
836
+ }
837
+ .toolbar-theme__menu[hidden] {
838
+ display: none !important;
839
+ }
840
+ .toolbar-theme__menuitem {
841
+ display: block;
842
+ width: 100%;
843
+ margin: 0;
844
+ padding: 8px 10px;
845
+ border: 0;
846
+ border-radius: 6px;
847
+ font: inherit;
848
+ font-size: var(--cr-ui-fs);
849
+ font-weight: 500;
850
+ text-align: left;
851
+ cursor: pointer;
852
+ color: CanvasText;
853
+ background: transparent;
854
+ }
855
+ .toolbar-theme__menuitem:hover {
856
+ background: color-mix(in oklab, CanvasText 8%, Canvas);
857
+ }
858
+ .toolbar-theme__menuitem:focus-visible {
859
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
860
+ outline-offset: 0;
861
+ }
862
+ .toolbar-theme__menuitem[aria-checked="true"] {
863
+ background: color-mix(in oklab, CanvasText 10%, Canvas);
864
+ font-weight: 500;
865
+ }
631
866
  .toolbar .file-path {
632
867
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
633
868
  font-size: var(--cr-ui-fs);
@@ -640,9 +875,10 @@ const CODE_BROWSER_STYLES = `
640
875
  }
641
876
  .toolbar .file-path__dir--root { letter-spacing: 0; }
642
877
  .toolbar .file-path__base {
643
- color: CanvasText; font-weight: 600;
878
+ color: CanvasText;
879
+ font-weight: 500;
644
880
  }
645
- .toolbar .file-path--title { font-weight: 600; }
881
+ .toolbar .file-path--title { font-weight: 500; }
646
882
  .toolbar-related {
647
883
  display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px;
648
884
  max-width: min(520px, 90vw);
@@ -650,7 +886,7 @@ const CODE_BROWSER_STYLES = `
650
886
  line-height: 1.35;
651
887
  color: color-mix(in oklab, CanvasText 88%, Canvas);
652
888
  }
653
- .toolbar-related__prefix { font-weight: 600; opacity: 0.85; white-space: nowrap; }
889
+ .toolbar-related__prefix { font-weight: 500; opacity: 0.88; white-space: nowrap; }
654
890
  .toolbar-related__links { min-width: 0; }
655
891
  .toolbar-related a {
656
892
  color: inherit; text-decoration: underline; text-underline-offset: 2px; font-weight: 500;
@@ -727,15 +963,83 @@ const CODE_BROWSER_STYLES = `
727
963
  -webkit-box-decoration-break: clone;
728
964
  }
729
965
  @media (prefers-color-scheme: dark) {
730
- .search-results mark.search-hit {
966
+ :root:is(:not([data-commentray-theme]), [data-commentray-theme="system"]) .search-results mark.search-hit {
731
967
  background: color-mix(in oklab, #c9a227 55%, Canvas);
732
968
  }
733
969
  }
734
- .shell { display: flex; flex-direction: row; flex: 1; min-height: 0; }
970
+ :root[data-commentray-theme="dark"] .search-results mark.search-hit {
971
+ background: color-mix(in oklab, #c9a227 55%, Canvas);
972
+ }
973
+ .shell:not(.shell--stretch-rows) {
974
+ display: flex;
975
+ flex-direction: column;
976
+ flex: 1;
977
+ min-height: 0;
978
+ min-width: 0;
979
+ --split-pct: 46%;
980
+ }
735
981
  .app__main .shell { flex: 1 1 auto; }
982
+ .shell__panes {
983
+ display: flex;
984
+ flex-direction: row;
985
+ flex: 1 1 auto;
986
+ min-height: 0;
987
+ min-width: 0;
988
+ }
989
+ .shell__pair-context {
990
+ flex: 0 0 auto;
991
+ display: flex;
992
+ flex-direction: row;
993
+ align-items: stretch;
994
+ padding: 6px 0 8px;
995
+ border-bottom: 1px solid color-mix(in oklab, CanvasText 15%, Canvas);
996
+ background: color-mix(in oklab, CanvasText 3%, Canvas);
997
+ font-size: var(--cr-ui-fs);
998
+ line-height: 1.3;
999
+ }
1000
+ .shell__pair-cell {
1001
+ display: flex;
1002
+ flex-direction: row;
1003
+ align-items: center;
1004
+ gap: 8px;
1005
+ min-width: 0;
1006
+ }
1007
+ .shell__pair-cell--src {
1008
+ flex: 0 0 var(--split-pct);
1009
+ padding-left: var(--cr-pane-inline-pad);
1010
+ }
1011
+ .shell__pair-gutter-spacer {
1012
+ flex: 0 0 14px;
1013
+ min-width: 14px;
1014
+ align-self: stretch;
1015
+ }
1016
+ .shell__pair-cell--doc {
1017
+ flex: 1 1 auto;
1018
+ min-width: 0;
1019
+ padding-left: var(--cr-pane-inline-pad);
1020
+ }
1021
+ .shell__pair-lab {
1022
+ flex: 0 0 auto;
1023
+ font-size: var(--cr-label-caps-fs);
1024
+ font-weight: 700;
1025
+ letter-spacing: var(--cr-label-caps-track);
1026
+ text-transform: uppercase;
1027
+ opacity: 0.72;
1028
+ }
1029
+ .shell__pair-path {
1030
+ flex: 1 1 auto;
1031
+ min-width: 0;
1032
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
1033
+ font-size: var(--cr-ui-fs);
1034
+ color: CanvasText;
1035
+ overflow: hidden;
1036
+ text-overflow: ellipsis;
1037
+ white-space: nowrap;
1038
+ }
1039
+ .shell__pair-path--secondary { opacity: 0.88; }
736
1040
  .pane--code {
737
- flex: 0 0 50%;
738
- min-width: 120px; overflow: auto; padding: 12px 16px;
1041
+ flex: 0 0 var(--split-pct, 46%);
1042
+ min-width: 120px; overflow: auto; padding: 12px var(--cr-pane-inline-pad);
739
1043
  border-right: 1px solid color-mix(in oklab, CanvasText 15%, Canvas);
740
1044
  --code-line-font-size: 13px;
741
1045
  --code-line-height: 1.5;
@@ -743,9 +1047,10 @@ const CODE_BROWSER_STYLES = `
743
1047
  .pane--code .code-line-stack { --code-ln-min-ch: 3; }
744
1048
  .pane--code .code-line {
745
1049
  display: grid;
746
- grid-template-columns: max-content 1fr;
1050
+ grid-template-columns: max-content minmax(0, 1fr);
747
1051
  column-gap: 10px;
748
1052
  align-items: start;
1053
+ min-width: 0;
749
1054
  }
750
1055
  .pane--code .code-line pre {
751
1056
  margin: 0;
@@ -790,7 +1095,12 @@ const CODE_BROWSER_STYLES = `
790
1095
  --commentray-ray-accent: #3b7dd8;
791
1096
  }
792
1097
  @media (prefers-color-scheme: dark) {
793
- .gutter { --commentray-ray-accent: #6eb0ff; }
1098
+ :root:is(:not([data-commentray-theme]), [data-commentray-theme="system"]) .gutter {
1099
+ --commentray-ray-accent: #6eb0ff;
1100
+ }
1101
+ }
1102
+ :root[data-commentray-theme="dark"] .gutter {
1103
+ --commentray-ray-accent: #6eb0ff;
794
1104
  }
795
1105
  .gutter__rays {
796
1106
  position: absolute; inset: 0; pointer-events: none; z-index: 1;
@@ -816,40 +1126,360 @@ const CODE_BROWSER_STYLES = `
816
1126
  }
817
1127
  .pane--doc {
818
1128
  flex: 1 1 auto; min-width: 0; min-height: 0;
819
- display: flex; flex-direction: column; overflow: hidden; padding: 12px 16px;
1129
+ display: flex; flex-direction: column; overflow: hidden; padding: 12px var(--cr-pane-inline-pad);
1130
+ background: Canvas;
1131
+ color: CanvasText;
1132
+ }
1133
+ /* #doc-pane-body.wrap beats pre code.hljs from the hljs theme so fenced blocks follow the toggle. */
1134
+ #doc-pane-body.wrap pre,
1135
+ #doc-pane-body.wrap pre code {
1136
+ white-space: pre-wrap;
1137
+ word-break: break-word;
1138
+ }
1139
+ #doc-pane-body:not(.wrap) pre,
1140
+ #doc-pane-body:not(.wrap) pre code {
1141
+ white-space: pre;
1142
+ word-break: normal;
820
1143
  }
821
1144
  .doc-pane-body {
822
1145
  flex: 1 1 auto; min-height: 0; overflow: auto;
823
1146
  }
1147
+ /** Wide GFM tables: intrinsic width so the doc pane scrolls sideways instead of squeezing columns. */
1148
+ .pane--doc .doc-pane-body :where(table) {
1149
+ width: max-content;
1150
+ max-width: none;
1151
+ border-collapse: collapse;
1152
+ }
1153
+ .pane--doc .doc-pane-body .commentray-mermaid {
1154
+ overflow-x: auto;
1155
+ max-width: 100%;
1156
+ }
1157
+ /** Wrap on: break long URLs/words in prose; tables opt out so they stay wide + scroll with the body. */
1158
+ #doc-pane-body.wrap {
1159
+ overflow-wrap: break-word;
1160
+ }
1161
+ #doc-pane-body.wrap :where(table) {
1162
+ overflow-wrap: normal;
1163
+ word-break: normal;
1164
+ }
1165
+ #doc-pane-body:not(.wrap) {
1166
+ overflow-wrap: normal;
1167
+ word-break: normal;
1168
+ }
824
1169
  .toolbar-angle-picker {
825
1170
  display: inline-flex;
826
1171
  align-items: center;
827
- gap: 8px;
1172
+ gap: 6px;
828
1173
  flex: 0 0 auto;
829
- color: color-mix(in oklab, CanvasText 88%, Canvas);
830
1174
  }
831
- .toolbar-angle-picker > label {
832
- font-size: var(--cr-label-caps-fs);
833
- font-weight: 700;
834
- letter-spacing: var(--cr-label-caps-track);
835
- text-transform: uppercase;
836
- opacity: 0.72;
1175
+ /* Angle caption uses the same class as the Search label (.nav-rail__search-label). */
1176
+ .toolbar-angle-picker__lab {
1177
+ display: inline-block;
1178
+ margin: 0;
1179
+ padding: 0;
1180
+ cursor: default;
1181
+ flex: 0 0 auto;
1182
+ white-space: nowrap;
1183
+ user-select: none;
837
1184
  }
838
1185
  .toolbar-angle-picker select {
839
1186
  font: inherit;
840
1187
  font-size: var(--cr-ui-fs);
1188
+ font-weight: 500;
841
1189
  min-height: var(--cr-control-h);
842
1190
  height: var(--cr-control-h);
843
1191
  padding: 0 10px;
844
1192
  border-radius: var(--cr-control-radius);
845
1193
  border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
846
1194
  background: Canvas;
847
- color: CanvasText;
1195
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
848
1196
  }
849
1197
  .toolbar-angle-picker select:focus-visible {
850
1198
  outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
851
1199
  outline-offset: 2px;
852
1200
  }
1201
+ /* Single-pane + compact chrome below typical tablet / Bootstrap md threshold (768px). */
1202
+ @media (max-width: 767px) {
1203
+ html,
1204
+ body {
1205
+ overflow-x: auto;
1206
+ overflow-y: auto;
1207
+ }
1208
+ .app {
1209
+ height: auto;
1210
+ min-height: 100vh;
1211
+ min-height: 100dvh;
1212
+ min-width: 0;
1213
+ overflow-x: auto;
1214
+ overflow-y: visible;
1215
+ }
1216
+ .app__main {
1217
+ flex: 0 0 auto;
1218
+ width: 100%;
1219
+ min-height: 0;
1220
+ }
1221
+ .app__main > #shell:not(.shell--stretch-rows) {
1222
+ flex: none !important;
1223
+ min-height: auto !important;
1224
+ overflow: visible !important;
1225
+ }
1226
+ .app__main > #shell:not(.shell--stretch-rows) .shell__panes {
1227
+ flex: none !important;
1228
+ min-height: auto !important;
1229
+ min-width: 0;
1230
+ width: 100%;
1231
+ max-width: 100%;
1232
+ box-sizing: border-box;
1233
+ }
1234
+ .app__main > #shell:not(.shell--stretch-rows) .pane--code,
1235
+ .app__main > #shell:not(.shell--stretch-rows) .pane--doc {
1236
+ flex: none !important;
1237
+ min-height: auto !important;
1238
+ overflow: visible !important;
1239
+ max-height: none !important;
1240
+ /* flex:none + basis:auto otherwise sizes to max-content so line-wrap has no width cap */
1241
+ width: 100%;
1242
+ max-width: 100%;
1243
+ min-width: 0 !important;
1244
+ box-sizing: border-box;
1245
+ }
1246
+ .app__main > #shell:not(.shell--stretch-rows) .pane--doc {
1247
+ display: block;
1248
+ }
1249
+ .app__main > #shell:not(.shell--stretch-rows) .doc-pane-body {
1250
+ flex: none !important;
1251
+ min-height: auto !important;
1252
+ min-width: 0;
1253
+ overflow: visible !important;
1254
+ }
1255
+ .app__footer {
1256
+ margin-top: auto;
1257
+ flex-shrink: 0;
1258
+ padding: 5px 10px 8px;
1259
+ font-size: 10px;
1260
+ line-height: 1.35;
1261
+ }
1262
+ .app__main > #shell.shell--stretch-rows {
1263
+ flex: 1 1 auto;
1264
+ min-height: min(72vh, 720px);
1265
+ min-height: min(72dvh, 720px);
1266
+ overflow: auto;
1267
+ }
1268
+ .toolbar {
1269
+ padding: 5px 8px 5px;
1270
+ row-gap: 4px;
1271
+ }
1272
+ .toolbar__primary {
1273
+ display: flex;
1274
+ flex-direction: row;
1275
+ flex-wrap: nowrap;
1276
+ align-items: center;
1277
+ gap: 6px;
1278
+ min-width: 0;
1279
+ width: 100%;
1280
+ box-sizing: border-box;
1281
+ }
1282
+ .toolbar__primary-main {
1283
+ flex: 1 1 auto;
1284
+ min-width: 0;
1285
+ flex-wrap: nowrap;
1286
+ overflow-x: auto;
1287
+ overflow-y: visible;
1288
+ -webkit-overflow-scrolling: touch;
1289
+ gap: 6px;
1290
+ scrollbar-width: thin;
1291
+ }
1292
+ .toolbar__primary-trail {
1293
+ flex: 0 0 auto;
1294
+ flex-wrap: nowrap;
1295
+ align-self: center;
1296
+ }
1297
+ .toolbar-angle-picker {
1298
+ position: relative;
1299
+ flex: 0 1 auto;
1300
+ min-width: 0;
1301
+ max-width: 100%;
1302
+ }
1303
+ .toolbar-angle-picker__lab {
1304
+ position: absolute;
1305
+ width: 1px;
1306
+ height: 1px;
1307
+ padding: 0;
1308
+ margin: -1px;
1309
+ overflow: hidden;
1310
+ clip: rect(0, 0, 0, 0);
1311
+ white-space: nowrap;
1312
+ border: 0;
1313
+ opacity: 0;
1314
+ pointer-events: none;
1315
+ }
1316
+ .toolbar-angle-picker select {
1317
+ max-width: min(200px, 52vw);
1318
+ min-width: 0;
1319
+ text-overflow: ellipsis;
1320
+ }
1321
+ .app__chrome {
1322
+ padding: 5px 8px 6px;
1323
+ gap: 5px;
1324
+ max-height: min(36vh, 360px);
1325
+ }
1326
+ /* Compact chrome: avoid heavy rings on inline fields (clear stays a real button). */
1327
+ .chrome__search-row input[type="search"]:focus-visible {
1328
+ outline: none;
1329
+ border-color: color-mix(in oklab, CanvasText 42%, Canvas);
1330
+ }
1331
+ .chrome__search-row #search-clear:focus-visible {
1332
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
1333
+ outline-offset: 2px;
1334
+ }
1335
+ .chrome__search-label__caption {
1336
+ position: absolute;
1337
+ width: 1px;
1338
+ height: 1px;
1339
+ padding: 0;
1340
+ margin: -1px;
1341
+ overflow: hidden;
1342
+ clip: rect(0, 0, 0, 0);
1343
+ white-space: nowrap;
1344
+ border: 0;
1345
+ }
1346
+ .chrome__search-label__glyph {
1347
+ display: inline-flex;
1348
+ align-items: center;
1349
+ justify-content: center;
1350
+ padding: 0 2px;
1351
+ margin: 0;
1352
+ color: color-mix(in oklab, CanvasText 72%, Canvas);
1353
+ }
1354
+ .chrome__search-label__glyph:hover {
1355
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
1356
+ }
1357
+ .chrome__search-label:focus-within {
1358
+ outline: none;
1359
+ }
1360
+ .chrome__search-label__glyph svg {
1361
+ display: block;
1362
+ flex: 0 0 auto;
1363
+ }
1364
+ .toolbar-angle-picker select:focus-visible {
1365
+ outline: none;
1366
+ border-color: color-mix(in oklab, CanvasText 42%, Canvas);
1367
+ }
1368
+ .toolbar-icon-btn--flip-only-narrow {
1369
+ display: inline-flex;
1370
+ }
1371
+ /**
1372
+ * Secondary flip: only on narrow viewports, only while the toolbar flip is off-screen
1373
+ * (see client IntersectionObserver). Same control as toolbar; fixed so it stays reachable.
1374
+ */
1375
+ .toolbar-icon-btn--flip-scroll-narrow {
1376
+ display: none;
1377
+ }
1378
+ #mobile-pane-flip-scroll.toolbar-icon-btn--flip-scroll-narrow.is-visible {
1379
+ display: inline-flex;
1380
+ position: fixed;
1381
+ top: calc(10px + env(safe-area-inset-top, 0px));
1382
+ right: calc(12px + env(safe-area-inset-right, 0px));
1383
+ z-index: 50;
1384
+ box-shadow:
1385
+ 0 1px 2px color-mix(in oklab, CanvasText 12%, transparent),
1386
+ 0 4px 14px color-mix(in oklab, CanvasText 18%, transparent);
1387
+ }
1388
+ /** Region connector lines are not needed on the narrow single-pane layout (gutter is hidden). */
1389
+ .shell:not(.shell--stretch-rows) .gutter .gutter__rays {
1390
+ opacity: 0 !important;
1391
+ pointer-events: none !important;
1392
+ }
1393
+ .nav-rail__doc-hub-summary {
1394
+ min-width: var(--cr-control-h);
1395
+ padding: 0 10px;
1396
+ justify-content: center;
1397
+ gap: 0;
1398
+ }
1399
+ .nav-rail__doc-hub-summary__caption {
1400
+ position: absolute;
1401
+ width: 1px;
1402
+ height: 1px;
1403
+ padding: 0;
1404
+ margin: -1px;
1405
+ overflow: hidden;
1406
+ clip: rect(0, 0, 0, 0);
1407
+ white-space: nowrap;
1408
+ border: 0;
1409
+ }
1410
+ .nav-rail__doc-hub-summary__glyph {
1411
+ display: inline-flex;
1412
+ }
1413
+ .toolbar-wrap-lines {
1414
+ min-width: var(--cr-control-h);
1415
+ padding: 0;
1416
+ justify-content: center;
1417
+ gap: 0;
1418
+ font-weight: 500;
1419
+ }
1420
+ .toolbar-wrap-lines__caption {
1421
+ position: absolute;
1422
+ width: 1px;
1423
+ height: 1px;
1424
+ padding: 0;
1425
+ margin: -1px;
1426
+ overflow: hidden;
1427
+ clip: rect(0, 0, 0, 0);
1428
+ white-space: nowrap;
1429
+ border: 0;
1430
+ }
1431
+ .toolbar-wrap-lines__box {
1432
+ display: none;
1433
+ }
1434
+ .toolbar-wrap-lines__face {
1435
+ display: inline-flex;
1436
+ position: relative;
1437
+ width: 100%;
1438
+ height: 100%;
1439
+ min-height: var(--cr-control-h);
1440
+ min-width: var(--cr-control-h);
1441
+ }
1442
+ .toolbar-wrap-lines:has(.toolbar-wrap-lines__input:checked) .toolbar-wrap-lines__face::after {
1443
+ content: "✓";
1444
+ position: absolute;
1445
+ right: 1px;
1446
+ bottom: 0;
1447
+ font-size: 11px;
1448
+ line-height: 1;
1449
+ font-weight: 800;
1450
+ color: CanvasText;
1451
+ text-shadow: 0 0 2px Canvas, 0 0 3px Canvas;
1452
+ }
1453
+ .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="code"] .pane--doc,
1454
+ .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="code"] .gutter {
1455
+ display: none !important;
1456
+ }
1457
+ .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="doc"] .pane--code,
1458
+ .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="doc"] .gutter {
1459
+ display: none !important;
1460
+ }
1461
+ .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="code"] .pane--code,
1462
+ .shell:not(.shell--stretch-rows)[data-dual-mobile-pane="doc"] .pane--doc {
1463
+ border-right: 0 !important;
1464
+ }
1465
+ .shell:not(.shell--stretch-rows) .shell__pair-context {
1466
+ flex-direction: column;
1467
+ align-items: stretch;
1468
+ gap: 4px;
1469
+ padding: 4px 0 6px;
1470
+ }
1471
+ .shell:not(.shell--stretch-rows) .shell__pair-gutter-spacer {
1472
+ display: none;
1473
+ }
1474
+ .shell:not(.shell--stretch-rows) .shell__pair-cell--src {
1475
+ flex: 1 1 auto;
1476
+ padding-left: var(--cr-pane-inline-pad);
1477
+ }
1478
+ .shell:not(.shell--stretch-rows) .shell__pair-cell--doc {
1479
+ flex: 1 1 auto;
1480
+ padding-left: var(--cr-pane-inline-pad);
1481
+ }
1482
+ }
853
1483
  .pane--doc { font-size: 15px; line-height: 1.45; }
854
1484
  .pane--doc img { max-width: 100%; height: auto; }
855
1485
  .pane--doc .commentray-line-anchor {
@@ -878,8 +1508,15 @@ const CODE_BROWSER_STYLES = `
878
1508
  border-bottom: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
879
1509
  font-size: 15px;
880
1510
  line-height: 1.45;
1511
+ overflow-x: auto;
1512
+ max-width: 100%;
881
1513
  }
882
1514
  .shell--stretch-rows .stretch-preamble img { max-width: 100%; height: auto; }
1515
+ .shell--stretch-rows .stretch-preamble :where(table) {
1516
+ width: max-content;
1517
+ max-width: none;
1518
+ border-collapse: collapse;
1519
+ }
883
1520
  .block-stretch {
884
1521
  width: 100%;
885
1522
  border-collapse: collapse;
@@ -900,8 +1537,30 @@ const CODE_BROWSER_STYLES = `
900
1537
  .block-stretch td.stretch-doc .stretch-doc-inner {
901
1538
  font-size: 15px;
902
1539
  line-height: 1.45;
1540
+ min-width: 0;
1541
+ overflow-x: auto;
903
1542
  }
904
1543
  .block-stretch td.stretch-doc .stretch-doc-inner img { max-width: 100%; height: auto; }
1544
+ .block-stretch td.stretch-doc .stretch-doc-inner :where(table) {
1545
+ width: max-content;
1546
+ max-width: none;
1547
+ border-collapse: collapse;
1548
+ }
1549
+ .block-stretch td.stretch-doc .stretch-doc-inner .commentray-mermaid {
1550
+ overflow-x: auto;
1551
+ max-width: 100%;
1552
+ }
1553
+ .block-stretch.wrap td.stretch-doc .stretch-doc-inner {
1554
+ overflow-wrap: break-word;
1555
+ }
1556
+ .block-stretch.wrap td.stretch-doc .stretch-doc-inner :where(table) {
1557
+ overflow-wrap: normal;
1558
+ word-break: normal;
1559
+ }
1560
+ .block-stretch:not(.wrap) td.stretch-doc .stretch-doc-inner {
1561
+ overflow-wrap: normal;
1562
+ word-break: normal;
1563
+ }
905
1564
  .block-stretch td.stretch-doc--gap {
906
1565
  color: color-mix(in oklab, CanvasText 38%, Canvas);
907
1566
  font-size: 13px;
@@ -948,19 +1607,38 @@ const CODE_BROWSER_STYLES = `
948
1607
  .block-stretch.wrap .code-line pre code { white-space: pre-wrap; word-break: break-word; }
949
1608
  .block-stretch:not(.wrap) .code-line pre,
950
1609
  .block-stretch:not(.wrap) .code-line pre code { white-space: pre; }
1610
+ .block-stretch.wrap .stretch-doc-inner pre,
1611
+ .block-stretch.wrap .stretch-doc-inner pre code {
1612
+ white-space: pre-wrap;
1613
+ word-break: break-word;
1614
+ }
1615
+ .block-stretch:not(.wrap) .stretch-doc-inner pre,
1616
+ .block-stretch:not(.wrap) .stretch-doc-inner pre code {
1617
+ white-space: pre;
1618
+ }
951
1619
  `;
952
1620
  /** Native tooltip on #search-q (short hint is visible under the search row). */
953
1621
  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).";
954
1622
  function buildCodeBrowserPageHtml(p) {
955
1623
  const shellClass = p.layout === "stretch" ? "shell shell--stretch-rows" : "shell";
1624
+ const dualFlipControlHtml = p.layout === "dual"
1625
+ ? `<button type="button" id="mobile-pane-flip" class="toolbar-icon-btn toolbar-icon-btn--flip-only-narrow" aria-label="Switch between source code and commentary" title="Switch between source code and commentary">${TOOLBAR_ICON_FLIP_PANES_SVG}</button>`
1626
+ : "";
1627
+ const dualFlipScrollAffordanceHtml = p.layout === "dual"
1628
+ ? `<button type="button" id="mobile-pane-flip-scroll" class="toolbar-icon-btn toolbar-icon-btn--flip-scroll-narrow" hidden aria-label="Switch between source code and commentary" title="Switch between source code and commentary">${TOOLBAR_ICON_FLIP_PANES_SVG}</button>`
1629
+ : "";
956
1630
  return `<!doctype html>
957
- <html lang="en">
1631
+ <html lang="en" data-commentray-theme="system">
958
1632
  <head>
959
1633
  <meta charset="utf-8" />
960
1634
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1635
+ ${COMMENTRAY_FAVICON_LINK_HTML}
961
1636
  ${p.metaDescriptionHtml}${p.generatorMetaHtml}<title>${escapeHtml(p.title)}</title>
962
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/${escapeHtml(p.hljs)}.min.css" media="(prefers-color-scheme: light)" />
963
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/${escapeHtml(p.hljsDark)}.min.css" media="(prefers-color-scheme: dark)" />
1637
+ <link rel="stylesheet" id="commentray-hljs-light" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/${escapeHtml(p.hljs)}.min.css" media="(prefers-color-scheme: light)" />
1638
+ <link rel="stylesheet" id="commentray-hljs-dark" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/${escapeHtml(p.hljsDark)}.min.css" media="(prefers-color-scheme: dark)" />
1639
+ <script>
1640
+ ${commentrayColorThemeHeadBoot()}
1641
+ </script>
964
1642
  <style>
965
1643
  ${CODE_BROWSER_STYLES}
966
1644
  </style>
@@ -970,38 +1648,48 @@ ${CODE_BROWSER_STYLES}
970
1648
  <div class="app">
971
1649
  <header class="toolbar" role="banner" aria-label="View options">
972
1650
  <h1 class="sr-only">${escapeHtml(p.title)}</h1>
973
- <div class="toolbar__main">
1651
+ <div class="toolbar__primary">
1652
+ <div class="toolbar__primary-main">
974
1653
  ${p.toolbarSiteHubHtml}
975
- ${p.navRailContextHtml}
976
1654
  ${p.navRailDocumentedHtml}
977
1655
  ${p.angleSelectHtml}
1656
+ <label class="toolbar-wrap-lines" title="Wrap long lines in the source pane; in commentary, wrap long words and fenced code when on (wide tables and diagrams scroll horizontally).">
1657
+ <input type="checkbox" id="wrap-lines" class="toolbar-wrap-lines__input" />
1658
+ <span class="toolbar-wrap-lines__box" aria-hidden="true"></span>
1659
+ <span class="toolbar-wrap-lines__face" aria-hidden="true">${TOOLBAR_ICON_WRAP_SVG}</span>
1660
+ <span class="toolbar-wrap-lines__caption">Wrap lines</span>
1661
+ </label>
1662
+ ${dualFlipControlHtml}
978
1663
  ${p.toolbarDocHubHtml}
979
1664
  ${p.relatedNavHtml}
980
- <label><input type="checkbox" id="wrap-lines" /> Wrap code lines</label>
981
- </div>
1665
+ </div>
1666
+ <div class="toolbar__primary-trail">
982
1667
  ${p.toolbarEndHtml}
1668
+ ${TOOLBAR_COLOR_THEME_HTML}
1669
+ </div>
1670
+ </div>
983
1671
  </header>
1672
+ ${dualFlipScrollAffordanceHtml}
984
1673
  <header class="app__chrome" role="region" aria-label="Search">
985
1674
  <div class="chrome__search-row">
986
- <label class="chrome__search-label nav-rail__search-label" for="search-q">Search</label>
1675
+ <label class="chrome__search-label" for="search-q" aria-label="Search" title="Search"><span class="chrome__search-label__caption nav-rail__search-label">Search</span><span class="chrome__search-label__glyph" aria-hidden="true">${CHROME_ICON_SEARCH_SVG}</span></label>
987
1676
  <input type="search" id="search-q" placeholder="${escapeHtml(p.searchPlaceholder)}" title="${escapeHtml(CODE_BROWSER_SEARCH_INPUT_TITLE)}" autocomplete="off" spellcheck="false" />
988
- <button type="button" id="search-clear" title="Clear search">Clear</button>
1677
+ <button type="button" id="search-clear" aria-label="Clear search" title="Clear search">Clear</button>
989
1678
  </div>
990
1679
  <div class="search-results" id="search-results" hidden aria-live="polite"></div>
991
- <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>
992
1680
  </header>
993
1681
  <main id="main-content" class="app__main" tabindex="-1">
994
- <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}>
1682
+ <div class="${shellClass}" id="shell" data-layout="${p.layout}"${p.layout === "dual" ? ' data-dual-mobile-pane="doc"' : ""} 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}${p.shellPairDocDataAttr}>
995
1683
  ${p.shellInner}
996
1684
  </div>
997
1685
  </main>
998
1686
  ${p.pageFooterHtml}
999
1687
  </div>
1000
1688
  <script type="text/plain" id="commentray-multi-angle-b64">${p.multiAngleScriptBlock}</script>
1689
+ ${p.mermaidScript}
1001
1690
  <script>
1002
1691
  ${loadCodeBrowserClientBundle()}
1003
1692
  </script>
1004
- ${p.mermaidScript}
1005
1693
  </body>
1006
1694
  </html>`;
1007
1695
  }
@@ -1064,16 +1752,9 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
1064
1752
  return `<option value="${escapeHtml(a.id)}"${a.id === defaultId ? " selected" : ""}>${lab}</option>`;
1065
1753
  })
1066
1754
  .join("");
1067
- const angleSelectHtml = `<span class="toolbar-angle-picker"><label for="angle-select">Angle</label><select id="angle-select" aria-label="Commentray angle">${selOpts}</select></span>`;
1068
- const shellInner = ` <section class="pane--code" id="code-pane" aria-label="Source code">` +
1069
- ` ${codeHtml}\n` +
1070
- ` </section>\n` +
1071
- ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
1072
- ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
1073
- ` <div id="doc-pane-body" class="doc-pane-body">\n` +
1074
- ` ${defaultPaneHtml}\n` +
1075
- ` </div>\n` +
1076
- ` </section>\n`;
1755
+ const angleSelectHtml = `<span class="toolbar-angle-picker"><label class="toolbar-angle-picker__lab nav-rail__search-label" for="angle-select">Angle</label><select id="angle-select" aria-label="Commentray angle">${selOpts}</select></span>`;
1756
+ const pairHtml = renderShellPairContextHtml(opts.filePath, defaultPathSearch);
1757
+ const shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, defaultPaneHtml));
1077
1758
  const payloadObj = { defaultAngleId: defaultId, angles: jsonAngles };
1078
1759
  const multiAnglePayloadB64 = Buffer.from(JSON.stringify(payloadObj), "utf8").toString("base64");
1079
1760
  return {
@@ -1136,16 +1817,8 @@ async function buildCodeBrowserShell(opts, layoutPref) {
1136
1817
  commentrayOutputUrls: opts.commentrayOutputUrls,
1137
1818
  }),
1138
1819
  ]);
1139
- shellInner =
1140
- ` <section class="pane--code" id="code-pane" aria-label="Source code">` +
1141
- ` ${codeHtml}\n` +
1142
- ` </section>\n` +
1143
- ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
1144
- ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
1145
- ` <div id="doc-pane-body" class="doc-pane-body">\n` +
1146
- ` ${commentrayHtml}\n` +
1147
- ` </div>\n` +
1148
- ` </section>\n`;
1820
+ const pairHtml = renderShellPairContextHtml(opts.filePath, (opts.commentrayPathForSearch ?? "").trim());
1821
+ shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, commentrayHtml));
1149
1822
  }
1150
1823
  return {
1151
1824
  layout,
@@ -1177,28 +1850,30 @@ function shellDocumentedPairsAttrFromOptions(opts) {
1177
1850
  function codeBrowserPageTitle(opts) {
1178
1851
  return opts.title ?? opts.filePath ?? "Commentray";
1179
1852
  }
1180
- function codeBrowserHljsThemes(opts) {
1181
- const hljs = opts.hljsTheme ?? "github";
1182
- const hljsDark = opts.hljsTheme?.includes("dark") ? opts.hljsTheme : "github-dark";
1183
- return { hljs, hljsDark };
1184
- }
1185
1853
  function toolbarCommentrayGithubFromShell(shell, opts) {
1186
1854
  return shell.multiShell?.commentrayOnGithubUrl ?? opts.commentrayOnGithubUrl;
1187
1855
  }
1188
- function toolbarCommentrayStaticBrowseFromShell(shell, opts) {
1189
- const t = (shell.multiShell?.commentrayStaticBrowseUrl ??
1190
- opts.commentrayStaticBrowseUrl ??
1191
- "").trim();
1192
- return t.length > 0 ? t : undefined;
1193
- }
1194
1856
  function rawMdB64FromShell(shell, opts) {
1195
1857
  return (shell.multiShell?.rawMdB64 ?? Buffer.from(opts.commentrayMarkdown, "utf8").toString("base64"));
1196
1858
  }
1197
- function navRailCommentrayPathFromShell(shell, opts) {
1198
- const trimmed = (shell.multiShell?.commentrayPathForSearch ??
1199
- opts.commentrayPathForSearch ??
1859
+ /** Canonical doc target for static validation: same-site `./browse/…` when present, else GitHub blob. */
1860
+ function shellPairDocDataAttr(shell, opts) {
1861
+ if (shell.layout !== "dual")
1862
+ return "";
1863
+ const browseRaw = (shell.multiShell?.commentrayStaticBrowseUrl ??
1864
+ opts.commentrayStaticBrowseUrl ??
1200
1865
  "").trim();
1201
- return trimmed.length > 0 ? trimmed : undefined;
1866
+ if (browseRaw.length > 0) {
1867
+ const href = safeToolbarNavigationHref(browseRaw);
1868
+ if (href !== null) {
1869
+ return ` data-commentray-pair-browse-href="${escapeHtml(href)}"`;
1870
+ }
1871
+ }
1872
+ const gh = safeExternalHttpUrl(toolbarCommentrayGithubFromShell(shell, opts));
1873
+ if (gh !== null) {
1874
+ return ` data-commentray-pair-browse-href="${escapeHtml(gh)}"`;
1875
+ }
1876
+ return "";
1202
1877
  }
1203
1878
  function shellSearchAttrsWithNavJson(shellSearchAttrsBase, documentedNavJsonUrl) {
1204
1879
  const navJson = documentedNavJsonUrl?.trim() ?? "";
@@ -1218,9 +1893,13 @@ export async function renderCodeBrowserHtml(opts) {
1218
1893
  const builtAt = opts.builtAt ?? new Date();
1219
1894
  const renderSemver = commentrayRenderVersion();
1220
1895
  const toolbarSiteHubHtml = buildToolbarSiteHubHtml(opts.siteHubUrl);
1221
- const toolbarEndHtml = buildToolbarEndHtml(opts.githubRepoUrl, opts.toolHomeUrl, renderSemver, opts.siteHubUrl);
1222
- const pageFooterHtml = renderPageFooterHtml(builtAt);
1223
- const { hljs, hljsDark } = codeBrowserHljsThemes(opts);
1896
+ const toolbarEndHtml = buildToolbarEndHtml(opts.githubRepoUrl, opts.siteHubUrl);
1897
+ const pageFooterHtml = renderPageFooterHtml({
1898
+ builtAt,
1899
+ toolHomeUrl: opts.toolHomeUrl,
1900
+ commentrayRenderSemver: renderSemver,
1901
+ });
1902
+ const { hljsLight, hljsDark } = hljsStylesheetThemes(opts.hljsTheme);
1224
1903
  const mermaidScript = mermaidRuntimeScriptHtml(opts.includeMermaidRuntime);
1225
1904
  const relatedNavHtml = renderRelatedGithubNavHtml(opts.relatedGithubNav ?? []);
1226
1905
  const generatorMetaHtml = renderGeneratorMetaHtml(opts.generatorLabel);
@@ -1235,17 +1914,13 @@ export async function renderCodeBrowserHtml(opts) {
1235
1914
  const { searchPlaceholder, shellSearchAttrs: shellSearchAttrsBase } = searchChromeFromOptions(opts, shell.multiShell?.commentrayPathForSearch);
1236
1915
  const shellDocumentedPairsAttr = shellDocumentedPairsAttrFromOptions(opts);
1237
1916
  const shellSearchAttrs = shellSearchAttrsWithNavJson(shellSearchAttrsBase, opts.documentedNavJsonUrl);
1238
- const navRailContextHtml = renderNavRailContextHtml(opts.filePath, navRailCommentrayPathFromShell(shell, opts), {
1239
- sourceOnGithubUrl: opts.sourceOnGithubUrl,
1240
- commentrayOnGithubUrl: toolbarCommentrayGithubFromShell(shell, opts),
1241
- commentrayStaticBrowseUrl: toolbarCommentrayStaticBrowseFromShell(shell, opts),
1242
- });
1917
+ const pairDocDataAttr = shellPairDocDataAttr(shell, opts);
1243
1918
  return buildCodeBrowserPageHtml({
1244
1919
  title,
1245
1920
  metaDescriptionHtml,
1246
1921
  generatorMetaHtml,
1247
1922
  toolbarSiteHubHtml,
1248
- navRailContextHtml,
1923
+ shellPairDocDataAttr: pairDocDataAttr,
1249
1924
  angleSelectHtml: shell.angleSelectHtml,
1250
1925
  toolbarDocHubHtml,
1251
1926
  navRailDocumentedHtml,
@@ -1258,7 +1933,7 @@ export async function renderCodeBrowserHtml(opts) {
1258
1933
  rawMdB64,
1259
1934
  scrollBlockLinksB64,
1260
1935
  shellDocumentedPairsAttr,
1261
- hljs,
1936
+ hljs: hljsLight,
1262
1937
  hljsDark,
1263
1938
  mermaidScript,
1264
1939
  searchPlaceholder,