@commentray/render 0.0.9 → 0.1.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 (82) hide show
  1. package/README.md +1 -1
  2. package/dist/block-stretch-layout.d.ts.map +1 -1
  3. package/dist/block-stretch-layout.js +2 -1
  4. package/dist/block-stretch-layout.js.map +1 -1
  5. package/dist/browse-page-slug.d.ts +6 -1
  6. package/dist/browse-page-slug.d.ts.map +1 -1
  7. package/dist/browse-page-slug.js +6 -1
  8. package/dist/browse-page-slug.js.map +1 -1
  9. package/dist/browse-pair-html-test-fixtures.d.ts +10 -0
  10. package/dist/browse-pair-html-test-fixtures.d.ts.map +1 -0
  11. package/dist/browse-pair-html-test-fixtures.js +19 -0
  12. package/dist/browse-pair-html-test-fixtures.js.map +1 -0
  13. package/dist/build-commentray-nav-search.d.ts +4 -4
  14. package/dist/build-commentray-nav-search.js +1 -1
  15. package/dist/code-browser-block-rays.d.ts +29 -0
  16. package/dist/code-browser-block-rays.d.ts.map +1 -1
  17. package/dist/code-browser-block-rays.js +120 -0
  18. package/dist/code-browser-block-rays.js.map +1 -1
  19. package/dist/code-browser-client.bundle.js +24 -11
  20. package/dist/code-browser-client.js +766 -116
  21. package/dist/code-browser-client.js.map +1 -1
  22. package/dist/code-browser-intro.css +187 -0
  23. package/dist/code-browser-pair-nav.d.ts +3 -3
  24. package/dist/code-browser-pair-nav.d.ts.map +1 -1
  25. package/dist/code-browser-pair-nav.js +25 -13
  26. package/dist/code-browser-pair-nav.js.map +1 -1
  27. package/dist/code-browser-wide-intro-controller.d.ts +4 -0
  28. package/dist/code-browser-wide-intro-controller.d.ts.map +1 -0
  29. package/dist/code-browser-wide-intro-controller.js +148 -0
  30. package/dist/code-browser-wide-intro-controller.js.map +1 -0
  31. package/dist/code-browser-wide-intro-layout.d.ts +3 -0
  32. package/dist/code-browser-wide-intro-layout.d.ts.map +1 -0
  33. package/dist/code-browser-wide-intro-layout.js +84 -0
  34. package/dist/code-browser-wide-intro-layout.js.map +1 -0
  35. package/dist/code-browser-wide-intro-steps.d.ts +11 -0
  36. package/dist/code-browser-wide-intro-steps.d.ts.map +1 -0
  37. package/dist/code-browser-wide-intro-steps.js +108 -0
  38. package/dist/code-browser-wide-intro-steps.js.map +1 -0
  39. package/dist/code-browser-wide-intro-ui.d.ts +14 -0
  40. package/dist/code-browser-wide-intro-ui.d.ts.map +1 -0
  41. package/dist/code-browser-wide-intro-ui.js +67 -0
  42. package/dist/code-browser-wide-intro-ui.js.map +1 -0
  43. package/dist/code-browser.d.ts +18 -4
  44. package/dist/code-browser.d.ts.map +1 -1
  45. package/dist/code-browser.js +506 -154
  46. package/dist/code-browser.js.map +1 -1
  47. package/dist/commentray-anchor-viewport-probe.d.ts +9 -0
  48. package/dist/commentray-anchor-viewport-probe.d.ts.map +1 -0
  49. package/dist/commentray-anchor-viewport-probe.js +13 -0
  50. package/dist/commentray-anchor-viewport-probe.js.map +1 -0
  51. package/dist/commentray-preview-html.d.ts +13 -0
  52. package/dist/commentray-preview-html.d.ts.map +1 -0
  53. package/dist/commentray-preview-html.js +12 -0
  54. package/dist/commentray-preview-html.js.map +1 -0
  55. package/dist/companion-markdown-preview-entry.d.ts +7 -0
  56. package/dist/companion-markdown-preview-entry.d.ts.map +1 -0
  57. package/dist/companion-markdown-preview-entry.js +6 -0
  58. package/dist/companion-markdown-preview-entry.js.map +1 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/inject-md-line-anchors.d.ts +18 -0
  64. package/dist/inject-md-line-anchors.d.ts.map +1 -0
  65. package/dist/inject-md-line-anchors.js +250 -0
  66. package/dist/inject-md-line-anchors.js.map +1 -0
  67. package/dist/inline-favicon.js +15 -15
  68. package/dist/markdown-pipeline.d.ts +5 -0
  69. package/dist/markdown-pipeline.d.ts.map +1 -1
  70. package/dist/markdown-pipeline.js +47 -1
  71. package/dist/markdown-pipeline.js.map +1 -1
  72. package/dist/side-by-side-layout-css.d.ts +1 -1
  73. package/dist/side-by-side-layout-css.d.ts.map +1 -1
  74. package/dist/side-by-side-layout-css.js +48 -0
  75. package/dist/side-by-side-layout-css.js.map +1 -1
  76. package/package.json +8 -3
  77. package/dist/code-browser-client.d.ts +0 -2
  78. package/dist/side-by-side-layout.css +0 -58
  79. package/dist/side-by-side-layout.embedded.d.ts +0 -3
  80. package/dist/side-by-side-layout.embedded.d.ts.map +0 -1
  81. package/dist/side-by-side-layout.embedded.js +0 -3
  82. package/dist/side-by-side-layout.embedded.js.map +0 -1
@@ -1,11 +1,13 @@
1
1
  import { FuzzySearcher, PrefixSearcher, Query, SearcherFactory, SubstringSearcher, } from "@m31coding/fuzzy-search";
2
- import { activeBlockIdForViewport, clampViewportYToGutterLocal, codeLineDomIndex0, gutterRayBezierPaths, sortBlockLinksBySource, } from "./code-browser-block-rays.js";
2
+ import { activeBlockIdForCommentrayLine0, activeBlockIdForViewport, clampViewportYToGutterLocal, codeLineDomIndex0, dedupeBlockScrollLinksById, gutterRayBezierPaths, maxRenderableCommentaryContentBottomViewport, nextBlockLinkInCommentrayOrder, sortBlockLinksBySource, } from "./code-browser-block-rays.js";
3
3
  import { mirroredScrollTop, pickCommentrayLineForSourceScroll, pickSourceLine0ForCommentrayScroll, } from "./code-browser-scroll-sync.js";
4
+ import { maxCommentrayAnchorLine0AtOrAboveViewportY } from "./commentray-anchor-viewport-probe.js";
4
5
  import { decodeBase64Utf8 } from "./code-browser-encoding.js";
5
6
  import { readEmbeddedRawB64Strings } from "./code-browser-embedded-payload.js";
6
7
  import { escapeHtmlHighlightingSearchTokens, filterPairsByDocumentedTreeQuery, findOrderedTokenSpans, lineAtIndex, offsetToLineIndex, pathRowsFromDocumentedPairs, tokenizeQuery, uniqueSourceFilePreviewRows, } from "./code-browser-search.js";
7
- import { findDocumentedPair, isHubRelativeStaticBrowseHref, isSameDocumentedPair, normPosixPath, resolveStaticBrowseHref, staticBrowseHrefForShellDataAttribute, } from "./code-browser-pair-nav.js";
8
+ import { findDocumentedPair, isHubRelativeStaticBrowseHref, isSameDocumentedPair, normPosixPath, resolveStaticBrowseHref, siteRootPathnameFromPathname, staticBrowseHrefForShellDataAttribute, } from "./code-browser-pair-nav.js";
8
9
  import { COMMENTRAY_COLOR_THEME_STORAGE_KEY, applyCommentrayColorTheme, nextCommentrayColorThemeMode, parseCommentrayColorThemeMode, } from "./code-browser-color-theme.js";
10
+ import { wireWideModeIntroTour } from "./code-browser-wide-intro-controller.js";
9
11
  import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
10
12
  /**
11
13
  * Hub pages emit `./browse/…` relative to the site root. From `/…/browse/current.html` the browser
@@ -104,9 +106,13 @@ function windowScrollRatio() {
104
106
  const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
105
107
  return maxY > 0 ? clamp(root.scrollTop / maxY, 0, 1) : 0;
106
108
  }
107
- function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
109
+ function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan, lineIdPrefix = "code-line-") {
110
+ const narrowSinglePane = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ).matches;
108
111
  if (plan.k === "block") {
109
- const el = codePane.querySelector(`#code-line-${String(plan.src0)}`);
112
+ const exact = codePane.querySelector(`#${lineIdPrefix}${String(plan.src0)}`);
113
+ const el = exact instanceof HTMLElement
114
+ ? exact
115
+ : findAnchorAtOrAfter(sourceAnchorsFromPrefix(lineIdPrefix), plan.src0);
110
116
  if (el) {
111
117
  applyRevealChildInPane(codePane, el, 2);
112
118
  }
@@ -119,6 +125,8 @@ function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
119
125
  if (paneUsesInternalYScroll(codePane)) {
120
126
  const maxC = Math.max(0, codePane.scrollHeight - codePane.clientHeight);
121
127
  applyScrollTopClamped(codePane, plan.ratio * maxC);
128
+ if (narrowSinglePane)
129
+ applyWindowScrollRatio(plan.ratio);
122
130
  }
123
131
  else {
124
132
  applyWindowScrollRatio(plan.ratio);
@@ -128,6 +136,10 @@ function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
128
136
  const nextTop = mirroredScrollTop(plan.docTop, plan.docSH, plan.docCH, codePane.scrollHeight, codePane.clientHeight);
129
137
  if (paneUsesInternalYScroll(codePane)) {
130
138
  applyScrollTopClamped(codePane, nextTop);
139
+ if (narrowSinglePane) {
140
+ const denom = Math.max(1, codePane.scrollHeight - codePane.clientHeight);
141
+ applyWindowScrollRatio(clamp(nextTop / denom, 0, 1));
142
+ }
131
143
  return;
132
144
  }
133
145
  const denom = Math.max(1, codePane.scrollHeight - codePane.clientHeight);
@@ -163,7 +175,12 @@ function applyCodeToDocFlipPlanImpl(_codePane, docPane, plan) {
163
175
  applyWindowScrollRatio(clamp(nextTop / denom, 0, 1));
164
176
  }
165
177
  function buildDocToCodeFlipPlanBlockAware(docPane, getLinks) {
166
- const winRatio = windowScrollRatio();
178
+ const winRatio = paneUsesInternalYScroll(docPane)
179
+ ? clamp(docPane.scrollTop / Math.max(1, docPane.scrollHeight - docPane.clientHeight), 0, 1)
180
+ : windowScrollRatio();
181
+ const pulledSrc0 = pulledSourceLine0FromPageBreak(docPane);
182
+ if (pulledSrc0 !== null)
183
+ return { k: "block", src0: pulledSrc0, winRatio };
167
184
  const links = getLinks();
168
185
  const mdLine0 = probeCommentrayLine0FromDoc(docPane);
169
186
  const src0 = pickSourceLine0ForCommentrayScroll(links, mdLine0);
@@ -179,10 +196,10 @@ function buildDocToCodeFlipPlanBlockAware(docPane, getLinks) {
179
196
  }
180
197
  return { k: "mirrorW", ratio: winRatio };
181
198
  }
182
- function buildCodeToDocFlipPlanBlockAware(codePane, _docPane, getLinks) {
199
+ function buildCodeToDocFlipPlanBlockAware(codePane, _docPane, getLinks, lineIdPrefix = "code-line-") {
183
200
  const winRatio = windowScrollRatio();
184
201
  const links = getLinks();
185
- const line1 = probeCodeLine1FromViewport(codePane);
202
+ const line1 = probeCodeLine1FromViewport(codePane, lineIdPrefix);
186
203
  const mdLine0 = pickCommentrayLineForSourceScroll(links, line1);
187
204
  if (mdLine0 === null) {
188
205
  if (paneUsesInternalYScroll(codePane)) {
@@ -684,11 +701,22 @@ function parseScrollBlockLinksFromShell(b64) {
684
701
  typeof o.commentrayLine === "number" &&
685
702
  typeof o.sourceStart === "number" &&
686
703
  typeof o.sourceEnd === "number") {
704
+ const mvRaw = o.markerViewportHalfOpen1Based;
705
+ const mv = typeof mvRaw === "object" &&
706
+ mvRaw !== null &&
707
+ typeof mvRaw.lo === "number" &&
708
+ typeof mvRaw.hiExclusive === "number"
709
+ ? {
710
+ lo: mvRaw.lo,
711
+ hiExclusive: mvRaw.hiExclusive,
712
+ }
713
+ : { lo: o.sourceStart, hiExclusive: o.sourceEnd + 1 };
687
714
  out.push({
688
715
  id: o.id,
689
716
  commentrayLine: o.commentrayLine,
690
717
  sourceStart: o.sourceStart,
691
718
  sourceEnd: o.sourceEnd,
719
+ markerViewportHalfOpen1Based: mv,
692
720
  });
693
721
  }
694
722
  }
@@ -703,14 +731,41 @@ function rootScrollNearDocumentEnd(edgePx = 56) {
703
731
  const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
704
732
  return maxY > 0 && root.scrollTop >= maxY - edgePx;
705
733
  }
706
- function probeCodeLine1FromViewport(codePane) {
707
- const rows = codePane.querySelectorAll('[id^="code-line-"]');
734
+ /** When the pane itself is the scrollport (dual desktop), mirror root “near end” behavior. */
735
+ function paneScrollNearEnd(pane, edgePx = 56) {
736
+ const maxY = Math.max(0, pane.scrollHeight - pane.clientHeight);
737
+ return maxY > 0 && pane.scrollTop >= maxY - edgePx;
738
+ }
739
+ function readCommentrayLine0FromAnchor(el) {
740
+ const lineAttr = el.getAttribute("data-commentray-line");
741
+ if (lineAttr === null || lineAttr === "")
742
+ return null;
743
+ return Number(lineAttr);
744
+ }
745
+ function bestCommentrayAnchorLine0AtOrAboveY(anchors, y) {
746
+ const readings = [];
747
+ for (const a of anchors) {
748
+ const line0 = readCommentrayLine0FromAnchor(a);
749
+ if (line0 === null)
750
+ continue;
751
+ readings.push({ line0, top: a.getBoundingClientRect().top });
752
+ }
753
+ return maxCommentrayAnchorLine0AtOrAboveViewportY(readings, y);
754
+ }
755
+ function lastCommentrayAnchorLine0(anchors) {
756
+ const last = anchors[anchors.length - 1];
757
+ if (!last)
758
+ return 0;
759
+ return readCommentrayLine0FromAnchor(last) ?? 0;
760
+ }
761
+ function probeCodeLine1FromViewport(codePane, lineIdPrefix = "code-line-") {
762
+ const rows = codePane.querySelectorAll(`[id^="${lineIdPrefix}"]`);
708
763
  if (rows.length === 0)
709
764
  return 1;
710
765
  if (!paneUsesInternalYScroll(codePane)) {
711
766
  if (rootScrollNearDocumentEnd()) {
712
767
  const last = rows[rows.length - 1];
713
- const m = /^code-line-(\d+)$/.exec(last.id);
768
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(last.id);
714
769
  if (m)
715
770
  return Number(m[1]) + 1;
716
771
  return rows.length;
@@ -723,7 +778,7 @@ function probeCodeLine1FromViewport(codePane) {
723
778
  for (const el of rows) {
724
779
  const r = el.getBoundingClientRect();
725
780
  if (r.bottom > y - 1e-3) {
726
- const m = /^code-line-(\d+)$/.exec(el.id);
781
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(el.id);
727
782
  if (m)
728
783
  return Number(m[1]) + 1;
729
784
  return 1;
@@ -731,12 +786,19 @@ function probeCodeLine1FromViewport(codePane) {
731
786
  }
732
787
  return rows.length;
733
788
  }
789
+ if (paneScrollNearEnd(codePane)) {
790
+ const last = rows[rows.length - 1];
791
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(last.id);
792
+ if (m)
793
+ return Number(m[1]) + 1;
794
+ return rows.length;
795
+ }
734
796
  const sr = codePane.getBoundingClientRect();
735
797
  const y = sr.top + codePane.clientTop + 2;
736
798
  for (const el of rows) {
737
799
  const r = el.getBoundingClientRect();
738
800
  if (r.bottom > y - 1e-3) {
739
- const m = /^code-line-(\d+)$/.exec(el.id);
801
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(el.id);
740
802
  if (m)
741
803
  return Number(m[1]) + 1;
742
804
  return 1;
@@ -749,41 +811,75 @@ function probeCommentrayLine0FromDoc(docPane) {
749
811
  if (anchors.length === 0)
750
812
  return 0;
751
813
  if (!paneUsesInternalYScroll(docPane)) {
752
- if (rootScrollNearDocumentEnd()) {
753
- const last = anchors[anchors.length - 1];
754
- const lineAttr = last.getAttribute("data-commentray-line");
755
- return lineAttr !== null && lineAttr !== "" ? Number(lineAttr) : 0;
756
- }
814
+ if (rootScrollNearDocumentEnd())
815
+ return lastCommentrayAnchorLine0(anchors);
757
816
  const dr = docPane.getBoundingClientRect();
758
817
  const vh = globalThis.innerHeight;
759
818
  const clipT = Math.max(0, dr.top);
760
819
  const clipB = Math.min(vh, dr.bottom);
761
820
  const y = clipT + Math.max(2, Math.min(40, (clipB - clipT) * 0.15));
762
- let best = 0;
763
- for (const a of anchors) {
764
- const lineAttr = a.getAttribute("data-commentray-line");
765
- if (lineAttr === null || lineAttr === "")
766
- continue;
767
- if (a.getBoundingClientRect().top <= y + 1 + 1e-3)
768
- best = Number(lineAttr);
769
- else
770
- break;
771
- }
772
- return best;
821
+ return bestCommentrayAnchorLine0AtOrAboveY(anchors, y);
773
822
  }
823
+ if (paneScrollNearEnd(docPane))
824
+ return lastCommentrayAnchorLine0(anchors);
774
825
  const dr = docPane.getBoundingClientRect();
775
- const y = dr.top + docPane.clientTop + 2;
776
- let best = 0;
777
- for (const a of anchors) {
778
- const lineAttr = a.getAttribute("data-commentray-line");
779
- if (lineAttr === null || lineAttr === "")
826
+ /** Same band as the root-scroll branch: a few px below the pane top so block anchors sit inside `top <= y` while their prose is what the reader sees first. */
827
+ const y = dr.top + docPane.clientTop + Math.max(2, Math.min(40, docPane.clientHeight * 0.15));
828
+ return bestCommentrayAnchorLine0AtOrAboveY(anchors, y);
829
+ }
830
+ function pageBreakPullEnabled() {
831
+ const shell = document.getElementById("shell");
832
+ if (!(shell instanceof HTMLElement))
833
+ return false;
834
+ return shell.getAttribute("data-page-breaks-enabled") === "true";
835
+ }
836
+ function docProbeTopY(docPane) {
837
+ if (!paneUsesInternalYScroll(docPane)) {
838
+ const dr = docPane.getBoundingClientRect();
839
+ const vh = globalThis.innerHeight;
840
+ const clipT = Math.max(0, dr.top);
841
+ const clipB = Math.min(vh, dr.bottom);
842
+ return clipT + Math.max(2, Math.min(40, (clipB - clipT) * 0.15));
843
+ }
844
+ const dr = docPane.getBoundingClientRect();
845
+ return dr.top + docPane.clientTop + 2;
846
+ }
847
+ /**
848
+ * In long synthetic page-break gaps, shift source toward the next block once
849
+ * the break itself occupies the top reading position.
850
+ */
851
+ function pulledSourceLine0FromPageBreak(docPane) {
852
+ if (!pageBreakPullEnabled())
853
+ return null;
854
+ const topY = docProbeTopY(docPane);
855
+ const breaks = Array.from(docPane.querySelectorAll(".commentray-page-break[data-next-source-start]"));
856
+ for (const pageBreak of breaks) {
857
+ const nextSourceStartRaw = pageBreak.getAttribute("data-next-source-start");
858
+ if (!nextSourceStartRaw)
780
859
  continue;
781
- if (a.getBoundingClientRect().top <= y + 1 + 1e-3)
782
- best = Number(lineAttr);
783
- else
784
- break;
860
+ const nextSourceStart = Number.parseInt(nextSourceStartRaw, 10);
861
+ if (!Number.isFinite(nextSourceStart) || nextSourceStart <= 0)
862
+ continue;
863
+ const breakTop = pageBreak.getBoundingClientRect().top;
864
+ const nextLineRaw = pageBreak.getAttribute("data-next-commentray-line");
865
+ const nextLine0 = nextLineRaw ? Number.parseInt(nextLineRaw, 10) : Number.NaN;
866
+ const nextAnchor = Number.isFinite(nextLine0) && nextLine0 >= 0
867
+ ? docPane.querySelector(`[data-commentray-line="${String(nextLine0)}"]`)
868
+ : null;
869
+ const nextTop = nextAnchor
870
+ ? nextAnchor.getBoundingClientRect().top
871
+ : breakTop + pageBreak.clientHeight;
872
+ if (!(breakTop <= topY && topY < nextTop))
873
+ continue;
874
+ const denom = Math.max(1, nextTop - breakTop);
875
+ const progress = clamp((topY - breakTop) / denom, 0, 1);
876
+ const narrow = globalThis.matchMedia("(max-width: 767px)").matches;
877
+ const pullThreshold = narrow ? 0.2 : 0.35;
878
+ if (progress < pullThreshold)
879
+ return null;
880
+ return nextSourceStart - 1;
785
881
  }
786
- return best;
882
+ return null;
787
883
  }
788
884
  /**
789
885
  * Programmatic `scrollTop` on the partner pane can emit a `scroll` event after the
@@ -832,16 +928,20 @@ function wireBidirectionalScroll(codePane, docPane, syncFromCode, syncFromDoc) {
832
928
  }, { passive: true });
833
929
  }
834
930
  /** Index-backed scroll sync when `data-scroll-block-links-b64` is present; else see proportional fallback. */
835
- function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
931
+ function wireBlockAwareScrollSync(codePane, docPane, getLinks, lineIdPrefix, shouldUseProportionalDocToCodeOnMobileFlip) {
836
932
  let pendingDocToCode = null;
837
933
  let pendingCodeToDoc = null;
838
934
  const syncFromCodeToDoc = () => {
839
- applyCodeToDocFlipPlanImpl(codePane, docPane, buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks));
935
+ applyCodeToDocFlipPlanImpl(codePane, docPane, buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks, lineIdPrefix()));
840
936
  };
841
937
  const syncFromDocToCode = () => {
842
- applyDocToCodeFlipPlanImpl(codePane, docPane, buildDocToCodeFlipPlanBlockAware(docPane, getLinks));
938
+ applyDocToCodeFlipPlanImpl(codePane, docPane, buildDocToCodeFlipPlanBlockAware(docPane, getLinks), lineIdPrefix());
843
939
  };
844
940
  const prepareMobileFlipToCode = () => {
941
+ if (shouldUseProportionalDocToCodeOnMobileFlip?.() === true) {
942
+ pendingDocToCode = { k: "mirrorW", ratio: windowScrollRatio() };
943
+ return;
944
+ }
845
945
  pendingDocToCode = buildDocToCodeFlipPlanBlockAware(docPane, getLinks);
846
946
  };
847
947
  const finishMobileFlipToCode = () => {
@@ -849,10 +949,10 @@ function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
849
949
  return;
850
950
  const p = pendingDocToCode;
851
951
  pendingDocToCode = null;
852
- applyDocToCodeFlipPlanImpl(codePane, docPane, p);
952
+ applyDocToCodeFlipPlanImpl(codePane, docPane, p, lineIdPrefix());
853
953
  };
854
954
  const prepareMobileFlipToDoc = () => {
855
- pendingCodeToDoc = buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks);
955
+ pendingCodeToDoc = buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks, lineIdPrefix());
856
956
  };
857
957
  const finishMobileFlipToDoc = () => {
858
958
  if (!pendingCodeToDoc)
@@ -924,18 +1024,76 @@ function centerYInViewport(el) {
924
1024
  function codeLineHighlightCenterYViewport(lineEl) {
925
1025
  return centerYInViewport(lineEl);
926
1026
  }
927
- function commentaryBandEndYViewport(docScrollEl, next, docTop) {
1027
+ function commentaryBandEndYViewport(docScrollEl, next, docTop, clipThroughPageBreakGaps) {
928
1028
  if (next) {
929
1029
  const nextEl = document.getElementById(`commentray-block-${next.id}`);
930
- return nextEl ? nextEl.getBoundingClientRect().top - 3 : centerYInViewport(docTop);
1030
+ if (!nextEl)
1031
+ return centerYInViewport(docTop);
1032
+ const nextTop = nextEl.getBoundingClientRect().top - 3;
1033
+ if (!clipThroughPageBreakGaps)
1034
+ return nextTop;
1035
+ const docBandTop = docTop.getBoundingClientRect().top + 4;
1036
+ const contentBottom = maxRenderableCommentaryContentBottomViewport(docScrollEl, docTop, nextEl);
1037
+ return Math.min(nextTop, Math.max(docBandTop, contentBottom));
931
1038
  }
932
1039
  const dr = docScrollEl.getBoundingClientRect();
933
1040
  let bottom = dr.bottom - 4;
934
1041
  const lastKid = docScrollEl.children[docScrollEl.children.length - 1];
935
1042
  if (lastKid)
936
1043
  bottom = Math.min(bottom, lastKid.getBoundingClientRect().bottom - 4);
1044
+ if (clipThroughPageBreakGaps) {
1045
+ const docBandTop = docTop.getBoundingClientRect().top + 4;
1046
+ const contentBottom = maxRenderableCommentaryContentBottomViewport(docScrollEl, docTop, null);
1047
+ bottom = Math.min(bottom, Math.max(docBandTop, contentBottom));
1048
+ }
937
1049
  return bottom;
938
1050
  }
1051
+ function sourceAnchorIndexFromId(id, prefix) {
1052
+ if (!id.startsWith(prefix))
1053
+ return null;
1054
+ const n = Number.parseInt(id.slice(prefix.length), 10);
1055
+ return Number.isFinite(n) ? n : null;
1056
+ }
1057
+ function findAnchorAtOrAfter(anchors, line0) {
1058
+ let lo = 0;
1059
+ let hi = anchors.length - 1;
1060
+ let ans = -1;
1061
+ while (lo <= hi) {
1062
+ const mid = (lo + hi) >> 1;
1063
+ const line = anchors[mid]?.line0 ?? -1;
1064
+ if (line >= line0) {
1065
+ ans = mid;
1066
+ hi = mid - 1;
1067
+ }
1068
+ else {
1069
+ lo = mid + 1;
1070
+ }
1071
+ }
1072
+ return ans >= 0 ? (anchors[ans]?.el ?? null) : null;
1073
+ }
1074
+ function findAnchorAtOrBefore(anchors, line0) {
1075
+ let lo = 0;
1076
+ let hi = anchors.length - 1;
1077
+ let ans = -1;
1078
+ while (lo <= hi) {
1079
+ const mid = (lo + hi) >> 1;
1080
+ const line = anchors[mid]?.line0 ?? -1;
1081
+ if (line <= line0) {
1082
+ ans = mid;
1083
+ lo = mid + 1;
1084
+ }
1085
+ else {
1086
+ hi = mid - 1;
1087
+ }
1088
+ }
1089
+ return ans >= 0 ? (anchors[ans]?.el ?? null) : null;
1090
+ }
1091
+ function sourceAnchorsFromPrefix(prefix) {
1092
+ return Array.from(document.querySelectorAll(`[id^="${prefix}"]`))
1093
+ .map((el) => ({ line0: sourceAnchorIndexFromId(el.id, prefix), el }))
1094
+ .filter((x) => x.line0 !== null)
1095
+ .sort((a, b) => a.line0 - b.line0);
1096
+ }
939
1097
  function subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw) {
940
1098
  const onScrollOrResize = () => scheduleDraw();
941
1099
  codePane.addEventListener("scroll", onScrollOrResize, { passive: true });
@@ -949,8 +1107,8 @@ function subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw) {
949
1107
  if (shell)
950
1108
  ro.observe(shell);
951
1109
  }
952
- function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based) {
953
- const links = getLinks();
1110
+ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based, lineIdPrefix) {
1111
+ const links = dedupeBlockScrollLinksById(getLinks());
954
1112
  const sorted = sortBlockLinksBySource(links);
955
1113
  const gutterRect = gutter.getBoundingClientRect();
956
1114
  const w = gutterRect.width;
@@ -959,23 +1117,33 @@ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSource
959
1117
  svg.replaceChildren();
960
1118
  return;
961
1119
  }
962
- const activeId = activeBlockIdForViewport(links, probeTopSourceLine1Based());
1120
+ /** Doc-aligned active block matches visible commentary; code-only probe can lag in page gaps. */
1121
+ const activeId = docScrollEl.querySelector(".commentray-block-anchor") !== null
1122
+ ? activeBlockIdForCommentrayLine0(links, probeCommentrayLine0FromDoc(docScrollEl))
1123
+ : activeBlockIdForViewport(links, probeTopSourceLine1Based());
1124
+ const clipGutterRaysThroughPageBreakGaps = pageBreakPullEnabled();
963
1125
  svg.setAttribute("viewBox", `0 0 ${String(w)} ${String(h)}`);
964
1126
  svg.setAttribute("preserveAspectRatio", "none");
965
1127
  const parts = [];
1128
+ const sourceAnchors = Array.from(document.querySelectorAll(`[id^="${lineIdPrefix}"]`))
1129
+ .map((el) => ({ line0: sourceAnchorIndexFromId(el.id, lineIdPrefix), el }))
1130
+ .filter((x) => x.line0 !== null)
1131
+ .sort((a, b) => a.line0 - b.line0);
966
1132
  for (let i = 0; i < sorted.length; i++) {
967
1133
  const link = sorted[i];
968
1134
  if (!link)
969
1135
  continue;
970
- const next = sorted[i + 1];
1136
+ const next = nextBlockLinkInCommentrayOrder(links, link);
971
1137
  const i0 = codeLineDomIndex0(link.sourceStart);
972
1138
  const i1 = codeLineDomIndex0(link.sourceEnd);
973
- const codeTop = document.getElementById(`code-line-${String(i0)}`);
974
- const codeBot = document.getElementById(`code-line-${String(i1)}`);
1139
+ const codeTop = document.getElementById(`${lineIdPrefix}${String(i0)}`) ??
1140
+ findAnchorAtOrAfter(sourceAnchors, i0);
1141
+ const codeBot = document.getElementById(`${lineIdPrefix}${String(i1)}`) ??
1142
+ findAnchorAtOrBefore(sourceAnchors, i1);
975
1143
  const docTop = document.getElementById(`commentray-block-${link.id}`);
976
1144
  if (!codeTop || !codeBot || !docTop)
977
1145
  continue;
978
- const docEndYViewport = commentaryBandEndYViewport(docScrollEl, next, docTop);
1146
+ const docEndYViewport = commentaryBandEndYViewport(docScrollEl, next, docTop, clipGutterRaysThroughPageBreakGaps);
979
1147
  const yCodeTop = codeLineHighlightCenterYViewport(codeTop);
980
1148
  const yCodeBot = codeLineHighlightCenterYViewport(codeBot);
981
1149
  const yDocTop = docTop.getBoundingClientRect().top + 2;
@@ -1009,13 +1177,14 @@ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSource
1009
1177
  }
1010
1178
  /**
1011
1179
  * Splines in the gutter between each block’s source range and its commentary band (dual pane,
1012
- * index-backed blocks). Emphasizes the block aligned with the current source viewport; clamps
1013
- * off-screen endpoints so readers see which way to scroll.
1180
+ * index-backed blocks). Emphasizes the block aligned with the **doc** viewport when block anchors
1181
+ * exist; otherwise the source viewport. Clamps off-screen endpoints so readers see which way to scroll.
1014
1182
  *
1015
1183
  * @returns Request a redraw after DOM changes that do not resize the panes (e.g. multi-angle body swap).
1016
1184
  */
1017
1185
  function wireBlockRayConnectors(args) {
1018
1186
  const { gutter, codePane, docScrollEl, getLinks, probeTopSourceLine1Based } = args;
1187
+ const sourceLineIdPrefix = args.sourceLineIdPrefix ?? (() => "code-line-");
1019
1188
  const svgNs = "http://www.w3.org/2000/svg";
1020
1189
  const host = document.createElement("div");
1021
1190
  host.className = "gutter__rays";
@@ -1029,7 +1198,7 @@ function wireBlockRayConnectors(args) {
1029
1198
  return;
1030
1199
  raf = globalThis.requestAnimationFrame(() => {
1031
1200
  raf = 0;
1032
- drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based);
1201
+ drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based, sourceLineIdPrefix());
1033
1202
  });
1034
1203
  }
1035
1204
  subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw);
@@ -1183,6 +1352,40 @@ function treeFileLinkTitle(pr) {
1183
1352
  }
1184
1353
  return pr.sourcePath;
1185
1354
  }
1355
+ function clearDocumentedTreePairHighlights(tree) {
1356
+ for (const el of tree.querySelectorAll("a.tree-file-link")) {
1357
+ if (!(el instanceof HTMLAnchorElement))
1358
+ continue;
1359
+ el.classList.remove("tree-file-link--current");
1360
+ el.removeAttribute("aria-current");
1361
+ }
1362
+ }
1363
+ function markFirstDocumentedTreeLinkMatchingPair(tree, curSrc, curCr) {
1364
+ for (const el of tree.querySelectorAll("a.tree-file-link")) {
1365
+ if (!(el instanceof HTMLAnchorElement))
1366
+ continue;
1367
+ const sp = el.getAttribute("data-pair-source-path")?.trim() ?? "";
1368
+ const cp = el.getAttribute("data-pair-commentray-path")?.trim() ?? "";
1369
+ if (!isSameDocumentedPair({ sourcePath: sp, commentrayPath: cp }, curSrc, curCr))
1370
+ continue;
1371
+ el.classList.add("tree-file-link--current");
1372
+ el.setAttribute("aria-current", "page");
1373
+ break;
1374
+ }
1375
+ }
1376
+ /** Marks the tree link for the pair shown in `#shell` (pair paths from server or multi-angle swap). */
1377
+ function applyDocumentedTreeCurrentPairHighlight() {
1378
+ const shell = document.getElementById("shell");
1379
+ const tree = document.getElementById("documented-files-tree");
1380
+ if (!(shell instanceof HTMLElement) || !(tree instanceof HTMLElement))
1381
+ return;
1382
+ clearDocumentedTreePairHighlights(tree);
1383
+ const curSrc = shell.getAttribute("data-commentray-pair-source-path")?.trim() ?? "";
1384
+ const curCr = shell.getAttribute("data-commentray-pair-commentray-path")?.trim() ?? "";
1385
+ if (curSrc.length === 0 || curCr.length === 0)
1386
+ return;
1387
+ markFirstDocumentedTreeLinkMatchingPair(tree, curSrc, curCr);
1388
+ }
1186
1389
  function renderDocumentedTreeHtml(node) {
1187
1390
  const keys = [...node.children.keys()].sort((a, b) => a.localeCompare(b));
1188
1391
  if (keys.length === 0)
@@ -1202,10 +1405,12 @@ function renderDocumentedTreeHtml(node) {
1202
1405
  const label = escapeHtmlText(treeFileLinkLabel(pr, multi));
1203
1406
  const title = escapeHtmlText(treeFileLinkTitle(pr));
1204
1407
  const href = escapeHtmlText(treeFileLinkHref(pr));
1408
+ const spAttr = escapeHtmlText(normPosixPath(pr.sourcePath));
1409
+ const crAttr = escapeHtmlText(normPosixPath(pr.commentrayPath));
1205
1410
  const useSiteBrowse = (pr.staticBrowseUrl?.trim() ?? "").length > 0;
1206
1411
  const external = useSiteBrowse ? "" : ' target="_blank" rel="noopener noreferrer"';
1207
1412
  lis.push(`<li><div class="tree-file">` +
1208
- `<a class="tree-file-link" href="${href}"${external} title="${title}">${label}</a>` +
1413
+ `<a class="tree-file-link" href="${href}" data-pair-source-path="${spAttr}" data-pair-commentray-path="${crAttr}"${external} title="${title}">${label}</a>` +
1209
1414
  `</div></li>`);
1210
1415
  }
1211
1416
  }
@@ -1223,6 +1428,7 @@ function renderDocumentedPairsIntoHost(treeHost, pairs, emptyBecauseFilter) {
1223
1428
  for (const p of pairs)
1224
1429
  insertSourcePathTrie(root, p);
1225
1430
  treeHost.innerHTML = renderDocumentedTreeHtml(root);
1431
+ applyDocumentedTreeCurrentPairHighlight();
1226
1432
  }
1227
1433
  function loadDocumentedPairs(jsonUrl, embeddedB64) {
1228
1434
  let loaded = null;
@@ -1298,6 +1504,12 @@ function wireDocumentedFilesTreeMobileFlyout(hub) {
1298
1504
  globalThis.addEventListener("scroll", placeFlyout, true);
1299
1505
  return placeFlyout;
1300
1506
  }
1507
+ function focusDocumentedFilesFilterInput() {
1508
+ const el = document.getElementById("documented-files-filter");
1509
+ if (!(el instanceof HTMLInputElement))
1510
+ return;
1511
+ el.focus({ preventScroll: true });
1512
+ }
1301
1513
  function wireDocumentedFilesTree() {
1302
1514
  const hub = document.getElementById("documented-files-hub");
1303
1515
  const treeHost = document.getElementById("documented-files-tree");
@@ -1306,12 +1518,13 @@ function wireDocumentedFilesTree() {
1306
1518
  if (!(hub instanceof HTMLDetailsElement) || !(treeHost instanceof HTMLElement)) {
1307
1519
  return;
1308
1520
  }
1521
+ const detailsHub = hub;
1309
1522
  const treeMount = treeHost;
1310
- const jsonUrl = hub.getAttribute("data-nav-json-url")?.trim() ?? "";
1523
+ const jsonUrl = detailsHub.getAttribute("data-nav-json-url")?.trim() ?? "";
1311
1524
  const embeddedB64 = shell?.getAttribute("data-documented-pairs-b64")?.trim() ?? "";
1312
1525
  if (jsonUrl.length === 0 && embeddedB64.length === 0)
1313
1526
  return;
1314
- const placeDocHubFlyout = wireDocumentedFilesTreeMobileFlyout(hub);
1527
+ const placeDocHubFlyout = wireDocumentedFilesTreeMobileFlyout(detailsHub);
1315
1528
  const ensureLoaded = loadDocumentedPairs(jsonUrl, embeddedB64);
1316
1529
  let cachedPairs = null;
1317
1530
  function applyFilterAndRender() {
@@ -1334,18 +1547,31 @@ function wireDocumentedFilesTree() {
1334
1547
  '<p class="nav-rail__doc-hub-hint" role="alert">Could not load the file list.</p>';
1335
1548
  }
1336
1549
  }
1337
- hub.addEventListener("toggle", () => {
1550
+ detailsHub.addEventListener("toggle", () => {
1338
1551
  placeDocHubFlyout();
1339
- if (hub.open) {
1340
- globalThis.requestAnimationFrame(placeDocHubFlyout);
1552
+ if (detailsHub.open) {
1553
+ globalThis.requestAnimationFrame(() => {
1554
+ placeDocHubFlyout();
1555
+ focusDocumentedFilesFilterInput();
1556
+ });
1341
1557
  }
1342
- if (!hub.open)
1558
+ if (!detailsHub.open)
1343
1559
  return;
1344
1560
  void hydrateTree();
1345
1561
  });
1562
+ function onDocumentedFilesHubEscape(ev) {
1563
+ if (!detailsHub.open || ev.key !== "Escape")
1564
+ return;
1565
+ ev.preventDefault();
1566
+ detailsHub.open = false;
1567
+ const sum = detailsHub.querySelector("summary");
1568
+ if (sum instanceof HTMLElement)
1569
+ sum.focus({ preventScroll: true });
1570
+ }
1571
+ document.addEventListener("keydown", onDocumentedFilesHubEscape, true);
1346
1572
  if (filterInput instanceof HTMLInputElement) {
1347
1573
  filterInput.addEventListener("input", () => {
1348
- if (!hub.open || cachedPairs === null)
1574
+ if (!detailsHub.open || cachedPairs === null)
1349
1575
  return;
1350
1576
  applyFilterAndRender();
1351
1577
  });
@@ -1384,11 +1610,61 @@ function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
1384
1610
  const STORAGE_SPLIT_PCT = "commentray.codeCommentrayStatic.splitPct";
1385
1611
  const STORAGE_WRAP_LINES = "commentray.codeCommentrayStatic.wrap";
1386
1612
  const STORAGE_DUAL_MOBILE_PANE = "commentray.codeCommentrayStatic.dualMobilePane";
1613
+ const STORAGE_SOURCE_MARKDOWN_PANE_MODE = "commentray.codeCommentrayStatic.sourceMarkdownPaneMode";
1614
+ const STORAGE_PAGE_BREAKS_ENABLED = "commentray.codeCommentrayStatic.pageBreaksEnabled";
1387
1615
  /** Matches `code-browser.ts` `@media (max-width: 767px)` (dual column from 768px up). */
1388
1616
  const DUAL_MOBILE_SINGLE_PANE_MQ = "(max-width: 767px)";
1389
1617
  function normalizedDualMobilePane(v) {
1390
1618
  return v === "code" ? "code" : "doc";
1391
1619
  }
1620
+ function isNarrowViewport() {
1621
+ return globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ).matches;
1622
+ }
1623
+ function wireWideModeIntroTrigger(shell) {
1624
+ const btn = document.getElementById("commentray-help-tour");
1625
+ if (!(btn instanceof HTMLButtonElement))
1626
+ return;
1627
+ btn.addEventListener("click", () => {
1628
+ wireWideModeIntroTour(shell, isNarrowViewport, { force: true });
1629
+ });
1630
+ }
1631
+ function sourcePaneModeForShell(shell) {
1632
+ return shell.getAttribute("data-source-pane-mode") === "rendered-markdown"
1633
+ ? "rendered-markdown"
1634
+ : "source";
1635
+ }
1636
+ function pageBreaksEnabledFromStorage(raw) {
1637
+ const t = (raw ?? "").trim().toLowerCase();
1638
+ if (t === "0" || t === "false" || t === "off")
1639
+ return false;
1640
+ return true;
1641
+ }
1642
+ function applyPageBreakFeatureToggle(shell) {
1643
+ const enabled = pageBreaksEnabledFromStorage(readWebStorageItem(localStorage, STORAGE_PAGE_BREAKS_ENABLED));
1644
+ shell.setAttribute("data-page-breaks-enabled", enabled ? "true" : "false");
1645
+ }
1646
+ function wireResponsivePageBreakHeight(shell) {
1647
+ const setHeight = () => {
1648
+ const viewportHeight = Math.max(globalThis.innerHeight, document.documentElement?.clientHeight ?? 0);
1649
+ if (!Number.isFinite(viewportHeight) || viewportHeight <= 0)
1650
+ return;
1651
+ const minHeightPx = Math.round(clamp(viewportHeight * 0.72, 260, 820));
1652
+ shell.style.setProperty("--commentray-page-break-min-height", `${String(minHeightPx)}px`);
1653
+ };
1654
+ globalThis.addEventListener("resize", setHeight, { passive: true });
1655
+ globalThis.addEventListener("orientationchange", setHeight, { passive: true });
1656
+ globalThis.visualViewport?.addEventListener("resize", setHeight, { passive: true });
1657
+ setHeight();
1658
+ }
1659
+ function syncWrapLinesVisibilityForSourcePaneMode(shell) {
1660
+ const wrapToggle = document.querySelector("label.toolbar-wrap-lines");
1661
+ if (!(wrapToggle instanceof HTMLLabelElement))
1662
+ return;
1663
+ wrapToggle.hidden = sourcePaneModeForShell(shell) === "rendered-markdown";
1664
+ }
1665
+ function sourceLineIdPrefixForShell(shell) {
1666
+ return sourcePaneModeForShell(shell) === "rendered-markdown" ? "code-md-line-" : "code-line-";
1667
+ }
1392
1668
  /** When the commentary pane is visible, (re)run Mermaid so diagrams are not laid out under display:none. */
1393
1669
  function scheduleMermaidWhenDualDocPaneVisible(shell, mq) {
1394
1670
  const kick = () => {
@@ -1440,6 +1716,105 @@ function wireDualMobilePaneFlipScrollAffordance(primaryFlip, scrollFlip, mq) {
1440
1716
  mq.addEventListener("change", tick);
1441
1717
  globalThis.requestAnimationFrame(tick);
1442
1718
  }
1719
+ function wireSourceMarkdownPaneFlipAffordance(primaryFlip, scrollFlip) {
1720
+ const hideScroll = () => {
1721
+ scrollFlip.hidden = true;
1722
+ scrollFlip.classList.remove("is-visible");
1723
+ };
1724
+ const showScroll = () => {
1725
+ scrollFlip.hidden = false;
1726
+ scrollFlip.classList.add("is-visible");
1727
+ };
1728
+ const tick = () => {
1729
+ const r = primaryFlip.getBoundingClientRect();
1730
+ const vh = globalThis.innerHeight;
1731
+ const margin = 10;
1732
+ const offScreen = r.bottom < margin || r.top > vh - margin;
1733
+ if (offScreen)
1734
+ showScroll();
1735
+ else
1736
+ hideScroll();
1737
+ };
1738
+ globalThis.addEventListener("scroll", tick, { passive: true });
1739
+ globalThis.addEventListener("resize", tick, { passive: true });
1740
+ globalThis.requestAnimationFrame(tick);
1741
+ }
1742
+ function closestSourceLine0ForPaneTop(codePane, idPrefix) {
1743
+ const rows = codePane.querySelectorAll(`[id^="${idPrefix}"]`);
1744
+ if (rows.length === 0)
1745
+ return null;
1746
+ const y = paneUsesInternalYScroll(codePane)
1747
+ ? codePane.getBoundingClientRect().top + codePane.clientTop + 2
1748
+ : Math.max(0, codePane.getBoundingClientRect().top) + 2;
1749
+ for (const el of rows) {
1750
+ const r = el.getBoundingClientRect();
1751
+ if (r.bottom > y - 1e-3) {
1752
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(el.id);
1753
+ if (!m?.[1])
1754
+ return null;
1755
+ return Number.parseInt(m[1], 10);
1756
+ }
1757
+ }
1758
+ const last = rows[rows.length - 1];
1759
+ if (!last)
1760
+ return null;
1761
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(last.id);
1762
+ if (!m?.[1])
1763
+ return null;
1764
+ return Number.parseInt(m[1], 10);
1765
+ }
1766
+ function wireSourceMarkdownPaneFlip(shell, codePane, flipBtn, flipScrollBtn, onAfterFlip) {
1767
+ function syncSourceMarkdownFlipA11y() {
1768
+ const mode = sourcePaneModeForShell(shell);
1769
+ const renderedActive = mode === "rendered-markdown";
1770
+ const nextModeLabel = renderedActive ? "markdown source" : "rendered markdown";
1771
+ const ariaLabel = `Switch source pane to ${nextModeLabel}`;
1772
+ const title = `Source pane: ${renderedActive ? "rendered markdown" : "markdown source"} (click to switch)`;
1773
+ const apply = (btn) => {
1774
+ if (!(btn instanceof HTMLButtonElement))
1775
+ return;
1776
+ btn.setAttribute("aria-pressed", renderedActive ? "true" : "false");
1777
+ btn.setAttribute("aria-label", ariaLabel);
1778
+ btn.title = title;
1779
+ };
1780
+ apply(flipBtn);
1781
+ apply(flipScrollBtn);
1782
+ }
1783
+ // Keep initial behavior deterministic: source pane starts in rendered markdown mode.
1784
+ shell.setAttribute("data-source-pane-mode", "rendered-markdown");
1785
+ syncSourceMarkdownFlipA11y();
1786
+ syncWrapLinesVisibilityForSourcePaneMode(shell);
1787
+ const runFlip = () => {
1788
+ const cur = sourcePaneModeForShell(shell);
1789
+ const currentPrefix = cur === "rendered-markdown" ? "code-md-line-" : "code-line-";
1790
+ const line0 = closestSourceLine0ForPaneTop(codePane, currentPrefix);
1791
+ const next = cur === "rendered-markdown" ? "source" : "rendered-markdown";
1792
+ const nextPrefix = next === "rendered-markdown" ? "code-md-line-" : "code-line-";
1793
+ shell.setAttribute("data-source-pane-mode", next);
1794
+ writeWebStorageItem(localStorage, STORAGE_SOURCE_MARKDOWN_PANE_MODE, next);
1795
+ syncSourceMarkdownFlipA11y();
1796
+ syncWrapLinesVisibilityForSourcePaneMode(shell);
1797
+ if (line0 !== null) {
1798
+ const row = codePane.querySelector(`#${nextPrefix}${String(line0)}`);
1799
+ if (row instanceof HTMLElement) {
1800
+ applyRevealChildInPane(codePane, row, 2);
1801
+ }
1802
+ }
1803
+ if (next === "rendered-markdown") {
1804
+ const sourceMdBody = document.getElementById("code-pane-markdown-body");
1805
+ if (sourceMdBody instanceof HTMLElement) {
1806
+ runMermaidOnFreshDocNodes(sourceMdBody);
1807
+ rewriteHubRelativeBrowseAnchorsIn(sourceMdBody);
1808
+ }
1809
+ }
1810
+ onAfterFlip?.();
1811
+ };
1812
+ flipBtn.addEventListener("click", runFlip);
1813
+ if (flipScrollBtn) {
1814
+ flipScrollBtn.addEventListener("click", runFlip);
1815
+ wireSourceMarkdownPaneFlipAffordance(flipBtn, flipScrollBtn);
1816
+ }
1817
+ }
1443
1818
  function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
1444
1819
  const mq = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ);
1445
1820
  function readStoredPane() {
@@ -1458,6 +1833,7 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
1458
1833
  return;
1459
1834
  const cur = normalizedDualMobilePane(shell.getAttribute("data-dual-mobile-pane"));
1460
1835
  const next = cur === "code" ? "doc" : "code";
1836
+ const rootTopBeforeFlip = rootScrollingElement().scrollTop;
1461
1837
  if (next === "code") {
1462
1838
  scrollRunners.prepareMobileFlipToCode();
1463
1839
  }
@@ -1470,6 +1846,11 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
1470
1846
  globalThis.requestAnimationFrame(() => {
1471
1847
  if (next === "code") {
1472
1848
  scrollRunners.finishMobileFlipToCode();
1849
+ const root = rootScrollingElement();
1850
+ if (rootTopBeforeFlip > 5 && root.scrollTop <= 1) {
1851
+ const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
1852
+ root.scrollTop = clamp(rootTopBeforeFlip, 0, maxY);
1853
+ }
1473
1854
  }
1474
1855
  else {
1475
1856
  scrollRunners.finishMobileFlipToDoc();
@@ -1630,75 +2011,108 @@ function wireDualPaneNavSearchFetch(shell, embeddedPairs, indexState, mutable, r
1630
2011
  }
1631
2012
  })();
1632
2013
  }
2014
+ function applySelectedMultiAngle(args) {
2015
+ const { angle, docBody, mutable, rebuildSearcher, scrollLinksRef, shell, searchInput, searchResults, requestBlockRayRedraw, } = args;
2016
+ docBody.innerHTML = decodeBase64Utf8(angle.docInnerHtmlB64);
2017
+ runMermaidOnFreshDocNodes(docBody);
2018
+ rewriteHubRelativeBrowseAnchorsIn(docBody);
2019
+ mutable.rawMd = decodeBase64Utf8(angle.rawMdB64);
2020
+ mutable.mdLines = mutable.rawMd.split("\n");
2021
+ mutable.commentrayPathLabel = angle.commentrayPathForSearch;
2022
+ rebuildSearcher();
2023
+ scrollLinksRef.current = parseScrollBlockLinksFromShell(angle.scrollBlockLinksB64);
2024
+ shell.setAttribute("data-scroll-block-links-b64", angle.scrollBlockLinksB64);
2025
+ shell.setAttribute("data-search-commentray-path", angle.commentrayPathForSearch);
2026
+ const crIdentity = normPosixPath(angle.commentrayPathForSearch);
2027
+ if (crIdentity.length > 0)
2028
+ shell.setAttribute("data-commentray-pair-commentray-path", crIdentity);
2029
+ else
2030
+ shell.removeAttribute("data-commentray-pair-commentray-path");
2031
+ applyDocumentedTreeCurrentPairHighlight();
2032
+ const docPathEl = document.getElementById("nav-rail-doc-path");
2033
+ if (docPathEl) {
2034
+ const path = angle.commentrayPathForSearch.trim();
2035
+ docPathEl.textContent = path.length > 0 ? path : "—";
2036
+ if (path.length > 0)
2037
+ docPathEl.setAttribute("title", path);
2038
+ else
2039
+ docPathEl.removeAttribute("title");
2040
+ }
2041
+ const browse = angle.staticBrowseUrl?.trim() ?? "";
2042
+ if (browse.length > 0) {
2043
+ const resolved = staticBrowseHrefForShellDataAttribute(browse, globalThis.location.pathname, globalThis.location.origin);
2044
+ shell.setAttribute("data-commentray-pair-browse-href", resolved);
2045
+ }
2046
+ else {
2047
+ const ghu = angle.commentrayOnGithubUrl?.trim();
2048
+ if (ghu)
2049
+ shell.setAttribute("data-commentray-pair-browse-href", ghu);
2050
+ else
2051
+ shell.removeAttribute("data-commentray-pair-browse-href");
2052
+ }
2053
+ searchInput.value = "";
2054
+ searchResults.innerHTML = "";
2055
+ searchResults.hidden = true;
2056
+ requestBlockRayRedraw?.();
2057
+ globalThis.requestAnimationFrame(() => {
2058
+ requestBlockRayRedraw?.();
2059
+ globalThis.requestAnimationFrame(() => {
2060
+ requestBlockRayRedraw?.();
2061
+ });
2062
+ });
2063
+ }
1633
2064
  function wireDualPaneMultiAngleAndScroll(args) {
1634
2065
  const { codePane, docScrollEl, docBody, shell, scrollLinksRef, multiPayload, mutable, rebuildSearcher, searchInput, searchResults, requestBlockRayRedraw, } = args;
1635
2066
  if (multiPayload) {
1636
- const runners = wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
2067
+ const runners = wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current, () => sourceLineIdPrefixForShell(shell), () => sourcePaneModeForShell(shell) === "rendered-markdown");
1637
2068
  const angleSel = document.getElementById("angle-select");
1638
2069
  if (angleSel && docBody) {
1639
2070
  angleSel.addEventListener("change", () => {
1640
2071
  const a = multiPayload.angles.find((x) => x.id === angleSel.value);
1641
2072
  if (!a)
1642
2073
  return;
1643
- docBody.innerHTML = decodeBase64Utf8(a.docInnerHtmlB64);
1644
- runMermaidOnFreshDocNodes(docBody);
1645
- rewriteHubRelativeBrowseAnchorsIn(docBody);
1646
- mutable.rawMd = decodeBase64Utf8(a.rawMdB64);
1647
- mutable.mdLines = mutable.rawMd.split("\n");
1648
- mutable.commentrayPathLabel = a.commentrayPathForSearch;
1649
- rebuildSearcher();
1650
- scrollLinksRef.current = parseScrollBlockLinksFromShell(a.scrollBlockLinksB64);
1651
- shell.setAttribute("data-scroll-block-links-b64", a.scrollBlockLinksB64);
1652
- shell.setAttribute("data-search-commentray-path", a.commentrayPathForSearch);
1653
- const docPathEl = document.getElementById("nav-rail-doc-path");
1654
- if (docPathEl) {
1655
- const path = a.commentrayPathForSearch.trim();
1656
- docPathEl.textContent = path.length > 0 ? path : "—";
1657
- if (path.length > 0)
1658
- docPathEl.setAttribute("title", path);
1659
- else
1660
- docPathEl.removeAttribute("title");
1661
- }
1662
- const browse = a.staticBrowseUrl?.trim() ?? "";
1663
- if (browse.length > 0) {
1664
- const resolved = staticBrowseHrefForShellDataAttribute(browse, globalThis.location.pathname, globalThis.location.origin);
1665
- shell.setAttribute("data-commentray-pair-browse-href", resolved);
1666
- }
1667
- else {
1668
- const ghu = a.commentrayOnGithubUrl?.trim();
1669
- if (ghu) {
1670
- shell.setAttribute("data-commentray-pair-browse-href", ghu);
1671
- }
1672
- else {
1673
- shell.removeAttribute("data-commentray-pair-browse-href");
1674
- }
1675
- }
1676
- searchInput.value = "";
1677
- searchResults.innerHTML = "";
1678
- searchResults.hidden = true;
1679
- requestBlockRayRedraw?.();
1680
- globalThis.requestAnimationFrame(() => {
1681
- requestBlockRayRedraw?.();
1682
- globalThis.requestAnimationFrame(() => {
1683
- requestBlockRayRedraw?.();
1684
- });
2074
+ applySelectedMultiAngle({
2075
+ angle: a,
2076
+ docBody,
2077
+ mutable,
2078
+ rebuildSearcher,
2079
+ scrollLinksRef,
2080
+ shell,
2081
+ searchInput,
2082
+ searchResults,
2083
+ requestBlockRayRedraw,
1685
2084
  });
1686
2085
  });
1687
2086
  }
1688
2087
  return runners;
1689
2088
  }
1690
2089
  if (scrollLinksRef.current.length > 0) {
1691
- return wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
2090
+ return wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current, () => sourceLineIdPrefixForShell(shell), () => sourcePaneModeForShell(shell) === "rendered-markdown");
1692
2091
  }
1693
2092
  return wireProportionalScrollSync(codePane, docScrollEl);
1694
2093
  }
1695
2094
  function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
2095
+ function commentrayMdLineFromLocationHash(rawHash) {
2096
+ const hash = rawHash.replace(/^#/, "").trim();
2097
+ if (hash.length === 0)
2098
+ return null;
2099
+ const tokens = hash
2100
+ .split(/--|&/)
2101
+ .map((t) => t.trim())
2102
+ .filter((t) => t.length > 0);
2103
+ for (const token of tokens) {
2104
+ const m = /^commentray-md-line-(\d+)$/.exec(token);
2105
+ if (!m?.[1])
2106
+ continue;
2107
+ const line0 = Number.parseInt(m[1], 10);
2108
+ if (Number.isFinite(line0))
2109
+ return line0;
2110
+ }
2111
+ return null;
2112
+ }
1696
2113
  function applyCommentrayLocationHash() {
1697
- const m = /^commentray-md-line-(\d+)$/.exec(globalThis.location.hash.slice(1));
1698
- if (!m?.[1])
1699
- return;
1700
- const line0 = Number.parseInt(m[1], 10);
1701
- if (!Number.isFinite(line0))
2114
+ const line0 = commentrayMdLineFromLocationHash(globalThis.location.hash);
2115
+ if (line0 === null)
1702
2116
  return;
1703
2117
  scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount());
1704
2118
  }
@@ -1707,6 +2121,23 @@ function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
1707
2121
  globalThis.requestAnimationFrame(applyCommentrayLocationHash);
1708
2122
  });
1709
2123
  }
2124
+ function initializeSourceMarkdownPane(shell) {
2125
+ if (sourcePaneModeForShell(shell) !== "rendered-markdown")
2126
+ return;
2127
+ const sourceMdBody = document.getElementById("code-pane-markdown-body");
2128
+ if (!(sourceMdBody instanceof HTMLElement))
2129
+ return;
2130
+ runMermaidOnFreshDocNodes(sourceMdBody);
2131
+ rewriteHubRelativeBrowseAnchorsIn(sourceMdBody);
2132
+ }
2133
+ function wireSourceMarkdownControls(shell, codePane, onAfterFlip) {
2134
+ const sourceMdFlip = document.getElementById("source-markdown-pane-flip");
2135
+ const sourceMdFlipScroll = document.getElementById("source-markdown-pane-flip-scroll");
2136
+ if (!(sourceMdFlip instanceof HTMLButtonElement))
2137
+ return;
2138
+ wireSourceMarkdownPaneFlip(shell, codePane, sourceMdFlip, sourceMdFlipScroll instanceof HTMLButtonElement ? sourceMdFlipScroll : null, onAfterFlip);
2139
+ initializeSourceMarkdownPane(shell);
2140
+ }
1710
2141
  function buildDualPaneSearcherBundle(shell, codePane) {
1711
2142
  const { rawCodeB64, rawMdB64 } = readEmbeddedRawB64Strings(shell, codePane);
1712
2143
  const rawCode = decodeBase64Utf8(rawCodeB64);
@@ -1778,12 +2209,11 @@ function wireDualPaneCodeBrowser(shell, codePane) {
1778
2209
  shell.style.setProperty("--split-pct", `${String(pct)}%`);
1779
2210
  const docPaneEl = document.getElementById("doc-pane");
1780
2211
  const docPaneForWrap = docPaneEl instanceof HTMLElement ? docPaneEl : null;
2212
+ const sourceMdBodyForWrap = document.getElementById("code-pane-markdown-body");
1781
2213
  const blockRayRedraw = {};
1782
2214
  wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb, () => {
1783
2215
  blockRayRedraw.request?.();
1784
- codePane.dispatchEvent(new Event("scroll"));
1785
- docScrollEl.dispatchEvent(new Event("scroll"));
1786
- }, docPaneForWrap, docBody);
2216
+ }, docPaneForWrap, docBody, sourceMdBodyForWrap instanceof HTMLElement ? sourceMdBodyForWrap : null);
1787
2217
  wireSplitter(STORAGE_SPLIT_PCT, shell, codePane, gutter, pct);
1788
2218
  const multiScript = document.getElementById("commentray-multi-angle-b64");
1789
2219
  const multiPayload = parseMultiAnglePayload(multiScript);
@@ -1794,7 +2224,8 @@ function wireDualPaneCodeBrowser(shell, codePane) {
1794
2224
  codePane,
1795
2225
  docScrollEl,
1796
2226
  getLinks: () => bundle.scrollLinksRef.current,
1797
- probeTopSourceLine1Based: () => probeCodeLine1FromViewport(codePane),
2227
+ probeTopSourceLine1Based: () => probeCodeLine1FromViewport(codePane, sourceLineIdPrefixForShell(shell)),
2228
+ sourceLineIdPrefix: () => sourceLineIdPrefixForShell(shell),
1798
2229
  })
1799
2230
  : undefined;
1800
2231
  blockRayRedraw.request = requestBlockRayRedraw;
@@ -1816,6 +2247,10 @@ function wireDualPaneCodeBrowser(shell, codePane) {
1816
2247
  if (flipBtn instanceof HTMLButtonElement) {
1817
2248
  wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn instanceof HTMLButtonElement ? flipScrollBtn : null);
1818
2249
  }
2250
+ wireSourceMarkdownControls(shell, codePane, () => {
2251
+ requestBlockRayRedraw?.();
2252
+ });
2253
+ wireWideModeIntroTour(shell, isNarrowViewport);
1819
2254
  wireDualPaneCommentrayLocationHash(docScrollEl, () => bundle.mutable.mdLines.length);
1820
2255
  }
1821
2256
  function commentrayThemeModeLabel(mode) {
@@ -1917,7 +2352,218 @@ function wireColorThemeToolbar() {
1917
2352
  document.addEventListener("keydown", onDocumentKeydown, true);
1918
2353
  syncUi();
1919
2354
  }
2355
+ function safePermalinkHref(raw) {
2356
+ const t = raw.trim();
2357
+ if (t.length === 0)
2358
+ return null;
2359
+ if (/^(javascript|data):/i.test(t))
2360
+ return null;
2361
+ try {
2362
+ return new URL(t, globalThis.location.href).toString();
2363
+ }
2364
+ catch {
2365
+ return null;
2366
+ }
2367
+ }
2368
+ function humaneBrowseAliasPathForSource(sourcePath) {
2369
+ return sourcePath
2370
+ .split("/")
2371
+ .filter((seg) => seg.length > 0)
2372
+ .map((seg) => seg.startsWith(".") ? `%2E${encodeURIComponent(seg.slice(1))}` : encodeURIComponent(seg))
2373
+ .join("/");
2374
+ }
2375
+ function companionStemFromCommentrayPath(commentrayPath) {
2376
+ const norm = normPosixPath(commentrayPath);
2377
+ const last = norm.split("/").filter(Boolean).at(-1) ?? "";
2378
+ return last.replace(/\.md$/i, "").trim();
2379
+ }
2380
+ function makeAbsoluteUrlAgainst(raw, baseHref) {
2381
+ return new URL(raw, baseHref).toString();
2382
+ }
2383
+ function shellEligibleForHumaneBackfill(shell, pathname) {
2384
+ if ((shell.getAttribute("data-layout") ?? "dual") !== "dual")
2385
+ return false;
2386
+ if (pathname.includes("/browse/"))
2387
+ return false;
2388
+ return pathname.endsWith("/") || pathname.endsWith("/index.html");
2389
+ }
2390
+ function nextHumaneBrowsePathForShell(shell, pathname) {
2391
+ const sourcePath = normPosixPath(shell.getAttribute("data-commentray-pair-source-path") ?? "");
2392
+ if (sourcePath.length === 0)
2393
+ return null;
2394
+ const alias = humaneBrowseAliasPathForSource(sourcePath);
2395
+ if (alias.length === 0)
2396
+ return null;
2397
+ const angleSel = document.getElementById("angle-select");
2398
+ const selectedAngle = angleSel instanceof HTMLSelectElement ? angleSel.value.trim() : "";
2399
+ const commentrayPath = shell.getAttribute("data-commentray-pair-commentray-path") ?? "";
2400
+ const stem = companionStemFromCommentrayPath(commentrayPath);
2401
+ const angleName = selectedAngle.length > 0 ? selectedAngle : stem;
2402
+ if (angleName.length === 0)
2403
+ return null;
2404
+ const angleAlias = `${alias}@${encodeURIComponent(angleName)}.html`;
2405
+ const siteRoot = siteRootPathnameFromPathname(pathname);
2406
+ return siteRoot === "/" ? `/browse/${angleAlias}` : `${siteRoot}/browse/${angleAlias}`;
2407
+ }
2408
+ function absolutizeNavJsonUrls(shell, beforeHref) {
2409
+ const navSearchRaw = shell.getAttribute("data-nav-search-json-url")?.trim() ?? "";
2410
+ if (navSearchRaw.length > 0) {
2411
+ shell.setAttribute("data-nav-search-json-url", makeAbsoluteUrlAgainst(navSearchRaw, beforeHref));
2412
+ }
2413
+ const navTree = document.getElementById("documented-files-hub");
2414
+ if (navTree instanceof HTMLElement) {
2415
+ const navRaw = navTree.getAttribute("data-nav-json-url")?.trim() ?? "";
2416
+ if (navRaw.length > 0) {
2417
+ navTree.setAttribute("data-nav-json-url", makeAbsoluteUrlAgainst(navRaw, beforeHref));
2418
+ }
2419
+ }
2420
+ }
2421
+ function normalizePairBrowseHrefForCurrentPath(shell, pathname) {
2422
+ const pairBrowseRaw = shell.getAttribute("data-commentray-pair-browse-href")?.trim() ?? "";
2423
+ if (pairBrowseRaw.length > 0 && isHubRelativeStaticBrowseHref(pairBrowseRaw)) {
2424
+ shell.setAttribute("data-commentray-pair-browse-href", resolveStaticBrowseHref(pairBrowseRaw, pathname, globalThis.location.origin));
2425
+ }
2426
+ }
2427
+ function normalizeDocumentationHomeHrefForCurrentPath() {
2428
+ const home = document.querySelector('a[aria-label="Documentation home"]');
2429
+ if (!(home instanceof HTMLAnchorElement))
2430
+ return;
2431
+ const siteRoot = siteRootPathnameFromPathname(globalThis.location.pathname);
2432
+ const normalized = siteRoot === "/" ? "/" : `${siteRoot}/`;
2433
+ home.setAttribute("href", normalized);
2434
+ }
2435
+ function activeCommentrayHashTokenFromViewport() {
2436
+ const docPane = document.getElementById("doc-pane");
2437
+ if (!(docPane instanceof HTMLElement))
2438
+ return null;
2439
+ const docBody = document.getElementById("doc-pane-body");
2440
+ const docScrollEl = docBody instanceof HTMLElement ? docBody : docPane;
2441
+ const anchors = docScrollEl.querySelectorAll(".commentray-block-anchor");
2442
+ if (anchors.length === 0)
2443
+ return null;
2444
+ const mdLine0 = probeCommentrayLine0FromDoc(docScrollEl);
2445
+ if (!Number.isFinite(mdLine0) || mdLine0 < 0)
2446
+ return null;
2447
+ return `commentray-md-line-${String(mdLine0)}`;
2448
+ }
2449
+ function maybeBackfillAddressBarWithHumanePairLink() {
2450
+ const shell = document.getElementById("shell");
2451
+ if (!(shell instanceof HTMLElement))
2452
+ return;
2453
+ const pathname = globalThis.location.pathname;
2454
+ normalizeDocumentationHomeHrefForCurrentPath();
2455
+ if (!shellEligibleForHumaneBackfill(shell, pathname))
2456
+ return;
2457
+ const beforeHref = globalThis.location.href;
2458
+ absolutizeNavJsonUrls(shell, beforeHref);
2459
+ normalizePairBrowseHrefForCurrentPath(shell, pathname);
2460
+ /** Prefer the same `staticBrowseUrl` the static build put on `#shell` (slug or `…/index.html` shims). */
2461
+ const canonicalBrowsePathname = (() => {
2462
+ const raw = shell.getAttribute("data-commentray-pair-browse-href")?.trim() ?? "";
2463
+ if (raw.length === 0)
2464
+ return null;
2465
+ try {
2466
+ const u = new URL(raw, globalThis.location.href);
2467
+ if (u.origin !== globalThis.location.origin)
2468
+ return null;
2469
+ if (!u.pathname.includes("/browse/"))
2470
+ return null;
2471
+ return u.pathname;
2472
+ }
2473
+ catch {
2474
+ return null;
2475
+ }
2476
+ })();
2477
+ const nextPath = canonicalBrowsePathname ?? nextHumaneBrowsePathForShell(shell, pathname);
2478
+ if (nextPath === null)
2479
+ return;
2480
+ globalThis.history.replaceState(null, "", `${nextPath}${globalThis.location.search}${globalThis.location.hash}`);
2481
+ }
2482
+ function permalinkHashSuffixFromUi() {
2483
+ const tokens = [];
2484
+ const pushUnique = (token) => {
2485
+ const t = token.trim();
2486
+ if (t.length === 0)
2487
+ return;
2488
+ if (!tokens.includes(t))
2489
+ tokens.push(t);
2490
+ };
2491
+ const angleSel = document.getElementById("angle-select");
2492
+ if (angleSel instanceof HTMLSelectElement) {
2493
+ const id = angleSel.value.trim();
2494
+ if (id.length > 0) {
2495
+ pushUnique(`angle-${encodeURIComponent(id)}`);
2496
+ }
2497
+ }
2498
+ const activeAnchor = activeCommentrayHashTokenFromViewport();
2499
+ if (activeAnchor)
2500
+ pushUnique(activeAnchor);
2501
+ return tokens.length > 0 ? `#${tokens.join("&")}` : "";
2502
+ }
2503
+ function sharePermalinkFromShell(shell) {
2504
+ const raw = shell.getAttribute("data-commentray-pair-browse-href") ?? "";
2505
+ const canonical = isHubRelativeStaticBrowseHref(raw.trim()) && raw.trim().length > 0
2506
+ ? resolveStaticBrowseHref(raw.trim(), globalThis.location.pathname, globalThis.location.origin)
2507
+ : safePermalinkHref(raw);
2508
+ const base = canonical ?? globalThis.location.href;
2509
+ const u = new URL(base, globalThis.location.href);
2510
+ const hash = permalinkHashSuffixFromUi();
2511
+ u.hash = hash.length > 0 ? hash.slice(1) : "";
2512
+ return u.toString();
2513
+ }
2514
+ async function writeTextToClipboard(text) {
2515
+ try {
2516
+ await navigator.clipboard.writeText(text);
2517
+ return true;
2518
+ }
2519
+ catch {
2520
+ const ta = document.createElement("textarea");
2521
+ ta.value = text;
2522
+ ta.setAttribute("readonly", "true");
2523
+ ta.style.position = "fixed";
2524
+ ta.style.top = "-1000px";
2525
+ ta.style.left = "-1000px";
2526
+ document.body.appendChild(ta);
2527
+ ta.select();
2528
+ try {
2529
+ return document.execCommand("copy");
2530
+ }
2531
+ catch {
2532
+ return false;
2533
+ }
2534
+ finally {
2535
+ document.body.removeChild(ta);
2536
+ }
2537
+ }
2538
+ }
2539
+ function wireSharePermalinkButton() {
2540
+ const shell = document.getElementById("shell");
2541
+ const btn = document.getElementById("commentray-share-link");
2542
+ if (!(shell instanceof HTMLElement) || !(btn instanceof HTMLButtonElement))
2543
+ return;
2544
+ const baseLabel = "Copy shareable permalink";
2545
+ let copiedTimer;
2546
+ btn.addEventListener("click", () => {
2547
+ void (async () => {
2548
+ const shareUrl = sharePermalinkFromShell(shell);
2549
+ const copied = await writeTextToClipboard(shareUrl);
2550
+ if (!copied)
2551
+ return;
2552
+ btn.dataset.copied = "true";
2553
+ btn.setAttribute("aria-label", "Permalink copied");
2554
+ btn.title = "Permalink copied";
2555
+ if (copiedTimer !== undefined)
2556
+ globalThis.clearTimeout(copiedTimer);
2557
+ copiedTimer = globalThis.setTimeout(() => {
2558
+ delete btn.dataset.copied;
2559
+ btn.setAttribute("aria-label", baseLabel);
2560
+ btn.title = baseLabel;
2561
+ }, 1200);
2562
+ })();
2563
+ });
2564
+ }
1920
2565
  function main() {
2566
+ wireSharePermalinkButton();
1921
2567
  wireColorThemeToolbar();
1922
2568
  wireDocumentedFilesTree();
1923
2569
  const shell = document.getElementById("shell");
@@ -1925,12 +2571,16 @@ function main() {
1925
2571
  if (!shell || !codePane) {
1926
2572
  return;
1927
2573
  }
2574
+ applyPageBreakFeatureToggle(shell);
2575
+ wireResponsivePageBreakHeight(shell);
2576
+ wireWideModeIntroTrigger(shell);
1928
2577
  const layout = shell.getAttribute("data-layout") || "dual";
1929
2578
  if (layout === "stretch") {
1930
2579
  wireStretchLayoutChrome(codePane);
1931
2580
  return;
1932
2581
  }
1933
2582
  wireDualPaneCodeBrowser(shell, codePane);
2583
+ maybeBackfillAddressBarWithHumanePairLink();
1934
2584
  }
1935
2585
  if (document.readyState === "loading") {
1936
2586
  document.addEventListener("DOMContentLoaded", main);