rosary: add show/hide images toggle, fix PiP timing and breakpoint

- Add "Bilder anzeigen" / "Show Images" toggle persisted to localStorage
- Bump mystery image column/PiP breakpoint from 900px to 1200px so
  prayers keep full width on medium screens
- Fix PiP not appearing on page load by splitting $effect and using
  tick() to wait for DOM before measuring element dimensions
- Fix Toggle checkbox default margin causing misalignment
This commit is contained in:
2026-02-09 09:12:32 +01:00
parent a4738134fe
commit a5e119f976
2 changed files with 33 additions and 8 deletions

View File

@@ -34,6 +34,7 @@
.toggle-wrapper input[type="checkbox"] { .toggle-wrapper input[type="checkbox"] {
appearance: none; appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
margin: 0;
width: 44px; width: 44px;
height: 24px; height: 24px;
background: var(--nord2); background: var(--nord2);

View File

@@ -1,5 +1,5 @@
<script> <script>
import { onMount } from "svelte"; import { onMount, tick } from "svelte";
import { createLanguageContext } from "$lib/contexts/languageContext.js"; import { createLanguageContext } from "$lib/contexts/languageContext.js";
import { createPip } from "$lib/js/pip.svelte"; import { createPip } from "$lib/js/pip.svelte";
import "$lib/css/christ.css"; import "$lib/css/christ.css";
@@ -183,6 +183,9 @@ const mysteryTitlesEnglish = {
// Toggle for including Luminous mysteries (initialized from URL param or default) // Toggle for including Luminous mysteries (initialized from URL param or default)
let includeLuminous = $state(data.initialLuminous); let includeLuminous = $state(data.initialLuminous);
// Toggle for showing mystery images
let showImages = $state(true);
// Flag to prevent saving before we've loaded from localStorage // Flag to prevent saving before we've loaded from localStorage
let hasLoadedFromStorage = false; let hasLoadedFromStorage = false;
@@ -208,6 +211,7 @@ const labels = $derived({
glorious: isEnglish ? 'Glorious' : 'Glorreichen', glorious: isEnglish ? 'Glorious' : 'Glorreichen',
luminous: isEnglish ? 'Luminous' : 'Lichtreichen', luminous: isEnglish ? 'Luminous' : 'Lichtreichen',
includeLuminous: isEnglish ? 'Include Luminous Mysteries' : 'Lichtreiche Geheimnisse einbeziehen', includeLuminous: isEnglish ? 'Include Luminous Mysteries' : 'Lichtreiche Geheimnisse einbeziehen',
showImages: isEnglish ? 'Show Images' : 'Bilder anzeigen',
beginning: isEnglish ? 'Beginning' : 'Anfang', beginning: isEnglish ? 'Beginning' : 'Anfang',
signOfCross: isEnglish ? '♱ Sign of the Cross' : '♱ Das Kreuzzeichen', signOfCross: isEnglish ? '♱ Sign of the Cross' : '♱ Das Kreuzzeichen',
ourFather: isEnglish ? 'Our Father' : 'Vater unser', ourFather: isEnglish ? 'Our Father' : 'Vater unser',
@@ -230,12 +234,17 @@ const labels = $derived({
mysteryLove: isEnglish ? 'Jesus, who may kindle our love' : 'Jesus, der in uns die Liebe entzünde' mysteryLove: isEnglish ? 'Jesus, who may kindle our love' : 'Jesus, der in uns die Liebe entzünde'
}); });
// Save luminous toggle state to localStorage whenever it changes (but only after initial load) // Save toggle states to localStorage whenever they change (but only after initial load)
$effect(() => { $effect(() => {
if (typeof localStorage !== 'undefined' && hasLoadedFromStorage) { if (typeof localStorage !== 'undefined' && hasLoadedFromStorage) {
localStorage.setItem('rosary_includeLuminous', includeLuminous.toString()); localStorage.setItem('rosary_includeLuminous', includeLuminous.toString());
} }
}); });
$effect(() => {
if (typeof localStorage !== 'undefined' && hasLoadedFromStorage) {
localStorage.setItem('rosary_showImages', showImages.toString());
}
});
// Function to get the appropriate mystery for a given weekday // Function to get the appropriate mystery for a given weekday
function getMysteryForWeekday(date, includeLuminous) { function getMysteryForWeekday(date, includeLuminous) {
@@ -315,7 +324,7 @@ let sectionElements = {};
let svgContainer; let svgContainer;
// Whether the rosary has mystery images (stable, doesn't change during scroll) // Whether the rosary has mystery images (stable, doesn't change during scroll)
const hasMysteryImages = $derived(selectedMystery === 'schmerzhaften'); const hasMysteryImages = $derived(showImages && selectedMystery === 'schmerzhaften');
// Mystery image scroll target based on active section // Mystery image scroll target based on active section
function getMysteryScrollTarget(section) { function getMysteryScrollTarget(section) {
@@ -359,14 +368,20 @@ let rosaryPipEl = $state(null);
let lastPipSrc = $state(null); let lastPipSrc = $state(null);
function isMobilePip() { function isMobilePip() {
return !window.matchMedia('(min-width: 900px)').matches; return !window.matchMedia('(min-width: 1200px)').matches;
} }
$effect(() => { $effect(() => {
if (mysteryPipSrc) lastPipSrc = mysteryPipSrc; if (mysteryPipSrc) lastPipSrc = mysteryPipSrc;
});
$effect(() => {
if (!rosaryPipEl || !isMobilePip()) return; if (!rosaryPipEl || !isMobilePip()) return;
if (mysteryPipSrc) { if (mysteryPipSrc) {
pip.show(rosaryPipEl); // Wait for DOM update so the <img> has rendered with dimensions
tick().then(() => {
if (rosaryPipEl) pip.show(rosaryPipEl);
});
} else { } else {
pip.hide(); pip.hide();
} }
@@ -489,13 +504,17 @@ for (let d = 1; d < 5; d++) {
const pos = sectionPositions; const pos = sectionPositions;
onMount(() => { onMount(() => {
// Load toggle state from localStorage only if not overridden by URL params // Load toggle states from localStorage only if not overridden by URL params
if (!data.hasUrlLuminous) { if (!data.hasUrlLuminous) {
const savedIncludeLuminous = localStorage.getItem('rosary_includeLuminous'); const savedIncludeLuminous = localStorage.getItem('rosary_includeLuminous');
if (savedIncludeLuminous !== null) { if (savedIncludeLuminous !== null) {
includeLuminous = savedIncludeLuminous === 'true'; includeLuminous = savedIncludeLuminous === 'true';
} }
} }
const savedShowImages = localStorage.getItem('rosary_showImages');
if (savedShowImages !== null) {
showImages = savedShowImages === 'true';
}
// If no mystery was specified in URL, recompute based on loaded preferences // If no mystery was specified in URL, recompute based on loaded preferences
if (!data.hasUrlMystery) { if (!data.hasUrlMystery) {
@@ -1387,7 +1406,7 @@ h1 {
display: none; display: none;
} }
@media (min-width: 900px) { @media (min-width: 1200px) {
.rosary-layout.has-mystery-image { .rosary-layout.has-mystery-image {
grid-template-columns: clamp(250px, 30vw, 400px) 1fr auto; grid-template-columns: clamp(250px, 30vw, 400px) 1fr auto;
} }
@@ -1445,7 +1464,7 @@ h1 {
.mystery-pip.enlarged img { .mystery-pip.enlarged img {
height: 37.5vh; height: 37.5vh;
} }
@media (min-width: 900px) { @media (min-width: 1200px) {
.mystery-pip { .mystery-pip {
display: none; display: none;
} }
@@ -1530,6 +1549,11 @@ h1 {
href={luminousToggleHref} href={luminousToggleHref}
/> />
<Toggle
bind:checked={showImages}
label={labels.showImages}
/>
<!-- Language Toggle (link for no-JS, enhanced with onclick for JS) --> <!-- Language Toggle (link for no-JS, enhanced with onclick for JS) -->
<LanguageToggle <LanguageToggle
initialLatin={data.initialLatin} initialLatin={data.initialLatin}