@commentray/render 0.0.5 → 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 (59) hide show
  1. package/dist/browse-page-slug.d.ts +6 -0
  2. package/dist/browse-page-slug.d.ts.map +1 -0
  3. package/dist/browse-page-slug.js +9 -0
  4. package/dist/browse-page-slug.js.map +1 -0
  5. package/dist/build-commentray-nav-search.d.ts +11 -5
  6. package/dist/build-commentray-nav-search.d.ts.map +1 -1
  7. package/dist/build-commentray-nav-search.js +11 -11
  8. package/dist/build-commentray-nav-search.js.map +1 -1
  9. package/dist/code-browser-block-rays.d.ts +48 -0
  10. package/dist/code-browser-block-rays.d.ts.map +1 -0
  11. package/dist/code-browser-block-rays.js +95 -0
  12. package/dist/code-browser-block-rays.js.map +1 -0
  13. package/dist/code-browser-client.bundle.js +12 -12
  14. package/dist/code-browser-client.js +1106 -145
  15. package/dist/code-browser-client.js.map +1 -1
  16. package/dist/code-browser-color-theme.d.ts +15 -0
  17. package/dist/code-browser-color-theme.d.ts.map +1 -0
  18. package/dist/code-browser-color-theme.js +73 -0
  19. package/dist/code-browser-color-theme.js.map +1 -0
  20. package/dist/code-browser-pair-nav.d.ts +31 -0
  21. package/dist/code-browser-pair-nav.d.ts.map +1 -0
  22. package/dist/code-browser-pair-nav.js +77 -0
  23. package/dist/code-browser-pair-nav.js.map +1 -0
  24. package/dist/code-browser-scroll-sync.js +1 -1
  25. package/dist/code-browser-scroll-sync.js.map +1 -1
  26. package/dist/code-browser-search.d.ts +45 -0
  27. package/dist/code-browser-search.d.ts.map +1 -1
  28. package/dist/code-browser-search.js +89 -0
  29. package/dist/code-browser-search.js.map +1 -1
  30. package/dist/code-browser.d.ts +24 -3
  31. package/dist/code-browser.d.ts.map +1 -1
  32. package/dist/code-browser.js +1202 -288
  33. package/dist/code-browser.js.map +1 -1
  34. package/dist/hljs-stylesheet-themes.d.ts +13 -0
  35. package/dist/hljs-stylesheet-themes.d.ts.map +1 -0
  36. package/dist/hljs-stylesheet-themes.js +19 -0
  37. package/dist/hljs-stylesheet-themes.js.map +1 -0
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/inline-favicon.d.ts +2 -0
  43. package/dist/inline-favicon.d.ts.map +1 -0
  44. package/dist/inline-favicon.js +25 -0
  45. package/dist/inline-favicon.js.map +1 -0
  46. package/dist/markdown-pipeline.d.ts.map +1 -1
  47. package/dist/markdown-pipeline.js +38 -2
  48. package/dist/markdown-pipeline.js.map +1 -1
  49. package/dist/mermaid-runtime-html.d.ts.map +1 -1
  50. package/dist/mermaid-runtime-html.js +13 -3
  51. package/dist/mermaid-runtime-html.js.map +1 -1
  52. package/dist/package-version.d.ts.map +1 -1
  53. package/dist/package-version.js +4 -4
  54. package/dist/package-version.js.map +1 -1
  55. package/dist/side-by-side-layout.css +58 -0
  56. package/dist/side-by-side.d.ts.map +1 -1
  57. package/dist/side-by-side.js +10 -12
  58. package/dist/side-by-side.js.map +1 -1
  59. 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";
@@ -15,10 +17,29 @@ function renderGeneratorMetaHtml(label) {
15
17
  return "";
16
18
  return `<meta name="generator" content="${escapeHtml(t)}" />\n `;
17
19
  }
20
+ const META_DESCRIPTION_MAX_LEN = 320;
21
+ function codeBrowserMetaDescription(opts, title) {
22
+ const custom = opts.metaDescription?.trim();
23
+ if (custom)
24
+ return custom.slice(0, META_DESCRIPTION_MAX_LEN);
25
+ const fallback = `${title} — Side-by-side source and commentray documentation.`;
26
+ return fallback.slice(0, META_DESCRIPTION_MAX_LEN);
27
+ }
28
+ function renderMetaDescriptionHtml(opts, title) {
29
+ const content = codeBrowserMetaDescription(opts, title);
30
+ return `<meta name="description" content="${escapeHtml(content)}" />\n `;
31
+ }
18
32
  /** Single capture: marker id (avoid a wrapping group around the whole comment — that shifted indices). */
19
33
  const BLOCK_MARKER_HTML_LINE = new RegExp(`^<!--\\s*commentray:block\\s+id=(${MARKER_ID_BODY})\\s*-->$`, "i");
20
34
  function trimEndSpacesTabs(s) {
21
- return s.replace(/[ \t]+$/, "");
35
+ let end = s.length;
36
+ while (end > 0) {
37
+ const c = s[end - 1];
38
+ if (c !== " " && c !== "\t")
39
+ break;
40
+ end--;
41
+ }
42
+ return s.slice(0, end);
22
43
  }
23
44
  function isSetextUnderlineLine(line) {
24
45
  const t = trimEndSpacesTabs(line);
@@ -111,6 +132,33 @@ function injectCommentrayDocAnchors(markdown, links) {
111
132
  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">' +
112
133
  '<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>' +
113
134
  "</svg>";
135
+ /** Simple home glyph for same-site hub link (matches Octocat control size). */
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">' +
137
+ '<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>' +
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>";
114
162
  function safeExternalHttpUrl(url) {
115
163
  const t = url?.trim();
116
164
  if (!t)
@@ -119,26 +167,48 @@ function safeExternalHttpUrl(url) {
119
167
  return null;
120
168
  return t;
121
169
  }
122
- function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl, commentrayRenderSemver) {
170
+ /** Allows relative static browse links (`./browse/…`) and `http(s):` URLs; rejects `javascript:` / `data:`. */
171
+ function safeToolbarNavigationHref(url) {
172
+ const t = url?.trim();
173
+ if (!t)
174
+ return null;
175
+ if (/^(javascript|data):/i.test(t))
176
+ return null;
177
+ return t;
178
+ }
179
+ function buildToolbarSiteHubHtml(siteHubUrl) {
180
+ const site = safeToolbarNavigationHref(siteHubUrl);
181
+ if (!site)
182
+ return "";
183
+ const se = escapeHtml(site);
184
+ return `<a class="toolbar-github" href="${se}" aria-label="Documentation home" title="Back to this site (hub)">${SITE_HOME_SVG}</a>`;
185
+ }
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) {
188
+ const site = safeToolbarNavigationHref(siteHubUrl);
123
189
  const gh = safeExternalHttpUrl(githubRepoUrl);
124
- const tool = safeExternalHttpUrl(toolHomeUrl);
125
- const bits = [];
126
- if (gh) {
190
+ if (!site && gh) {
127
191
  const he = escapeHtml(gh);
128
- 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>`;
129
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);
130
201
  if (tool) {
131
202
  const te = escapeHtml(tool);
132
203
  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>`);
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>`);
134
211
  }
135
- if (bits.length === 0)
136
- return "";
137
- return `<div class="toolbar__end">${bits.join("")}</div>`;
138
- }
139
- function renderPageFooterHtml(builtAt) {
140
- const iso = builtAt.toISOString();
141
- const human = formatCommentrayBuiltAtLocal(builtAt);
142
212
  return (`<footer class="app__footer" role="contentinfo">` +
143
213
  `<p class="app__footer-line">HTML generated <time datetime="${escapeHtml(iso)}">${escapeHtml(human)}</time></p>` +
144
214
  `</footer>`);
@@ -160,49 +230,61 @@ function renderToolbarDocHubHtml(opts) {
160
230
  const navAttr = escapeHtml(nav ?? "");
161
231
  const navRailDocumentedHtml = showDocumentedTree
162
232
  ? `<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>
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>
164
234
  <div class="nav-rail__doc-hub-inner">
235
+ <div class="nav-rail__doc-hub-filter-row">
236
+ <label class="nav-rail__doc-hub-filter-label" for="documented-files-filter">Filter</label>
237
+ <input type="search" id="documented-files-filter" class="nav-rail__doc-hub-filter" placeholder="Filter by path…" autocomplete="off" spellcheck="false" />
238
+ </div>
165
239
  <div id="documented-files-tree" class="documented-files-tree" role="tree"></div>
166
240
  </div>
167
241
  </details>`
168
242
  : "";
169
243
  return { toolbarDocHubHtml, navRailDocumentedHtml };
170
244
  }
171
- 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) {
172
258
  const fpRaw = (filePath ?? "").trim();
173
259
  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) {
260
+ if (fpRaw.length === 0 && crRaw.length === 0)
177
261
  return "";
178
- }
179
262
  const fp = escapeHtml(fpRaw);
180
263
  const cr = escapeHtml(crRaw);
181
264
  const fpDisp = fpRaw.length > 0 ? fp : "—";
182
265
  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>
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>
199
276
  </div>`;
200
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
+ }
201
282
  /** IIFE produced by `npm run build -w @commentray/render` (esbuild of `code-browser-client.ts`). */
202
283
  function loadCodeBrowserClientBundle() {
203
- const here = dirname(fileURLToPath(import.meta.url));
204
- const inDist = join(here, "code-browser-client.bundle.js");
205
- 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");
206
288
  for (const p of [inDist, fromSrc]) {
207
289
  if (existsSync(p)) {
208
290
  return readFileSync(p, "utf8");
@@ -210,10 +292,94 @@ function loadCodeBrowserClientBundle() {
210
292
  }
211
293
  throw new Error("Missing code-browser-client.bundle.js. Run `npm run build -w @commentray/render` to bundle the browser client.");
212
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
+ `;
213
312
  const CODE_BROWSER_STYLES = `
214
- :root { color-scheme: light dark; }
313
+ :root {
314
+ --cr-control-h: 32px;
315
+ --cr-control-radius: 8px;
316
+ --cr-icon-inner: 18px;
317
+ --cr-label-caps-fs: 10px;
318
+ --cr-label-caps-track: 0.06em;
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;
331
+ }
215
332
  * { box-sizing: border-box; }
216
- 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
+ }
340
+ .skip-link {
341
+ position: absolute;
342
+ left: -9999px;
343
+ top: 0;
344
+ z-index: 10000;
345
+ padding: 8px 16px;
346
+ margin: 0;
347
+ font: inherit;
348
+ font-size: 14px;
349
+ text-decoration: none;
350
+ border-radius: 8px;
351
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
352
+ background: Canvas;
353
+ color: CanvasText;
354
+ }
355
+ .skip-link:focus {
356
+ left: 12px;
357
+ top: 8px;
358
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
359
+ outline-offset: 2px;
360
+ }
361
+ .skip-link:focus:not(:focus-visible) {
362
+ left: -9999px;
363
+ top: 0;
364
+ outline: none;
365
+ }
366
+ .skip-link:focus-visible {
367
+ left: 12px;
368
+ top: 8px;
369
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
370
+ outline-offset: 2px;
371
+ }
372
+ .sr-only {
373
+ position: absolute;
374
+ width: 1px;
375
+ height: 1px;
376
+ padding: 0;
377
+ margin: -1px;
378
+ overflow: hidden;
379
+ clip: rect(0, 0, 0, 0);
380
+ white-space: nowrap;
381
+ border: 0;
382
+ }
217
383
  .app {
218
384
  display: flex;
219
385
  flex-direction: column;
@@ -244,148 +410,101 @@ const CODE_BROWSER_STYLES = `
244
410
  .chrome__search-row input[type="search"] {
245
411
  flex: 1 1 auto;
246
412
  min-width: 140px;
247
- padding: 8px 10px;
413
+ min-height: var(--cr-control-h);
414
+ padding: 0 12px;
248
415
  font: inherit;
249
- font-size: 14px;
250
- border-radius: 8px;
416
+ font-size: var(--cr-ui-fs);
417
+ line-height: 1.25;
418
+ border-radius: var(--cr-control-radius);
251
419
  border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
252
420
  background: Canvas;
253
421
  color: CanvasText;
254
422
  }
255
423
  .chrome__search-row #search-clear {
256
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;
257
430
  font: inherit;
258
- padding: 6px 14px;
259
- border-radius: 8px;
431
+ font-size: var(--cr-ui-fs);
432
+ font-weight: 500;
433
+ border-radius: var(--cr-control-radius);
260
434
  cursor: pointer;
261
435
  border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
262
436
  background: color-mix(in oklab, CanvasText 6%, Canvas);
263
437
  color: CanvasText;
264
- }
265
- .chrome__search-label {
266
- flex: 0 0 auto;
267
438
  white-space: nowrap;
268
439
  }
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;
440
+ .chrome__search-row #search-clear:hover {
441
+ background: color-mix(in oklab, CanvasText 11%, Canvas);
281
442
  }
282
- .nav-rail__pair {
443
+ .chrome__search-row input[type="search"]:focus-visible,
444
+ .chrome__search-row #search-clear:focus-visible {
445
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
446
+ outline-offset: 2px;
447
+ }
448
+ .chrome__search-label {
449
+ flex: 0 0 auto;
283
450
  display: inline-flex;
284
451
  flex-direction: row;
285
452
  align-items: center;
286
453
  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
454
  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;
455
+ cursor: default;
313
456
  user-select: none;
314
- padding: 0 2px;
315
457
  }
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);
458
+ /* Wide viewports: same legible caps word as historic Pages shell (icon hidden). */
459
+ .chrome__search-label__glyph {
460
+ display: none;
353
461
  }
354
462
  .nav-rail__search-label {
355
- font-size: 11px;
463
+ font-size: var(--cr-label-caps-fs);
356
464
  font-weight: 700;
357
- letter-spacing: 0.05em;
465
+ letter-spacing: var(--cr-label-caps-track);
358
466
  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
467
  opacity: 0.78;
366
468
  }
367
- .nav-rail__code {
368
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
369
- font-size: 10px;
370
- }
371
469
  .nav-rail__doc-hub {
372
470
  position: relative;
373
471
  flex: 0 0 auto;
374
472
  align-self: center;
375
473
  display: block;
376
474
  border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
377
- border-radius: 6px;
475
+ border-radius: var(--cr-control-radius);
378
476
  background: Canvas;
379
477
  overflow: visible;
380
478
  }
381
479
  .nav-rail__doc-hub-summary {
382
480
  cursor: pointer;
383
- font-size: 12px;
384
- font-weight: 600;
385
- padding: 4px 10px;
481
+ font-size: var(--cr-ui-fs);
482
+ font-weight: 500;
483
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
484
+ padding: 0 12px;
485
+ min-height: var(--cr-control-h);
486
+ display: inline-flex;
487
+ flex-direction: row;
488
+ align-items: center;
489
+ justify-content: flex-start;
490
+ gap: 8px;
491
+ box-sizing: border-box;
386
492
  list-style: none;
387
493
  user-select: none;
388
- line-height: 1.35;
494
+ line-height: 1.25;
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;
389
508
  }
390
509
  .nav-rail__doc-hub-summary::-webkit-details-marker { display: none; }
391
510
  .nav-rail__doc-hub-inner {
@@ -396,7 +515,10 @@ const CODE_BROWSER_STYLES = `
396
515
  min-width: min(280px, 78vw);
397
516
  max-width: min(440px, 94vw);
398
517
  max-height: min(52vh, 400px);
399
- overflow: auto;
518
+ display: flex;
519
+ flex-direction: column;
520
+ gap: 8px;
521
+ overflow: hidden;
400
522
  padding: 8px 10px;
401
523
  font-size: 12px;
402
524
  border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
@@ -404,6 +526,33 @@ const CODE_BROWSER_STYLES = `
404
526
  background: Canvas;
405
527
  box-shadow: 0 8px 28px color-mix(in oklab, CanvasText 12%, transparent);
406
528
  }
529
+ .nav-rail__doc-hub-filter-row {
530
+ flex: 0 0 auto;
531
+ }
532
+ .nav-rail__doc-hub-filter-label {
533
+ display: block;
534
+ font-size: var(--cr-label-caps-fs);
535
+ font-weight: 700;
536
+ letter-spacing: var(--cr-label-caps-track);
537
+ text-transform: uppercase;
538
+ opacity: 0.78;
539
+ margin-bottom: 4px;
540
+ }
541
+ .nav-rail__doc-hub-filter {
542
+ width: 100%;
543
+ box-sizing: border-box;
544
+ font: inherit;
545
+ font-size: 12px;
546
+ padding: 4px 8px;
547
+ border-radius: 6px;
548
+ border: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
549
+ background: color-mix(in oklab, CanvasText 4%, Canvas);
550
+ color: CanvasText;
551
+ }
552
+ .nav-rail__doc-hub-filter:focus {
553
+ outline: 2px solid color-mix(in oklab, CanvasText 40%, Canvas);
554
+ outline-offset: 1px;
555
+ }
407
556
  .nav-rail__doc-hub-hint {
408
557
  margin: 0 0 8px;
409
558
  opacity: 0.78;
@@ -426,41 +575,298 @@ const CODE_BROWSER_STYLES = `
426
575
  line-height: 1.4;
427
576
  color: color-mix(in oklab, CanvasText 72%, Canvas);
428
577
  }
429
- .app__footer-line { margin: 0; }
430
- .app__footer time { font-variant-numeric: tabular-nums; }
431
- .toolbar {
432
- display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; padding: 8px 12px;
433
- border-bottom: 1px solid color-mix(in oklab, CanvasText 18%, Canvas);
434
- font-size: 13px; flex: 0 0 auto;
578
+ .app__footer-line { margin: 0; }
579
+ .app__footer time { font-variant-numeric: tabular-nums; }
580
+ .toolbar {
581
+ position: relative;
582
+ display: flex;
583
+ flex-wrap: wrap;
584
+ align-items: center;
585
+ gap: 10px 14px;
586
+ padding: 8px 12px;
587
+ border-bottom: 1px solid color-mix(in oklab, CanvasText 18%, Canvas);
588
+ background: color-mix(in oklab, CanvasText 4%, Canvas);
589
+ font-size: var(--cr-ui-fs);
590
+ flex: 0 0 auto;
591
+ min-width: 0;
592
+ }
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;
608
+ flex: 0 1 auto;
609
+ min-width: 0;
610
+ }
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;
618
+ margin-left: auto;
619
+ min-width: 0;
620
+ }
621
+ .toolbar__end {
622
+ display: flex;
623
+ flex-wrap: wrap;
624
+ align-items: center;
625
+ justify-content: flex-end;
626
+ gap: 10px 14px;
627
+ min-width: 0;
628
+ }
629
+ .toolbar-github {
630
+ display: inline-flex; align-items: center; justify-content: center;
631
+ width: var(--cr-control-h);
632
+ height: var(--cr-control-h);
633
+ border-radius: var(--cr-control-radius);
634
+ border: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
635
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
636
+ color: CanvasText;
637
+ }
638
+ .toolbar-github svg {
639
+ width: var(--cr-icon-inner);
640
+ height: var(--cr-icon-inner);
641
+ display: block;
642
+ }
643
+ .toolbar-github:hover { background: color-mix(in oklab, CanvasText 11%, Canvas); }
644
+ .toolbar-github:focus-visible { outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas); outline-offset: 2px; }
645
+ .app__footer-attribution {
646
+ margin: 0;
647
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
648
+ }
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; }
656
+ .toolbar label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
657
+ .toolbar-wrap-lines {
658
+ position: relative;
659
+ margin: 0;
660
+ min-height: var(--cr-control-h);
661
+ padding: 0 12px 0 10px;
662
+ border-radius: var(--cr-control-radius);
663
+ border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
664
+ background: Canvas;
665
+ display: inline-flex;
666
+ flex-direction: row;
667
+ align-items: center;
668
+ justify-content: flex-start;
669
+ gap: 8px;
670
+ font-size: var(--cr-ui-fs);
671
+ font-weight: 500;
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;
774
+ }
775
+ .toolbar label input:focus-visible {
776
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
777
+ outline-offset: 2px;
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;
435
816
  }
436
- .toolbar__main {
437
- display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
438
- flex: 0 1 auto;
439
- min-width: 0;
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;
440
821
  }
441
- .toolbar__end {
442
- display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
443
- margin-left: auto;
444
- justify-content: flex-end;
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);
445
836
  }
446
- .toolbar-github {
447
- display: inline-flex; align-items: center; justify-content: center;
448
- width: 34px; height: 34px; border-radius: 8px;
449
- border: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
450
- background: color-mix(in oklab, CanvasText 6%, Canvas);
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;
451
852
  color: CanvasText;
853
+ background: transparent;
452
854
  }
453
- .toolbar-github:hover { background: color-mix(in oklab, CanvasText 11%, Canvas); }
454
- .toolbar-github:focus-visible { outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas); outline-offset: 2px; }
455
- .toolbar-attribution {
456
- font-size: 11px; line-height: 1.35; opacity: 0.82; max-width: min(360px, 42vw);
457
- text-align: right; color: color-mix(in oklab, CanvasText 88%, Canvas);
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;
458
865
  }
459
- .toolbar-attribution a { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
460
- .toolbar label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
461
866
  .toolbar .file-path {
462
867
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
463
- font-size: 13px; font-weight: 500;
868
+ font-size: var(--cr-ui-fs);
869
+ font-weight: 500;
464
870
  display: inline-flex; align-items: baseline; gap: 0; margin-right: 4px;
465
871
  max-width: 60vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
466
872
  }
@@ -469,21 +875,29 @@ const CODE_BROWSER_STYLES = `
469
875
  }
470
876
  .toolbar .file-path__dir--root { letter-spacing: 0; }
471
877
  .toolbar .file-path__base {
472
- color: CanvasText; font-weight: 600;
878
+ color: CanvasText;
879
+ font-weight: 500;
473
880
  }
474
- .toolbar .file-path--title { font-weight: 600; }
881
+ .toolbar .file-path--title { font-weight: 500; }
475
882
  .toolbar-related {
476
883
  display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px;
477
- max-width: min(520px, 90vw); font-size: 12px; line-height: 1.35;
884
+ max-width: min(520px, 90vw);
885
+ font-size: var(--cr-ui-fs);
886
+ line-height: 1.35;
478
887
  color: color-mix(in oklab, CanvasText 88%, Canvas);
479
888
  }
480
- .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; }
481
890
  .toolbar-related__links { min-width: 0; }
482
891
  .toolbar-related a {
483
892
  color: inherit; text-decoration: underline; text-underline-offset: 2px; font-weight: 500;
484
893
  word-break: break-word;
485
894
  }
486
895
  .toolbar-related__sep { opacity: 0.55; user-select: none; }
896
+ #documented-files-tree {
897
+ flex: 1 1 auto;
898
+ min-height: 0;
899
+ overflow: auto;
900
+ }
487
901
  .documented-files-tree ul { list-style: none; margin: 0; padding-left: 12px; }
488
902
  .documented-files-tree > ul { padding-left: 0; }
489
903
  .documented-files-tree li { margin: 2px 0; line-height: 1.35; }
@@ -504,8 +918,15 @@ const CODE_BROWSER_STYLES = `
504
918
  opacity: 0.92;
505
919
  }
506
920
  .toolbar button {
507
- font: inherit; padding: 4px 10px; border-radius: 6px; cursor: pointer;
508
- border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas); background: color-mix(in oklab, CanvasText 6%, Canvas);
921
+ font: inherit;
922
+ font-size: var(--cr-ui-fs);
923
+ font-weight: 500;
924
+ min-height: var(--cr-control-h);
925
+ padding: 0 12px;
926
+ border-radius: var(--cr-control-radius);
927
+ cursor: pointer;
928
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
929
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
509
930
  color: CanvasText;
510
931
  }
511
932
  .search-results {
@@ -542,15 +963,83 @@ const CODE_BROWSER_STYLES = `
542
963
  -webkit-box-decoration-break: clone;
543
964
  }
544
965
  @media (prefers-color-scheme: dark) {
545
- .search-results mark.search-hit {
966
+ :root:is(:not([data-commentray-theme]), [data-commentray-theme="system"]) .search-results mark.search-hit {
546
967
  background: color-mix(in oklab, #c9a227 55%, Canvas);
547
968
  }
548
969
  }
549
- .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
+ }
550
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; }
551
1040
  .pane--code {
552
- flex: 0 0 50%;
553
- 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);
554
1043
  border-right: 1px solid color-mix(in oklab, CanvasText 15%, Canvas);
555
1044
  --code-line-font-size: 13px;
556
1045
  --code-line-height: 1.5;
@@ -558,9 +1047,10 @@ const CODE_BROWSER_STYLES = `
558
1047
  .pane--code .code-line-stack { --code-ln-min-ch: 3; }
559
1048
  .pane--code .code-line {
560
1049
  display: grid;
561
- grid-template-columns: max-content 1fr;
1050
+ grid-template-columns: max-content minmax(0, 1fr);
562
1051
  column-gap: 10px;
563
1052
  align-items: start;
1053
+ min-width: 0;
564
1054
  }
565
1055
  .pane--code .code-line pre {
566
1056
  margin: 0;
@@ -600,8 +1090,35 @@ const CODE_BROWSER_STYLES = `
600
1090
  white-space: pre;
601
1091
  }
602
1092
  .gutter {
603
- flex: 0 0 8px; cursor: col-resize; background: color-mix(in oklab, CanvasText 12%, Canvas);
1093
+ flex: 0 0 14px; cursor: col-resize; background: color-mix(in oklab, CanvasText 12%, Canvas);
604
1094
  position: relative;
1095
+ --commentray-ray-accent: #3b7dd8;
1096
+ }
1097
+ @media (prefers-color-scheme: dark) {
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;
1104
+ }
1105
+ .gutter__rays {
1106
+ position: absolute; inset: 0; pointer-events: none; z-index: 1;
1107
+ }
1108
+ .gutter__rays svg { width: 100%; height: 100%; display: block; overflow: visible; }
1109
+ .gutter__rays-path {
1110
+ fill: none; stroke-linecap: round; vector-effect: non-scaling-stroke;
1111
+ stroke: color-mix(in oklab, var(--commentray-ray-accent) 72%, CanvasText);
1112
+ stroke-width: 1.35px; opacity: 0.26;
1113
+ }
1114
+ .gutter__rays-path--active {
1115
+ stroke-width: 2.4px; opacity: 0.88;
1116
+ }
1117
+ .gutter__rays-path--trail {
1118
+ stroke-dasharray: 3 4; opacity: 0.42;
1119
+ }
1120
+ .gutter__rays-path--active.gutter__rays-path--trail {
1121
+ opacity: 0.72;
605
1122
  }
606
1123
  .gutter:hover { background: color-mix(in oklab, CanvasText 22%, Canvas); }
607
1124
  .gutter::after {
@@ -609,18 +1126,359 @@ const CODE_BROWSER_STYLES = `
609
1126
  }
610
1127
  .pane--doc {
611
1128
  flex: 1 1 auto; min-width: 0; min-height: 0;
612
- 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;
613
1143
  }
614
1144
  .doc-pane-body {
615
1145
  flex: 1 1 auto; min-height: 0; overflow: auto;
616
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
+ }
617
1169
  .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);
1170
+ display: inline-flex;
1171
+ align-items: center;
1172
+ gap: 6px;
1173
+ flex: 0 0 auto;
1174
+ }
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;
620
1184
  }
621
1185
  .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;
1186
+ font: inherit;
1187
+ font-size: var(--cr-ui-fs);
1188
+ font-weight: 500;
1189
+ min-height: var(--cr-control-h);
1190
+ height: var(--cr-control-h);
1191
+ padding: 0 10px;
1192
+ border-radius: var(--cr-control-radius);
1193
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
1194
+ background: Canvas;
1195
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
1196
+ }
1197
+ .toolbar-angle-picker select:focus-visible {
1198
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
1199
+ outline-offset: 2px;
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
+ }
624
1482
  }
625
1483
  .pane--doc { font-size: 15px; line-height: 1.45; }
626
1484
  .pane--doc img { max-width: 100%; height: auto; }
@@ -637,7 +1495,6 @@ const CODE_BROWSER_STYLES = `
637
1495
  border-top: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
638
1496
  pointer-events: none;
639
1497
  }
640
- .pane h2.pane-title { margin: 0 0 10px; font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.75; }
641
1498
  .shell--stretch-rows {
642
1499
  flex: 1;
643
1500
  min-height: 0;
@@ -651,8 +1508,15 @@ const CODE_BROWSER_STYLES = `
651
1508
  border-bottom: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
652
1509
  font-size: 15px;
653
1510
  line-height: 1.45;
1511
+ overflow-x: auto;
1512
+ max-width: 100%;
654
1513
  }
655
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
+ }
656
1520
  .block-stretch {
657
1521
  width: 100%;
658
1522
  border-collapse: collapse;
@@ -673,8 +1537,30 @@ const CODE_BROWSER_STYLES = `
673
1537
  .block-stretch td.stretch-doc .stretch-doc-inner {
674
1538
  font-size: 15px;
675
1539
  line-height: 1.45;
1540
+ min-width: 0;
1541
+ overflow-x: auto;
676
1542
  }
677
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
+ }
678
1564
  .block-stretch td.stretch-doc--gap {
679
1565
  color: color-mix(in oklab, CanvasText 38%, Canvas);
680
1566
  font-size: 13px;
@@ -721,68 +1607,117 @@ const CODE_BROWSER_STYLES = `
721
1607
  .block-stretch.wrap .code-line pre code { white-space: pre-wrap; word-break: break-word; }
722
1608
  .block-stretch:not(.wrap) .code-line pre,
723
1609
  .block-stretch:not(.wrap) .code-line pre code { white-space: pre; }
724
- .block-stretch-headings {
725
- display: grid;
726
- grid-template-columns: 1fr 1fr;
727
- gap: 0 16px;
728
- padding: 4px 12px 8px;
729
- border-bottom: 1px solid color-mix(in oklab, CanvasText 10%, Canvas);
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;
730
1618
  }
731
- .block-stretch-headings .pane-title { margin: 0; }
732
1619
  `;
733
1620
  /** Native tooltip on #search-q (short hint is visible under the search row). */
734
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).";
735
1622
  function buildCodeBrowserPageHtml(p) {
736
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
+ : "";
737
1630
  return `<!doctype html>
738
- <html lang="en">
1631
+ <html lang="en" data-commentray-theme="system">
739
1632
  <head>
740
1633
  <meta charset="utf-8" />
741
1634
  <meta name="viewport" content="width=device-width, initial-scale=1" />
742
- ${p.generatorMetaHtml}<title>${escapeHtml(p.title)}</title>
743
- <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)" />
744
- <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)" />
1635
+ ${COMMENTRAY_FAVICON_LINK_HTML}
1636
+ ${p.metaDescriptionHtml}${p.generatorMetaHtml}<title>${escapeHtml(p.title)}</title>
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>
745
1642
  <style>
746
1643
  ${CODE_BROWSER_STYLES}
747
1644
  </style>
748
1645
  </head>
749
1646
  <body>
1647
+ <a class="skip-link" href="#main-content">Skip to main content</a>
750
1648
  <div class="app">
751
- <header class="app__chrome" role="region" aria-label="Search and navigation">
1649
+ <header class="toolbar" role="banner" aria-label="View options">
1650
+ <h1 class="sr-only">${escapeHtml(p.title)}</h1>
1651
+ <div class="toolbar__primary">
1652
+ <div class="toolbar__primary-main">
1653
+ ${p.toolbarSiteHubHtml}
1654
+ ${p.navRailDocumentedHtml}
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}
1663
+ ${p.toolbarDocHubHtml}
1664
+ ${p.relatedNavHtml}
1665
+ </div>
1666
+ <div class="toolbar__primary-trail">
1667
+ ${p.toolbarEndHtml}
1668
+ ${TOOLBAR_COLOR_THEME_HTML}
1669
+ </div>
1670
+ </div>
1671
+ </header>
1672
+ ${dualFlipScrollAffordanceHtml}
1673
+ <header class="app__chrome" role="region" aria-label="Search">
752
1674
  <div class="chrome__search-row">
753
- <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>
754
1676
  <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>
1677
+ <button type="button" id="search-clear" aria-label="Clear search" title="Clear search">Clear</button>
756
1678
  </div>
757
1679
  <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>
759
1680
  </header>
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}>
1681
+ <main id="main-content" class="app__main" tabindex="-1">
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}>
773
1683
  ${p.shellInner}
774
1684
  </div>
775
- </div>
1685
+ </main>
776
1686
  ${p.pageFooterHtml}
777
1687
  </div>
778
1688
  <script type="text/plain" id="commentray-multi-angle-b64">${p.multiAngleScriptBlock}</script>
1689
+ ${p.mermaidScript}
779
1690
  <script>
780
1691
  ${loadCodeBrowserClientBundle()}
781
1692
  </script>
782
- ${p.mermaidScript}
783
1693
  </body>
784
1694
  </html>`;
785
1695
  }
1696
+ async function multiAngleJsonRowAndDocHtml(opts, spec) {
1697
+ const rows = spec.blockStretchRows;
1698
+ const links = rows !== undefined
1699
+ ? buildBlockScrollLinks(rows.index, rows.sourceRelative, rows.commentrayPathRel, spec.markdown, opts.code)
1700
+ : [];
1701
+ const mdForDoc = injectCommentrayDocAnchors(spec.markdown, links.length > 0 ? links : undefined);
1702
+ const scrollB64 = links.length > 0 ? Buffer.from(JSON.stringify(links), "utf8").toString("base64") : "";
1703
+ const commentrayHtml = await renderMarkdownToHtml(mdForDoc, {
1704
+ commentrayOutputUrls: opts.commentrayOutputUrls,
1705
+ });
1706
+ return {
1707
+ jsonRow: {
1708
+ id: spec.id,
1709
+ title: spec.title?.trim() || spec.id,
1710
+ docInnerHtmlB64: Buffer.from(commentrayHtml, "utf8").toString("base64"),
1711
+ rawMdB64: Buffer.from(spec.markdown, "utf8").toString("base64"),
1712
+ scrollBlockLinksB64: scrollB64,
1713
+ commentrayPathForSearch: spec.commentrayPathRel.trim(),
1714
+ commentrayOnGithubUrl: spec.commentrayOnGithubUrl,
1715
+ staticBrowseUrl: spec.staticBrowseUrl,
1716
+ },
1717
+ commentrayHtml,
1718
+ scrollB64,
1719
+ };
1720
+ }
786
1721
  async function buildMultiAngleDualPaneShell(opts, multi) {
787
1722
  const defaultId = multi.angles.some((a) => a.id === multi.defaultAngleId)
788
1723
  ? multi.defaultAngleId
@@ -792,34 +1727,24 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
792
1727
  let defaultScrollB64 = "";
793
1728
  let defaultPathSearch = (opts.commentrayPathForSearch ?? "").trim();
794
1729
  let defaultGh = opts.commentrayOnGithubUrl;
1730
+ let defaultStaticBrowse = (opts.commentrayStaticBrowseUrl ?? "").trim();
795
1731
  let defaultPaneHtml = "";
796
1732
  const codeHtml = await renderHighlightedCodeLineRows(opts.code, opts.language);
797
1733
  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
- });
1734
+ const { jsonRow, commentrayHtml, scrollB64 } = await multiAngleJsonRowAndDocHtml(opts, spec);
807
1735
  if (spec.id === defaultId) {
808
1736
  defaultMarkdown = spec.markdown;
809
1737
  defaultScrollB64 = scrollB64;
810
1738
  defaultPathSearch = spec.commentrayPathRel.trim();
811
1739
  defaultGh = spec.commentrayOnGithubUrl;
1740
+ {
1741
+ const sb = (spec.staticBrowseUrl ?? "").trim();
1742
+ if (sb.length > 0)
1743
+ defaultStaticBrowse = sb;
1744
+ }
812
1745
  defaultPaneHtml = commentrayHtml;
813
1746
  }
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
- });
1747
+ jsonAngles.push(jsonRow);
823
1748
  }
824
1749
  const selOpts = multi.angles
825
1750
  .map((a) => {
@@ -827,18 +1752,9 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
827
1752
  return `<option value="${escapeHtml(a.id)}"${a.id === defaultId ? " selected" : ""}>${lab}</option>`;
828
1753
  })
829
1754
  .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`;
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));
842
1758
  const payloadObj = { defaultAngleId: defaultId, angles: jsonAngles };
843
1759
  const multiAnglePayloadB64 = Buffer.from(JSON.stringify(payloadObj), "utf8").toString("base64");
844
1760
  return {
@@ -848,6 +1764,7 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
848
1764
  scrollBlockLinksB64: defaultScrollB64,
849
1765
  commentrayPathForSearch: defaultPathSearch,
850
1766
  commentrayOnGithubUrl: defaultGh,
1767
+ ...(defaultStaticBrowse.length > 0 ? { commentrayStaticBrowseUrl: defaultStaticBrowse } : {}),
851
1768
  },
852
1769
  angleSelectHtml,
853
1770
  multiAnglePayloadB64,
@@ -883,13 +1800,7 @@ async function buildCodeBrowserShell(opts, layoutPref) {
883
1800
  });
884
1801
  if (stretched) {
885
1802
  layout = "stretch";
886
- shellInner =
887
- ` <div class="block-stretch-headings">` +
888
- `<h2 class="pane-title">Code</h2>` +
889
- `<h2 class="pane-title">Commentray</h2>` +
890
- `</div>\n` +
891
- ` ${stretched.preambleHtml}\n` +
892
- ` ${stretched.tableInnerHtml}\n`;
1803
+ shellInner = ` ${stretched.preambleHtml}\n` + ` ${stretched.tableInnerHtml}\n`;
893
1804
  }
894
1805
  }
895
1806
  if (layout === "dual") {
@@ -906,18 +1817,8 @@ async function buildCodeBrowserShell(opts, layoutPref) {
906
1817
  commentrayOutputUrls: opts.commentrayOutputUrls,
907
1818
  }),
908
1819
  ]);
909
- shellInner =
910
- ` <section class="pane--code" id="code-pane" aria-label="Source code">` +
911
- `<h2 class="pane-title">Code</h2>\n` +
912
- ` ${codeHtml}\n` +
913
- ` </section>\n` +
914
- ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
915
- ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
916
- ` <h2 class="pane-title">Commentray</h2>\n` +
917
- ` <div id="doc-pane-body" class="doc-pane-body">\n` +
918
- ` ${commentrayHtml}\n` +
919
- ` </div>\n` +
920
- ` </section>\n`;
1820
+ const pairHtml = renderShellPairContextHtml(opts.filePath, (opts.commentrayPathForSearch ?? "").trim());
1821
+ shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, commentrayHtml));
921
1822
  }
922
1823
  return {
923
1824
  layout,
@@ -949,22 +1850,30 @@ function shellDocumentedPairsAttrFromOptions(opts) {
949
1850
  function codeBrowserPageTitle(opts) {
950
1851
  return opts.title ?? opts.filePath ?? "Commentray";
951
1852
  }
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
1853
  function toolbarCommentrayGithubFromShell(shell, opts) {
958
1854
  return shell.multiShell?.commentrayOnGithubUrl ?? opts.commentrayOnGithubUrl;
959
1855
  }
960
1856
  function rawMdB64FromShell(shell, opts) {
961
1857
  return (shell.multiShell?.rawMdB64 ?? Buffer.from(opts.commentrayMarkdown, "utf8").toString("base64"));
962
1858
  }
963
- function navRailCommentrayPathFromShell(shell, opts) {
964
- const trimmed = (shell.multiShell?.commentrayPathForSearch ??
965
- 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 ??
966
1865
  "").trim();
967
- 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 "";
968
1877
  }
969
1878
  function shellSearchAttrsWithNavJson(shellSearchAttrsBase, documentedNavJsonUrl) {
970
1879
  const navJson = documentedNavJsonUrl?.trim() ?? "";
@@ -980,11 +1889,17 @@ function shellSearchAttrsWithNavJson(shellSearchAttrsBase, documentedNavJsonUrl)
980
1889
  export async function renderCodeBrowserHtml(opts) {
981
1890
  const rawCodeB64 = Buffer.from(opts.code, "utf8").toString("base64");
982
1891
  const title = codeBrowserPageTitle(opts);
1892
+ const metaDescriptionHtml = renderMetaDescriptionHtml(opts, title);
983
1893
  const builtAt = opts.builtAt ?? new Date();
984
1894
  const renderSemver = commentrayRenderVersion();
985
- const toolbarEndHtml = buildToolbarEndHtml(opts.githubRepoUrl, opts.toolHomeUrl, renderSemver);
986
- const pageFooterHtml = renderPageFooterHtml(builtAt);
987
- const { hljs, hljsDark } = codeBrowserHljsThemes(opts);
1895
+ const toolbarSiteHubHtml = buildToolbarSiteHubHtml(opts.siteHubUrl);
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);
988
1903
  const mermaidScript = mermaidRuntimeScriptHtml(opts.includeMermaidRuntime);
989
1904
  const relatedNavHtml = renderRelatedGithubNavHtml(opts.relatedGithubNav ?? []);
990
1905
  const generatorMetaHtml = renderGeneratorMetaHtml(opts.generatorLabel);
@@ -999,14 +1914,13 @@ export async function renderCodeBrowserHtml(opts) {
999
1914
  const { searchPlaceholder, shellSearchAttrs: shellSearchAttrsBase } = searchChromeFromOptions(opts, shell.multiShell?.commentrayPathForSearch);
1000
1915
  const shellDocumentedPairsAttr = shellDocumentedPairsAttrFromOptions(opts);
1001
1916
  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
- });
1917
+ const pairDocDataAttr = shellPairDocDataAttr(shell, opts);
1006
1918
  return buildCodeBrowserPageHtml({
1007
1919
  title,
1920
+ metaDescriptionHtml,
1008
1921
  generatorMetaHtml,
1009
- navRailContextHtml,
1922
+ toolbarSiteHubHtml,
1923
+ shellPairDocDataAttr: pairDocDataAttr,
1010
1924
  angleSelectHtml: shell.angleSelectHtml,
1011
1925
  toolbarDocHubHtml,
1012
1926
  navRailDocumentedHtml,
@@ -1019,7 +1933,7 @@ export async function renderCodeBrowserHtml(opts) {
1019
1933
  rawMdB64,
1020
1934
  scrollBlockLinksB64,
1021
1935
  shellDocumentedPairsAttr,
1022
- hljs,
1936
+ hljs: hljsLight,
1023
1937
  hljsDark,
1024
1938
  mermaidScript,
1025
1939
  searchPlaceholder,