@commentray/render 0.1.2 → 0.3.0

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 (73) 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 +121 -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.d.ts.map +1 -1
  15. package/dist/build-commentray-nav-search.js +8 -3
  16. package/dist/build-commentray-nav-search.js.map +1 -1
  17. package/dist/code-browser-block-rays.d.ts +11 -0
  18. package/dist/code-browser-block-rays.d.ts.map +1 -1
  19. package/dist/code-browser-block-rays.js +25 -5
  20. package/dist/code-browser-block-rays.js.map +1 -1
  21. package/dist/code-browser-client.bundle.js +12 -11
  22. package/dist/code-browser-client.js +1484 -265
  23. package/dist/code-browser-client.js.map +1 -1
  24. package/dist/code-browser-pair-nav.d.ts +9 -2
  25. package/dist/code-browser-pair-nav.d.ts.map +1 -1
  26. package/dist/code-browser-pair-nav.js +53 -14
  27. package/dist/code-browser-pair-nav.js.map +1 -1
  28. package/dist/code-browser-scroll-buffer-equalize.d.ts +25 -0
  29. package/dist/code-browser-scroll-buffer-equalize.d.ts.map +1 -0
  30. package/dist/code-browser-scroll-buffer-equalize.js +316 -0
  31. package/dist/code-browser-scroll-buffer-equalize.js.map +1 -0
  32. package/dist/code-browser-scroll-sync-monotonic.d.ts +17 -0
  33. package/dist/code-browser-scroll-sync-monotonic.d.ts.map +1 -0
  34. package/dist/code-browser-scroll-sync-monotonic.js +22 -0
  35. package/dist/code-browser-scroll-sync-monotonic.js.map +1 -0
  36. package/dist/code-browser-scroll-sync-strategy.d.ts +12 -0
  37. package/dist/code-browser-scroll-sync-strategy.d.ts.map +1 -0
  38. package/dist/code-browser-scroll-sync-strategy.js +28 -0
  39. package/dist/code-browser-scroll-sync-strategy.js.map +1 -0
  40. package/dist/code-browser-scroll-sync.d.ts +2 -2
  41. package/dist/code-browser-scroll-sync.d.ts.map +1 -1
  42. package/dist/code-browser-scroll-sync.js +1 -1
  43. package/dist/code-browser-scroll-sync.js.map +1 -1
  44. package/dist/code-browser-smooth-reveal-dedup.d.ts +25 -0
  45. package/dist/code-browser-smooth-reveal-dedup.d.ts.map +1 -0
  46. package/dist/code-browser-smooth-reveal-dedup.js +25 -0
  47. package/dist/code-browser-smooth-reveal-dedup.js.map +1 -0
  48. package/dist/code-browser.d.ts +25 -8
  49. package/dist/code-browser.d.ts.map +1 -1
  50. package/dist/code-browser.js +359 -86
  51. package/dist/code-browser.js.map +1 -1
  52. package/dist/commentray-anchor-viewport-probe.d.ts +5 -1
  53. package/dist/commentray-anchor-viewport-probe.d.ts.map +1 -1
  54. package/dist/commentray-anchor-viewport-probe.js +8 -2
  55. package/dist/commentray-anchor-viewport-probe.js.map +1 -1
  56. package/dist/index.d.ts +2 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +1 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/inject-md-line-anchors.d.ts +1 -1
  61. package/dist/inject-md-line-anchors.d.ts.map +1 -1
  62. package/dist/inject-md-line-anchors.js +9 -5
  63. package/dist/inject-md-line-anchors.js.map +1 -1
  64. package/dist/markdown-pipeline.js +1 -1
  65. package/dist/markdown-pipeline.js.map +1 -1
  66. package/dist/mermaid-runtime-html.d.ts.map +1 -1
  67. package/dist/mermaid-runtime-html.js +4 -1
  68. package/dist/mermaid-runtime-html.js.map +1 -1
  69. package/dist/reading-viewport-comfort.d.ts +12 -0
  70. package/dist/reading-viewport-comfort.d.ts.map +1 -0
  71. package/dist/reading-viewport-comfort.js +14 -0
  72. package/dist/reading-viewport-comfort.js.map +1 -0
  73. 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, 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
  }
@@ -1012,6 +1030,11 @@ ${CODE_BROWSER_INTRO_STYLES}
1012
1030
  margin: -1px -3px;
1013
1031
  background: color-mix(in oklab, CanvasText 10%, Canvas);
1014
1032
  }
1033
+ .documented-files-tree .tree-file-link:focus-visible {
1034
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
1035
+ outline-offset: 2px;
1036
+ border-radius: 3px;
1037
+ }
1015
1038
  .toolbar button {
1016
1039
  font: inherit;
1017
1040
  font-size: var(--cr-ui-fs);
@@ -1044,6 +1067,10 @@ ${CODE_BROWSER_INTRO_STYLES}
1044
1067
  font: inherit;
1045
1068
  }
1046
1069
  .search-results button.hit:hover { background: color-mix(in oklab, CanvasText 10%, Canvas); }
1070
+ .search-results button.hit:focus-visible {
1071
+ outline: 2px solid color-mix(in oklab, CanvasText 45%, Canvas);
1072
+ outline-offset: 2px;
1073
+ }
1047
1074
  .search-results button.hit .meta { opacity: 0.8; font-size: 12px; }
1048
1075
  .search-results button.hit .src-tag { opacity: 0.75; font-weight: 500; font-size: 11px; }
1049
1076
  .search-results button.hit .snippet {
@@ -1123,7 +1150,66 @@ ${CODE_BROWSER_INTRO_STYLES}
1123
1150
  text-overflow: ellipsis;
1124
1151
  white-space: nowrap;
1125
1152
  }
1126
- .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
+ }
1127
1213
  .pane--code {
1128
1214
  flex: 0 0 var(--split-pct, 46%);
1129
1215
  min-width: 120px; overflow: auto; padding: 12px var(--cr-pane-inline-pad);
@@ -1261,6 +1347,8 @@ ${CODE_BROWSER_INTRO_STYLES}
1261
1347
  .doc-pane-body {
1262
1348
  flex: 1 1 auto; min-height: 0; overflow: auto;
1263
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. */
1264
1352
  /* Inline backtick code chips (GitHub-like): prose context only, never fenced pre/code blocks. */
1265
1353
  .pane--doc .doc-pane-body :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
1266
1354
  .shell--stretch-rows .stretch-preamble :where(p, li, blockquote, td, th, h1, h2, h3, h4, h5, h6) > code,
@@ -1751,6 +1839,22 @@ ${CODE_BROWSER_INTRO_STYLES}
1751
1839
  flex: 1 1 auto;
1752
1840
  padding-left: var(--cr-pane-inline-pad);
1753
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
+ }
1754
1858
  }
1755
1859
  .pane--doc { font-size: 15px; line-height: 1.45; }
1756
1860
  .pane--doc img { max-width: 100%; height: auto; }
@@ -1762,9 +1866,8 @@ ${CODE_BROWSER_INTRO_STYLES}
1762
1866
  .pane--doc .commentray-block-anchor {
1763
1867
  display: block;
1764
1868
  height: 0;
1765
- margin: 14px 0 0;
1869
+ margin: 0;
1766
1870
  border: 0;
1767
- border-top: 1px solid color-mix(in oklab, CanvasText 22%, Canvas);
1768
1871
  pointer-events: none;
1769
1872
  }
1770
1873
  .shell--stretch-rows {
@@ -1772,15 +1875,17 @@ ${CODE_BROWSER_INTRO_STYLES}
1772
1875
  min-height: 0;
1773
1876
  overflow: auto;
1774
1877
  display: block;
1775
- padding: 0 12px 20px;
1878
+ padding: 0 0 20px;
1776
1879
  }
1777
1880
  .shell--stretch-rows .stretch-preamble {
1778
1881
  padding: 8px 4px 16px;
1779
1882
  margin-bottom: 8px;
1780
- border-bottom: 1px solid color-mix(in oklab, CanvasText 12%, Canvas);
1781
1883
  font-size: 15px;
1782
1884
  line-height: 1.45;
1783
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;
1784
1889
  max-width: 100%;
1785
1890
  }
1786
1891
  .shell--stretch-rows .stretch-preamble img { max-width: 100%; height: auto; }
@@ -1789,27 +1894,49 @@ ${CODE_BROWSER_INTRO_STYLES}
1789
1894
  border-collapse: collapse;
1790
1895
  table-layout: fixed;
1791
1896
  }
1792
- .stretch-col-code { width: 50%; }
1793
- .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%)); }
1794
1902
  .block-stretch td.stretch-code {
1795
1903
  vertical-align: top;
1796
1904
  padding: 0 12px 0 0;
1797
- 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;
1798
1922
  }
1799
1923
  .block-stretch td.stretch-doc {
1800
1924
  vertical-align: top;
1801
1925
  padding: 0 0 0 12px;
1802
- 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);
1803
1927
  }
1804
1928
  .block-stretch td.stretch-doc .stretch-doc-inner {
1805
1929
  font-size: 15px;
1806
1930
  line-height: 1.45;
1807
1931
  min-width: 0;
1808
1932
  overflow-x: auto;
1933
+ /* Keep vertical scrolling on the shell; see stretch-preamble rule above. */
1934
+ overflow-y: hidden;
1809
1935
  }
1810
1936
  .block-stretch td.stretch-doc .stretch-doc-inner img { max-width: 100%; height: auto; }
1811
1937
  .block-stretch td.stretch-doc .stretch-doc-inner .commentray-mermaid {
1812
1938
  overflow-x: auto;
1939
+ overflow-y: hidden;
1813
1940
  max-width: 100%;
1814
1941
  }
1815
1942
  .block-stretch.wrap td.stretch-doc .stretch-doc-inner {
@@ -1828,7 +1955,6 @@ ${CODE_BROWSER_INTRO_STYLES}
1828
1955
  font-size: 13px;
1829
1956
  vertical-align: top;
1830
1957
  }
1831
- .block-stretch .stretch-gap-mark { display: inline-block; padding-top: 2px; }
1832
1958
  .block-stretch .stretch-code-stack {
1833
1959
  display: flex;
1834
1960
  flex-direction: column;
@@ -1878,15 +2004,39 @@ ${CODE_BROWSER_INTRO_STYLES}
1878
2004
  .block-stretch:not(.wrap) .stretch-doc-inner pre code {
1879
2005
  white-space: pre;
1880
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
+ }
1881
2031
  `;
1882
2032
  /** Native tooltip on #search-q (short hint is visible under the search row). */
1883
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).";
1884
2034
  function buildCodeBrowserPageHtml(p) {
1885
2035
  const shellClass = p.layout === "stretch" ? "shell shell--stretch-rows" : "shell";
1886
- const dualFlipControlHtml = p.layout === "dual"
2036
+ const dualFlipControlHtml = p.layout === "dual" || p.layout === "stretch"
1887
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>`
1888
2038
  : "";
1889
- const dualFlipScrollAffordanceHtml = p.layout === "dual"
2039
+ const dualFlipScrollAffordanceHtml = p.layout === "dual" || p.layout === "stretch"
1890
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>`
1891
2041
  : "";
1892
2042
  return `<!doctype html>
@@ -1915,13 +2065,13 @@ ${CODE_BROWSER_STYLES}
1915
2065
  ${p.toolbarSiteHubHtml}
1916
2066
  ${p.navRailDocumentedHtml}
1917
2067
  ${p.angleSelectHtml}
2068
+ ${dualFlipControlHtml}
1918
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).">
1919
2070
  <input type="checkbox" id="wrap-lines" class="toolbar-wrap-lines__input" />
1920
2071
  <span class="toolbar-wrap-lines__box" aria-hidden="true"></span>
1921
2072
  <span class="toolbar-wrap-lines__face" aria-hidden="true">${TOOLBAR_ICON_WRAP_SVG}</span>
1922
2073
  <span class="toolbar-wrap-lines__caption">Wrap lines</span>
1923
2074
  </label>
1924
- ${dualFlipControlHtml}
1925
2075
  ${p.sourceMarkdownToggleHtml}
1926
2076
  ${p.toolbarDocHubHtml}
1927
2077
  ${p.relatedNavHtml}
@@ -1945,7 +2095,7 @@ ${TOOLBAR_COLOR_THEME_HTML}
1945
2095
  <div class="search-results" id="search-results" hidden aria-live="polite"></div>
1946
2096
  </header>
1947
2097
  <main id="main-content" class="app__main" tabindex="-1">
1948
- <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 ?? ""}>
1949
2099
  ${p.shellInner}
1950
2100
  </div>
1951
2101
  </main>
@@ -1962,6 +2112,27 @@ ${loadCodeBrowserClientBundle()}
1962
2112
  function firstNonEmpty(values) {
1963
2113
  return values.find((v) => v.trim().length > 0);
1964
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
+ }
1965
2136
  function resolveMultiAngleDefaultSelection(args) {
1966
2137
  const { multi, defaultId, opts, builtAngles } = args;
1967
2138
  let defaultMarkdown = opts.commentrayMarkdown;
@@ -1999,11 +2170,8 @@ function resolveMultiAngleDefaultSelection(args) {
1999
2170
  }
2000
2171
  async function multiAngleJsonRowAndDocHtml(opts, spec) {
2001
2172
  const rows = spec.blockStretchRows;
2173
+ const rowsPathOk = angleBlockStretchRowsPathOk(spec, opts);
2002
2174
  const angleCrNorm = normalizeRepoRelativePath(spec.commentrayPathRel.replaceAll("\\", "/"));
2003
- const primaryNorm = normalizeRepoRelativePath((opts.filePath ?? "").replaceAll("\\", "/"));
2004
- const rowsPathOk = rows !== undefined &&
2005
- normalizeRepoRelativePath(rows.commentrayPathRel.replaceAll("\\", "/")) === angleCrNorm &&
2006
- normalizeRepoRelativePath(rows.sourceRelative.replaceAll("\\", "/")) === primaryNorm;
2007
2175
  const links = rows !== undefined && rowsPathOk
2008
2176
  ? buildBlockScrollLinks(rows.index, rows.sourceRelative, angleCrNorm, spec.markdown, opts.code)
2009
2177
  : [];
@@ -2027,6 +2195,85 @@ async function multiAngleJsonRowAndDocHtml(opts, spec) {
2027
2195
  scrollB64,
2028
2196
  };
2029
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
+ }
2030
2277
  async function buildMultiAngleDualPaneShell(opts, multi) {
2031
2278
  const defaultId = multi.angles.some((a) => a.id === multi.defaultAngleId)
2032
2279
  ? multi.defaultAngleId
@@ -2050,14 +2297,8 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
2050
2297
  jsonAngles.push(jsonRow);
2051
2298
  }
2052
2299
  const { defaultMarkdown, defaultScrollB64, defaultPathSearch, defaultGh, defaultStaticBrowse, defaultPaneHtml, } = resolveMultiAngleDefaultSelection({ multi, defaultId, opts, builtAngles });
2053
- const selOpts = multi.angles
2054
- .map((a) => {
2055
- const lab = escapeHtml(a.title?.trim() || a.id);
2056
- return `<option value="${escapeHtml(a.id)}"${a.id === defaultId ? " selected" : ""}>${lab}</option>`;
2057
- })
2058
- .join("");
2059
- 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>`;
2060
- const pairHtml = renderShellPairContextHtml(opts.filePath, defaultPathSearch);
2300
+ const angleSelectHtml = multiAngleToolbarAngleSelectHtml(multi, defaultId);
2301
+ const pairHtml = renderShellPairContextHtml(shellPairSourcePath(opts.filePath, opts.blockStretchRows?.sourceRelative), defaultPathSearch);
2061
2302
  const shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, defaultPaneHtml, sourceMarkdownPaneHtml));
2062
2303
  const payloadObj = { defaultAngleId: defaultId, angles: jsonAngles };
2063
2304
  const multiAnglePayloadB64 = Buffer.from(JSON.stringify(payloadObj), "utf8").toString("base64");
@@ -2076,26 +2317,47 @@ async function buildMultiAngleDualPaneShell(opts, multi) {
2076
2317
  sourcePaneDefaultMode: sourceMarkdownEnabled ? "rendered-markdown" : "source",
2077
2318
  };
2078
2319
  }
2079
- async function buildCodeBrowserShell(opts, layoutPref) {
2080
- let layout = "dual";
2081
- 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);
2082
2326
  let scrollBlockLinksB64 = "";
2083
- const multi = opts.multiAngleBrowsing;
2084
- const multiActive = Boolean(multi && multi.angles.length >= 2);
2085
- if (multiActive && multi) {
2086
- const built = await buildMultiAngleDualPaneShell(opts, multi);
2087
- const ms = built.multiShell;
2088
- return {
2089
- layout: "dual",
2090
- shellInner: built.shellInner,
2091
- scrollBlockLinksB64: ms.scrollBlockLinksB64,
2092
- angleSelectHtml: built.angleSelectHtml,
2093
- multiAnglePayloadB64: built.multiAnglePayloadB64,
2094
- sourceMarkdownToggleEnabled: built.sourceMarkdownToggleEnabled,
2095
- sourcePaneDefaultMode: built.sourcePaneDefaultMode,
2096
- multiShell: ms,
2097
- };
2327
+ if (links.length > 0) {
2328
+ scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
2098
2329
  }
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: sourceMarkdownEnabled ? "rendered-markdown" : "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;
2099
2361
  if (opts.blockStretchRows && layoutPref !== "dual") {
2100
2362
  const stretched = await tryBuildBlockStretchTableHtml({
2101
2363
  code: opts.code,
@@ -2105,45 +2367,24 @@ async function buildCodeBrowserShell(opts, layoutPref) {
2105
2367
  sourceRelative: opts.blockStretchRows.sourceRelative,
2106
2368
  commentrayPathRel: opts.blockStretchRows.commentrayPathRel,
2107
2369
  commentrayOutputUrls: opts.commentrayOutputUrls,
2370
+ sourceMarkdownOutputUrls: sourceMarkdownUrls,
2371
+ stretchBufferSync: stretchBufferSyncFromOpts(opts),
2108
2372
  });
2109
2373
  if (stretched) {
2110
2374
  layout = "stretch";
2111
- shellInner = ` ${stretched.preambleHtml}\n` + ` ${stretched.tableInnerHtml}\n`;
2375
+ shellInner = wrapShellInnerWithPairContext(renderShellPairContextHtml(shellPairSourcePath(opts.filePath, opts.blockStretchRows?.sourceRelative), shellPairCommentrayPath(opts.commentrayPathForSearch, opts.blockStretchRows?.commentrayPathRel)), ` ${stretched.preambleHtml}\n` + ` ${stretched.tableInnerHtml}\n`);
2112
2376
  }
2113
2377
  }
2114
2378
  if (layout === "dual") {
2115
- const links = opts.blockStretchRows !== undefined
2116
- ? buildBlockScrollLinks(opts.blockStretchRows.index, opts.blockStretchRows.sourceRelative, opts.blockStretchRows.commentrayPathRel, opts.commentrayMarkdown, opts.code)
2117
- : [];
2118
- const mdForDoc = injectCommentrayDocAnchors(opts.commentrayMarkdown, links.length > 0 ? links : undefined);
2119
- if (links.length > 0) {
2120
- scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
2121
- }
2122
- const sourceMarkdownEnabled = isMarkdownLikeSource(opts);
2123
- const sourceMdForPane = sourceMarkdownEnabled ? injectSourceMarkdownAnchors(opts.code) : "";
2124
- const sourcePaneUrls = sourcePaneOutputUrls(opts);
2125
- const [codeHtml, commentrayHtml, sourceMarkdownPaneHtml] = await Promise.all([
2126
- renderHighlightedCodeLineRows(opts.code, opts.language),
2127
- renderMarkdownToHtml(mdForDoc, {
2128
- commentrayOutputUrls: opts.commentrayOutputUrls,
2129
- }),
2130
- sourceMarkdownEnabled
2131
- ? renderMarkdownToHtml(sourceMdForPane, {
2132
- commentrayOutputUrls: sourcePaneUrls,
2133
- })
2134
- : Promise.resolve(""),
2135
- ]);
2136
- const pairHtml = renderShellPairContextHtml(opts.filePath, (opts.commentrayPathForSearch ?? "").trim());
2137
- shellInner = wrapDualShellInner(pairHtml, dualPanePanesInnerHtml(codeHtml, commentrayHtml, sourceMarkdownPaneHtml));
2138
- return {
2139
- layout,
2140
- shellInner,
2141
- scrollBlockLinksB64,
2142
- angleSelectHtml: "",
2143
- multiAnglePayloadB64: "",
2144
- sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2145
- sourcePaneDefaultMode: sourceMarkdownEnabled ? "rendered-markdown" : "source",
2146
- };
2379
+ return buildDualPaneSingleAngleShell(opts);
2380
+ }
2381
+ const rows = opts.blockStretchRows;
2382
+ const links = rows !== undefined
2383
+ ? buildBlockScrollLinks(rows.index, rows.sourceRelative, rows.commentrayPathRel, opts.commentrayMarkdown, opts.code)
2384
+ : [];
2385
+ let scrollBlockLinksB64 = "";
2386
+ if (links.length > 0) {
2387
+ scrollBlockLinksB64 = Buffer.from(JSON.stringify(links), "utf8").toString("base64");
2147
2388
  }
2148
2389
  return {
2149
2390
  layout,
@@ -2151,10 +2392,35 @@ async function buildCodeBrowserShell(opts, layoutPref) {
2151
2392
  scrollBlockLinksB64,
2152
2393
  angleSelectHtml: "",
2153
2394
  multiAnglePayloadB64: "",
2154
- sourceMarkdownToggleEnabled: false,
2395
+ sourceMarkdownToggleEnabled: sourceMarkdownEnabled,
2155
2396
  sourcePaneDefaultMode: "source",
2397
+ stretchBufferSync: stretchBufferSyncFromOpts(opts),
2156
2398
  };
2157
2399
  }
2400
+ async function buildCodeBrowserShell(opts, layoutPref) {
2401
+ const multi = opts.multiAngleBrowsing;
2402
+ const multiActive = Boolean(multi && multi.angles.length >= 2);
2403
+ if (multiActive && multi) {
2404
+ if (layoutPref !== "dual") {
2405
+ const stretchMulti = await buildMultiAngleBlockStretchShell(opts, multi);
2406
+ if (stretchMulti !== null)
2407
+ return stretchMulti;
2408
+ }
2409
+ const built = await buildMultiAngleDualPaneShell(opts, multi);
2410
+ const ms = built.multiShell;
2411
+ return {
2412
+ layout: "dual",
2413
+ shellInner: built.shellInner,
2414
+ scrollBlockLinksB64: ms.scrollBlockLinksB64,
2415
+ angleSelectHtml: built.angleSelectHtml,
2416
+ multiAnglePayloadB64: built.multiAnglePayloadB64,
2417
+ sourceMarkdownToggleEnabled: built.sourceMarkdownToggleEnabled,
2418
+ sourcePaneDefaultMode: built.sourcePaneDefaultMode,
2419
+ multiShell: ms,
2420
+ };
2421
+ }
2422
+ return buildSingleAngleCodeBrowserShell(opts, layoutPref);
2423
+ }
2158
2424
  function searchChromeFromOptions(opts, commentrayPathOverride) {
2159
2425
  const crPath = (commentrayPathOverride ?? opts.commentrayPathForSearch ?? "").trim();
2160
2426
  if (opts.staticSearchScope === "commentray-and-paths") {
@@ -2202,8 +2468,6 @@ function shellPairIdentityDataAttrs(shell, opts) {
2202
2468
  }
2203
2469
  /** Canonical doc target for static validation: same-site `./browse/…` when present, else GitHub blob. */
2204
2470
  function shellPairDocDataAttr(shell, opts) {
2205
- if (shell.layout !== "dual")
2206
- return "";
2207
2471
  const browseRaw = (shell.multiShell?.commentrayStaticBrowseUrl ??
2208
2472
  opts.commentrayStaticBrowseUrl ??
2209
2473
  "").trim();
@@ -2263,6 +2527,13 @@ export async function renderCodeBrowserHtml(opts) {
2263
2527
  const pairIdentityDataAttrs = shellPairIdentityDataAttrs(shell, opts);
2264
2528
  const sourceMarkdownToggles = sourceMarkdownToggleControlsHtml(shell.sourceMarkdownToggleEnabled);
2265
2529
  const sourcePaneModeAttr = ` data-source-pane-mode="${shell.sourcePaneDefaultMode}"`;
2530
+ const scrollSyncStrategyShellAttr = opts.dualPaneScrollSyncStrategy !== undefined &&
2531
+ opts.dualPaneScrollSyncStrategy !== DEFAULT_DUAL_PANE_SCROLL_SYNC_STRATEGY
2532
+ ? ` data-scroll-sync-strategy="${escapeHtml(opts.dualPaneScrollSyncStrategy)}"`
2533
+ : "";
2534
+ const stretchBufferSyncShellAttr = shell.layout === "stretch" && shell.stretchBufferSync === "flow-synchronizer"
2535
+ ? ` data-stretch-buffer-sync="flow-synchronizer"`
2536
+ : "";
2266
2537
  return buildCodeBrowserPageHtml({
2267
2538
  title,
2268
2539
  metaDescriptionHtml,
@@ -2291,6 +2562,8 @@ export async function renderCodeBrowserHtml(opts) {
2291
2562
  sourceMarkdownToggleHtml: sourceMarkdownToggles.sourceMarkdownToggleHtml,
2292
2563
  sourceMarkdownFlipScrollAffordanceHtml: sourceMarkdownToggles.sourceMarkdownFlipScrollAffordanceHtml,
2293
2564
  sourcePaneModeAttr,
2565
+ scrollSyncStrategyShellAttr,
2566
+ stretchBufferSyncShellAttr,
2294
2567
  });
2295
2568
  }
2296
2569
  //# sourceMappingURL=code-browser.js.map