@commentray/render 0.2.0 → 0.3.1

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 (68) hide show
  1. package/dist/block-stretch-buffer-sync.d.ts +16 -0
  2. package/dist/block-stretch-buffer-sync.d.ts.map +1 -0
  3. package/dist/block-stretch-buffer-sync.js +271 -0
  4. package/dist/block-stretch-buffer-sync.js.map +1 -0
  5. package/dist/block-stretch-layout.d.ts +18 -5
  6. package/dist/block-stretch-layout.d.ts.map +1 -1
  7. package/dist/block-stretch-layout.js +119 -43
  8. package/dist/block-stretch-layout.js.map +1 -1
  9. package/dist/browse-page-slug.d.ts +3 -4
  10. package/dist/browse-page-slug.d.ts.map +1 -1
  11. package/dist/browse-page-slug.js +3 -4
  12. package/dist/browse-page-slug.js.map +1 -1
  13. package/dist/build-commentray-nav-search.d.ts +2 -2
  14. package/dist/build-commentray-nav-search.js +1 -1
  15. package/dist/code-browser-block-rays.d.ts +11 -0
  16. package/dist/code-browser-block-rays.d.ts.map +1 -1
  17. package/dist/code-browser-block-rays.js +25 -5
  18. package/dist/code-browser-block-rays.js.map +1 -1
  19. package/dist/code-browser-client.bundle.js +12 -11
  20. package/dist/code-browser-client.js +1366 -257
  21. package/dist/code-browser-client.js.map +1 -1
  22. package/dist/code-browser-pair-nav.d.ts +9 -2
  23. package/dist/code-browser-pair-nav.d.ts.map +1 -1
  24. package/dist/code-browser-pair-nav.js +53 -14
  25. package/dist/code-browser-pair-nav.js.map +1 -1
  26. package/dist/code-browser-scroll-sync-monotonic.d.ts +17 -0
  27. package/dist/code-browser-scroll-sync-monotonic.d.ts.map +1 -0
  28. package/dist/code-browser-scroll-sync-monotonic.js +22 -0
  29. package/dist/code-browser-scroll-sync-monotonic.js.map +1 -0
  30. package/dist/code-browser-scroll-sync-strategy.d.ts +12 -0
  31. package/dist/code-browser-scroll-sync-strategy.d.ts.map +1 -0
  32. package/dist/code-browser-scroll-sync-strategy.js +28 -0
  33. package/dist/code-browser-scroll-sync-strategy.js.map +1 -0
  34. package/dist/code-browser-scroll-sync.d.ts +2 -2
  35. package/dist/code-browser-scroll-sync.d.ts.map +1 -1
  36. package/dist/code-browser-scroll-sync.js +1 -1
  37. package/dist/code-browser-scroll-sync.js.map +1 -1
  38. package/dist/code-browser-smooth-reveal-dedup.d.ts +25 -0
  39. package/dist/code-browser-smooth-reveal-dedup.d.ts.map +1 -0
  40. package/dist/code-browser-smooth-reveal-dedup.js +25 -0
  41. package/dist/code-browser-smooth-reveal-dedup.js.map +1 -0
  42. package/dist/code-browser.d.ts +25 -8
  43. package/dist/code-browser.d.ts.map +1 -1
  44. package/dist/code-browser.js +382 -93
  45. package/dist/code-browser.js.map +1 -1
  46. package/dist/commentray-anchor-viewport-probe.d.ts +5 -1
  47. package/dist/commentray-anchor-viewport-probe.d.ts.map +1 -1
  48. package/dist/commentray-anchor-viewport-probe.js +8 -2
  49. package/dist/commentray-anchor-viewport-probe.js.map +1 -1
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/inject-md-line-anchors.d.ts +1 -1
  55. package/dist/inject-md-line-anchors.d.ts.map +1 -1
  56. package/dist/inject-md-line-anchors.js +9 -5
  57. package/dist/inject-md-line-anchors.js.map +1 -1
  58. package/dist/markdown-pipeline.d.ts.map +1 -1
  59. package/dist/markdown-pipeline.js +10 -3
  60. package/dist/markdown-pipeline.js.map +1 -1
  61. package/dist/mermaid-runtime-html.d.ts.map +1 -1
  62. package/dist/mermaid-runtime-html.js +4 -1
  63. package/dist/mermaid-runtime-html.js.map +1 -1
  64. package/dist/reading-viewport-comfort.d.ts +12 -0
  65. package/dist/reading-viewport-comfort.d.ts.map +1 -0
  66. package/dist/reading-viewport-comfort.js +14 -0
  67. package/dist/reading-viewport-comfort.js.map +1 -0
  68. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import path, { join } from "node:path";
3
- import { buildBlockScrollLinks, findMonorepoPackagesDir, monorepoLayoutStartDir, normalizeRepoRelativePath, } from "@commentray/core";
4
- import { tryBuildBlockStretchTableHtml } from "./block-stretch-layout.js";
3
+ import { buildBlockScrollLinks, CURRENT_SCHEMA_VERSION, DEFAULT_STRETCH_BUFFER_SYNC, findMonorepoPackagesDir, monorepoLayoutStartDir, normalizeRepoRelativePath, } from "@commentray/core";
4
+ import { tryBuildBlockStretchTableHtml, } from "./block-stretch-layout.js";
5
5
  import { formatCommentrayBuiltAtLocal } from "./build-stamp.js";
6
6
  import { escapeHtml } from "./html-utils.js";
7
7
  import { commentrayColorThemeHeadBoot } from "./code-browser-color-theme.js";
@@ -13,6 +13,7 @@ import { renderMarkdownToHtml } from "./markdown-pipeline.js";
13
13
  import { commentrayRenderVersion } from "./package-version.js";
14
14
  import { normPosixPath } from "./code-browser-pair-nav.js";
15
15
  import { injectCommentrayDocAnchors, injectSourceMarkdownAnchors, } from "./inject-md-line-anchors.js";
16
+ import { DEFAULT_DUAL_PANE_SCROLL_SYNC_STRATEGY, } from "./code-browser-scroll-sync-strategy.js";
16
17
  function renderGeneratorMetaHtml(label) {
17
18
  const t = label?.trim();
18
19
  if (!t)
@@ -184,7 +185,7 @@ function renderToolbarDocHubHtml(opts) {
184
185
  }
185
186
  function dualPanePanesInnerHtml(codeHtml, commentrayHtml, sourceMarkdownRenderedHtml) {
186
187
  const sourceRenderedPaneHtml = typeof sourceMarkdownRenderedHtml === "string" && sourceMarkdownRenderedHtml.trim().length > 0
187
- ? ` <div class="source-pane source-pane--rendered-md" id="code-pane-markdown-body">${sourceMarkdownRenderedHtml}</div>\n`
188
+ ? ` <div class="source-pane source-pane--rendered-md" id="code-pane-markdown-body" data-source-markdown-body="true">${sourceMarkdownRenderedHtml}</div>\n`
188
189
  : "";
189
190
  return (` <section class="pane--code" id="code-pane" aria-label="Source code">` +
190
191
  ` <div class="source-pane source-pane--code" id="code-pane-code-body">${codeHtml}</div>\n` +
@@ -214,6 +215,8 @@ function isMarkdownLikeSource(opts) {
214
215
  const lang = opts.language.trim().toLowerCase();
215
216
  if (lang === "md" || lang === "markdown" || lang === "mdx")
216
217
  return true;
218
+ if (lang.length > 0)
219
+ return false;
217
220
  const filePath = (opts.filePath ?? "").trim().toLowerCase();
218
221
  return filePath.endsWith(".md") || filePath.endsWith(".mdx") || filePath.endsWith(".markdown");
219
222
  }
@@ -252,9 +255,24 @@ function renderShellPairContextHtml(filePath, commentrayPath) {
252
255
  </div>
253
256
  </div>`;
254
257
  }
255
- function wrapDualShellInner(pairContextHtml, panesHtml) {
258
+ function shellPairSourcePath(filePath, sourceRelative) {
259
+ const filePathTrimmed = (filePath ?? "").trim();
260
+ if (filePathTrimmed.length > 0)
261
+ return filePathTrimmed;
262
+ return (sourceRelative ?? "").trim();
263
+ }
264
+ function shellPairCommentrayPath(commentrayPath, fallbackCommentrayPath) {
265
+ const commentrayPathTrimmed = (commentrayPath ?? "").trim();
266
+ if (commentrayPathTrimmed.length > 0)
267
+ return commentrayPathTrimmed;
268
+ return (fallbackCommentrayPath ?? "").trim();
269
+ }
270
+ function wrapShellInnerWithPairContext(pairContextHtml, mainHtml) {
256
271
  const row = pairContextHtml.trim().length > 0 ? ` ${pairContextHtml.trim()}\n` : "";
257
- return `${row} <div class="shell__panes">\n${panesHtml} </div>\n`;
272
+ return `${row}${mainHtml}`;
273
+ }
274
+ function wrapDualShellInner(pairContextHtml, panesHtml) {
275
+ return wrapShellInnerWithPairContext(pairContextHtml, ` <div class="shell__panes">\n${panesHtml} </div>\n`);
258
276
  }
259
277
  /** IIFE produced by `npm run build -w @commentray/render` (esbuild of `code-browser-client.ts`). */
260
278
  function loadCodeBrowserClientBundle() {
@@ -609,7 +627,7 @@ const CODE_BROWSER_STYLES = `
609
627
  flex-wrap: wrap;
610
628
  align-items: center;
611
629
  justify-content: flex-end;
612
- gap: 10px 14px;
630
+ gap: 6px;
613
631
  margin-left: auto;
614
632
  min-width: 0;
615
633
  }
@@ -1132,7 +1150,66 @@ ${CODE_BROWSER_INTRO_STYLES}
1132
1150
  text-overflow: ellipsis;
1133
1151
  white-space: nowrap;
1134
1152
  }
1135
- .shell__pair-path--secondary { opacity: 0.88; }
1153
+ .shell__pair-path--secondary {
1154
+ opacity: 0.88;
1155
+ text-align: right;
1156
+ }
1157
+ .shell--stretch-rows .shell__pair-context {
1158
+ --shell-pair-gutter-w: 14px;
1159
+ --shell-pair-gutter-half: calc(var(--shell-pair-gutter-w) / 2);
1160
+ display: grid;
1161
+ grid-template-columns:
1162
+ minmax(0, calc(var(--stretch-code-pct, 50%) - var(--shell-pair-gutter-half)))
1163
+ var(--shell-pair-gutter-w)
1164
+ minmax(0, calc(100% - var(--stretch-code-pct, 50%) - var(--shell-pair-gutter-half)));
1165
+ width: 100%;
1166
+ box-sizing: border-box;
1167
+ }
1168
+ .shell--stretch-rows .shell__pair-cell--src,
1169
+ .shell--stretch-rows .shell__pair-cell--doc,
1170
+ .shell--stretch-rows .shell__pair-gutter-spacer {
1171
+ min-width: 0;
1172
+ }
1173
+ .shell--stretch-rows .shell__pair-cell--src {
1174
+ grid-column: 1;
1175
+ padding-left: var(--cr-pane-inline-pad);
1176
+ padding-right: 0;
1177
+ }
1178
+ .shell--stretch-rows .shell__pair-gutter-spacer {
1179
+ grid-column: 2;
1180
+ width: var(--shell-pair-gutter-w);
1181
+ min-width: var(--shell-pair-gutter-w);
1182
+ }
1183
+ .shell--stretch-rows .shell__pair-cell--doc {
1184
+ grid-column: 3;
1185
+ padding-left: var(--cr-pane-inline-pad);
1186
+ padding-right: var(--cr-pane-inline-pad);
1187
+ }
1188
+ .shell[data-mobile-single-pane="true"] .shell__pair-context {
1189
+ display: flex;
1190
+ flex-direction: column;
1191
+ align-items: stretch;
1192
+ gap: 4px;
1193
+ padding: 4px 0 6px;
1194
+ }
1195
+ .shell[data-mobile-single-pane="true"] .shell__pair-cell--src,
1196
+ .shell[data-mobile-single-pane="true"] .shell__pair-cell--doc {
1197
+ width: 100%;
1198
+ padding-right: var(--cr-pane-inline-pad);
1199
+ box-sizing: border-box;
1200
+ }
1201
+ .shell[data-mobile-single-pane="true"][data-dual-mobile-pane="code"] .shell__pair-cell--doc,
1202
+ .shell[data-mobile-single-pane="true"][data-dual-mobile-pane="code"] .shell__pair-gutter-spacer {
1203
+ display: none;
1204
+ }
1205
+ .shell[data-mobile-single-pane="true"][data-dual-mobile-pane="doc"] .shell__pair-cell--src,
1206
+ .shell[data-mobile-single-pane="true"][data-dual-mobile-pane="doc"] .shell__pair-gutter-spacer {
1207
+ display: none;
1208
+ }
1209
+ .shell[data-mobile-single-pane="true"]:not([data-dual-mobile-pane]) .shell__pair-cell--src,
1210
+ .shell[data-mobile-single-pane="true"]:not([data-dual-mobile-pane]) .shell__pair-gutter-spacer {
1211
+ display: none;
1212
+ }
1136
1213
  .pane--code {
1137
1214
  flex: 0 0 var(--split-pct, 46%);
1138
1215
  min-width: 120px; overflow: auto; padding: 12px var(--cr-pane-inline-pad);
@@ -1270,6 +1347,8 @@ ${CODE_BROWSER_INTRO_STYLES}
1270
1347
  .doc-pane-body {
1271
1348
  flex: 1 1 auto; min-height: 0; overflow: auto;
1272
1349
  }
1350
+ /* scroll-behavior: smooth was removed: it interpolates many scroll events per gesture and
1351
+ * exhausts the partner-echo guard in wireBidirectionalScroll, so doc↔code ping-pong returns. */
1273
1352
  /* Inline backtick code chips (GitHub-like): prose context only, never fenced pre/code blocks. */
1274
1353
  .pane--doc .doc-pane-body :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1275
1354
  .shell--stretch-rows .stretch-preamble :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
@@ -1760,6 +1839,22 @@ ${CODE_BROWSER_INTRO_STYLES}
1760
1839
  flex: 1 1 auto;
1761
1840
  padding-left: var(--cr-pane-inline-pad);
1762
1841
  }
1842
+ .shell.shell--stretch-rows[data-dual-mobile-pane="code"] .stretch-col-doc,
1843
+ .shell.shell--stretch-rows[data-dual-mobile-pane="doc"] .stretch-col-code,
1844
+ .shell.shell--stretch-rows[data-dual-mobile-pane="code"] td.stretch-doc,
1845
+ .shell.shell--stretch-rows[data-dual-mobile-pane="doc"] td.stretch-code {
1846
+ display: none;
1847
+ }
1848
+ .shell.shell--stretch-rows[data-dual-mobile-pane="code"] td.stretch-code,
1849
+ .shell.shell--stretch-rows[data-dual-mobile-pane="doc"] td.stretch-doc {
1850
+ display: table-cell;
1851
+ width: 100%;
1852
+ padding-left: 0;
1853
+ padding-right: 0;
1854
+ }
1855
+ .shell.shell--stretch-rows[data-dual-mobile-pane] #stretch-gutter {
1856
+ display: none;
1857
+ }
1763
1858
  }
1764
1859
  .pane--doc { font-size: 15px; line-height: 1.45; }
1765
1860
  .pane--doc img { max-width: 100%; height: auto; }
@@ -1771,9 +1866,8 @@ ${CODE_BROWSER_INTRO_STYLES}
1771
1866
  .pane--doc .commentray-block-anchor {
1772
1867
  display: block;
1773
1868
  height: 0;
1774
- margin: 14px 0 0;
1869
+ margin: 0;
1775
1870
  border: 0;
1776
- border-top: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
1777
1871
  pointer-events: none;
1778
1872
  }
1779
1873
  .shell--stretch-rows {
@@ -1781,15 +1875,17 @@ ${CODE_BROWSER_INTRO_STYLES}
1781
1875
  min-height: 0;
1782
1876
  overflow: auto;
1783
1877
  display: block;
1784
- padding: 0 12px 20px;
1878
+ padding: 0 0 20px;
1785
1879
  }
1786
1880
  .shell--stretch-rows .stretch-preamble {
1787
1881
  padding: 8px 4px 16px;
1788
1882
  margin-bottom: 8px;
1789
- border-bottom: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
1790
1883
  font-size: 15px;
1791
1884
  line-height: 1.45;
1792
1885
  overflow-x: auto;
1886
+ /* overflow-x other than visible makes overflow-y:visible compute to auto, which traps
1887
+ * vertical wheel on this node instead of the shell scrollport when height is ever capped. */
1888
+ overflow-y: hidden;
1793
1889
  max-width: 100%;
1794
1890
  }
1795
1891
  .shell--stretch-rows .stretch-preamble img { max-width: 100%; height: auto; }
@@ -1798,27 +1894,49 @@ ${CODE_BROWSER_INTRO_STYLES}
1798
1894
  border-collapse: collapse;
1799
1895
  table-layout: fixed;
1800
1896
  }
1801
- .stretch-col-code { width: 50%; }
1802
- .stretch-col-doc { width: 50%; }
1897
+ .stretch-grid {
1898
+ position: relative;
1899
+ }
1900
+ .stretch-col-code { width: var(--stretch-code-pct, 50%); }
1901
+ .stretch-col-doc { width: calc(100% - var(--stretch-code-pct, 50%)); }
1803
1902
  .block-stretch td.stretch-code {
1804
1903
  vertical-align: top;
1805
1904
  padding: 0 12px 0 0;
1806
- border-bottom: 1px solid color-mix(in oklab, CanvasText 8%, Canvas);
1905
+ box-shadow: inset -1px 0 0 color-mix(in oklab, CanvasText 10%, Canvas);
1906
+ }
1907
+ #shell[data-stretch-buffer-sync="flow-synchronizer"] .block-stretch td.stretch-code > .stretch-cell-measure,
1908
+ #shell[data-stretch-buffer-sync="flow-synchronizer"] .block-stretch td.stretch-doc > .stretch-cell-measure {
1909
+ display: block;
1910
+ width: 100%;
1911
+ box-sizing: border-box;
1912
+ height: max-content;
1913
+ max-height: max-content;
1914
+ }
1915
+ #shell[data-stretch-buffer-sync="flow-synchronizer"] .block-stretch td.stretch-doc .stretch-doc-inner {
1916
+ height: max-content;
1917
+ max-height: max-content;
1918
+ }
1919
+ #shell[data-stretch-buffer-sync="flow-synchronizer"] .block-stretch .stretch-code-stack {
1920
+ height: max-content;
1921
+ max-height: max-content;
1807
1922
  }
1808
1923
  .block-stretch td.stretch-doc {
1809
1924
  vertical-align: top;
1810
1925
  padding: 0 0 0 12px;
1811
- border-bottom: 1px solid color-mix(in oklab, CanvasText 8%, Canvas);
1926
+ box-shadow: inset 1px 0 0 color-mix(in oklab, CanvasText 8%, Canvas);
1812
1927
  }
1813
1928
  .block-stretch td.stretch-doc .stretch-doc-inner {
1814
1929
  font-size: 15px;
1815
1930
  line-height: 1.45;
1816
1931
  min-width: 0;
1817
1932
  overflow-x: auto;
1933
+ /* Keep vertical scrolling on the shell; see stretch-preamble rule above. */
1934
+ overflow-y: hidden;
1818
1935
  }
1819
1936
  .block-stretch td.stretch-doc .stretch-doc-inner img { max-width: 100%; height: auto; }
1820
1937
  .block-stretch td.stretch-doc .stretch-doc-inner .commentray-mermaid {
1821
1938
  overflow-x: auto;
1939
+ overflow-y: hidden;
1822
1940
  max-width: 100%;
1823
1941
  }
1824
1942
  .block-stretch.wrap td.stretch-doc .stretch-doc-inner {
@@ -1837,7 +1955,6 @@ ${CODE_BROWSER_INTRO_STYLES}
1837
1955
  font-size: 13px;
1838
1956
  vertical-align: top;
1839
1957
  }
1840
- .block-stretch .stretch-gap-mark { display: inline-block; padding-top: 2px; }
1841
1958
  .block-stretch .stretch-code-stack {
1842
1959
  display: flex;
1843
1960
  flex-direction: column;
@@ -1887,15 +2004,39 @@ ${CODE_BROWSER_INTRO_STYLES}
1887
2004
  .block-stretch:not(.wrap) .stretch-doc-inner pre code {
1888
2005
  white-space: pre;
1889
2006
  }
2007
+ .stretch-gutter {
2008
+ position: absolute;
2009
+ top: 0;
2010
+ bottom: 0;
2011
+ left: var(--stretch-code-pct, 50%);
2012
+ transform: translateX(-50%);
2013
+ width: 14px;
2014
+ cursor: col-resize;
2015
+ touch-action: none;
2016
+ border-left: 1px solid color-mix(in oklab, CanvasText 14%, Canvas);
2017
+ border-right: 1px solid color-mix(in oklab, CanvasText 10%, Canvas);
2018
+ background: color-mix(in oklab, CanvasText 8%, Canvas);
2019
+ border-radius: 999px;
2020
+ --commentray-ray-accent: #3b7dd8;
2021
+ z-index: 6;
2022
+ }
2023
+ @media (prefers-color-scheme: dark) {
2024
+ :root:is(:not([data-commentray-theme]), [data-commentray-theme="system"]) .stretch-gutter {
2025
+ --commentray-ray-accent: #6eb0ff;
2026
+ }
2027
+ }
2028
+ :root[data-commentray-theme="dark"] .stretch-gutter {
2029
+ --commentray-ray-accent: #6eb0ff;
2030
+ }
1890
2031
  `;
1891
2032
  /** Native tooltip on #search-q (short hint is visible under the search row). */
1892
2033
  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).";
1893
2034
  function buildCodeBrowserPageHtml(p) {
1894
2035
  const shellClass = p.layout === "stretch" ? "shell shell--stretch-rows" : "shell";
1895
- const dualFlipControlHtml = p.layout === "dual"
2036
+ const dualFlipControlHtml = p.layout === "dual" || p.layout === "stretch"
1896
2037
  ? `<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>`
1897
2038
  : "";
1898
- const dualFlipScrollAffordanceHtml = p.layout === "dual"
2039
+ const dualFlipScrollAffordanceHtml = p.layout === "dual" || p.layout === "stretch"
1899
2040
  ? `<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>`
1900
2041
  : "";
1901
2042
  return `<!doctype html>
@@ -1924,13 +2065,13 @@ ${CODE_BROWSER_STYLES}
1924
2065
  ${p.toolbarSiteHubHtml}
1925
2066
  ${p.navRailDocumentedHtml}
1926
2067
  ${p.angleSelectHtml}
2068
+ ${dualFlipControlHtml}
1927
2069
  <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).">
1928
2070
  <input type="checkbox" id="wrap-lines" class="toolbar-wrap-lines__input" />
1929
2071
  <span class="toolbar-wrap-lines__box" aria-hidden="true"></span>
1930
2072
  <span class="toolbar-wrap-lines__face" aria-hidden="true">${TOOLBAR_ICON_WRAP_SVG}</span>
1931
2073
  <span class="toolbar-wrap-lines__caption">Wrap lines</span>
1932
2074
  </label>
1933
- ${dualFlipControlHtml}
1934
2075
  ${p.sourceMarkdownToggleHtml}
1935
2076
  ${p.toolbarDocHubHtml}
1936
2077
  ${p.relatedNavHtml}
@@ -1954,7 +2095,7 @@ ${TOOLBAR_COLOR_THEME_HTML}
1954
2095
  <div class="search-results" id="search-results" hidden aria-live="polite"></div>
1955
2096
  </header>
1956
2097
  <main id="main-content" class="app__main" tabindex="-1">
1957
- <div class="${shellClass}" id="shell" data-layout="${p.layout}"${p.layout === "dual" ? ' data-dual-mobile-pane="doc"' : ""}${p.sourcePaneModeAttr} 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.shellPairIdentityDataAttrs}${p.shellPairDocDataAttr}>
2098
+ <div class="${shellClass}" id="shell" data-layout="${p.layout}"${p.layout === "dual" || p.layout === "stretch" ? ' data-dual-mobile-pane="doc"' : ""}${p.sourcePaneModeAttr} 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.shellPairIdentityDataAttrs}${p.shellPairDocDataAttr}${p.scrollSyncStrategyShellAttr ?? ""}${p.stretchBufferSyncShellAttr ?? ""}>
1958
2099
  ${p.shellInner}
1959
2100
  </div>
1960
2101
  </main>
@@ -1971,6 +2112,27 @@ ${loadCodeBrowserClientBundle()}
1971
2112
  function firstNonEmpty(values) {
1972
2113
  return values.find((v) => v.trim().length > 0);
1973
2114
  }
2115
+ function stretchBufferSyncFromOpts(opts) {
2116
+ return opts.stretchBufferSync ?? DEFAULT_STRETCH_BUFFER_SYNC;
2117
+ }
2118
+ function angleBlockStretchRowsPathOk(spec, opts) {
2119
+ const rows = spec.blockStretchRows;
2120
+ if (rows === undefined)
2121
+ return false;
2122
+ const angleCrNorm = normalizeRepoRelativePath(spec.commentrayPathRel.replaceAll("\\", "/"));
2123
+ const primaryNorm = normalizeRepoRelativePath((opts.filePath ?? "").replaceAll("\\", "/"));
2124
+ return (normalizeRepoRelativePath(rows.commentrayPathRel.replaceAll("\\", "/")) === angleCrNorm &&
2125
+ normalizeRepoRelativePath(rows.sourceRelative.replaceAll("\\", "/")) === primaryNorm);
2126
+ }
2127
+ function multiAngleToolbarAngleSelectHtml(multi, defaultId) {
2128
+ const selOpts = multi.angles
2129
+ .map((a) => {
2130
+ const lab = escapeHtml(a.title?.trim() || a.id);
2131
+ return `<option value="${escapeHtml(a.id)}"${a.id === defaultId ? " selected" : ""}>${lab}</option>`;
2132
+ })
2133
+ .join("");
2134
+ return `<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>`;
2135
+ }
1974
2136
  function resolveMultiAngleDefaultSelection(args) {
1975
2137
  const { multi, defaultId, opts, builtAngles } = args;
1976
2138
  let defaultMarkdown = opts.commentrayMarkdown;
@@ -2008,11 +2170,8 @@ function resolveMultiAngleDefaultSelection(args) {
2008
2170
  }
2009
2171
  async function multiAngleJsonRowAndDocHtml(opts, spec) {
2010
2172
  const rows = spec.blockStretchRows;
2173
+ const rowsPathOk = angleBlockStretchRowsPathOk(spec, opts);
2011
2174
  const angleCrNorm = normalizeRepoRelativePath(spec.commentrayPathRel.replaceAll("\\", "/"));
2012
- const primaryNorm = normalizeRepoRelativePath((opts.filePath ?? "").replaceAll("\\", "/"));
2013
- const rowsPathOk = rows !== undefined &&
2014
- normalizeRepoRelativePath(rows.commentrayPathRel.replaceAll("\\", "/")) === angleCrNorm &&
2015
- normalizeRepoRelativePath(rows.sourceRelative.replaceAll("\\", "/")) === primaryNorm;
2016
2175
  const links = rows !== undefined && rowsPathOk
2017
2176
  ? buildBlockScrollLinks(rows.index, rows.sourceRelative, angleCrNorm, spec.markdown, opts.code)
2018
2177
  : [];
@@ -2036,6 +2195,85 @@ async function multiAngleJsonRowAndDocHtml(opts, spec) {
2036
2195
  scrollB64,
2037
2196
  };
2038
2197
  }
2198
+ /**
2199
+ * When every angle has a valid block index and `tryBuildBlockStretchTableHtml` succeeds for each,
2200
+ * emit one outer scroll (table rows = aligned block “arithmetics”) instead of dual-pane sync.
2201
+ */
2202
+ async function buildMultiAngleBlockStretchShell(opts, multi) {
2203
+ const defaultId = multi.angles.some((a) => a.id === multi.defaultAngleId)
2204
+ ? multi.defaultAngleId
2205
+ : (multi.angles[0]?.id ?? "main");
2206
+ const sourceMarkdownEnabled = isMarkdownLikeSource(opts);
2207
+ const sourceMarkdownUrls = sourceMarkdownEnabled ? sourcePaneOutputUrls(opts) : undefined;
2208
+ const perAngle = [];
2209
+ for (const spec of multi.angles) {
2210
+ if (!angleBlockStretchRowsPathOk(spec, opts))
2211
+ return null;
2212
+ const rows = spec.blockStretchRows;
2213
+ if (rows === undefined)
2214
+ return null;
2215
+ const stretched = await tryBuildBlockStretchTableHtml({
2216
+ code: opts.code,
2217
+ language: opts.language,
2218
+ commentrayMarkdown: spec.markdown,
2219
+ index: rows.index,
2220
+ sourceRelative: rows.sourceRelative,
2221
+ commentrayPathRel: rows.commentrayPathRel,
2222
+ commentrayOutputUrls: opts.commentrayOutputUrls,
2223
+ sourceMarkdownOutputUrls: sourceMarkdownUrls,
2224
+ stretchBufferSync: stretchBufferSyncFromOpts(opts),
2225
+ });
2226
+ if (stretched === null)
2227
+ return null;
2228
+ const { jsonRow, commentrayHtml, scrollB64 } = await multiAngleJsonRowAndDocHtml(opts, spec);
2229
+ const stretchPairHtml = renderShellPairContextHtml(shellPairSourcePath(opts.filePath, rows.sourceRelative), jsonRow.commentrayPathForSearch);
2230
+ const stretchSwapInner = wrapShellInnerWithPairContext(stretchPairHtml, ` ${stretched.preambleHtml}\n ${stretched.tableInnerHtml}\n`);
2231
+ perAngle.push({
2232
+ spec,
2233
+ stretched,
2234
+ jsonRow: {
2235
+ ...jsonRow,
2236
+ stretchSwapInnerB64: Buffer.from(stretchSwapInner, "utf8").toString("base64"),
2237
+ },
2238
+ commentrayHtml,
2239
+ scrollB64,
2240
+ });
2241
+ }
2242
+ const builtAngles = perAngle.map((p) => ({
2243
+ spec: p.spec,
2244
+ commentrayHtml: p.commentrayHtml,
2245
+ scrollB64: p.scrollB64,
2246
+ }));
2247
+ const { defaultMarkdown, defaultScrollB64, defaultPathSearch, defaultGh, defaultStaticBrowse } = resolveMultiAngleDefaultSelection({ multi, defaultId, opts, builtAngles });
2248
+ const defaultStretch = perAngle.find((p) => p.spec.id === defaultId) ?? perAngle[0];
2249
+ if (defaultStretch === undefined)
2250
+ return null;
2251
+ const shellInner = wrapShellInnerWithPairContext(renderShellPairContextHtml(shellPairSourcePath(opts.filePath, defaultStretch.spec.blockStretchRows?.sourceRelative), defaultPathSearch), ` ${defaultStretch.stretched.preambleHtml}\n` +
2252
+ ` ${defaultStretch.stretched.tableInnerHtml}\n`);
2253
+ const payloadObj = {
2254
+ layoutMode: "stretch",
2255
+ defaultAngleId: defaultId,
2256
+ angles: perAngle.map((p) => p.jsonRow),
2257
+ };
2258
+ const multiAnglePayloadB64 = Buffer.from(JSON.stringify(payloadObj), "utf8").toString("base64");
2259
+ return {
2260
+ layout: "stretch",
2261
+ shellInner,
2262
+ scrollBlockLinksB64: defaultScrollB64,
2263
+ angleSelectHtml: multiAngleToolbarAngleSelectHtml(multi, defaultId),
2264
+ multiAnglePayloadB64,
2265
+ sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2266
+ sourcePaneDefaultMode: "source",
2267
+ stretchBufferSync: stretchBufferSyncFromOpts(opts),
2268
+ multiShell: {
2269
+ rawMdB64: Buffer.from(defaultMarkdown, "utf8").toString("base64"),
2270
+ scrollBlockLinksB64: defaultScrollB64,
2271
+ commentrayPathForSearch: defaultPathSearch,
2272
+ commentrayOnGithubUrl: defaultGh,
2273
+ ...(defaultStaticBrowse.length > 0 ? { commentrayStaticBrowseUrl: defaultStaticBrowse } : {}),
2274
+ },
2275
+ };
2276
+ }
2039
2277
  async function buildMultiAngleDualPaneShell(opts, multi) {
2040
2278
  const defaultId = multi.angles.some((a) => a.id === multi.defaultAngleId)
2041
2279
  ? multi.defaultAngleId
@@ -2059,14 +2297,8 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
2059
2297
  jsonAngles.push(jsonRow);
2060
2298
  }
2061
2299
  const { defaultMarkdown, defaultScrollB64, defaultPathSearch, defaultGh, defaultStaticBrowse, defaultPaneHtml, } = resolveMultiAngleDefaultSelection({ multi, defaultId, opts, builtAngles });
2062
- const selOpts = multi.angles
2063
- .map((a) => {
2064
- const lab = escapeHtml(a.title?.trim() || a.id);
2065
- return `<option value="${escapeHtml(a.id)}"${a.id === defaultId ? " selected" : ""}>${lab}</option>`;
2066
- })
2067
- .join("");
2068
- 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>`;
2069
- const pairHtml = renderShellPairContextHtml(opts.filePath, defaultPathSearch);
2300
+ const angleSelectHtml = multiAngleToolbarAngleSelectHtml(multi, defaultId);
2301
+ const pairHtml = renderShellPairContextHtml(shellPairSourcePath(opts.filePath, opts.blockStretchRows?.sourceRelative), defaultPathSearch);
2070
2302
  const shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, defaultPaneHtml, sourceMarkdownPaneHtml));
2071
2303
  const payloadObj = { defaultAngleId: defaultId, angles: jsonAngles };
2072
2304
  const multiAnglePayloadB64 = Buffer.from(JSON.stringify(payloadObj), "utf8").toString("base64");
@@ -2085,74 +2317,100 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
2085
2317
  sourcePaneDefaultMode: sourceMarkdownEnabled ? "rendered-markdown" : "source",
2086
2318
  };
2087
2319
  }
2088
- async function buildCodeBrowserShell(opts, layoutPref) {
2089
- let layout = "dual";
2090
- let shellInner = "";
2320
+ async function buildDualPaneSingleAngleShell(opts) {
2321
+ const rows = opts.blockStretchRows;
2322
+ const links = rows !== undefined
2323
+ ? buildBlockScrollLinks(rows.index, rows.sourceRelative, rows.commentrayPathRel, opts.commentrayMarkdown, opts.code)
2324
+ : [];
2325
+ const mdForDoc = injectCommentrayDocAnchors(opts.commentrayMarkdown, links.length > 0 ? links : undefined);
2091
2326
  let scrollBlockLinksB64 = "";
2092
- const multi = opts.multiAngleBrowsing;
2093
- const multiActive = Boolean(multi && multi.angles.length >= 2);
2094
- if (multiActive && multi) {
2095
- const built = await buildMultiAngleDualPaneShell(opts, multi);
2096
- const ms = built.multiShell;
2097
- return {
2098
- layout: "dual",
2099
- shellInner: built.shellInner,
2100
- scrollBlockLinksB64: ms.scrollBlockLinksB64,
2101
- angleSelectHtml: built.angleSelectHtml,
2102
- multiAnglePayloadB64: built.multiAnglePayloadB64,
2103
- sourceMarkdownToggleEnabled: built.sourceMarkdownToggleEnabled,
2104
- sourcePaneDefaultMode: built.sourcePaneDefaultMode,
2105
- multiShell: ms,
2106
- };
2327
+ if (links.length > 0) {
2328
+ scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
2107
2329
  }
2108
- if (opts.blockStretchRows && layoutPref !== "dual") {
2330
+ const sourceMarkdownEnabled = isMarkdownLikeSource(opts);
2331
+ const sourceMdForPane = sourceMarkdownEnabled ? injectSourceMarkdownAnchors(opts.code) : "";
2332
+ const sourcePaneUrls = sourcePaneOutputUrls(opts);
2333
+ const [codeHtml, commentrayHtml, sourceMarkdownPaneHtml] = await Promise.all([
2334
+ renderHighlightedCodeLineRows(opts.code, opts.language),
2335
+ renderMarkdownToHtml(mdForDoc, {
2336
+ commentrayOutputUrls: opts.commentrayOutputUrls,
2337
+ }),
2338
+ sourceMarkdownEnabled
2339
+ ? renderMarkdownToHtml(sourceMdForPane, {
2340
+ commentrayOutputUrls: sourcePaneUrls,
2341
+ })
2342
+ : Promise.resolve(""),
2343
+ ]);
2344
+ const pairHtml = renderShellPairContextHtml(shellPairSourcePath(opts.filePath, opts.blockStretchRows?.sourceRelative), shellPairCommentrayPath(opts.commentrayPathForSearch, opts.blockStretchRows?.commentrayPathRel));
2345
+ const shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, commentrayHtml, sourceMarkdownPaneHtml));
2346
+ return {
2347
+ layout: "dual",
2348
+ shellInner,
2349
+ scrollBlockLinksB64,
2350
+ angleSelectHtml: "",
2351
+ multiAnglePayloadB64: "",
2352
+ sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2353
+ sourcePaneDefaultMode: "source",
2354
+ };
2355
+ }
2356
+ async function buildSingleAngleCodeBrowserShell(opts, layoutPref) {
2357
+ let layout = "dual";
2358
+ let shellInner = "";
2359
+ const sourceMarkdownEnabled = isMarkdownLikeSource(opts);
2360
+ const sourceMarkdownUrls = sourceMarkdownEnabled ? sourcePaneOutputUrls(opts) : undefined;
2361
+ if (layoutPref !== "dual") {
2362
+ const fallbackSourceRelative = (opts.filePath ?? "").trim().length > 0 ? (opts.filePath ?? "").trim() : "source";
2363
+ const fallbackCommentrayPathRel = (opts.commentrayPathForSearch ?? "").trim().length > 0
2364
+ ? (opts.commentrayPathForSearch ?? "").trim()
2365
+ : "commentray.md";
2366
+ const fallbackSourceLineCount = Math.max(1, opts.code.split("\n").length);
2367
+ const fallbackBlockId = "commentray-full";
2368
+ const fallbackStretchMarkdown = opts.blockStretchRows === undefined
2369
+ ? `<!-- commentray:block id=${fallbackBlockId} -->\n${opts.commentrayMarkdown}`
2370
+ : opts.commentrayMarkdown;
2371
+ const stretchRows = opts.blockStretchRows ??
2372
+ {
2373
+ index: {
2374
+ schemaVersion: CURRENT_SCHEMA_VERSION,
2375
+ byCommentrayPath: {
2376
+ [fallbackCommentrayPathRel]: {
2377
+ sourcePath: fallbackSourceRelative,
2378
+ commentrayPath: fallbackCommentrayPathRel,
2379
+ blocks: [
2380
+ { id: fallbackBlockId, anchor: `lines:1-${String(fallbackSourceLineCount)}` },
2381
+ ],
2382
+ },
2383
+ },
2384
+ },
2385
+ sourceRelative: fallbackSourceRelative,
2386
+ commentrayPathRel: fallbackCommentrayPathRel,
2387
+ };
2109
2388
  const stretched = await tryBuildBlockStretchTableHtml({
2110
2389
  code: opts.code,
2111
2390
  language: opts.language,
2112
- commentrayMarkdown: opts.commentrayMarkdown,
2113
- index: opts.blockStretchRows.index,
2114
- sourceRelative: opts.blockStretchRows.sourceRelative,
2115
- commentrayPathRel: opts.blockStretchRows.commentrayPathRel,
2391
+ commentrayMarkdown: fallbackStretchMarkdown,
2392
+ index: stretchRows.index,
2393
+ sourceRelative: stretchRows.sourceRelative,
2394
+ commentrayPathRel: stretchRows.commentrayPathRel,
2116
2395
  commentrayOutputUrls: opts.commentrayOutputUrls,
2396
+ sourceMarkdownOutputUrls: sourceMarkdownUrls,
2397
+ stretchBufferSync: stretchBufferSyncFromOpts(opts),
2117
2398
  });
2118
2399
  if (stretched) {
2119
2400
  layout = "stretch";
2120
- shellInner = ` ${stretched.preambleHtml}\n` + ` ${stretched.tableInnerHtml}\n`;
2401
+ shellInner = wrapShellInnerWithPairContext(renderShellPairContextHtml(shellPairSourcePath(opts.filePath, stretchRows.sourceRelative), shellPairCommentrayPath(opts.commentrayPathForSearch, stretchRows.commentrayPathRel)), ` ${stretched.preambleHtml}\n` + ` ${stretched.tableInnerHtml}\n`);
2121
2402
  }
2122
2403
  }
2123
2404
  if (layout === "dual") {
2124
- const links = opts.blockStretchRows !== undefined
2125
- ? buildBlockScrollLinks(opts.blockStretchRows.index, opts.blockStretchRows.sourceRelative, opts.blockStretchRows.commentrayPathRel, opts.commentrayMarkdown, opts.code)
2126
- : [];
2127
- const mdForDoc = injectCommentrayDocAnchors(opts.commentrayMarkdown, links.length > 0 ? links : undefined);
2128
- if (links.length > 0) {
2129
- scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
2130
- }
2131
- const sourceMarkdownEnabled = isMarkdownLikeSource(opts);
2132
- const sourceMdForPane = sourceMarkdownEnabled ? injectSourceMarkdownAnchors(opts.code) : "";
2133
- const sourcePaneUrls = sourcePaneOutputUrls(opts);
2134
- const [codeHtml, commentrayHtml, sourceMarkdownPaneHtml] = await Promise.all([
2135
- renderHighlightedCodeLineRows(opts.code, opts.language),
2136
- renderMarkdownToHtml(mdForDoc, {
2137
- commentrayOutputUrls: opts.commentrayOutputUrls,
2138
- }),
2139
- sourceMarkdownEnabled
2140
- ? renderMarkdownToHtml(sourceMdForPane, {
2141
- commentrayOutputUrls: sourcePaneUrls,
2142
- })
2143
- : Promise.resolve(""),
2144
- ]);
2145
- const pairHtml = renderShellPairContextHtml(opts.filePath, (opts.commentrayPathForSearch ?? "").trim());
2146
- shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, commentrayHtml, sourceMarkdownPaneHtml));
2147
- return {
2148
- layout,
2149
- shellInner,
2150
- scrollBlockLinksB64,
2151
- angleSelectHtml: "",
2152
- multiAnglePayloadB64: "",
2153
- sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2154
- sourcePaneDefaultMode: sourceMarkdownEnabled ? "rendered-markdown" : "source",
2155
- };
2405
+ return buildDualPaneSingleAngleShell(opts);
2406
+ }
2407
+ const rows = opts.blockStretchRows;
2408
+ const links = rows !== undefined
2409
+ ? buildBlockScrollLinks(rows.index, rows.sourceRelative, rows.commentrayPathRel, opts.commentrayMarkdown, opts.code)
2410
+ : [];
2411
+ let scrollBlockLinksB64 = "";
2412
+ if (links.length > 0) {
2413
+ scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
2156
2414
  }
2157
2415
  return {
2158
2416
  layout,
@@ -2160,10 +2418,35 @@ async function buildCodeBrowserShell(opts, layoutPref) {
2160
2418
  scrollBlockLinksB64,
2161
2419
  angleSelectHtml: "",
2162
2420
  multiAnglePayloadB64: "",
2163
- sourceMarkdownToggleEnabled: false,
2421
+ sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2164
2422
  sourcePaneDefaultMode: "source",
2423
+ stretchBufferSync: stretchBufferSyncFromOpts(opts),
2165
2424
  };
2166
2425
  }
2426
+ async function buildCodeBrowserShell(opts, layoutPref) {
2427
+ const multi = opts.multiAngleBrowsing;
2428
+ const multiActive = Boolean(multi && multi.angles.length >= 2);
2429
+ if (multiActive && multi) {
2430
+ if (layoutPref !== "dual") {
2431
+ const stretchMulti = await buildMultiAngleBlockStretchShell(opts, multi);
2432
+ if (stretchMulti !== null)
2433
+ return stretchMulti;
2434
+ }
2435
+ const built = await buildMultiAngleDualPaneShell(opts, multi);
2436
+ const ms = built.multiShell;
2437
+ return {
2438
+ layout: "dual",
2439
+ shellInner: built.shellInner,
2440
+ scrollBlockLinksB64: ms.scrollBlockLinksB64,
2441
+ angleSelectHtml: built.angleSelectHtml,
2442
+ multiAnglePayloadB64: built.multiAnglePayloadB64,
2443
+ sourceMarkdownToggleEnabled: built.sourceMarkdownToggleEnabled,
2444
+ sourcePaneDefaultMode: built.sourcePaneDefaultMode,
2445
+ multiShell: ms,
2446
+ };
2447
+ }
2448
+ return buildSingleAngleCodeBrowserShell(opts, layoutPref);
2449
+ }
2167
2450
  function searchChromeFromOptions(opts, commentrayPathOverride) {
2168
2451
  const crPath = (commentrayPathOverride ?? opts.commentrayPathForSearch ?? "").trim();
2169
2452
  if (opts.staticSearchScope === "commentray-and-paths") {
@@ -2198,8 +2481,7 @@ function currentPairCommentrayPathRel(shell, opts) {
2198
2481
  opts.blockStretchRows?.commentrayPathRel ??
2199
2482
  "").trim();
2200
2483
  }
2201
- /**
2202
- * Repo-relative source + companion Markdown paths for matching the current page to nav pairs
2484
+ /** Repo-relative source + companion Markdown paths for matching the current page to nav pairs
2203
2485
  * (see `code-browser-client.ts` documented-files tree).
2204
2486
  */
2205
2487
  function shellPairIdentityDataAttrs(shell, opts) {
@@ -2211,8 +2493,6 @@ function shellPairIdentityDataAttrs(shell, opts) {
2211
2493
  }
2212
2494
  /** Canonical doc target for static validation: same-site `./browse/…` when present, else GitHub blob. */
2213
2495
  function shellPairDocDataAttr(shell, opts) {
2214
- if (shell.layout !== "dual")
2215
- return "";
2216
2496
  const browseRaw = (shell.multiShell?.commentrayStaticBrowseUrl ??
2217
2497
  opts.commentrayStaticBrowseUrl ??
2218
2498
  "").trim();
@@ -2272,6 +2552,13 @@ export async function renderCodeBrowserHtml(opts) {
2272
2552
  const pairIdentityDataAttrs = shellPairIdentityDataAttrs(shell, opts);
2273
2553
  const sourceMarkdownToggles = sourceMarkdownToggleControlsHtml(shell.sourceMarkdownToggleEnabled);
2274
2554
  const sourcePaneModeAttr = ` data-source-pane-mode="${shell.sourcePaneDefaultMode}"`;
2555
+ const scrollSyncStrategyShellAttr = opts.dualPaneScrollSyncStrategy !== undefined &&
2556
+ opts.dualPaneScrollSyncStrategy !== DEFAULT_DUAL_PANE_SCROLL_SYNC_STRATEGY
2557
+ ? ` data-scroll-sync-strategy="${escapeHtml(opts.dualPaneScrollSyncStrategy)}"`
2558
+ : "";
2559
+ const stretchBufferSyncShellAttr = shell.layout === "stretch" && shell.stretchBufferSync === "flow-synchronizer"
2560
+ ? ` data-stretch-buffer-sync="flow-synchronizer"`
2561
+ : "";
2275
2562
  return buildCodeBrowserPageHtml({
2276
2563
  title,
2277
2564
  metaDescriptionHtml,
@@ -2300,6 +2587,8 @@ export async function renderCodeBrowserHtml(opts) {
2300
2587
  sourceMarkdownToggleHtml: sourceMarkdownToggles.sourceMarkdownToggleHtml,
2301
2588
  sourceMarkdownFlipScrollAffordanceHtml: sourceMarkdownToggles.sourceMarkdownFlipScrollAffordanceHtml,
2302
2589
  sourcePaneModeAttr,
2590
+ scrollSyncStrategyShellAttr,
2591
+ stretchBufferSyncShellAttr,
2303
2592
  });
2304
2593
  }
2305
2594
  //# sourceMappingURL=code-browser.js.map