rosary: split final prayers into individual bead sections with scroll tracking
All checks were successful
CI / update (push) Successful in 1m27s

Map each ending bead to its corresponding prayer (Gloria/Fatima,
Salve Regina, Schlussgebet, St. Michael, Paternoster, Sign of the Cross),
add scroll-to-top button with action_button styling, and fix SVG scroll
lock to prevent snap-back when scrolling to top.
This commit is contained in:
2026-02-04 11:17:40 +01:00
parent 3831cd17de
commit 767b43e2ff

View File

@@ -2,6 +2,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { createLanguageContext } from "$lib/contexts/languageContext.js"; import { createLanguageContext } from "$lib/contexts/languageContext.js";
import "$lib/css/christ.css"; import "$lib/css/christ.css";
import "$lib/css/action_button.css";
import Kreuzzeichen from "$lib/components/prayers/Kreuzzeichen.svelte"; import Kreuzzeichen from "$lib/components/prayers/Kreuzzeichen.svelte";
import Credo from "$lib/components/prayers/Credo.svelte"; import Credo from "$lib/components/prayers/Credo.svelte";
import Paternoster from "$lib/components/prayers/Paternoster.svelte"; import Paternoster from "$lib/components/prayers/Paternoster.svelte";
@@ -370,7 +371,12 @@ const sectionPositions = {
secret4: 1120, secret4: 1120,
secret4_transition: 1360, secret4_transition: 1360,
secret5: 1400, secret5: 1400,
final_transition: 1640 final_transition: 1690,
final_salve: 1730,
final_schlussgebet: 1760,
final_michael: 1790,
final_paternoster: 1830,
final_cross: 1920
}; };
onMount(() => { onMount(() => {
@@ -569,7 +575,7 @@ onMount(() => {
// Get the first and final prayer sections // Get the first and final prayer sections
const firstSection = sectionElements.cross; const firstSection = sectionElements.cross;
const finalSection = sectionElements.final_transition; const finalSection = sectionElements.final_cross;
if (!firstSection || !finalSection) return; if (!firstSection || !finalSection) return;
const firstSectionRect = firstSection.getBoundingClientRect(); const firstSectionRect = firstSection.getBoundingClientRect();
@@ -579,6 +585,7 @@ onMount(() => {
if (scrollY < 50) { if (scrollY < 50) {
// Scroll SVG to top // Scroll SVG to top
if (svgContainer.scrollTop > 10) { // Only if not already at top if (svgContainer.scrollTop > 10) { // Only if not already at top
setScrollLock('prayer');
smoothScrollElement(svgContainer, 0); smoothScrollElement(svgContainer, 0);
} }
} }
@@ -587,6 +594,7 @@ onMount(() => {
// Scroll SVG to bottom // Scroll SVG to bottom
const maxScroll = svgContainer.scrollHeight - svgContainer.clientHeight; const maxScroll = svgContainer.scrollHeight - svgContainer.clientHeight;
if (svgContainer.scrollTop < maxScroll - 10) { // Only if not already at bottom if (svgContainer.scrollTop < maxScroll - 10) { // Only if not already at bottom
setScrollLock('prayer');
smoothScrollElement(svgContainer, maxScroll); smoothScrollElement(svgContainer, maxScroll);
} }
} }
@@ -594,6 +602,7 @@ onMount(() => {
else if (firstSectionRect.top > viewportHeight * 0.6) { else if (firstSectionRect.top > viewportHeight * 0.6) {
// Scroll SVG to top // Scroll SVG to top
if (svgContainer.scrollTop > 10) { // Only if not already at top if (svgContainer.scrollTop > 10) { // Only if not already at top
setScrollLock('prayer');
smoothScrollElement(svgContainer, 0); smoothScrollElement(svgContainer, 0);
} }
} }
@@ -602,6 +611,7 @@ onMount(() => {
// Scroll SVG to bottom // Scroll SVG to bottom
const maxScroll = svgContainer.scrollHeight - svgContainer.clientHeight; const maxScroll = svgContainer.scrollHeight - svgContainer.clientHeight;
if (svgContainer.scrollTop < maxScroll - 10) { // Only if not already at bottom if (svgContainer.scrollTop < maxScroll - 10) { // Only if not already at bottom
setScrollLock('prayer');
smoothScrollElement(svgContainer, maxScroll); smoothScrollElement(svgContainer, maxScroll);
} }
} }
@@ -1227,6 +1237,14 @@ h1 {
margin-right: 0.5em; margin-right: 0.5em;
color: var(--nord11); color: var(--nord11);
} }
.scroll-top-button {
margin: 2rem auto 0;
}
.scroll-padding {
height: 50vh;
}
</style> </style>
<svelte:head> <svelte:head>
<title>{labels.pageTitle}</title> <title>{labels.pageTitle}</title>
@@ -1313,7 +1331,7 @@ h1 {
<div class="rosary-visualization" bind:this={svgContainer}> <div class="rosary-visualization" bind:this={svgContainer}>
<svg class="linear-rosary" viewBox="-100 -100 250 2200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMin meet"> <svg class="linear-rosary" viewBox="-100 -100 250 2200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMin meet">
<!-- Vertical chain --> <!-- Vertical chain -->
<line x1="25" y1="35" x2="25" y2="1655" class="chain" /> <line x1="25" y1="35" x2="25" y2="1865" class="chain" />
<!-- Cross (at top) --> <!-- Cross (at top) -->
<g id="cross-section" data-section="cross"> <g id="cross-section" data-section="cross">
@@ -1382,8 +1400,20 @@ h1 {
class:counted-bead={i < decadeCounters.secret5} class:counted-bead={i < decadeCounters.secret5}
data-section="secret5" /> data-section="secret5" />
{/each} {/each}
<image href="/glaube/benedictus.svg" x="5" y="1620" width="40" height="40" />
<!-- Final transition: Gloria + Fatima --> <!-- Final transition: Gloria + Fatima -->
<circle cx="25" cy="1640" r="15" class="large-bead" class:active-large-bead={activeSection === 'final_transition'} data-section="final_transition" /> <circle cx="25" cy="1690" r="15" class="large-bead" class:active-large-bead={activeSection === 'final_transition'} data-section="final_transition" />
<circle cx="25" cy="1730" r="10" class="bead" class:active-bead={activeSection === 'final_salve'} data-section="final_salve" />
<circle cx="25" cy="1760" r="10" class="bead" class:active-bead={activeSection === 'final_schlussgebet'} data-section="final_schlussgebet" />
<circle cx="25" cy="1790" r="10" class="bead" class:active-bead={activeSection === 'final_michael'} data-section="final_michael" />
<circle cx="25" cy="1830" r="15" class="large-bead" class:active-large-bead={activeSection === 'final_paternoster'} data-section="final_paternoster" />
<g data-section="final_cross">
<text x="25" y="1920" text-anchor="middle" font-size="80"
class="cross-symbol" class:active-cross={activeSection === 'final_cross'}>♱</text>
</g>
<!-- Invisible hitboxes for larger tap targets --> <!-- Invisible hitboxes for larger tap targets -->
<g class="hitboxes"> <g class="hitboxes">
@@ -1409,8 +1439,14 @@ h1 {
<circle cx="25" cy="800" r="25" data-section="secret2_transition" /> <circle cx="25" cy="800" r="25" data-section="secret2_transition" />
<circle cx="25" cy="1080" r="25" data-section="secret3_transition" /> <circle cx="25" cy="1080" r="25" data-section="secret3_transition" />
<circle cx="25" cy="1360" r="25" data-section="secret4_transition" /> <circle cx="25" cy="1360" r="25" data-section="secret4_transition" />
<circle cx="25" cy="1640" r="25" data-section="final_transition" /> <circle cx="25" cy="1690" r="25" data-section="final_transition" />
<circle cx="25" cy="1730" r="20" data-section="final_salve" />
<circle cx="25" cy="1760" r="20" data-section="final_schlussgebet" />
<circle cx="25" cy="1790" r="20" data-section="final_michael" />
<circle cx="25" cy="1830" r="25" data-section="final_paternoster" />
<rect x="-15" y="1870" width="80" height="80" data-section="final_cross" />
</g> </g>
</svg> </svg>
</div> </div>
</div> </div>
@@ -1565,21 +1601,59 @@ h1 {
<h3>{labels.fatimaPrayer} <span class="repeat-count">({labels.optional})</span></h3> <h3>{labels.fatimaPrayer} <span class="repeat-count">({labels.optional})</span></h3>
<FatimaGebet /> <FatimaGebet />
</div>
<div
class="prayer-section"
bind:this={sectionElements.final_salve}
data-section="final_salve"
>
<h3>Salve Regina</h3> <h3>Salve Regina</h3>
<SalveRegina /> <SalveRegina />
</div>
<div
class="prayer-section"
bind:this={sectionElements.final_schlussgebet}
data-section="final_schlussgebet"
>
<h3>{labels.finalPrayer}</h3> <h3>{labels.finalPrayer}</h3>
<RosaryFinalPrayer /> <RosaryFinalPrayer />
</div>
<div
class="prayer-section"
bind:this={sectionElements.final_michael}
data-section="final_michael"
>
<h3>{labels.saintMichael}</h3> <h3>{labels.saintMichael}</h3>
<MichaelGebet /> <MichaelGebet />
</div>
<h3 style="text-align: center; font-size: 2.5rem; margin-top: 2rem;"></h3> <div
class="prayer-section"
bind:this={sectionElements.final_paternoster}
data-section="final_paternoster"
>
<h3>{labels.ourFather}</h3>
<Paternoster />
</div>
<div
class="prayer-section"
bind:this={sectionElements.final_cross}
data-section="final_cross"
>
<h3>{labels.signOfCross}</h3>
<Kreuzzeichen />
<div class="footnotes-section"> <div class="footnotes-section">
<p><span class="symbol"></span>{labels.footnoteSign}</p> <p><span class="symbol"></span>{labels.footnoteSign}</p>
</div> </div>
</div> </div>
<button class="scroll-top-button action_button" onclick={() => window.scrollTo({ top: 0 })} aria-label="Scroll to top">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>
</button>
<div class="scroll-padding"></div>
</div> </div>
</div> </div>