@commentray/render 0.0.5 → 0.0.6

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 (38) 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 +388 -70
  15. package/dist/code-browser-client.js.map +1 -1
  16. package/dist/code-browser-pair-nav.d.ts +23 -0
  17. package/dist/code-browser-pair-nav.d.ts.map +1 -0
  18. package/dist/code-browser-pair-nav.js +59 -0
  19. package/dist/code-browser-pair-nav.js.map +1 -0
  20. package/dist/code-browser-search.d.ts +45 -0
  21. package/dist/code-browser-search.d.ts.map +1 -1
  22. package/dist/code-browser-search.js +89 -0
  23. package/dist/code-browser-search.js.map +1 -1
  24. package/dist/code-browser.d.ts +22 -1
  25. package/dist/code-browser.d.ts.map +1 -1
  26. package/dist/code-browser.js +342 -103
  27. package/dist/code-browser.js.map +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/markdown-pipeline.d.ts.map +1 -1
  33. package/dist/markdown-pipeline.js +2 -1
  34. package/dist/markdown-pipeline.js.map +1 -1
  35. package/dist/mermaid-runtime-html.d.ts.map +1 -1
  36. package/dist/mermaid-runtime-html.js +3 -1
  37. package/dist/mermaid-runtime-html.js.map +1 -1
  38. package/package.json +2 -2
@@ -15,10 +15,29 @@ function renderGeneratorMetaHtml(label) {
15
15
  return "";
16
16
  return `<meta name="generator" content="${escapeHtml(t)}" />\n `;
17
17
  }
18
+ const META_DESCRIPTION_MAX_LEN = 320;
19
+ function codeBrowserMetaDescription(opts, title) {
20
+ const custom = opts.metaDescription?.trim();
21
+ if (custom)
22
+ return custom.slice(0, META_DESCRIPTION_MAX_LEN);
23
+ const fallback = `${title} — Side-by-side source and commentray documentation.`;
24
+ return fallback.slice(0, META_DESCRIPTION_MAX_LEN);
25
+ }
26
+ function renderMetaDescriptionHtml(opts, title) {
27
+ const content = codeBrowserMetaDescription(opts, title);
28
+ return `<meta name="description" content="${escapeHtml(content)}" />\n `;
29
+ }
18
30
  /** Single capture: marker id (avoid a wrapping group around the whole comment — that shifted indices). */
19
31
  const BLOCK_MARKER_HTML_LINE = new RegExp(`^<!--\\s*commentray:block\\s+id=(${MARKER_ID_BODY})\\s*-->$`, "i");
20
32
  function trimEndSpacesTabs(s) {
21
- return s.replace(/[ \t]+$/, "");
33
+ let end = s.length;
34
+ while (end > 0) {
35
+ const c = s[end - 1];
36
+ if (c !== " " && c !== "\t")
37
+ break;
38
+ end--;
39
+ }
40
+ return s.slice(0, end);
22
41
  }
23
42
  function isSetextUnderlineLine(line) {
24
43
  const t = trimEndSpacesTabs(line);
@@ -111,6 +130,10 @@ function injectCommentrayDocAnchors(markdown, links) {
111
130
  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
131
  '<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
132
  "</svg>";
133
+ /** Simple home glyph for same-site hub link (matches Octocat control size). */
134
+ 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
+ '<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>' +
136
+ "</svg>";
114
137
  function safeExternalHttpUrl(url) {
115
138
  const t = url?.trim();
116
139
  if (!t)
@@ -119,11 +142,28 @@ function safeExternalHttpUrl(url) {
119
142
  return null;
120
143
  return t;
121
144
  }
122
- function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl, commentrayRenderSemver) {
145
+ /** Allows relative static browse links (`./browse/…`) and `http(s):` URLs; rejects `javascript:` / `data:`. */
146
+ function safeToolbarNavigationHref(url) {
147
+ const t = url?.trim();
148
+ if (!t)
149
+ return null;
150
+ if (/^(javascript|data):/i.test(t))
151
+ return null;
152
+ return t;
153
+ }
154
+ function buildToolbarSiteHubHtml(siteHubUrl) {
155
+ const site = safeToolbarNavigationHref(siteHubUrl);
156
+ if (!site)
157
+ return "";
158
+ const se = escapeHtml(site);
159
+ return `<a class="toolbar-github" href="${se}" aria-label="Documentation home" title="Back to this site (hub)">${SITE_HOME_SVG}</a>`;
160
+ }
161
+ function buildToolbarEndHtml(githubRepoUrl, toolHomeUrl, commentrayRenderSemver, siteHubUrl) {
162
+ const site = safeToolbarNavigationHref(siteHubUrl);
123
163
  const gh = safeExternalHttpUrl(githubRepoUrl);
124
164
  const tool = safeExternalHttpUrl(toolHomeUrl);
125
165
  const bits = [];
126
- if (gh) {
166
+ if (!site && gh) {
127
167
  const he = escapeHtml(gh);
128
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>`);
129
169
  }
@@ -160,8 +200,12 @@ function renderToolbarDocHubHtml(opts) {
160
200
  const navAttr = escapeHtml(nav ?? "");
161
201
  const navRailDocumentedHtml = showDocumentedTree
162
202
  ? `<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>
203
+ <summary class="nav-rail__doc-hub-summary">Comment-rayed files</summary>
164
204
  <div class="nav-rail__doc-hub-inner">
205
+ <div class="nav-rail__doc-hub-filter-row">
206
+ <label class="nav-rail__doc-hub-filter-label" for="documented-files-filter">Filter</label>
207
+ <input type="search" id="documented-files-filter" class="nav-rail__doc-hub-filter" placeholder="Filter by path…" autocomplete="off" spellcheck="false" />
208
+ </div>
165
209
  <div id="documented-files-tree" class="documented-files-tree" role="tree"></div>
166
210
  </div>
167
211
  </details>`
@@ -173,19 +217,22 @@ function renderNavRailContextHtml(filePath, commentrayPath, opts) {
173
217
  const crRaw = (commentrayPath ?? "").trim();
174
218
  const srcUrl = safeExternalHttpUrl(opts?.sourceOnGithubUrl);
175
219
  const crUrl = safeExternalHttpUrl(opts?.commentrayOnGithubUrl);
176
- if (fpRaw.length === 0 && crRaw.length === 0 && srcUrl === null && crUrl === null) {
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 === "") {
177
230
  return "";
178
231
  }
179
232
  const fp = escapeHtml(fpRaw);
180
233
  const cr = escapeHtml(crRaw);
181
234
  const fpDisp = fpRaw.length > 0 ? fp : "—";
182
235
  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
236
  return `<div class="nav-rail__context nav-rail__context--compact" aria-label="Current documentation pair">
190
237
  <span class="nav-rail__pair">
191
238
  <span class="nav-rail__pair-lab">Src</span>
@@ -194,7 +241,7 @@ function renderNavRailContextHtml(filePath, commentrayPath, opts) {
194
241
  <span class="nav-rail__pair-sep" aria-hidden="true">·</span>
195
242
  <span class="nav-rail__pair">
196
243
  <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}
244
+ <span class="nav-rail__pair-path nav-rail__pair-path--secondary" id="nav-rail-doc-path" title="${cr}">${crDisp}</span>${crGh}
198
245
  </span>
199
246
  </div>`;
200
247
  }
@@ -211,9 +258,60 @@ function loadCodeBrowserClientBundle() {
211
258
  throw new Error("Missing code-browser-client.bundle.js. Run `npm run build -w @commentray/render` to bundle the browser client.");
212
259
  }
213
260
  const CODE_BROWSER_STYLES = `
214
- :root { color-scheme: light dark; }
261
+ :root {
262
+ color-scheme: light dark;
263
+ --cr-control-h: 32px;
264
+ --cr-control-radius: 8px;
265
+ --cr-icon-inner: 18px;
266
+ --cr-label-caps-fs: 10px;
267
+ --cr-label-caps-track: 0.06em;
268
+ --cr-ui-fs: 12px;
269
+ }
215
270
  * { box-sizing: border-box; }
216
271
  body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
272
+ .skip-link {
273
+ position: absolute;
274
+ left: -9999px;
275
+ top: 0;
276
+ z-index: 10000;
277
+ padding: 8px 16px;
278
+ margin: 0;
279
+ font: inherit;
280
+ font-size: 14px;
281
+ text-decoration: none;
282
+ border-radius: 8px;
283
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
284
+ background: Canvas;
285
+ color: CanvasText;
286
+ }
287
+ .skip-link:focus {
288
+ left: 12px;
289
+ top: 8px;
290
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
291
+ outline-offset: 2px;
292
+ }
293
+ .skip-link:focus:not(:focus-visible) {
294
+ left: -9999px;
295
+ top: 0;
296
+ outline: none;
297
+ }
298
+ .skip-link:focus-visible {
299
+ left: 12px;
300
+ top: 8px;
301
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
302
+ outline-offset: 2px;
303
+ }
304
+ .sr-only {
305
+ position: absolute;
306
+ width: 1px;
307
+ height: 1px;
308
+ padding: 0;
309
+ margin: -1px;
310
+ overflow: hidden;
311
+ clip: rect(0, 0, 0, 0);
312
+ white-space: nowrap;
313
+ border: 0;
314
+ }
217
315
  .app {
218
316
  display: flex;
219
317
  flex-direction: column;
@@ -244,10 +342,12 @@ const CODE_BROWSER_STYLES = `
244
342
  .chrome__search-row input[type="search"] {
245
343
  flex: 1 1 auto;
246
344
  min-width: 140px;
247
- padding: 8px 10px;
345
+ min-height: var(--cr-control-h);
346
+ padding: 0 12px;
248
347
  font: inherit;
249
- font-size: 14px;
250
- border-radius: 8px;
348
+ font-size: var(--cr-ui-fs);
349
+ line-height: 1.25;
350
+ border-radius: var(--cr-control-radius);
251
351
  border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
252
352
  background: Canvas;
253
353
  color: CanvasText;
@@ -255,13 +355,21 @@ const CODE_BROWSER_STYLES = `
255
355
  .chrome__search-row #search-clear {
256
356
  flex: 0 0 auto;
257
357
  font: inherit;
258
- padding: 6px 14px;
259
- border-radius: 8px;
358
+ font-size: var(--cr-ui-fs);
359
+ font-weight: 500;
360
+ min-height: var(--cr-control-h);
361
+ padding: 0 16px;
362
+ border-radius: var(--cr-control-radius);
260
363
  cursor: pointer;
261
364
  border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
262
365
  background: color-mix(in oklab, CanvasText 6%, Canvas);
263
366
  color: CanvasText;
264
367
  }
368
+ .chrome__search-row input[type="search"]:focus-visible,
369
+ .chrome__search-row #search-clear:focus-visible {
370
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
371
+ outline-offset: 2px;
372
+ }
265
373
  .chrome__search-label {
266
374
  flex: 0 0 auto;
267
375
  white-space: nowrap;
@@ -273,10 +381,10 @@ const CODE_BROWSER_STYLES = `
273
381
  align-items: center;
274
382
  gap: 6px 10px;
275
383
  padding: 5px 10px;
276
- border-radius: 8px;
384
+ border-radius: var(--cr-control-radius);
277
385
  border: 1px solid color-mix(in oklab, CanvasText 14%, Canvas);
278
386
  background: Canvas;
279
- font-size: 12px;
387
+ font-size: var(--cr-ui-fs);
280
388
  line-height: 1.3;
281
389
  }
282
390
  .nav-rail__pair {
@@ -290,9 +398,9 @@ const CODE_BROWSER_STYLES = `
290
398
  }
291
399
  .nav-rail__pair-lab {
292
400
  flex: 0 0 auto;
293
- font-size: 9px;
401
+ font-size: var(--cr-label-caps-fs);
294
402
  font-weight: 700;
295
- letter-spacing: 0.06em;
403
+ letter-spacing: var(--cr-label-caps-track);
296
404
  text-transform: uppercase;
297
405
  opacity: 0.72;
298
406
  }
@@ -300,7 +408,7 @@ const CODE_BROWSER_STYLES = `
300
408
  flex: 1 1 auto;
301
409
  min-width: 0;
302
410
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
303
- font-size: 11px;
411
+ font-size: var(--cr-ui-fs);
304
412
  color: CanvasText;
305
413
  overflow: hidden;
306
414
  text-overflow: ellipsis;
@@ -318,9 +426,9 @@ const CODE_BROWSER_STYLES = `
318
426
  display: inline-flex;
319
427
  align-items: center;
320
428
  justify-content: center;
321
- width: 26px;
322
- height: 26px;
323
- border-radius: 6px;
429
+ width: var(--cr-control-h);
430
+ height: var(--cr-control-h);
431
+ border-radius: var(--cr-control-radius);
324
432
  border: 1px solid color-mix(in oklab, CanvasText 20%, Canvas);
325
433
  background: color-mix(in oklab, CanvasText 5%, Canvas);
326
434
  color: CanvasText;
@@ -333,8 +441,8 @@ const CODE_BROWSER_STYLES = `
333
441
  outline-offset: 2px;
334
442
  }
335
443
  .nav-rail__pair-gh svg {
336
- width: 14px;
337
- height: 14px;
444
+ width: var(--cr-icon-inner);
445
+ height: var(--cr-icon-inner);
338
446
  display: block;
339
447
  }
340
448
  .toolbar .nav-rail__context--compact {
@@ -352,11 +460,11 @@ const CODE_BROWSER_STYLES = `
352
460
  max-width: min(44vw, 420px);
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;
467
+ opacity: 0.78;
360
468
  }
361
469
  .nav-rail__search-hint {
362
470
  margin: 0;
@@ -374,18 +482,22 @@ const CODE_BROWSER_STYLES = `
374
482
  align-self: center;
375
483
  display: block;
376
484
  border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
377
- border-radius: 6px;
485
+ border-radius: var(--cr-control-radius);
378
486
  background: Canvas;
379
487
  overflow: visible;
380
488
  }
381
489
  .nav-rail__doc-hub-summary {
382
490
  cursor: pointer;
383
- font-size: 12px;
491
+ font-size: var(--cr-ui-fs);
384
492
  font-weight: 600;
385
- padding: 4px 10px;
493
+ padding: 0 12px;
494
+ min-height: var(--cr-control-h);
495
+ display: inline-flex;
496
+ align-items: center;
497
+ box-sizing: border-box;
386
498
  list-style: none;
387
499
  user-select: none;
388
- line-height: 1.35;
500
+ line-height: 1.25;
389
501
  }
390
502
  .nav-rail__doc-hub-summary::-webkit-details-marker { display: none; }
391
503
  .nav-rail__doc-hub-inner {
@@ -396,7 +508,10 @@ const CODE_BROWSER_STYLES = `
396
508
  min-width: min(280px, 78vw);
397
509
  max-width: min(440px, 94vw);
398
510
  max-height: min(52vh, 400px);
399
- overflow: auto;
511
+ display: flex;
512
+ flex-direction: column;
513
+ gap: 8px;
514
+ overflow: hidden;
400
515
  padding: 8px 10px;
401
516
  font-size: 12px;
402
517
  border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
@@ -404,6 +519,33 @@ const CODE_BROWSER_STYLES = `
404
519
  background: Canvas;
405
520
  box-shadow: 0 8px 28px color-mix(in oklab, CanvasText 12%, transparent);
406
521
  }
522
+ .nav-rail__doc-hub-filter-row {
523
+ flex: 0 0 auto;
524
+ }
525
+ .nav-rail__doc-hub-filter-label {
526
+ display: block;
527
+ font-size: var(--cr-label-caps-fs);
528
+ font-weight: 700;
529
+ letter-spacing: var(--cr-label-caps-track);
530
+ text-transform: uppercase;
531
+ opacity: 0.78;
532
+ margin-bottom: 4px;
533
+ }
534
+ .nav-rail__doc-hub-filter {
535
+ width: 100%;
536
+ box-sizing: border-box;
537
+ font: inherit;
538
+ font-size: 12px;
539
+ padding: 4px 8px;
540
+ border-radius: 6px;
541
+ border: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
542
+ background: color-mix(in oklab, CanvasText 4%, Canvas);
543
+ color: CanvasText;
544
+ }
545
+ .nav-rail__doc-hub-filter:focus {
546
+ outline: 2px solid color-mix(in oklab, CanvasText 40%, Canvas);
547
+ outline-offset: 1px;
548
+ }
407
549
  .nav-rail__doc-hub-hint {
408
550
  margin: 0 0 8px;
409
551
  opacity: 0.78;
@@ -429,9 +571,11 @@ const CODE_BROWSER_STYLES = `
429
571
  .app__footer-line { margin: 0; }
430
572
  .app__footer time { font-variant-numeric: tabular-nums; }
431
573
  .toolbar {
574
+ position: relative;
432
575
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; padding: 8px 12px;
433
576
  border-bottom: 1px solid color-mix(in oklab, CanvasText 18%, Canvas);
434
- font-size: 13px; flex: 0 0 auto;
577
+ font-size: var(--cr-ui-fs);
578
+ flex: 0 0 auto;
435
579
  }
436
580
  .toolbar__main {
437
581
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
@@ -445,22 +589,49 @@ const CODE_BROWSER_STYLES = `
445
589
  }
446
590
  .toolbar-github {
447
591
  display: inline-flex; align-items: center; justify-content: center;
448
- width: 34px; height: 34px; border-radius: 8px;
592
+ width: var(--cr-control-h);
593
+ height: var(--cr-control-h);
594
+ border-radius: var(--cr-control-radius);
449
595
  border: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
450
596
  background: color-mix(in oklab, CanvasText 6%, Canvas);
451
597
  color: CanvasText;
452
598
  }
599
+ .toolbar-github svg {
600
+ width: var(--cr-icon-inner);
601
+ height: var(--cr-icon-inner);
602
+ display: block;
603
+ }
453
604
  .toolbar-github:hover { background: color-mix(in oklab, CanvasText 11%, Canvas); }
454
605
  .toolbar-github:focus-visible { outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas); outline-offset: 2px; }
455
606
  .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);
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;
612
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
458
613
  }
459
614
  .toolbar-attribution a { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
460
615
  .toolbar label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
616
+ .toolbar__main > label:has(#wrap-lines) {
617
+ margin: 0;
618
+ min-height: var(--cr-control-h);
619
+ padding: 0 12px 0 10px;
620
+ border-radius: var(--cr-control-radius);
621
+ border: 1px solid color-mix(in oklab, CanvasText 16%, Canvas);
622
+ background: color-mix(in oklab, CanvasText 4%, Canvas);
623
+ font-size: var(--cr-ui-fs);
624
+ font-weight: 500;
625
+ gap: 8px;
626
+ }
627
+ .toolbar label input:focus-visible {
628
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
629
+ outline-offset: 2px;
630
+ }
461
631
  .toolbar .file-path {
462
632
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
463
- font-size: 13px; font-weight: 500;
633
+ font-size: var(--cr-ui-fs);
634
+ font-weight: 500;
464
635
  display: inline-flex; align-items: baseline; gap: 0; margin-right: 4px;
465
636
  max-width: 60vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
466
637
  }
@@ -474,7 +645,9 @@ const CODE_BROWSER_STYLES = `
474
645
  .toolbar .file-path--title { font-weight: 600; }
475
646
  .toolbar-related {
476
647
  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;
648
+ max-width: min(520px, 90vw);
649
+ font-size: var(--cr-ui-fs);
650
+ line-height: 1.35;
478
651
  color: color-mix(in oklab, CanvasText 88%, Canvas);
479
652
  }
480
653
  .toolbar-related__prefix { font-weight: 600; opacity: 0.85; white-space: nowrap; }
@@ -484,6 +657,11 @@ const CODE_BROWSER_STYLES = `
484
657
  word-break: break-word;
485
658
  }
486
659
  .toolbar-related__sep { opacity: 0.55; user-select: none; }
660
+ #documented-files-tree {
661
+ flex: 1 1 auto;
662
+ min-height: 0;
663
+ overflow: auto;
664
+ }
487
665
  .documented-files-tree ul { list-style: none; margin: 0; padding-left: 12px; }
488
666
  .documented-files-tree > ul { padding-left: 0; }
489
667
  .documented-files-tree li { margin: 2px 0; line-height: 1.35; }
@@ -504,8 +682,15 @@ const CODE_BROWSER_STYLES = `
504
682
  opacity: 0.92;
505
683
  }
506
684
  .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);
685
+ font: inherit;
686
+ font-size: var(--cr-ui-fs);
687
+ font-weight: 500;
688
+ min-height: var(--cr-control-h);
689
+ padding: 0 12px;
690
+ border-radius: var(--cr-control-radius);
691
+ cursor: pointer;
692
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
693
+ background: color-mix(in oklab, CanvasText 6%, Canvas);
509
694
  color: CanvasText;
510
695
  }
511
696
  .search-results {
@@ -600,8 +785,30 @@ const CODE_BROWSER_STYLES = `
600
785
  white-space: pre;
601
786
  }
602
787
  .gutter {
603
- flex: 0 0 8px; cursor: col-resize; background: color-mix(in oklab, CanvasText 12%, Canvas);
788
+ flex: 0 0 14px; cursor: col-resize; background: color-mix(in oklab, CanvasText 12%, Canvas);
604
789
  position: relative;
790
+ --commentray-ray-accent: #3b7dd8;
791
+ }
792
+ @media (prefers-color-scheme: dark) {
793
+ .gutter { --commentray-ray-accent: #6eb0ff; }
794
+ }
795
+ .gutter__rays {
796
+ position: absolute; inset: 0; pointer-events: none; z-index: 1;
797
+ }
798
+ .gutter__rays svg { width: 100%; height: 100%; display: block; overflow: visible; }
799
+ .gutter__rays-path {
800
+ fill: none; stroke-linecap: round; vector-effect: non-scaling-stroke;
801
+ stroke: color-mix(in oklab, var(--commentray-ray-accent) 72%, CanvasText);
802
+ stroke-width: 1.35px; opacity: 0.26;
803
+ }
804
+ .gutter__rays-path--active {
805
+ stroke-width: 2.4px; opacity: 0.88;
806
+ }
807
+ .gutter__rays-path--trail {
808
+ stroke-dasharray: 3 4; opacity: 0.42;
809
+ }
810
+ .gutter__rays-path--active.gutter__rays-path--trail {
811
+ opacity: 0.72;
605
812
  }
606
813
  .gutter:hover { background: color-mix(in oklab, CanvasText 22%, Canvas); }
607
814
  .gutter::after {
@@ -615,12 +822,33 @@ const CODE_BROWSER_STYLES = `
615
822
  flex: 1 1 auto; min-height: 0; overflow: auto;
616
823
  }
617
824
  .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);
825
+ display: inline-flex;
826
+ align-items: center;
827
+ gap: 8px;
828
+ flex: 0 0 auto;
829
+ color: color-mix(in oklab, CanvasText 88%, Canvas);
830
+ }
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;
620
837
  }
621
838
  .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;
839
+ font: inherit;
840
+ font-size: var(--cr-ui-fs);
841
+ min-height: var(--cr-control-h);
842
+ height: var(--cr-control-h);
843
+ padding: 0 10px;
844
+ border-radius: var(--cr-control-radius);
845
+ border: 1px solid color-mix(in oklab, CanvasText 25%, Canvas);
846
+ background: Canvas;
847
+ color: CanvasText;
848
+ }
849
+ .toolbar-angle-picker select:focus-visible {
850
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
851
+ outline-offset: 2px;
624
852
  }
625
853
  .pane--doc { font-size: 15px; line-height: 1.45; }
626
854
  .pane--doc img { max-width: 100%; height: auto; }
@@ -637,7 +865,6 @@ const CODE_BROWSER_STYLES = `
637
865
  border-top: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
638
866
  pointer-events: none;
639
867
  }
640
- .pane h2.pane-title { margin: 0 0 10px; font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.75; }
641
868
  .shell--stretch-rows {
642
869
  flex: 1;
643
870
  min-height: 0;
@@ -721,14 +948,6 @@ const CODE_BROWSER_STYLES = `
721
948
  .block-stretch.wrap .code-line pre code { white-space: pre-wrap; word-break: break-word; }
722
949
  .block-stretch:not(.wrap) .code-line pre,
723
950
  .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);
730
- }
731
- .block-stretch-headings .pane-title { margin: 0; }
732
951
  `;
733
952
  /** Native tooltip on #search-q (short hint is visible under the search row). */
734
953
  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).";
@@ -739,7 +958,7 @@ function buildCodeBrowserPageHtml(p) {
739
958
  <head>
740
959
  <meta charset="utf-8" />
741
960
  <meta name="viewport" content="width=device-width, initial-scale=1" />
742
- ${p.generatorMetaHtml}<title>${escapeHtml(p.title)}</title>
961
+ ${p.metaDescriptionHtml}${p.generatorMetaHtml}<title>${escapeHtml(p.title)}</title>
743
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)" />
744
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)" />
745
964
  <style>
@@ -747,8 +966,22 @@ ${CODE_BROWSER_STYLES}
747
966
  </style>
748
967
  </head>
749
968
  <body>
969
+ <a class="skip-link" href="#main-content">Skip to main content</a>
750
970
  <div class="app">
751
- <header class="app__chrome" role="region" aria-label="Search and navigation">
971
+ <header class="toolbar" role="banner" aria-label="View options">
972
+ <h1 class="sr-only">${escapeHtml(p.title)}</h1>
973
+ <div class="toolbar__main">
974
+ ${p.toolbarSiteHubHtml}
975
+ ${p.navRailContextHtml}
976
+ ${p.navRailDocumentedHtml}
977
+ ${p.angleSelectHtml}
978
+ ${p.toolbarDocHubHtml}
979
+ ${p.relatedNavHtml}
980
+ <label><input type="checkbox" id="wrap-lines" /> Wrap code lines</label>
981
+ </div>
982
+ ${p.toolbarEndHtml}
983
+ </header>
984
+ <header class="app__chrome" role="region" aria-label="Search">
752
985
  <div class="chrome__search-row">
753
986
  <label class="chrome__search-label nav-rail__search-label" for="search-q">Search</label>
754
987
  <input type="search" id="search-q" placeholder="${escapeHtml(p.searchPlaceholder)}" title="${escapeHtml(CODE_BROWSER_SEARCH_INPUT_TITLE)}" autocomplete="off" spellcheck="false" />
@@ -757,22 +990,11 @@ ${CODE_BROWSER_STYLES}
757
990
  <div class="search-results" id="search-results" hidden aria-live="polite"></div>
758
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>
759
992
  </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>
993
+ <main id="main-content" class="app__main" tabindex="-1">
772
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}>
773
995
  ${p.shellInner}
774
996
  </div>
775
- </div>
997
+ </main>
776
998
  ${p.pageFooterHtml}
777
999
  </div>
778
1000
  <script type="text/plain" id="commentray-multi-angle-b64">${p.multiAngleScriptBlock}</script>
@@ -783,6 +1005,31 @@ ${loadCodeBrowserClientBundle()}
783
1005
  </body>
784
1006
  </html>`;
785
1007
  }
1008
+ async function multiAngleJsonRowAndDocHtml(opts, spec) {
1009
+ const rows = spec.blockStretchRows;
1010
+ const links = rows !== undefined
1011
+ ? buildBlockScrollLinks(rows.index, rows.sourceRelative, rows.commentrayPathRel, spec.markdown, opts.code)
1012
+ : [];
1013
+ const mdForDoc = injectCommentrayDocAnchors(spec.markdown, links.length > 0 ? links : undefined);
1014
+ const scrollB64 = links.length > 0 ? Buffer.from(JSON.stringify(links), "utf8").toString("base64") : "";
1015
+ const commentrayHtml = await renderMarkdownToHtml(mdForDoc, {
1016
+ commentrayOutputUrls: opts.commentrayOutputUrls,
1017
+ });
1018
+ return {
1019
+ jsonRow: {
1020
+ id: spec.id,
1021
+ title: spec.title?.trim() || spec.id,
1022
+ docInnerHtmlB64: Buffer.from(commentrayHtml, "utf8").toString("base64"),
1023
+ rawMdB64: Buffer.from(spec.markdown, "utf8").toString("base64"),
1024
+ scrollBlockLinksB64: scrollB64,
1025
+ commentrayPathForSearch: spec.commentrayPathRel.trim(),
1026
+ commentrayOnGithubUrl: spec.commentrayOnGithubUrl,
1027
+ staticBrowseUrl: spec.staticBrowseUrl,
1028
+ },
1029
+ commentrayHtml,
1030
+ scrollB64,
1031
+ };
1032
+ }
786
1033
  async function buildMultiAngleDualPaneShell(opts, multi) {
787
1034
  const defaultId = multi.angles.some((a) => a.id === multi.defaultAngleId)
788
1035
  ? multi.defaultAngleId
@@ -792,34 +1039,24 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
792
1039
  let defaultScrollB64 = "";
793
1040
  let defaultPathSearch = (opts.commentrayPathForSearch ?? "").trim();
794
1041
  let defaultGh = opts.commentrayOnGithubUrl;
1042
+ let defaultStaticBrowse = (opts.commentrayStaticBrowseUrl ?? "").trim();
795
1043
  let defaultPaneHtml = "";
796
1044
  const codeHtml = await renderHighlightedCodeLineRows(opts.code, opts.language);
797
1045
  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
- });
1046
+ const { jsonRow, commentrayHtml, scrollB64 } = await multiAngleJsonRowAndDocHtml(opts, spec);
807
1047
  if (spec.id === defaultId) {
808
1048
  defaultMarkdown = spec.markdown;
809
1049
  defaultScrollB64 = scrollB64;
810
1050
  defaultPathSearch = spec.commentrayPathRel.trim();
811
1051
  defaultGh = spec.commentrayOnGithubUrl;
1052
+ {
1053
+ const sb = (spec.staticBrowseUrl ?? "").trim();
1054
+ if (sb.length > 0)
1055
+ defaultStaticBrowse = sb;
1056
+ }
812
1057
  defaultPaneHtml = commentrayHtml;
813
1058
  }
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
- });
1059
+ jsonAngles.push(jsonRow);
823
1060
  }
824
1061
  const selOpts = multi.angles
825
1062
  .map((a) => {
@@ -829,12 +1066,10 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
829
1066
  .join("");
830
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>`;
831
1068
  const shellInner = ` <section class="pane--code" id="code-pane" aria-label="Source code">` +
832
- `<h2 class="pane-title">Code</h2>\n` +
833
1069
  ` ${codeHtml}\n` +
834
1070
  ` </section>\n` +
835
1071
  ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
836
1072
  ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
837
- ` <h2 class="pane-title">Commentray</h2>\n` +
838
1073
  ` <div id="doc-pane-body" class="doc-pane-body">\n` +
839
1074
  ` ${defaultPaneHtml}\n` +
840
1075
  ` </div>\n` +
@@ -848,6 +1083,7 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
848
1083
  scrollBlockLinksB64: defaultScrollB64,
849
1084
  commentrayPathForSearch: defaultPathSearch,
850
1085
  commentrayOnGithubUrl: defaultGh,
1086
+ ...(defaultStaticBrowse.length > 0 ? { commentrayStaticBrowseUrl: defaultStaticBrowse } : {}),
851
1087
  },
852
1088
  angleSelectHtml,
853
1089
  multiAnglePayloadB64,
@@ -883,13 +1119,7 @@ async function buildCodeBrowserShell(opts, layoutPref) {
883
1119
  });
884
1120
  if (stretched) {
885
1121
  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`;
1122
+ shellInner = ` ${stretched.preambleHtml}\n` + ` ${stretched.tableInnerHtml}\n`;
893
1123
  }
894
1124
  }
895
1125
  if (layout === "dual") {
@@ -908,12 +1138,10 @@ async function buildCodeBrowserShell(opts, layoutPref) {
908
1138
  ]);
909
1139
  shellInner =
910
1140
  ` <section class="pane--code" id="code-pane" aria-label="Source code">` +
911
- `<h2 class="pane-title">Code</h2>\n` +
912
1141
  ` ${codeHtml}\n` +
913
1142
  ` </section>\n` +
914
1143
  ` <div class="gutter" id="gutter" role="separator" aria-orientation="vertical" aria-label="Resize panes"></div>\n` +
915
1144
  ` <section class="pane--doc commentray" id="doc-pane" aria-label="Commentray">\n` +
916
- ` <h2 class="pane-title">Commentray</h2>\n` +
917
1145
  ` <div id="doc-pane-body" class="doc-pane-body">\n` +
918
1146
  ` ${commentrayHtml}\n` +
919
1147
  ` </div>\n` +
@@ -957,6 +1185,12 @@ function codeBrowserHljsThemes(opts) {
957
1185
  function toolbarCommentrayGithubFromShell(shell, opts) {
958
1186
  return shell.multiShell?.commentrayOnGithubUrl ?? opts.commentrayOnGithubUrl;
959
1187
  }
1188
+ function toolbarCommentrayStaticBrowseFromShell(shell, opts) {
1189
+ const t = (shell.multiShell?.commentrayStaticBrowseUrl ??
1190
+ opts.commentrayStaticBrowseUrl ??
1191
+ "").trim();
1192
+ return t.length > 0 ? t : undefined;
1193
+ }
960
1194
  function rawMdB64FromShell(shell, opts) {
961
1195
  return (shell.multiShell?.rawMdB64 ?? Buffer.from(opts.commentrayMarkdown, "utf8").toString("base64"));
962
1196
  }
@@ -980,9 +1214,11 @@ function shellSearchAttrsWithNavJson(shellSearchAttrsBase, documentedNavJsonUrl)
980
1214
  export async function renderCodeBrowserHtml(opts) {
981
1215
  const rawCodeB64 = Buffer.from(opts.code, "utf8").toString("base64");
982
1216
  const title = codeBrowserPageTitle(opts);
1217
+ const metaDescriptionHtml = renderMetaDescriptionHtml(opts, title);
983
1218
  const builtAt = opts.builtAt ?? new Date();
984
1219
  const renderSemver = commentrayRenderVersion();
985
- const toolbarEndHtml = buildToolbarEndHtml(opts.githubRepoUrl, opts.toolHomeUrl, renderSemver);
1220
+ const toolbarSiteHubHtml = buildToolbarSiteHubHtml(opts.siteHubUrl);
1221
+ const toolbarEndHtml = buildToolbarEndHtml(opts.githubRepoUrl, opts.toolHomeUrl, renderSemver, opts.siteHubUrl);
986
1222
  const pageFooterHtml = renderPageFooterHtml(builtAt);
987
1223
  const { hljs, hljsDark } = codeBrowserHljsThemes(opts);
988
1224
  const mermaidScript = mermaidRuntimeScriptHtml(opts.includeMermaidRuntime);
@@ -1002,10 +1238,13 @@ export async function renderCodeBrowserHtml(opts) {
1002
1238
  const navRailContextHtml = renderNavRailContextHtml(opts.filePath, navRailCommentrayPathFromShell(shell, opts), {
1003
1239
  sourceOnGithubUrl: opts.sourceOnGithubUrl,
1004
1240
  commentrayOnGithubUrl: toolbarCommentrayGithubFromShell(shell, opts),
1241
+ commentrayStaticBrowseUrl: toolbarCommentrayStaticBrowseFromShell(shell, opts),
1005
1242
  });
1006
1243
  return buildCodeBrowserPageHtml({
1007
1244
  title,
1245
+ metaDescriptionHtml,
1008
1246
  generatorMetaHtml,
1247
+ toolbarSiteHubHtml,
1009
1248
  navRailContextHtml,
1010
1249
  angleSelectHtml: shell.angleSelectHtml,
1011
1250
  toolbarDocHubHtml,