From 23bd5a55ee4364f8a1fd200a02e413bf85a24ecc Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Sun, 8 Mar 2026 20:46:04 +0100 Subject: [PATCH] rosary: fix mystery image timing and SVG container clipping Show first mystery image at the Pater Noster instead of the Gloria Patri by removing the early lbead2 trigger. Fix IntersectionObserver to prefer the topmost intersecting entry so short _pater sections aren't skipped. Use full viewport height (100dvh) for the SVG container to prevent clipping at edges. --- .../[rosary=rosaryLang]/+page.svelte | 5 +- .../[rosary=rosaryLang]/rosaryScrollSync.js | 50 +++++++++++-------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte index a1e2d206..ef2cc050 100644 --- a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte +++ b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/+page.svelte @@ -137,7 +137,6 @@ const hasMysteryImages = $derived(showImages && (allMysteryImages[selectedMyster * @returns {number | 'before' | 'after'} */ function getMysteryScrollTarget(section) { - if (section === 'lbead2') return 1; const secretMatch = section.match(/^secret(\d)/); if (secretMatch) { const num = parseInt(secretMatch[1]); @@ -446,8 +445,8 @@ onMount(() => { .rosary-visualization { padding: 2rem 0; position: sticky; - top: 2rem; - max-height: calc(100vh - 2rem); + top: 0; + max-height: 100dvh; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; /* Firefox */ diff --git a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/rosaryScrollSync.js b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/rosaryScrollSync.js index 691daff2..07ae6c8d 100644 --- a/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/rosaryScrollSync.js +++ b/src/routes/[faithLang=faithLang]/[rosary=rosaryLang]/rosaryScrollSync.js @@ -59,29 +59,37 @@ export function setupScrollSync({ const svgContainer = getSvgContainer(); const sectionElements = getSectionElements(); - entries.forEach((entry) => { - if (entry.isIntersecting && scrollLock !== 'svg' && scrollLock !== 'click') { - // Skip observer updates when at the top — handleWindowScroll handles this - if (window.scrollY < 50) return; + if (scrollLock === 'svg' || scrollLock === 'click') return; + if (window.scrollY < 50) return; - const section = /** @type {HTMLElement} */ (entry.target).dataset.section; - if (!section) return; - setActiveSection(section); - - // Scroll SVG to keep active section visible at top - if (svgContainer && sectionPositions[section] !== undefined) { - const svg = /** @type {SVGSVGElement | null} */ (svgContainer.querySelector('svg')); - if (!svg) return; - - const pixelPosition = svgSectionToPixel(svg, section); - if (pixelPosition === null) return; - const targetScroll = pixelPosition - 100; - - setScrollLock('prayer'); - svgContainer.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' }); - } + // Pick the topmost intersecting entry so short sections (e.g. _pater) aren't + // immediately overridden by the taller section below them + let bestEntry = null; + for (const entry of entries) { + if (!entry.isIntersecting) continue; + if (!bestEntry || entry.boundingClientRect.top < bestEntry.boundingClientRect.top) { + bestEntry = entry; } - }); + } + + if (bestEntry) { + const section = /** @type {HTMLElement} */ (bestEntry.target).dataset.section; + if (!section) return; + setActiveSection(section); + + // Scroll SVG to keep active section visible at top + if (svgContainer && sectionPositions[section] !== undefined) { + const svg = /** @type {SVGSVGElement | null} */ (svgContainer.querySelector('svg')); + if (!svg) return; + + const pixelPosition = svgSectionToPixel(svg, section); + if (pixelPosition === null) return; + const targetScroll = pixelPosition - 100; + + setScrollLock('prayer'); + svgContainer.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' }); + } + } }, { root: null, rootMargin: "-20% 0px -60% 0px",