@commentray/render 0.3.4 → 0.3.5

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.
@@ -13,7 +13,7 @@ import { parseDualPaneScrollSyncStrategy } from "./code-browser-scroll-sync-stra
13
13
  import { DUAL_PANE_BLOCK_REVEAL_LEAD_CSS_PX, READING_LEAD_ALIGN_TOLERANCE_CSS_PX, READING_VIEWPORT_BOTTOM_EDGE_CSS_PX, readingViewportTopInsetCssPx, } from "./reading-viewport-comfort.js";
14
14
  import { wireWideModeIntroTour } from "./code-browser-wide-intro-controller.js";
15
15
  import { readWebStorageItem, writeWebStorageItem } from "./code-browser-web-storage.js";
16
- import { dispatchCommentrayMermaidDone, wireBlockStretchBufferSync, } from "./block-stretch-buffer-sync.js";
16
+ import { applyBlockStretchRowBuffers, dispatchCommentrayMermaidDone, wireBlockStretchBufferSync, } from "./block-stretch-buffer-sync.js";
17
17
  import { COMMENTRAY_MERMAID_MODULE_READY_EVENT } from "./commentray-mermaid-events.js";
18
18
  /**
19
19
  * Hub pages emit `./browse/…` relative to the site root. From `/…/browse/current.html` the browser
@@ -2490,6 +2490,38 @@ const DUAL_MOBILE_SINGLE_PANE_MQ = "(max-width: 767px)";
2490
2490
  function normalizedDualMobilePane(v) {
2491
2491
  return v === "code" ? "code" : "doc";
2492
2492
  }
2493
+ function splitLocationHashTokens(rawHash) {
2494
+ const hash = rawHash.replace(/^#/, "").trim();
2495
+ if (hash.length === 0)
2496
+ return [];
2497
+ return hash
2498
+ .split(/--|&/)
2499
+ .map((t) => t.trim())
2500
+ .filter((t) => t.length > 0);
2501
+ }
2502
+ function mobilePaneHashToken(pane) {
2503
+ return `mobile-pane-${pane}`;
2504
+ }
2505
+ function mobilePaneFromLocationHash(rawHash) {
2506
+ const tokens = splitLocationHashTokens(rawHash);
2507
+ for (const token of tokens) {
2508
+ if (token === "mobile-pane-code")
2509
+ return "code";
2510
+ if (token === "mobile-pane-doc")
2511
+ return "doc";
2512
+ }
2513
+ return null;
2514
+ }
2515
+ function updateLocationHashToken(token, shouldReplace) {
2516
+ const tokens = splitLocationHashTokens(globalThis.location.hash).filter((t) => !shouldReplace(t));
2517
+ tokens.push(token);
2518
+ const u = new URL(globalThis.location.href);
2519
+ u.hash = tokens.length > 0 ? tokens.join("&") : "";
2520
+ globalThis.history.replaceState(globalThis.history.state, "", u.toString());
2521
+ }
2522
+ function updateLocationHashForMobilePane(pane) {
2523
+ updateLocationHashToken(mobilePaneHashToken(pane), (t) => /^mobile-pane-(?:code|doc)$/.test(t));
2524
+ }
2493
2525
  function isNarrowViewport() {
2494
2526
  return globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ).matches;
2495
2527
  }
@@ -2713,6 +2745,7 @@ function wireSourceMarkdownPaneFlip(shell, codePane, flipBtn, flipScrollBtn, sig
2713
2745
  if (curPane === "doc") {
2714
2746
  shell.setAttribute("data-dual-mobile-pane", "code");
2715
2747
  writeWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE, "code");
2748
+ updateLocationHashForMobilePane("code");
2716
2749
  }
2717
2750
  }
2718
2751
  syncSourceMarkdownFlipA11y();
@@ -2741,6 +2774,9 @@ function wireSourceMarkdownPaneFlip(shell, codePane, flipBtn, flipScrollBtn, sig
2741
2774
  function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
2742
2775
  const mq = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ);
2743
2776
  function readStoredPane() {
2777
+ const fromHash = mobilePaneFromLocationHash(globalThis.location.hash);
2778
+ if (fromHash !== null)
2779
+ return fromHash;
2744
2780
  return normalizedDualMobilePane(readWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE));
2745
2781
  }
2746
2782
  function applyForViewport() {
@@ -2767,6 +2803,7 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
2767
2803
  }
2768
2804
  shell.setAttribute("data-dual-mobile-pane", next);
2769
2805
  writeWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE, next);
2806
+ updateLocationHashForMobilePane(next);
2770
2807
  globalThis.requestAnimationFrame(() => {
2771
2808
  globalThis.requestAnimationFrame(() => {
2772
2809
  if (next === "code") {
@@ -2793,49 +2830,64 @@ function wireDualMobilePaneFlip(shell, flipBtn, scrollRunners, flipScrollBtn) {
2793
2830
  wireDualMobilePaneFlipScrollAffordance(flipBtn, flipScrollBtn, mq);
2794
2831
  }
2795
2832
  mq.addEventListener("change", applyForViewport);
2833
+ globalThis.addEventListener("hashchange", applyForViewport);
2796
2834
  applyForViewport();
2797
2835
  }
2798
2836
  function wireStretchMobilePaneFlip(shell, codePane, flipBtn, flipScrollBtn, onAfterFlip) {
2799
- void codePane;
2800
- void onAfterFlip;
2801
- const mq = globalThis.matchMedia(DUAL_MOBILE_SINGLE_PANE_MQ);
2802
- function readStoredPane() {
2803
- return normalizedDualMobilePane(readWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE));
2804
- }
2805
- function applyForViewport() {
2806
- if (mq.matches) {
2807
- syncSinglePaneShellState(shell, true);
2808
- shell.setAttribute("data-dual-mobile-pane", readStoredPane());
2837
+ const stretchTable = codePane instanceof HTMLTableElement ? codePane : null;
2838
+ let anchorRow = null;
2839
+ let anchorTopBefore = 0;
2840
+ let rootTopBefore = 0;
2841
+ const captureAnchor = () => {
2842
+ const root = rootScrollingElement();
2843
+ rootTopBefore = root.scrollTop;
2844
+ if (stretchTable === null) {
2845
+ anchorRow = null;
2846
+ anchorTopBefore = 0;
2847
+ return;
2809
2848
  }
2810
- else {
2811
- syncSinglePaneShellState(shell, false);
2812
- shell.removeAttribute("data-dual-mobile-pane");
2849
+ const rows = stretchBlockRows(stretchTable);
2850
+ anchorRow = rows.find((row) => row.getBoundingClientRect().bottom > 0) ?? null;
2851
+ anchorTopBefore = anchorRow?.getBoundingClientRect().top ?? 0;
2852
+ };
2853
+ const restoreAnchor = () => {
2854
+ const root = rootScrollingElement();
2855
+ if (anchorRow !== null) {
2856
+ const delta = anchorRow.getBoundingClientRect().top - anchorTopBefore;
2857
+ if (Math.abs(delta) > 0.5) {
2858
+ const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
2859
+ root.scrollTop = clamp(root.scrollTop + delta, 0, maxY);
2860
+ return;
2861
+ }
2813
2862
  }
2814
- }
2815
- const runFlip = () => {
2816
- if (!mq.matches)
2817
- return;
2818
- const cur = normalizedDualMobilePane(shell.getAttribute("data-dual-mobile-pane"));
2819
- const next = cur === "code" ? "doc" : "code";
2820
- shell.setAttribute("data-dual-mobile-pane", next);
2821
- writeWebStorageItem(localStorage, STORAGE_DUAL_MOBILE_PANE, next);
2822
- // When revealing the doc pane, (re)run Mermaid — diagrams inside display:none cells are
2823
- // skipped on initial load, so they need a triggered pass once the cells become visible.
2824
- if (next === "doc") {
2825
- queueMicrotask(() => {
2863
+ const maxY = Math.max(0, root.scrollHeight - root.clientHeight);
2864
+ root.scrollTop = clamp(rootTopBefore, 0, maxY);
2865
+ };
2866
+ const applyBuffersAndRestore = () => {
2867
+ if (stretchTable !== null)
2868
+ applyBlockStretchRowBuffers(stretchTable);
2869
+ restoreAnchor();
2870
+ };
2871
+ const runners = {
2872
+ syncFromCodeToDoc: () => { },
2873
+ syncFromDocToCode: () => { },
2874
+ prepareMobileFlipToCode: captureAnchor,
2875
+ prepareMobileFlipToDoc: captureAnchor,
2876
+ finishMobileFlipToCode: () => {
2877
+ applyBuffersAndRestore();
2878
+ onAfterFlip?.();
2879
+ },
2880
+ finishMobileFlipToDoc: () => {
2881
+ applyBuffersAndRestore();
2882
+ onAfterFlip?.();
2883
+ void runMermaidOnFreshDocNodes(shell).then(() => {
2826
2884
  requestAnimationFrame(() => {
2827
- void runMermaidOnFreshDocNodes(shell);
2885
+ applyBuffersAndRestore();
2828
2886
  });
2829
2887
  });
2830
- }
2888
+ },
2831
2889
  };
2832
- flipBtn.addEventListener("click", runFlip);
2833
- if (flipScrollBtn) {
2834
- flipScrollBtn.addEventListener("click", runFlip);
2835
- wireDualMobilePaneFlipScrollAffordance(flipBtn, flipScrollBtn, mq);
2836
- }
2837
- mq.addEventListener("change", applyForViewport);
2838
- applyForViewport();
2890
+ wireDualMobilePaneFlip(shell, flipBtn, runners, flipScrollBtn);
2839
2891
  }
2840
2892
  /** Multi-angle stretch swaps `#shell` innerHTML; disconnect the previous table observer so listeners do not accumulate. */
2841
2893
  let stretchRowBufferSyncHandle = null;
@@ -3766,6 +3818,11 @@ function permalinkHashSuffixFromUi() {
3766
3818
  pushUnique(`angle-${encodeURIComponent(id)}`);
3767
3819
  }
3768
3820
  }
3821
+ const shell = document.getElementById("shell");
3822
+ if (shell instanceof HTMLElement && shell.getAttribute("data-mobile-single-pane") === "true") {
3823
+ const pane = normalizedDualMobilePane(shell.getAttribute("data-dual-mobile-pane"));
3824
+ pushUnique(mobilePaneHashToken(pane));
3825
+ }
3769
3826
  const activeAnchor = activeCommentrayHashTokenFromViewport();
3770
3827
  if (activeAnchor)
3771
3828
  pushUnique(activeAnchor);