@commentray/render 0.0.9 → 0.1.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 (32) hide show
  1. package/README.md +1 -1
  2. package/dist/code-browser-client.bundle.js +24 -11
  3. package/dist/code-browser-client.js +697 -100
  4. package/dist/code-browser-client.js.map +1 -1
  5. package/dist/code-browser-intro.css +187 -0
  6. package/dist/code-browser-wide-intro-controller.d.ts +4 -0
  7. package/dist/code-browser-wide-intro-controller.d.ts.map +1 -0
  8. package/dist/code-browser-wide-intro-controller.js +148 -0
  9. package/dist/code-browser-wide-intro-controller.js.map +1 -0
  10. package/dist/code-browser-wide-intro-layout.d.ts +3 -0
  11. package/dist/code-browser-wide-intro-layout.d.ts.map +1 -0
  12. package/dist/code-browser-wide-intro-layout.js +84 -0
  13. package/dist/code-browser-wide-intro-layout.js.map +1 -0
  14. package/dist/code-browser-wide-intro-steps.d.ts +11 -0
  15. package/dist/code-browser-wide-intro-steps.d.ts.map +1 -0
  16. package/dist/code-browser-wide-intro-steps.js +108 -0
  17. package/dist/code-browser-wide-intro-steps.js.map +1 -0
  18. package/dist/code-browser-wide-intro-ui.d.ts +14 -0
  19. package/dist/code-browser-wide-intro-ui.d.ts.map +1 -0
  20. package/dist/code-browser-wide-intro-ui.js +67 -0
  21. package/dist/code-browser-wide-intro-ui.js.map +1 -0
  22. package/dist/code-browser.d.ts.map +1 -1
  23. package/dist/code-browser.js +597 -42
  24. package/dist/code-browser.js.map +1 -1
  25. package/dist/markdown-pipeline.d.ts.map +1 -1
  26. package/dist/markdown-pipeline.js +7 -1
  27. package/dist/markdown-pipeline.js.map +1 -1
  28. package/dist/side-by-side-layout-css.d.ts +1 -1
  29. package/dist/side-by-side-layout-css.d.ts.map +1 -1
  30. package/dist/side-by-side-layout-css.js +48 -0
  31. package/dist/side-by-side-layout-css.js.map +1 -1
  32. package/package.json +4 -3
@@ -4,8 +4,9 @@ import { mirroredScrollTop, pickCommentrayLineForSourceScroll, pickSourceLine0Fo
4
4
  import { decodeBase64Utf8 } from "./code-browser-encoding.js";
5
5
  import { readEmbeddedRawB64Strings } from "./code-browser-embedded-payload.js";
6
6
  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";
7
+ import { findDocumentedPair, isHubRelativeStaticBrowseHref, isSameDocumentedPair, normPosixPath, resolveStaticBrowseHref, siteRootPathnameFromPathname, staticBrowseHrefForShellDataAttribute, } from "./code-browser-pair-nav.js";
8
8
  import { COMMENTRAY_COLOR_THEME_STORAGE_KEY, applyCommentrayColorTheme, nextCommentrayColorThemeMode, parseCommentrayColorThemeMode, } from "./code-browser-color-theme.js";
9
+ import { wireWideModeIntroTour } from "./code-browser-wide-intro-controller.js";
9
10
  import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
10
11
  /**
11
12
  * Hub pages emit `./browse/…` relative to the site root. From `/…/browse/current.html` the browser
@@ -104,9 +105,13 @@ function windowScrollRatio() {
104
105
  const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
105
106
  return maxY > 0 ? clamp(root.scrollTop / maxY, 0, 1) : 0;
106
107
  }
107
- function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
108
+ function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan, lineIdPrefix = "code-line-") {
109
+ const narrowSinglePane = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ).matches;
108
110
  if (plan.k === "block") {
109
- const el = codePane.querySelector(`#code-line-${String(plan.src0)}`);
111
+ const exact = codePane.querySelector(`#${lineIdPrefix}${String(plan.src0)}`);
112
+ const el = exact instanceof HTMLElement
113
+ ? exact
114
+ : findAnchorAtOrAfter(sourceAnchorsFromPrefix(lineIdPrefix), plan.src0);
110
115
  if (el) {
111
116
  applyRevealChildInPane(codePane, el, 2);
112
117
  }
@@ -119,6 +124,8 @@ function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
119
124
  if (paneUsesInternalYScroll(codePane)) {
120
125
  const maxC = Math.max(0, codePane.scrollHeight - codePane.clientHeight);
121
126
  applyScrollTopClamped(codePane, plan.ratio * maxC);
127
+ if (narrowSinglePane)
128
+ applyWindowScrollRatio(plan.ratio);
122
129
  }
123
130
  else {
124
131
  applyWindowScrollRatio(plan.ratio);
@@ -128,6 +135,10 @@ function applyDocToCodeFlipPlanImpl(codePane, _docPane, plan) {
128
135
  const nextTop = mirroredScrollTop(plan.docTop, plan.docSH, plan.docCH, codePane.scrollHeight, codePane.clientHeight);
129
136
  if (paneUsesInternalYScroll(codePane)) {
130
137
  applyScrollTopClamped(codePane, nextTop);
138
+ if (narrowSinglePane) {
139
+ const denom = Math.max(1, codePane.scrollHeight - codePane.clientHeight);
140
+ applyWindowScrollRatio(clamp(nextTop / denom, 0, 1));
141
+ }
131
142
  return;
132
143
  }
133
144
  const denom = Math.max(1, codePane.scrollHeight - codePane.clientHeight);
@@ -163,7 +174,12 @@ function applyCodeToDocFlipPlanImpl(_codePane, docPane, plan) {
163
174
  applyWindowScrollRatio(clamp(nextTop / denom, 0, 1));
164
175
  }
165
176
  function buildDocToCodeFlipPlanBlockAware(docPane, getLinks) {
166
- const winRatio = windowScrollRatio();
177
+ const winRatio = paneUsesInternalYScroll(docPane)
178
+ ? clamp(docPane.scrollTop / Math.max(1, docPane.scrollHeight - docPane.clientHeight), 0, 1)
179
+ : windowScrollRatio();
180
+ const pulledSrc0 = pulledSourceLine0FromPageBreak(docPane);
181
+ if (pulledSrc0 !== null)
182
+ return { k: "block", src0: pulledSrc0, winRatio };
167
183
  const links = getLinks();
168
184
  const mdLine0 = probeCommentrayLine0FromDoc(docPane);
169
185
  const src0 = pickSourceLine0ForCommentrayScroll(links, mdLine0);
@@ -179,10 +195,10 @@ function buildDocToCodeFlipPlanBlockAware(docPane, getLinks) {
179
195
  }
180
196
  return { k: "mirrorW", ratio: winRatio };
181
197
  }
182
- function buildCodeToDocFlipPlanBlockAware(codePane, _docPane, getLinks) {
198
+ function buildCodeToDocFlipPlanBlockAware(codePane, _docPane, getLinks, lineIdPrefix = "code-line-") {
183
199
  const winRatio = windowScrollRatio();
184
200
  const links = getLinks();
185
- const line1 = probeCodeLine1FromViewport(codePane);
201
+ const line1 = probeCodeLine1FromViewport(codePane, lineIdPrefix);
186
202
  const mdLine0 = pickCommentrayLineForSourceScroll(links, line1);
187
203
  if (mdLine0 === null) {
188
204
  if (paneUsesInternalYScroll(codePane)) {
@@ -703,14 +719,45 @@ function rootScrollNearDocumentEnd(edgePx = 56) {
703
719
  const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
704
720
  return maxY > 0 && root.scrollTop >= maxY - edgePx;
705
721
  }
706
- function probeCodeLine1FromViewport(codePane) {
707
- const rows = codePane.querySelectorAll('[id^="code-line-"]');
722
+ /** When the pane itself is the scrollport (dual desktop), mirror root “near end” behavior. */
723
+ function paneScrollNearEnd(pane, edgePx = 56) {
724
+ const maxY = Math.max(0, pane.scrollHeight - pane.clientHeight);
725
+ return maxY > 0 && pane.scrollTop >= maxY - edgePx;
726
+ }
727
+ function readCommentrayLine0FromAnchor(el) {
728
+ const lineAttr = el.getAttribute("data-commentray-line");
729
+ if (lineAttr === null || lineAttr === "")
730
+ return null;
731
+ return Number(lineAttr);
732
+ }
733
+ /** Greatest marker line whose anchor sits at or above viewport Y `y`. */
734
+ function bestCommentrayAnchorLine0AtOrAboveY(anchors, y) {
735
+ let best = 0;
736
+ for (const a of anchors) {
737
+ const line0 = readCommentrayLine0FromAnchor(a);
738
+ if (line0 === null)
739
+ continue;
740
+ if (a.getBoundingClientRect().top <= y + 1 + 1e-3)
741
+ best = line0;
742
+ else
743
+ break;
744
+ }
745
+ return best;
746
+ }
747
+ function lastCommentrayAnchorLine0(anchors) {
748
+ const last = anchors[anchors.length - 1];
749
+ if (!last)
750
+ return 0;
751
+ return readCommentrayLine0FromAnchor(last) ?? 0;
752
+ }
753
+ function probeCodeLine1FromViewport(codePane, lineIdPrefix = "code-line-") {
754
+ const rows = codePane.querySelectorAll(`[id^="${lineIdPrefix}"]`);
708
755
  if (rows.length === 0)
709
756
  return 1;
710
757
  if (!paneUsesInternalYScroll(codePane)) {
711
758
  if (rootScrollNearDocumentEnd()) {
712
759
  const last = rows[rows.length - 1];
713
- const m = /^code-line-(\d+)$/.exec(last.id);
760
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(last.id);
714
761
  if (m)
715
762
  return Number(m[1]) + 1;
716
763
  return rows.length;
@@ -723,7 +770,7 @@ function probeCodeLine1FromViewport(codePane) {
723
770
  for (const el of rows) {
724
771
  const r = el.getBoundingClientRect();
725
772
  if (r.bottom > y - 1e-3) {
726
- const m = /^code-line-(\d+)$/.exec(el.id);
773
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(el.id);
727
774
  if (m)
728
775
  return Number(m[1]) + 1;
729
776
  return 1;
@@ -731,12 +778,19 @@ function probeCodeLine1FromViewport(codePane) {
731
778
  }
732
779
  return rows.length;
733
780
  }
781
+ if (paneScrollNearEnd(codePane)) {
782
+ const last = rows[rows.length - 1];
783
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(last.id);
784
+ if (m)
785
+ return Number(m[1]) + 1;
786
+ return rows.length;
787
+ }
734
788
  const sr = codePane.getBoundingClientRect();
735
789
  const y = sr.top + codePane.clientTop + 2;
736
790
  for (const el of rows) {
737
791
  const r = el.getBoundingClientRect();
738
792
  if (r.bottom > y - 1e-3) {
739
- const m = /^code-line-(\d+)$/.exec(el.id);
793
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(el.id);
740
794
  if (m)
741
795
  return Number(m[1]) + 1;
742
796
  return 1;
@@ -749,41 +803,74 @@ function probeCommentrayLine0FromDoc(docPane) {
749
803
  if (anchors.length === 0)
750
804
  return 0;
751
805
  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
- }
806
+ if (rootScrollNearDocumentEnd())
807
+ return lastCommentrayAnchorLine0(anchors);
757
808
  const dr = docPane.getBoundingClientRect();
758
809
  const vh = globalThis.innerHeight;
759
810
  const clipT = Math.max(0, dr.top);
760
811
  const clipB = Math.min(vh, dr.bottom);
761
812
  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;
813
+ return bestCommentrayAnchorLine0AtOrAboveY(anchors, y);
773
814
  }
815
+ if (paneScrollNearEnd(docPane))
816
+ return lastCommentrayAnchorLine0(anchors);
774
817
  const dr = docPane.getBoundingClientRect();
775
818
  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 === "")
819
+ return bestCommentrayAnchorLine0AtOrAboveY(anchors, y);
820
+ }
821
+ function pageBreakPullEnabled() {
822
+ const shell = document.getElementById("shell");
823
+ if (!(shell instanceof HTMLElement))
824
+ return false;
825
+ return shell.getAttribute("data-page-breaks-enabled") === "true";
826
+ }
827
+ function docProbeTopY(docPane) {
828
+ if (!paneUsesInternalYScroll(docPane)) {
829
+ const dr = docPane.getBoundingClientRect();
830
+ const vh = globalThis.innerHeight;
831
+ const clipT = Math.max(0, dr.top);
832
+ const clipB = Math.min(vh, dr.bottom);
833
+ return clipT + Math.max(2, Math.min(40, (clipB - clipT) * 0.15));
834
+ }
835
+ const dr = docPane.getBoundingClientRect();
836
+ return dr.top + docPane.clientTop + 2;
837
+ }
838
+ /**
839
+ * In long synthetic page-break gaps, shift source toward the next block once
840
+ * the break itself occupies the top reading position.
841
+ */
842
+ function pulledSourceLine0FromPageBreak(docPane) {
843
+ if (!pageBreakPullEnabled())
844
+ return null;
845
+ const topY = docProbeTopY(docPane);
846
+ const breaks = Array.from(docPane.querySelectorAll(".commentray-page-break[data-next-source-start]"));
847
+ for (const pageBreak of breaks) {
848
+ const nextSourceStartRaw = pageBreak.getAttribute("data-next-source-start");
849
+ if (!nextSourceStartRaw)
780
850
  continue;
781
- if (a.getBoundingClientRect().top <= y + 1 + 1e-3)
782
- best = Number(lineAttr);
783
- else
784
- break;
851
+ const nextSourceStart = Number.parseInt(nextSourceStartRaw, 10);
852
+ if (!Number.isFinite(nextSourceStart) || nextSourceStart <= 0)
853
+ continue;
854
+ const breakTop = pageBreak.getBoundingClientRect().top;
855
+ const nextLineRaw = pageBreak.getAttribute("data-next-commentray-line");
856
+ const nextLine0 = nextLineRaw ? Number.parseInt(nextLineRaw, 10) : Number.NaN;
857
+ const nextAnchor = Number.isFinite(nextLine0) && nextLine0 >= 0
858
+ ? docPane.querySelector(`[data-commentray-line="${String(nextLine0)}"]`)
859
+ : null;
860
+ const nextTop = nextAnchor
861
+ ? nextAnchor.getBoundingClientRect().top
862
+ : breakTop + pageBreak.clientHeight;
863
+ if (!(breakTop <= topY && topY < nextTop))
864
+ continue;
865
+ const denom = Math.max(1, nextTop - breakTop);
866
+ const progress = clamp((topY - breakTop) / denom, 0, 1);
867
+ const narrow = globalThis.matchMedia("(max-width: 767px)").matches;
868
+ const pullThreshold = narrow ? 0.2 : 0.35;
869
+ if (progress < pullThreshold)
870
+ return null;
871
+ return nextSourceStart - 1;
785
872
  }
786
- return best;
873
+ return null;
787
874
  }
788
875
  /**
789
876
  * Programmatic `scrollTop` on the partner pane can emit a `scroll` event after the
@@ -832,16 +919,20 @@ function wireBidirectionalScroll(codePane, docPane, syncFromCode, syncFromDoc) {
832
919
  }, { passive: true });
833
920
  }
834
921
  /** Index-backed scroll sync when `data-scroll-block-links-b64` is present; else see proportional fallback. */
835
- function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
922
+ function wireBlockAwareScrollSync(codePane, docPane, getLinks, lineIdPrefix, shouldUseProportionalDocToCodeOnMobileFlip) {
836
923
  let pendingDocToCode = null;
837
924
  let pendingCodeToDoc = null;
838
925
  const syncFromCodeToDoc = () => {
839
- applyCodeToDocFlipPlanImpl(codePane, docPane, buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks));
926
+ applyCodeToDocFlipPlanImpl(codePane, docPane, buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks, lineIdPrefix()));
840
927
  };
841
928
  const syncFromDocToCode = () => {
842
- applyDocToCodeFlipPlanImpl(codePane, docPane, buildDocToCodeFlipPlanBlockAware(docPane, getLinks));
929
+ applyDocToCodeFlipPlanImpl(codePane, docPane, buildDocToCodeFlipPlanBlockAware(docPane, getLinks), lineIdPrefix());
843
930
  };
844
931
  const prepareMobileFlipToCode = () => {
932
+ if (shouldUseProportionalDocToCodeOnMobileFlip?.() === true) {
933
+ pendingDocToCode = { k: "mirrorW", ratio: windowScrollRatio() };
934
+ return;
935
+ }
845
936
  pendingDocToCode = buildDocToCodeFlipPlanBlockAware(docPane, getLinks);
846
937
  };
847
938
  const finishMobileFlipToCode = () => {
@@ -849,10 +940,10 @@ function wireBlockAwareScrollSync(codePane, docPane, getLinks) {
849
940
  return;
850
941
  const p = pendingDocToCode;
851
942
  pendingDocToCode = null;
852
- applyDocToCodeFlipPlanImpl(codePane, docPane, p);
943
+ applyDocToCodeFlipPlanImpl(codePane, docPane, p, lineIdPrefix());
853
944
  };
854
945
  const prepareMobileFlipToDoc = () => {
855
- pendingCodeToDoc = buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks);
946
+ pendingCodeToDoc = buildCodeToDocFlipPlanBlockAware(codePane, docPane, getLinks, lineIdPrefix());
856
947
  };
857
948
  const finishMobileFlipToDoc = () => {
858
949
  if (!pendingCodeToDoc)
@@ -936,6 +1027,52 @@ function commentaryBandEndYViewport(docScrollEl, next, docTop) {
936
1027
  bottom = Math.min(bottom, lastKid.getBoundingClientRect().bottom - 4);
937
1028
  return bottom;
938
1029
  }
1030
+ function sourceAnchorIndexFromId(id, prefix) {
1031
+ if (!id.startsWith(prefix))
1032
+ return null;
1033
+ const n = Number.parseInt(id.slice(prefix.length), 10);
1034
+ return Number.isFinite(n) ? n : null;
1035
+ }
1036
+ function findAnchorAtOrAfter(anchors, line0) {
1037
+ let lo = 0;
1038
+ let hi = anchors.length - 1;
1039
+ let ans = -1;
1040
+ while (lo <= hi) {
1041
+ const mid = (lo + hi) >> 1;
1042
+ const line = anchors[mid]?.line0 ?? -1;
1043
+ if (line >= line0) {
1044
+ ans = mid;
1045
+ hi = mid - 1;
1046
+ }
1047
+ else {
1048
+ lo = mid + 1;
1049
+ }
1050
+ }
1051
+ return ans >= 0 ? (anchors[ans]?.el ?? null) : null;
1052
+ }
1053
+ function findAnchorAtOrBefore(anchors, line0) {
1054
+ let lo = 0;
1055
+ let hi = anchors.length - 1;
1056
+ let ans = -1;
1057
+ while (lo <= hi) {
1058
+ const mid = (lo + hi) >> 1;
1059
+ const line = anchors[mid]?.line0 ?? -1;
1060
+ if (line <= line0) {
1061
+ ans = mid;
1062
+ lo = mid + 1;
1063
+ }
1064
+ else {
1065
+ hi = mid - 1;
1066
+ }
1067
+ }
1068
+ return ans >= 0 ? (anchors[ans]?.el ?? null) : null;
1069
+ }
1070
+ function sourceAnchorsFromPrefix(prefix) {
1071
+ return Array.from(document.querySelectorAll(`[id^="${prefix}"]`))
1072
+ .map((el) => ({ line0: sourceAnchorIndexFromId(el.id, prefix), el }))
1073
+ .filter((x) => x.line0 !== null)
1074
+ .sort((a, b) => a.line0 - b.line0);
1075
+ }
939
1076
  function subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw) {
940
1077
  const onScrollOrResize = () => scheduleDraw();
941
1078
  codePane.addEventListener("scroll", onScrollOrResize, { passive: true });
@@ -949,7 +1086,7 @@ function subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw) {
949
1086
  if (shell)
950
1087
  ro.observe(shell);
951
1088
  }
952
- function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based) {
1089
+ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based, lineIdPrefix) {
953
1090
  const links = getLinks();
954
1091
  const sorted = sortBlockLinksBySource(links);
955
1092
  const gutterRect = gutter.getBoundingClientRect();
@@ -963,6 +1100,10 @@ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSource
963
1100
  svg.setAttribute("viewBox", `0 0 ${String(w)} ${String(h)}`);
964
1101
  svg.setAttribute("preserveAspectRatio", "none");
965
1102
  const parts = [];
1103
+ const sourceAnchors = Array.from(document.querySelectorAll(`[id^="${lineIdPrefix}"]`))
1104
+ .map((el) => ({ line0: sourceAnchorIndexFromId(el.id, lineIdPrefix), el }))
1105
+ .filter((x) => x.line0 !== null)
1106
+ .sort((a, b) => a.line0 - b.line0);
966
1107
  for (let i = 0; i < sorted.length; i++) {
967
1108
  const link = sorted[i];
968
1109
  if (!link)
@@ -970,8 +1111,10 @@ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSource
970
1111
  const next = sorted[i + 1];
971
1112
  const i0 = codeLineDomIndex0(link.sourceStart);
972
1113
  const i1 = codeLineDomIndex0(link.sourceEnd);
973
- const codeTop = document.getElementById(`code-line-${String(i0)}`);
974
- const codeBot = document.getElementById(`code-line-${String(i1)}`);
1114
+ const codeTop = document.getElementById(`${lineIdPrefix}${String(i0)}`) ??
1115
+ findAnchorAtOrAfter(sourceAnchors, i0);
1116
+ const codeBot = document.getElementById(`${lineIdPrefix}${String(i1)}`) ??
1117
+ findAnchorAtOrBefore(sourceAnchors, i1);
975
1118
  const docTop = document.getElementById(`commentray-block-${link.id}`);
976
1119
  if (!codeTop || !codeBot || !docTop)
977
1120
  continue;
@@ -1016,6 +1159,7 @@ function drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSource
1016
1159
  */
1017
1160
  function wireBlockRayConnectors(args) {
1018
1161
  const { gutter, codePane, docScrollEl, getLinks, probeTopSourceLine1Based } = args;
1162
+ const sourceLineIdPrefix = args.sourceLineIdPrefix ?? (() => "code-line-");
1019
1163
  const svgNs = "http://www.w3.org/2000/svg";
1020
1164
  const host = document.createElement("div");
1021
1165
  host.className = "gutter__rays";
@@ -1029,7 +1173,7 @@ function wireBlockRayConnectors(args) {
1029
1173
  return;
1030
1174
  raf = globalThis.requestAnimationFrame(() => {
1031
1175
  raf = 0;
1032
- drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based);
1176
+ drawBlockRaysIntoSvg(svg, gutter, docScrollEl, getLinks, probeTopSourceLine1Based, sourceLineIdPrefix());
1033
1177
  });
1034
1178
  }
1035
1179
  subscribeBlockRayRedraw(gutter, codePane, docScrollEl, scheduleDraw);
@@ -1183,6 +1327,40 @@ function treeFileLinkTitle(pr) {
1183
1327
  }
1184
1328
  return pr.sourcePath;
1185
1329
  }
1330
+ function clearDocumentedTreePairHighlights(tree) {
1331
+ for (const el of tree.querySelectorAll("a.tree-file-link")) {
1332
+ if (!(el instanceof HTMLAnchorElement))
1333
+ continue;
1334
+ el.classList.remove("tree-file-link--current");
1335
+ el.removeAttribute("aria-current");
1336
+ }
1337
+ }
1338
+ function markFirstDocumentedTreeLinkMatchingPair(tree, curSrc, curCr) {
1339
+ for (const el of tree.querySelectorAll("a.tree-file-link")) {
1340
+ if (!(el instanceof HTMLAnchorElement))
1341
+ continue;
1342
+ const sp = el.getAttribute("data-pair-source-path")?.trim() ?? "";
1343
+ const cp = el.getAttribute("data-pair-commentray-path")?.trim() ?? "";
1344
+ if (!isSameDocumentedPair({ sourcePath: sp, commentrayPath: cp }, curSrc, curCr))
1345
+ continue;
1346
+ el.classList.add("tree-file-link--current");
1347
+ el.setAttribute("aria-current", "page");
1348
+ break;
1349
+ }
1350
+ }
1351
+ /** Marks the tree link for the pair shown in `#shell` (pair paths from server or multi-angle swap). */
1352
+ function applyDocumentedTreeCurrentPairHighlight() {
1353
+ const shell = document.getElementById("shell");
1354
+ const tree = document.getElementById("documented-files-tree");
1355
+ if (!(shell instanceof HTMLElement) || !(tree instanceof HTMLElement))
1356
+ return;
1357
+ clearDocumentedTreePairHighlights(tree);
1358
+ const curSrc = shell.getAttribute("data-commentray-pair-source-path")?.trim() ?? "";
1359
+ const curCr = shell.getAttribute("data-commentray-pair-commentray-path")?.trim() ?? "";
1360
+ if (curSrc.length === 0 || curCr.length === 0)
1361
+ return;
1362
+ markFirstDocumentedTreeLinkMatchingPair(tree, curSrc, curCr);
1363
+ }
1186
1364
  function renderDocumentedTreeHtml(node) {
1187
1365
  const keys = [...node.children.keys()].sort((a, b) => a.localeCompare(b));
1188
1366
  if (keys.length === 0)
@@ -1202,10 +1380,12 @@ function renderDocumentedTreeHtml(node) {
1202
1380
  const label = escapeHtmlText(treeFileLinkLabel(pr, multi));
1203
1381
  const title = escapeHtmlText(treeFileLinkTitle(pr));
1204
1382
  const href = escapeHtmlText(treeFileLinkHref(pr));
1383
+ const spAttr = escapeHtmlText(normPosixPath(pr.sourcePath));
1384
+ const crAttr = escapeHtmlText(normPosixPath(pr.commentrayPath));
1205
1385
  const useSiteBrowse = (pr.staticBrowseUrl?.trim() ?? "").length > 0;
1206
1386
  const external = useSiteBrowse ? "" : ' target="_blank" rel="noopener noreferrer"';
1207
1387
  lis.push(`<li><div class="tree-file">` +
1208
- `<a class="tree-file-link" href="${href}"${external} title="${title}">${label}</a>` +
1388
+ `<a class="tree-file-link" href="${href}" data-pair-source-path="${spAttr}" data-pair-commentray-path="${crAttr}"${external} title="${title}">${label}</a>` +
1209
1389
  `</div></li>`);
1210
1390
  }
1211
1391
  }
@@ -1223,6 +1403,7 @@ function renderDocumentedPairsIntoHost(treeHost, pairs, emptyBecauseFilter) {
1223
1403
  for (const p of pairs)
1224
1404
  insertSourcePathTrie(root, p);
1225
1405
  treeHost.innerHTML = renderDocumentedTreeHtml(root);
1406
+ applyDocumentedTreeCurrentPairHighlight();
1226
1407
  }
1227
1408
  function loadDocumentedPairs(jsonUrl, embeddedB64) {
1228
1409
  let loaded = null;
@@ -1298,6 +1479,12 @@ function wireDocumentedFilesTreeMobileFlyout(hub) {
1298
1479
  globalThis.addEventListener("scroll", placeFlyout, true);
1299
1480
  return placeFlyout;
1300
1481
  }
1482
+ function focusDocumentedFilesFilterInput() {
1483
+ const el = document.getElementById("documented-files-filter");
1484
+ if (!(el instanceof HTMLInputElement))
1485
+ return;
1486
+ el.focus({ preventScroll: true });
1487
+ }
1301
1488
  function wireDocumentedFilesTree() {
1302
1489
  const hub = document.getElementById("documented-files-hub");
1303
1490
  const treeHost = document.getElementById("documented-files-tree");
@@ -1337,7 +1524,10 @@ function wireDocumentedFilesTree() {
1337
1524
  hub.addEventListener("toggle", () => {
1338
1525
  placeDocHubFlyout();
1339
1526
  if (hub.open) {
1340
- globalThis.requestAnimationFrame(placeDocHubFlyout);
1527
+ globalThis.requestAnimationFrame(() => {
1528
+ placeDocHubFlyout();
1529
+ focusDocumentedFilesFilterInput();
1530
+ });
1341
1531
  }
1342
1532
  if (!hub.open)
1343
1533
  return;
@@ -1384,11 +1574,61 @@ function wireSplitter(storageSplit, shell, codePane, gutter, initialPct) {
1384
1574
  const STORAGE_SPLIT_PCT = "commentray.codeCommentrayStatic.splitPct";
1385
1575
  const STORAGE_WRAP_LINES = "commentray.codeCommentrayStatic.wrap";
1386
1576
  const STORAGE_DUAL_MOBILE_PANE = "commentray.codeCommentrayStatic.dualMobilePane";
1577
+ const STORAGE_SOURCE_MARKDOWN_PANE_MODE = "commentray.codeCommentrayStatic.sourceMarkdownPaneMode";
1578
+ const STORAGE_PAGE_BREAKS_ENABLED = "commentray.codeCommentrayStatic.pageBreaksEnabled";
1387
1579
  /** Matches `code-browser.ts` `@media (max-width: 767px)` (dual column from 768px up). */
1388
1580
  const DUAL_MOBILE_SINGLE_PANE_MQ = "(max-width: 767px)";
1389
1581
  function normalizedDualMobilePane(v) {
1390
1582
  return v === "code" ? "code" : "doc";
1391
1583
  }
1584
+ function isNarrowViewport() {
1585
+ return globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ).matches;
1586
+ }
1587
+ function wireWideModeIntroTrigger(shell) {
1588
+ const btn = document.getElementById("commentray-help-tour");
1589
+ if (!(btn instanceof HTMLButtonElement))
1590
+ return;
1591
+ btn.addEventListener("click", () => {
1592
+ wireWideModeIntroTour(shell, isNarrowViewport, { force: true });
1593
+ });
1594
+ }
1595
+ function sourcePaneModeForShell(shell) {
1596
+ return shell.getAttribute("data-source-pane-mode") === "rendered-markdown"
1597
+ ? "rendered-markdown"
1598
+ : "source";
1599
+ }
1600
+ function pageBreaksEnabledFromStorage(raw) {
1601
+ const t = (raw ?? "").trim().toLowerCase();
1602
+ if (t === "0" || t === "false" || t === "off")
1603
+ return false;
1604
+ return true;
1605
+ }
1606
+ function applyPageBreakFeatureToggle(shell) {
1607
+ const enabled = pageBreaksEnabledFromStorage(readWebStorageItem(localStorage, STORAGE_PAGE_BREAKS_ENABLED));
1608
+ shell.setAttribute("data-page-breaks-enabled", enabled ? "true" : "false");
1609
+ }
1610
+ function wireResponsivePageBreakHeight(shell) {
1611
+ const setHeight = () => {
1612
+ const viewportHeight = Math.max(globalThis.innerHeight, document.documentElement?.clientHeight ?? 0);
1613
+ if (!Number.isFinite(viewportHeight) || viewportHeight <= 0)
1614
+ return;
1615
+ const minHeightPx = Math.round(clamp(viewportHeight * 0.72, 260, 820));
1616
+ shell.style.setProperty("--commentray-page-break-min-height", `${String(minHeightPx)}px`);
1617
+ };
1618
+ globalThis.addEventListener("resize", setHeight, { passive: true });
1619
+ globalThis.addEventListener("orientationchange", setHeight, { passive: true });
1620
+ globalThis.visualViewport?.addEventListener("resize", setHeight, { passive: true });
1621
+ setHeight();
1622
+ }
1623
+ function syncWrapLinesVisibilityForSourcePaneMode(shell) {
1624
+ const wrapToggle = document.querySelector("label.toolbar-wrap-lines");
1625
+ if (!(wrapToggle instanceof HTMLLabelElement))
1626
+ return;
1627
+ wrapToggle.hidden = sourcePaneModeForShell(shell) === "rendered-markdown";
1628
+ }
1629
+ function sourceLineIdPrefixForShell(shell) {
1630
+ return sourcePaneModeForShell(shell) === "rendered-markdown" ? "code-md-line-" : "code-line-";
1631
+ }
1392
1632
  /** When the commentary pane is visible, (re)run Mermaid so diagrams are not laid out under display:none. */
1393
1633
  function scheduleMermaidWhenDualDocPaneVisible(shell, mq) {
1394
1634
  const kick = () => {
@@ -1440,6 +1680,105 @@ function wireDualMobilePaneFlipScrollAffordance(primaryFlip, scrollFlip, mq) {
1440
1680
  mq.addEventListener("change", tick);
1441
1681
  globalThis.requestAnimationFrame(tick);
1442
1682
  }
1683
+ function wireSourceMarkdownPaneFlipAffordance(primaryFlip, scrollFlip) {
1684
+ const hideScroll = () => {
1685
+ scrollFlip.hidden = true;
1686
+ scrollFlip.classList.remove("is-visible");
1687
+ };
1688
+ const showScroll = () => {
1689
+ scrollFlip.hidden = false;
1690
+ scrollFlip.classList.add("is-visible");
1691
+ };
1692
+ const tick = () => {
1693
+ const r = primaryFlip.getBoundingClientRect();
1694
+ const vh = globalThis.innerHeight;
1695
+ const margin = 10;
1696
+ const offScreen = r.bottom < margin || r.top > vh - margin;
1697
+ if (offScreen)
1698
+ showScroll();
1699
+ else
1700
+ hideScroll();
1701
+ };
1702
+ globalThis.addEventListener("scroll", tick, { passive: true });
1703
+ globalThis.addEventListener("resize", tick, { passive: true });
1704
+ globalThis.requestAnimationFrame(tick);
1705
+ }
1706
+ function closestSourceLine0ForPaneTop(codePane, idPrefix) {
1707
+ const rows = codePane.querySelectorAll(`[id^="${idPrefix}"]`);
1708
+ if (rows.length === 0)
1709
+ return null;
1710
+ const y = paneUsesInternalYScroll(codePane)
1711
+ ? codePane.getBoundingClientRect().top + codePane.clientTop + 2
1712
+ : Math.max(0, codePane.getBoundingClientRect().top) + 2;
1713
+ for (const el of rows) {
1714
+ const r = el.getBoundingClientRect();
1715
+ if (r.bottom > y - 1e-3) {
1716
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(el.id);
1717
+ if (!m?.[1])
1718
+ return null;
1719
+ return Number.parseInt(m[1], 10);
1720
+ }
1721
+ }
1722
+ const last = rows[rows.length - 1];
1723
+ if (!last)
1724
+ return null;
1725
+ const m = /^(?:code-line-|code-md-line-)(\d+)$/.exec(last.id);
1726
+ if (!m?.[1])
1727
+ return null;
1728
+ return Number.parseInt(m[1], 10);
1729
+ }
1730
+ function wireSourceMarkdownPaneFlip(shell, codePane, flipBtn, flipScrollBtn, onAfterFlip) {
1731
+ function syncSourceMarkdownFlipA11y() {
1732
+ const mode = sourcePaneModeForShell(shell);
1733
+ const renderedActive = mode === "rendered-markdown";
1734
+ const nextModeLabel = renderedActive ? "markdown source" : "rendered markdown";
1735
+ const ariaLabel = `Switch source pane to ${nextModeLabel}`;
1736
+ const title = `Source pane: ${renderedActive ? "rendered markdown" : "markdown source"} (click to switch)`;
1737
+ const apply = (btn) => {
1738
+ if (!(btn instanceof HTMLButtonElement))
1739
+ return;
1740
+ btn.setAttribute("aria-pressed", renderedActive ? "true" : "false");
1741
+ btn.setAttribute("aria-label", ariaLabel);
1742
+ btn.title = title;
1743
+ };
1744
+ apply(flipBtn);
1745
+ apply(flipScrollBtn);
1746
+ }
1747
+ // Keep initial behavior deterministic: source pane starts in rendered markdown mode.
1748
+ shell.setAttribute("data-source-pane-mode", "rendered-markdown");
1749
+ syncSourceMarkdownFlipA11y();
1750
+ syncWrapLinesVisibilityForSourcePaneMode(shell);
1751
+ const runFlip = () => {
1752
+ const cur = sourcePaneModeForShell(shell);
1753
+ const currentPrefix = cur === "rendered-markdown" ? "code-md-line-" : "code-line-";
1754
+ const line0 = closestSourceLine0ForPaneTop(codePane, currentPrefix);
1755
+ const next = cur === "rendered-markdown" ? "source" : "rendered-markdown";
1756
+ const nextPrefix = next === "rendered-markdown" ? "code-md-line-" : "code-line-";
1757
+ shell.setAttribute("data-source-pane-mode", next);
1758
+ writeWebStorageItem(localStorage, STORAGE_SOURCE_MARKDOWN_PANE_MODE, next);
1759
+ syncSourceMarkdownFlipA11y();
1760
+ syncWrapLinesVisibilityForSourcePaneMode(shell);
1761
+ if (line0 !== null) {
1762
+ const row = codePane.querySelector(`#${nextPrefix}${String(line0)}`);
1763
+ if (row instanceof HTMLElement) {
1764
+ applyRevealChildInPane(codePane, row, 2);
1765
+ }
1766
+ }
1767
+ if (next === "rendered-markdown") {
1768
+ const sourceMdBody = document.getElementById("code-pane-markdown-body");
1769
+ if (sourceMdBody instanceof HTMLElement) {
1770
+ runMermaidOnFreshDocNodes(sourceMdBody);
1771
+ rewriteHubRelativeBrowseAnchorsIn(sourceMdBody);
1772
+ }
1773
+ }
1774
+ onAfterFlip?.();
1775
+ };
1776
+ flipBtn.addEventListener("click", runFlip);
1777
+ if (flipScrollBtn) {
1778
+ flipScrollBtn.addEventListener("click", runFlip);
1779
+ wireSourceMarkdownPaneFlipAffordance(flipBtn, flipScrollBtn);
1780
+ }
1781
+ }
1443
1782
  function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
1444
1783
  const mq = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ);
1445
1784
  function readStoredPane() {
@@ -1458,6 +1797,7 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
1458
1797
  return;
1459
1798
  const cur = normalizedDualMobilePane(shell.getAttribute("data-dual-mobile-pane"));
1460
1799
  const next = cur === "code" ? "doc" : "code";
1800
+ const rootTopBeforeFlip = rootScrollingElement().scrollTop;
1461
1801
  if (next === "code") {
1462
1802
  scrollRunners.prepareMobileFlipToCode();
1463
1803
  }
@@ -1470,6 +1810,11 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
1470
1810
  globalThis.requestAnimationFrame(() => {
1471
1811
  if (next === "code") {
1472
1812
  scrollRunners.finishMobileFlipToCode();
1813
+ const root = rootScrollingElement();
1814
+ if (rootTopBeforeFlip > 5 && root.scrollTop <= 1) {
1815
+ const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
1816
+ root.scrollTop = clamp(rootTopBeforeFlip, 0, maxY);
1817
+ }
1473
1818
  }
1474
1819
  else {
1475
1820
  scrollRunners.finishMobileFlipToDoc();
@@ -1630,75 +1975,108 @@ function wireDualPaneNavSearchFetch(shell, embeddedPairs, indexState, mutable, r
1630
1975
  }
1631
1976
  })();
1632
1977
  }
1978
+ function applySelectedMultiAngle(args) {
1979
+ const { angle, docBody, mutable, rebuildSearcher, scrollLinksRef, shell, searchInput, searchResults, requestBlockRayRedraw, } = args;
1980
+ docBody.innerHTML = decodeBase64Utf8(angle.docInnerHtmlB64);
1981
+ runMermaidOnFreshDocNodes(docBody);
1982
+ rewriteHubRelativeBrowseAnchorsIn(docBody);
1983
+ mutable.rawMd = decodeBase64Utf8(angle.rawMdB64);
1984
+ mutable.mdLines = mutable.rawMd.split("\n");
1985
+ mutable.commentrayPathLabel = angle.commentrayPathForSearch;
1986
+ rebuildSearcher();
1987
+ scrollLinksRef.current = parseScrollBlockLinksFromShell(angle.scrollBlockLinksB64);
1988
+ shell.setAttribute("data-scroll-block-links-b64", angle.scrollBlockLinksB64);
1989
+ shell.setAttribute("data-search-commentray-path", angle.commentrayPathForSearch);
1990
+ const crIdentity = normPosixPath(angle.commentrayPathForSearch);
1991
+ if (crIdentity.length > 0)
1992
+ shell.setAttribute("data-commentray-pair-commentray-path", crIdentity);
1993
+ else
1994
+ shell.removeAttribute("data-commentray-pair-commentray-path");
1995
+ applyDocumentedTreeCurrentPairHighlight();
1996
+ const docPathEl = document.getElementById("nav-rail-doc-path");
1997
+ if (docPathEl) {
1998
+ const path = angle.commentrayPathForSearch.trim();
1999
+ docPathEl.textContent = path.length > 0 ? path : "—";
2000
+ if (path.length > 0)
2001
+ docPathEl.setAttribute("title", path);
2002
+ else
2003
+ docPathEl.removeAttribute("title");
2004
+ }
2005
+ const browse = angle.staticBrowseUrl?.trim() ?? "";
2006
+ if (browse.length > 0) {
2007
+ const resolved = staticBrowseHrefForShellDataAttribute(browse, globalThis.location.pathname, globalThis.location.origin);
2008
+ shell.setAttribute("data-commentray-pair-browse-href", resolved);
2009
+ }
2010
+ else {
2011
+ const ghu = angle.commentrayOnGithubUrl?.trim();
2012
+ if (ghu)
2013
+ shell.setAttribute("data-commentray-pair-browse-href", ghu);
2014
+ else
2015
+ shell.removeAttribute("data-commentray-pair-browse-href");
2016
+ }
2017
+ searchInput.value = "";
2018
+ searchResults.innerHTML = "";
2019
+ searchResults.hidden = true;
2020
+ requestBlockRayRedraw?.();
2021
+ globalThis.requestAnimationFrame(() => {
2022
+ requestBlockRayRedraw?.();
2023
+ globalThis.requestAnimationFrame(() => {
2024
+ requestBlockRayRedraw?.();
2025
+ });
2026
+ });
2027
+ }
1633
2028
  function wireDualPaneMultiAngleAndScroll(args) {
1634
2029
  const { codePane, docScrollEl, docBody, shell, scrollLinksRef, multiPayload, mutable, rebuildSearcher, searchInput, searchResults, requestBlockRayRedraw, } = args;
1635
2030
  if (multiPayload) {
1636
- const runners = wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
2031
+ const runners = wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current, () => sourceLineIdPrefixForShell(shell), () => sourcePaneModeForShell(shell) === "rendered-markdown");
1637
2032
  const angleSel = document.getElementById("angle-select");
1638
2033
  if (angleSel && docBody) {
1639
2034
  angleSel.addEventListener("change", () => {
1640
2035
  const a = multiPayload.angles.find((x) => x.id === angleSel.value);
1641
2036
  if (!a)
1642
2037
  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
- });
2038
+ applySelectedMultiAngle({
2039
+ angle: a,
2040
+ docBody,
2041
+ mutable,
2042
+ rebuildSearcher,
2043
+ scrollLinksRef,
2044
+ shell,
2045
+ searchInput,
2046
+ searchResults,
2047
+ requestBlockRayRedraw,
1685
2048
  });
1686
2049
  });
1687
2050
  }
1688
2051
  return runners;
1689
2052
  }
1690
2053
  if (scrollLinksRef.current.length > 0) {
1691
- return wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current);
2054
+ return wireBlockAwareScrollSync(codePane, docScrollEl, () => scrollLinksRef.current, () => sourceLineIdPrefixForShell(shell), () => sourcePaneModeForShell(shell) === "rendered-markdown");
1692
2055
  }
1693
2056
  return wireProportionalScrollSync(codePane, docScrollEl);
1694
2057
  }
1695
2058
  function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
2059
+ function commentrayMdLineFromLocationHash(rawHash) {
2060
+ const hash = rawHash.replace(/^#/, "").trim();
2061
+ if (hash.length === 0)
2062
+ return null;
2063
+ const tokens = hash
2064
+ .split(/--|&/)
2065
+ .map((t) => t.trim())
2066
+ .filter((t) => t.length > 0);
2067
+ for (const token of tokens) {
2068
+ const m = /^commentray-md-line-(\d+)$/.exec(token);
2069
+ if (!m?.[1])
2070
+ continue;
2071
+ const line0 = Number.parseInt(m[1], 10);
2072
+ if (Number.isFinite(line0))
2073
+ return line0;
2074
+ }
2075
+ return null;
2076
+ }
1696
2077
  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))
2078
+ const line0 = commentrayMdLineFromLocationHash(globalThis.location.hash);
2079
+ if (line0 === null)
1702
2080
  return;
1703
2081
  scrollDocToMarkdownLine0(docScrollEl, line0, mdLineCount());
1704
2082
  }
@@ -1707,6 +2085,23 @@ function wireDualPaneCommentrayLocationHash(docScrollEl, mdLineCount) {
1707
2085
  globalThis.requestAnimationFrame(applyCommentrayLocationHash);
1708
2086
  });
1709
2087
  }
2088
+ function initializeSourceMarkdownPane(shell) {
2089
+ if (sourcePaneModeForShell(shell) !== "rendered-markdown")
2090
+ return;
2091
+ const sourceMdBody = document.getElementById("code-pane-markdown-body");
2092
+ if (!(sourceMdBody instanceof HTMLElement))
2093
+ return;
2094
+ runMermaidOnFreshDocNodes(sourceMdBody);
2095
+ rewriteHubRelativeBrowseAnchorsIn(sourceMdBody);
2096
+ }
2097
+ function wireSourceMarkdownControls(shell, codePane, onAfterFlip) {
2098
+ const sourceMdFlip = document.getElementById("source-markdown-pane-flip");
2099
+ const sourceMdFlipScroll = document.getElementById("source-markdown-pane-flip-scroll");
2100
+ if (!(sourceMdFlip instanceof HTMLButtonElement))
2101
+ return;
2102
+ wireSourceMarkdownPaneFlip(shell, codePane, sourceMdFlip, sourceMdFlipScroll instanceof HTMLButtonElement ? sourceMdFlipScroll : null, onAfterFlip);
2103
+ initializeSourceMarkdownPane(shell);
2104
+ }
1710
2105
  function buildDualPaneSearcherBundle(shell, codePane) {
1711
2106
  const { rawCodeB64, rawMdB64 } = readEmbeddedRawB64Strings(shell, codePane);
1712
2107
  const rawCode = decodeBase64Utf8(rawCodeB64);
@@ -1778,12 +2173,11 @@ function wireDualPaneCodeBrowser(shell, codePane) {
1778
2173
  shell.style.setProperty("--split-pct", `${String(pct)}%`);
1779
2174
  const docPaneEl = document.getElementById("doc-pane");
1780
2175
  const docPaneForWrap = docPaneEl instanceof HTMLElement ? docPaneEl : null;
2176
+ const sourceMdBodyForWrap = document.getElementById("code-pane-markdown-body");
1781
2177
  const blockRayRedraw = {};
1782
2178
  wireWrapToggle(STORAGE_WRAP_LINES, codePane, wrapCb, () => {
1783
2179
  blockRayRedraw.request?.();
1784
- codePane.dispatchEvent(new Event("scroll"));
1785
- docScrollEl.dispatchEvent(new Event("scroll"));
1786
- }, docPaneForWrap, docBody);
2180
+ }, docPaneForWrap, docBody, sourceMdBodyForWrap instanceof HTMLElement ? sourceMdBodyForWrap : null);
1787
2181
  wireSplitter(STORAGE_SPLIT_PCT, shell, codePane, gutter, pct);
1788
2182
  const multiScript = document.getElementById("commentray-multi-angle-b64");
1789
2183
  const multiPayload = parseMultiAnglePayload(multiScript);
@@ -1794,7 +2188,8 @@ function wireDualPaneCodeBrowser(shell, codePane) {
1794
2188
  codePane,
1795
2189
  docScrollEl,
1796
2190
  getLinks: () => bundle.scrollLinksRef.current,
1797
- probeTopSourceLine1Based: () => probeCodeLine1FromViewport(codePane),
2191
+ probeTopSourceLine1Based: () => probeCodeLine1FromViewport(codePane, sourceLineIdPrefixForShell(shell)),
2192
+ sourceLineIdPrefix: () => sourceLineIdPrefixForShell(shell),
1798
2193
  })
1799
2194
  : undefined;
1800
2195
  blockRayRedraw.request = requestBlockRayRedraw;
@@ -1816,6 +2211,10 @@ function wireDualPaneCodeBrowser(shell, codePane) {
1816
2211
  if (flipBtn instanceof HTMLButtonElement) {
1817
2212
  wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn instanceof HTMLButtonElement ? flipScrollBtn : null);
1818
2213
  }
2214
+ wireSourceMarkdownControls(shell, codePane, () => {
2215
+ requestBlockRayRedraw?.();
2216
+ });
2217
+ wireWideModeIntroTour(shell, isNarrowViewport);
1819
2218
  wireDualPaneCommentrayLocationHash(docScrollEl, () => bundle.mutable.mdLines.length);
1820
2219
  }
1821
2220
  function commentrayThemeModeLabel(mode) {
@@ -1917,7 +2316,201 @@ function wireColorThemeToolbar() {
1917
2316
  document.addEventListener("keydown", onDocumentKeydown, true);
1918
2317
  syncUi();
1919
2318
  }
2319
+ function safePermalinkHref(raw) {
2320
+ const t = raw.trim();
2321
+ if (t.length === 0)
2322
+ return null;
2323
+ if (/^(javascript|data):/i.test(t))
2324
+ return null;
2325
+ try {
2326
+ return new URL(t, globalThis.location.href).toString();
2327
+ }
2328
+ catch {
2329
+ return null;
2330
+ }
2331
+ }
2332
+ function humaneBrowseAliasPathForSource(sourcePath) {
2333
+ return sourcePath
2334
+ .split("/")
2335
+ .filter((seg) => seg.length > 0)
2336
+ .map((seg) => encodeURIComponent(seg))
2337
+ .join("/");
2338
+ }
2339
+ function companionStemFromCommentrayPath(commentrayPath) {
2340
+ const norm = normPosixPath(commentrayPath);
2341
+ const last = norm.split("/").filter(Boolean).at(-1) ?? "";
2342
+ return last.replace(/\.md$/i, "").trim();
2343
+ }
2344
+ function makeAbsoluteUrlAgainst(raw, baseHref) {
2345
+ return new URL(raw, baseHref).toString();
2346
+ }
2347
+ function shellEligibleForHumaneBackfill(shell, pathname) {
2348
+ if ((shell.getAttribute("data-layout") ?? "dual") !== "dual")
2349
+ return false;
2350
+ if (pathname.includes("/browse/"))
2351
+ return false;
2352
+ return pathname.endsWith("/") || pathname.endsWith("/index.html");
2353
+ }
2354
+ function nextHumaneBrowsePathForShell(shell, pathname) {
2355
+ const sourcePath = normPosixPath(shell.getAttribute("data-commentray-pair-source-path") ?? "");
2356
+ if (sourcePath.length === 0)
2357
+ return null;
2358
+ const alias = humaneBrowseAliasPathForSource(sourcePath);
2359
+ if (alias.length === 0)
2360
+ return null;
2361
+ const angleSel = document.getElementById("angle-select");
2362
+ const selectedAngle = angleSel instanceof HTMLSelectElement ? angleSel.value.trim() : "";
2363
+ const commentrayPath = shell.getAttribute("data-commentray-pair-commentray-path") ?? "";
2364
+ const stem = companionStemFromCommentrayPath(commentrayPath);
2365
+ const angleName = selectedAngle.length > 0 ? selectedAngle : stem;
2366
+ if (angleName.length === 0)
2367
+ return null;
2368
+ const angleAlias = `${alias}@${encodeURIComponent(angleName)}.html`;
2369
+ const siteRoot = siteRootPathnameFromPathname(pathname);
2370
+ return siteRoot === "/" ? `/browse/${angleAlias}` : `${siteRoot}/browse/${angleAlias}`;
2371
+ }
2372
+ function absolutizeNavJsonUrls(shell, beforeHref) {
2373
+ const navSearchRaw = shell.getAttribute("data-nav-search-json-url")?.trim() ?? "";
2374
+ if (navSearchRaw.length > 0) {
2375
+ shell.setAttribute("data-nav-search-json-url", makeAbsoluteUrlAgainst(navSearchRaw, beforeHref));
2376
+ }
2377
+ const navTree = document.getElementById("documented-files-hub");
2378
+ if (navTree instanceof HTMLElement) {
2379
+ const navRaw = navTree.getAttribute("data-nav-json-url")?.trim() ?? "";
2380
+ if (navRaw.length > 0) {
2381
+ navTree.setAttribute("data-nav-json-url", makeAbsoluteUrlAgainst(navRaw, beforeHref));
2382
+ }
2383
+ }
2384
+ }
2385
+ function normalizePairBrowseHrefForCurrentPath(shell, pathname) {
2386
+ const pairBrowseRaw = shell.getAttribute("data-commentray-pair-browse-href")?.trim() ?? "";
2387
+ if (pairBrowseRaw.length > 0 && isHubRelativeStaticBrowseHref(pairBrowseRaw)) {
2388
+ shell.setAttribute("data-commentray-pair-browse-href", resolveStaticBrowseHref(pairBrowseRaw, pathname, globalThis.location.origin));
2389
+ }
2390
+ }
2391
+ function normalizeDocumentationHomeHrefForCurrentPath() {
2392
+ const home = document.querySelector('a[aria-label="Documentation home"]');
2393
+ if (!(home instanceof HTMLAnchorElement))
2394
+ return;
2395
+ const siteRoot = siteRootPathnameFromPathname(globalThis.location.pathname);
2396
+ const normalized = siteRoot === "/" ? "/" : `${siteRoot}/`;
2397
+ home.setAttribute("href", normalized);
2398
+ }
2399
+ function activeCommentrayHashTokenFromViewport() {
2400
+ const docPane = document.getElementById("doc-pane");
2401
+ if (!(docPane instanceof HTMLElement))
2402
+ return null;
2403
+ const docBody = document.getElementById("doc-pane-body");
2404
+ const docScrollEl = docBody instanceof HTMLElement ? docBody : docPane;
2405
+ const anchors = docScrollEl.querySelectorAll(".commentray-block-anchor");
2406
+ if (anchors.length === 0)
2407
+ return null;
2408
+ const mdLine0 = probeCommentrayLine0FromDoc(docScrollEl);
2409
+ if (!Number.isFinite(mdLine0) || mdLine0 < 0)
2410
+ return null;
2411
+ return `commentray-md-line-${String(mdLine0)}`;
2412
+ }
2413
+ function maybeBackfillAddressBarWithHumanePairLink() {
2414
+ const shell = document.getElementById("shell");
2415
+ if (!(shell instanceof HTMLElement))
2416
+ return;
2417
+ const pathname = globalThis.location.pathname;
2418
+ normalizeDocumentationHomeHrefForCurrentPath();
2419
+ if (!shellEligibleForHumaneBackfill(shell, pathname))
2420
+ return;
2421
+ const nextPath = nextHumaneBrowsePathForShell(shell, pathname);
2422
+ if (nextPath === null)
2423
+ return;
2424
+ const beforeHref = globalThis.location.href;
2425
+ absolutizeNavJsonUrls(shell, beforeHref);
2426
+ normalizePairBrowseHrefForCurrentPath(shell, pathname);
2427
+ globalThis.history.replaceState(null, "", `${nextPath}${globalThis.location.search}${globalThis.location.hash}`);
2428
+ }
2429
+ function permalinkHashSuffixFromUi() {
2430
+ const tokens = [];
2431
+ const pushUnique = (token) => {
2432
+ const t = token.trim();
2433
+ if (t.length === 0)
2434
+ return;
2435
+ if (!tokens.includes(t))
2436
+ tokens.push(t);
2437
+ };
2438
+ const angleSel = document.getElementById("angle-select");
2439
+ if (angleSel instanceof HTMLSelectElement) {
2440
+ const id = angleSel.value.trim();
2441
+ if (id.length > 0) {
2442
+ pushUnique(`angle-${encodeURIComponent(id)}`);
2443
+ }
2444
+ }
2445
+ const activeAnchor = activeCommentrayHashTokenFromViewport();
2446
+ if (activeAnchor)
2447
+ pushUnique(activeAnchor);
2448
+ return tokens.length > 0 ? `#${tokens.join("&")}` : "";
2449
+ }
2450
+ function sharePermalinkFromShell(shell) {
2451
+ const raw = shell.getAttribute("data-commentray-pair-browse-href") ?? "";
2452
+ const canonical = isHubRelativeStaticBrowseHref(raw.trim()) && raw.trim().length > 0
2453
+ ? resolveStaticBrowseHref(raw.trim(), globalThis.location.pathname, globalThis.location.origin)
2454
+ : safePermalinkHref(raw);
2455
+ const base = canonical ?? globalThis.location.href;
2456
+ const u = new URL(base, globalThis.location.href);
2457
+ const hash = permalinkHashSuffixFromUi();
2458
+ u.hash = hash.length > 0 ? hash.slice(1) : "";
2459
+ return u.toString();
2460
+ }
2461
+ async function writeTextToClipboard(text) {
2462
+ try {
2463
+ await navigator.clipboard.writeText(text);
2464
+ return true;
2465
+ }
2466
+ catch {
2467
+ const ta = document.createElement("textarea");
2468
+ ta.value = text;
2469
+ ta.setAttribute("readonly", "true");
2470
+ ta.style.position = "fixed";
2471
+ ta.style.top = "-1000px";
2472
+ ta.style.left = "-1000px";
2473
+ document.body.appendChild(ta);
2474
+ ta.select();
2475
+ try {
2476
+ return document.execCommand("copy");
2477
+ }
2478
+ catch {
2479
+ return false;
2480
+ }
2481
+ finally {
2482
+ document.body.removeChild(ta);
2483
+ }
2484
+ }
2485
+ }
2486
+ function wireSharePermalinkButton() {
2487
+ const shell = document.getElementById("shell");
2488
+ const btn = document.getElementById("commentray-share-link");
2489
+ if (!(shell instanceof HTMLElement) || !(btn instanceof HTMLButtonElement))
2490
+ return;
2491
+ const baseLabel = "Copy shareable permalink";
2492
+ let copiedTimer;
2493
+ btn.addEventListener("click", () => {
2494
+ void (async () => {
2495
+ const shareUrl = sharePermalinkFromShell(shell);
2496
+ const copied = await writeTextToClipboard(shareUrl);
2497
+ if (!copied)
2498
+ return;
2499
+ btn.dataset.copied = "true";
2500
+ btn.setAttribute("aria-label", "Permalink copied");
2501
+ btn.title = "Permalink copied";
2502
+ if (copiedTimer !== undefined)
2503
+ globalThis.clearTimeout(copiedTimer);
2504
+ copiedTimer = globalThis.setTimeout(() => {
2505
+ delete btn.dataset.copied;
2506
+ btn.setAttribute("aria-label", baseLabel);
2507
+ btn.title = baseLabel;
2508
+ }, 1200);
2509
+ })();
2510
+ });
2511
+ }
1920
2512
  function main() {
2513
+ wireSharePermalinkButton();
1921
2514
  wireColorThemeToolbar();
1922
2515
  wireDocumentedFilesTree();
1923
2516
  const shell = document.getElementById("shell");
@@ -1925,12 +2518,16 @@ function main() {
1925
2518
  if (!shell || !codePane) {
1926
2519
  return;
1927
2520
  }
2521
+ applyPageBreakFeatureToggle(shell);
2522
+ wireResponsivePageBreakHeight(shell);
2523
+ wireWideModeIntroTrigger(shell);
1928
2524
  const layout = shell.getAttribute("data-layout") || "dual";
1929
2525
  if (layout === "stretch") {
1930
2526
  wireStretchLayoutChrome(codePane);
1931
2527
  return;
1932
2528
  }
1933
2529
  wireDualPaneCodeBrowser(shell, codePane);
2530
+ maybeBackfillAddressBarWithHumanePairLink();
1934
2531
  }
1935
2532
  if (document.readyState === "loading") {
1936
2533
  document.addEventListener("DOMContentLoaded", main);