6 Commits

Author SHA1 Message Date
Alexander 9a97e41c28 fix(faith): no-Latin prayers always render in monolingual style
CI / update (push) Successful in 1m2s
Force showLatin=false in Prayer wrapper when hasLatin is false so spacing
and red rubric icons stay correct regardless of toggle state. Also hide
the Latin toggle on individual non-bilingual prayer routes.
2026-05-07 07:38:57 +02:00
Alexander 109ac8e13a feat(faith): add closing refrain to Jungfrau Mutter Gottes mein
Adds the traditional final stanza repeating the opening invocation.
2026-05-07 07:35:26 +02:00
Alexander 6275b526d8 feat(faith): info pip on streak counters explaining habit vs piety
New shared StreakInfoButton component — small (i) pip in the corner
of the rosary, Angelus, and Regina Cæli streak counters that opens a
modal with a short reflection on what the counter is for.

The text frames the streak as a tool for forming the *habit* of
regular prayer, not as a metric of piety; warns against mechanical
repetition with Mt 6:7 ("do not heap up empty phrases"); and grounds
the rest in CCC 2698 (rhythms of prayer), 2700 (heart present to him
to whom we are speaking), 2702 (body+spirit, habit forms us), and
2728 (the wounded pride that comes from treating prayer as personal
accomplishment).

Available in DE/EN/LA. Modal dismissable via X, click-outside, or
Escape; honours prefers-color-scheme.

Refactoring:
- StreakCounter and AngelusStreakCounter both render
  <StreakInfoButton {lang} /> instead of duplicating the pip+modal.
  Parents just declare position:relative as the anchor.
- AngelusStreakCounter is also used for Regina Cæli, so eastertide
  visitors get the same explanation there for free.

Bump 1.67.0 -> 1.67.1.
2026-05-05 18:15:50 +02:00
Alexander 6456804fc3 feat(faith): add 6 prayers (Marian devotions + meal blessings)
Six new prayers from the German prayer book images, with Latin and
English where canonical versions exist:

  43 Jungfrau, Mutter Gottes mein — German Marian devotional. No
     canonical Latin/English; best-effort EN translation provided so
     the EN route renders rather than blanking. bilingue: false.

  40 O meine Gebieterin / O Domina Mea / O My Queen — Marian act of
     self-dedication with the standard Latin and English forms.

  41 Memorare — attributed to St. Bernard. Standard Latin
     (Memento O piissima Virgo Maria) and the traditional English
     translation.

  42 Hilf, Maria, es ist Zeit — German Marian invocation. No
     canonical Latin; EN follows the user-provided translation
     ("Help, Blessed Mother, it is highest time…"). bilingue: false.

  37 Tischgebet vor dem Essen — composite of four sub-prayers:
     "O Gott von dem wir alles haben" (DE rhyme, EN best-effort),
     "Komm Herr Jesus sei unser Gast" (DE rhyme + standard EN
     "Come Lord Jesus be our guest"), Psalm 144:15-16 with Gloria
     Patri (full Latin/EN), and Benedic Domine (full Latin/EN).

  38 Tischgebet nach dem Essen — three sub-prayers: "Dir sei o Gott"
     (DE rhyme + best-effort EN), Agimus tibi gratias (full Latin/EN),
     and Retribuere (full Latin/EN).

New 'meal' prayer category (Tischgebete / Meal / Mensae) added to the
filter pills on the prayers index. Replaces the long-standing TODO
that gated meal prayers on the existence of a category for them.

Wiring:
- prayerSlugs.ts gets DE+EN slugs for each prayer (validates
  /faith/prayers/<slug> URLs and feeds the offline sync precache list
  + sitemap).
- de/en/la i18n files get six new prayer-name keys plus category_meal.
- [prayer]/+page.svelte: imports, prayerDefs entries, render block.
- [prayers]/+page.svelte: imports, labels, categories array (new meal
  pill), prayerCategories, prayers list with searchTerms, getPrayerName
  map, prayerMeta, render block.

Eastertide seasonal badge on the prayers index (Regina Cæli card)
moved from a bottom inline-block to absolute top-right, matching the
placement of the same badge on the rosary mystery cards. Adds a
position:relative anchor on the gebet_wrapper.

Bump 1.66.0 -> 1.67.0.
2026-05-05 07:55:54 +02:00
Alexander 585c03a11e feat(offline): hoist sync UI to homepage, slow auto-sync to weekly
Move OfflineSyncIndicator (logo pip) and OfflineSyncBanner from the
[recipeLang] layout/page to (main)/+layout.svelte and (main)/+page.svelte.
Sync is an app-wide concern, not recipe-specific, and surfacing it on the
homepage gives the entry point users actually see when they install the
PWA. Indicator pulls language from languageStore since (main) doesn't
have data.lang from a recipe-scoped load.

Drop the now-unused .banner-wrap CSS and OfflineSyncIndicator/Banner
imports from the recipe routes.

Auto-sync cadence:
- AUTO_SYNC_INTERVAL 30 min -> 1 week. Recipes don't change often enough
  to justify a half-hourly background download (the user explicitly
  wanted this dialed back).
- Internal poll tick 5 min -> 1 hour. Polling 12x an hour for a weekly
  event is wasted work; hourly is fine and still responsive when the
  weekly window opens.

Bump 1.65.3 -> 1.66.0.
2026-05-04 22:21:16 +02:00
Alexander 0372c50084 style(faith): eastertide indicators — fix badge in dark mode, pulse pip in nav
MysterySelector: Tempus Paschale badge on the rosary mystery card now
hardcodes white background + dark text, was rendering as muted grey
in dark mode via --color-bg-elevated. Liturgical white doesn't change
between themes anyway.

Faith layout nav: when eastertide is active and the Angelus link is
swapped to Regína Cæli, add a small pulsating white dot in the link's
top-right corner — same pattern as the recipe header sync indicator,
just white (Tempus Paschale color) and slow-breathing (4s). Dark mode
gets a bright white halo; light mode gets a dark drop shadow so the
white pip stays visible against the light nav bar. Honors
prefers-reduced-motion.

Bump 1.65.2 -> 1.65.3.
2026-05-04 22:14:34 +02:00
24 changed files with 752 additions and 59 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.65.2",
"version": "1.67.3",
"private": true,
"type": "module",
"scripts": {
@@ -2,6 +2,7 @@
import { browser } from '$app/environment';
import { getAngelusStreak, getCurrentTimeSlot, type TimeSlot } from '$lib/stores/angelusStreak.svelte';
import StreakAura from '$lib/components/faith/StreakAura.svelte';
import StreakInfoButton from '$lib/components/faith/StreakInfoButton.svelte';
import Coffee from '@lucide/svelte/icons/coffee';
import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon';
@@ -91,6 +92,7 @@ async function pray() {
</script>
<div class="angelus-streak">
<StreakInfoButton {lang} />
<div class="streak-display">
<StreakAura value={displayStreak} {burst}>
<span class="number">
@@ -146,6 +148,8 @@ async function pray() {
border-radius: 12px;
width: fit-content;
margin: 1.5rem auto;
/* Anchor for the absolute-positioned StreakInfoButton pip */
position: relative;
}
.streak-display {
@@ -2,6 +2,7 @@
import { browser } from '$app/environment';
import { getRosaryStreak } from '$lib/stores/rosaryStreak.svelte';
import StreakAura from '$lib/components/faith/StreakAura.svelte';
import StreakInfoButton from '$lib/components/faith/StreakInfoButton.svelte';
import { m, type FaithLang } from '$lib/js/faithI18n';
import { tick, onMount } from 'svelte';
@@ -42,6 +43,7 @@ async function pray() {
</script>
<div class="streak-container" class:no-js-hidden={!isLoggedIn}>
<StreakInfoButton {lang} />
<div class="streak-display">
<StreakAura value={displayLength} {burst} />
<span class="streak-label">{dayLabel}</span>
@@ -72,6 +74,8 @@ async function pray() {
background: var(--nord1);
border-radius: 12px;
width: fit-content;
/* Anchor for the absolute-positioned StreakInfoButton pip */
position: relative;
}
@media (prefers-color-scheme: light) {
@@ -0,0 +1,202 @@
<script lang="ts">
import Info from '@lucide/svelte/icons/info';
import X from '@lucide/svelte/icons/x';
import type { FaithLang } from '$lib/js/faithI18n';
let { lang = 'de' }: { lang?: FaithLang } = $props();
let open = $state(false);
function close() { open = false; }
function onKeydown(e: KeyboardEvent) {
if (open && e.key === 'Escape') close();
}
const labels = $derived(
lang === 'en'
? { trigger: 'About this counter', close: 'Close', title: 'About this counter' }
: lang === 'la'
? { trigger: 'De numero hoc', close: 'Claudere', title: 'De numero hoc' }
: { trigger: 'Über diese Zählung', close: 'Schliessen', title: 'Über diese Zählung' }
);
</script>
<svelte:window onkeydown={onKeydown} />
<button
class="info-btn"
type="button"
onclick={() => open = true}
aria-label={labels.trigger}
title={labels.trigger}
>
<Info size={14} strokeWidth={2} />
</button>
{#if open}
<div class="info-backdrop" onclick={close} role="presentation">
<div
class="info-dialog"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby="streak-info-title"
tabindex="-1"
>
<button class="info-close" type="button" onclick={close} aria-label={labels.close}>
<X size={16} strokeWidth={2} />
</button>
<h3 id="streak-info-title" class="info-title">{labels.title}</h3>
{#if lang === 'en'}
<p>
This counter tracks <em>consistency</em>, not piety. The Church proposes regular rhythms of prayer (<abbr title="Catechism of the Catholic Church">CCC</abbr> 2698) — we are creatures of body and spirit (CCC 2702), and habit forms us. On weary days the count can be a small nudge to keep faithful to that routine.
</p>
<p>
But the number itself is empty. Christ warns: <q>In praying do not heap up empty phrases as the Gentiles do; for they think that they will be heard for their many words</q> (Mt 6:7). What matters is <q>that the heart should be present to him to whom we are speaking</q> (CCC 2700). One prayer prayed with attention is worth more than thirty rushed to keep a count alive. And clinging to the streak as proof of one's piety only opens the door to the wounded pride the Catechism warns of (CCC 2728).
</p>
{:else if lang === 'la'}
<p>
Numerus iste <em>constantiam</em> metitur, non pietatem. Ecclesia rhythmos cotidianos orationis commendat (<abbr title="Catechismus Catholicae Ecclesiae">CCC</abbr> 2698) — homo enim corpus et spiritus est (CCC 2702), atque consuetudine formamur. Diebus laboriosis numerus parvulum incitamentum esse potest, ut consuetudini fideles maneamus.
</p>
<p>
Numerus tamen ipse vacuus est. Christus monet: <q>Orantes nolite multum loqui, sicut ethnici; putant enim quod in multiloquio suo exaudiantur</q> (Mt 6,7). Quod refert est ut <q>cor adsit Ei cui loquimur</q> (CCC 2700). Una oratio attente fusa pluris est quam triginta praecipitanter recitatae ut numerus servetur. Qui autem numerum tenet ut testimonium pietatis suae, ostium aperit superbiae læsæ, quam Catechismus monet (CCC 2728).
</p>
{:else}
<p>
Diese Zählung misst <em>Beständigkeit</em>, nicht Frömmigkeit. Die Kirche empfiehlt regelmässige Gebetsrhythmen (<abbr title="Katechismus der Katholischen Kirche">KKK</abbr> 2698) — der Mensch ist Leib und Geist (KKK 2702), und Gewohnheit formt uns. An müden Tagen kann die Zahl ein kleiner Anstoss sein, in der Routine zu bleiben.
</p>
<p>
Die Zahl selbst aber ist leer. Christus mahnt: <q>Plappert nicht wie die Heiden, die meinen, sie würden nur erhört, wenn sie viele Worte machen</q> (Mt 6,7). Worauf es ankommt, ist, <q>dass das Herz dem zugewandt ist, zu dem es spricht</q> (KKK 2700). Ein einziges aufmerksam gebetetes Gebet ist mehr wert als dreissig hastig durchgeleierte, nur um den Zähler zu retten. Und wer am Streak als Beweis seiner Frömmigkeit festhält, öffnet die Tür zum verletzten Stolz, vor dem der Katechismus warnt (KKK 2728).
</p>
{/if}
</div>
</div>
{/if}
<style>
/* Tiny info pip in the corner — opens a modal that explains the
streak counter is about habit, not piety. The parent container
must be position: relative for this to anchor correctly. */
.info-btn {
position: absolute;
top: 0.4rem;
right: 0.4rem;
display: grid;
place-items: center;
width: 1.5rem;
height: 1.5rem;
padding: 0;
border: none;
border-radius: 50%;
background: transparent;
color: var(--color-text-secondary, var(--nord4));
cursor: pointer;
opacity: 0.55;
transition: opacity 150ms, background 150ms;
z-index: 5;
}
.info-btn:hover,
.info-btn:focus-visible {
opacity: 1;
background: rgba(255, 255, 255, 0.08);
}
@media (prefers-color-scheme: light) {
:global(:root:not([data-theme="dark"])) .info-btn:hover,
:global(:root:not([data-theme="dark"])) .info-btn:focus-visible {
background: rgba(0, 0, 0, 0.06);
}
}
:global(:root[data-theme="light"]) .info-btn:hover,
:global(:root[data-theme="light"]) .info-btn:focus-visible {
background: rgba(0, 0, 0, 0.06);
}
.info-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
animation: streak-info-fade 150ms ease-out;
}
.info-dialog {
position: relative;
background: var(--color-surface, var(--nord1));
border: 1px solid var(--color-border, var(--nord3));
border-radius: 14px;
padding: 1.5rem 1.5rem 1.25rem;
max-width: 560px;
width: 100%;
max-height: calc(100vh - 2rem);
overflow-y: auto;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
animation: streak-info-scale 150ms ease-out;
color: var(--color-text-primary, var(--nord6));
}
.info-close {
position: absolute;
top: 0.55rem;
right: 0.55rem;
display: grid;
place-items: center;
width: 1.85rem;
height: 1.85rem;
padding: 0;
border: none;
border-radius: 50%;
background: transparent;
color: inherit;
opacity: 0.6;
cursor: pointer;
transition: opacity 150ms, background 150ms;
}
.info-close:hover,
.info-close:focus-visible {
opacity: 1;
background: rgba(255, 255, 255, 0.08);
}
@media (prefers-color-scheme: light) {
:global(:root:not([data-theme="dark"])) .info-close:hover,
:global(:root:not([data-theme="dark"])) .info-close:focus-visible {
background: rgba(0, 0, 0, 0.06);
}
}
:global(:root[data-theme="light"]) .info-close:hover,
:global(:root[data-theme="light"]) .info-close:focus-visible {
background: rgba(0, 0, 0, 0.06);
}
.info-title {
margin: 0 2rem 0.85rem 0;
font-size: 1.1rem;
font-weight: 700;
}
.info-dialog p {
margin: 0 0 0.85rem;
font-size: 0.92rem;
line-height: 1.55;
color: var(--color-text-secondary, var(--nord4));
}
.info-dialog p:last-child {
margin-bottom: 0;
}
.info-dialog q {
font-style: italic;
}
.info-dialog abbr {
text-decoration: none;
border-bottom: 1px dotted currentColor;
cursor: help;
}
@keyframes streak-info-fade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes streak-info-scale {
from { opacity: 0; transform: scale(0.96); }
to { opacity: 1; transform: scale(1); }
}
</style>
@@ -0,0 +1,23 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer hasLatin={false}>
{#snippet children(showLatin, urlLang)}
<p>
{#if urlLang === 'de'}<v lang="de">Hilf, Maria, es ist Zeit, hilf, Mutter der Barmherzigkeit.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Help, Blessed Mother, it is highest time, help Mother of Mercy.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du bist berufen, uns aus Nöten und Gefahren zu erretten,</v>{/if}
{#if urlLang === 'en'}<v lang="en">You are mighty, deliver us from distress and danger,</v>{/if}
{#if urlLang === 'de'}<v lang="de">denn wo Menschenhilf' gebricht, mangelt doch die deine nicht.</v>{/if}
{#if urlLang === 'en'}<v lang="en">for where human help is lacking, yours is not.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Nein, du kannst das heiße Flehen deiner Kinder nicht verschmähen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">No, you cannot spurn the fervent supplication of your children.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Zeige, dass du Mutter bist, wo die Not am größten ist.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Show that you are our mother where the need is greatest.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Hilf, Maria, es ist Zeit, hilf, Mutter der Barmherzigkeit.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Help, Blessed Mother, it is highest time, help Mother of Mercy.</v>{/if}
<v lang="und">Amen.</v>
</p>
{/snippet}
</Prayer>
@@ -0,0 +1,102 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer hasLatin={false}>
{#snippet children(showLatin, urlLang)}
<p>
{#if urlLang === 'de'}<v lang="de">Jungfrau, Mutter Gottes mein,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Virgin, Mother of God of mine,</v>{/if}
{#if urlLang === 'de'}<v lang="de">lass mich ganz dein Eigen sein!</v>{/if}
{#if urlLang === 'en'}<v lang="en">let me be wholly thine own!</v>{/if}
{#if urlLang === 'de'}<v lang="de">Dein im Leben, dein im Tod,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thine in life, thine in death,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dein in Unglück, Angst und Not;</v>{/if}
{#if urlLang === 'en'}<v lang="en">thine in misfortune, fear and need;</v>{/if}
{#if urlLang === 'de'}<v lang="de">dein in Kreuz und bitt'rem Leid,</v>{/if}
{#if urlLang === 'en'}<v lang="en">thine in cross and bitter sorrow,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dein für Zeit und Ewigkeit.</v>{/if}
{#if urlLang === 'en'}<v lang="en">thine for time and eternity.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Jungfrau, Mutter Gottes mein,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Virgin, Mother of God of mine,</v>{/if}
{#if urlLang === 'de'}<v lang="de">lass mich ganz dein Eigen sein!</v>{/if}
{#if urlLang === 'en'}<v lang="en">let me be wholly thine own!</v>{/if}
</p>
<p>
{#if urlLang === 'de'}<v lang="de">Mutter, auf dich hoff' und baue ich!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Mother, in thee I hope and trust!</v>{/if}
{#if urlLang === 'de'}<v lang="de">Mutter, zu dir ruf' und seufze ich!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Mother, to thee I call and sigh!</v>{/if}
{#if urlLang === 'de'}<v lang="de">Mutter, du Gütigste, steh mir bei!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Mother, most kind, stand by me!</v>{/if}
{#if urlLang === 'de'}<v lang="de">Mutter, du Mächtigste, Schutz mir verleih!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Mother, most mighty, grant me protection!</v>{/if}
</p>
<p>
{#if urlLang === 'de'}<v lang="de">O Mutter, so komm, hilf beten mir!</v>{/if}
{#if urlLang === 'en'}<v lang="en">O Mother, come and help me pray!</v>{/if}
{#if urlLang === 'de'}<v lang="de">O Mutter, so komm, hilf streiten mir!</v>{/if}
{#if urlLang === 'en'}<v lang="en">O Mother, come and help me strive!</v>{/if}
{#if urlLang === 'de'}<v lang="de">O Mutter, so komm, hilf leiden mir!</v>{/if}
{#if urlLang === 'en'}<v lang="en">O Mother, come and help me suffer!</v>{/if}
{#if urlLang === 'de'}<v lang="de">O Mutter, so komm und bleib bei mir!</v>{/if}
{#if urlLang === 'en'}<v lang="en">O Mother, come and stay with me!</v>{/if}
</p>
<p>
{#if urlLang === 'de'}<v lang="de">Du kannst mir ja helfen, o Mächtigste!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou canst help me, O most mighty!</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du willst mir ja helfen, o Gütigste!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou wilt help me, O most kind!</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du musst mir nun helfen, o Treueste!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou must help me now, O most faithful!</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du wirst mir auch helfen, Barmherzigste!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou wilt help me also, O most merciful!</v>{/if}
</p>
<p>
{#if urlLang === 'de'}<v lang="de">O Mutter der Gnade, der Christen Hort,</v>{/if}
{#if urlLang === 'en'}<v lang="en">O Mother of grace, refuge of Christians,</v>{/if}
{#if urlLang === 'de'}<v lang="de">du Zuflucht der Sünder, des Heiles Pfort',</v>{/if}
{#if urlLang === 'en'}<v lang="en">refuge of sinners, gate of salvation,</v>{/if}
{#if urlLang === 'de'}<v lang="de">du Hoffnung der Erde, des Himmels Zier,</v>{/if}
{#if urlLang === 'en'}<v lang="en">hope of the earth, ornament of heaven,</v>{/if}
{#if urlLang === 'de'}<v lang="de">du Trost der Betrübten, ihr Schutzpanier.</v>{/if}
{#if urlLang === 'en'}<v lang="en">comfort of the afflicted, their shielding banner.</v>{/if}
</p>
<p>
{#if urlLang === 'de'}<v lang="de">Wer hat je umsonst deine Hilf' angefleht?</v>{/if}
{#if urlLang === 'en'}<v lang="en">Who has ever begged thy help in vain?</v>{/if}
{#if urlLang === 'de'}<v lang="de">Wann hast du vergessen ein kindlich' Gebet?</v>{/if}
{#if urlLang === 'en'}<v lang="en">When hast thou forgotten a childlike prayer?</v>{/if}
{#if urlLang === 'de'}<v lang="de">Drum ruf' ich beharrlich in Kreuz und in Leid:</v>{/if}
{#if urlLang === 'en'}<v lang="en">Therefore I call out steadfastly in cross and sorrow:</v>{/if}
{#if urlLang === 'de'}<v lang="de">„Maria hilft immer! Sie hilft jederzeit!"</v>{/if}
{#if urlLang === 'en'}<v lang="en">"Mary helps always! She helps at all times!"</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ich ruf' voll Vertrauen in Leiden und Tod:</v>{/if}
{#if urlLang === 'en'}<v lang="en">I call full of trust in suffering and death:</v>{/if}
{#if urlLang === 'de'}<v lang="de">„Maria hilft immer, in jeglicher Not!"</v>{/if}
{#if urlLang === 'en'}<v lang="en">"Mary helps always, in every distress!"</v>{/if}
{#if urlLang === 'de'}<v lang="de">So glaub' ich und lebe und sterbe darauf:</v>{/if}
{#if urlLang === 'en'}<v lang="en">So I believe and live and die upon it:</v>{/if}
{#if urlLang === 'de'}<v lang="de">„Maria hilft mir in den Himmel hinauf."</v>{/if}
{#if urlLang === 'en'}<v lang="en">"Mary helps me up into heaven."</v>{/if}
</p>
<p>
{#if urlLang === 'de'}<v lang="de">Jungfrau, Mutter Gottes mein,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Virgin, Mother of God of mine,</v>{/if}
{#if urlLang === 'de'}<v lang="de">lass mich ganz dein Eigen sein!</v>{/if}
{#if urlLang === 'en'}<v lang="en">let me be wholly thine own!</v>{/if}
{#if urlLang === 'de'}<v lang="de">Dein im Leben, dein im Tod,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thine in life, thine in death,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dein in Unglück, Angst und Not;</v>{/if}
{#if urlLang === 'en'}<v lang="en">thine in misfortune, fear and need;</v>{/if}
{#if urlLang === 'de'}<v lang="de">dein in Kreuz und bitt'rem Leid,</v>{/if}
{#if urlLang === 'en'}<v lang="en">thine in cross and bitter sorrow,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dein für Zeit und Ewigkeit.</v>{/if}
{#if urlLang === 'en'}<v lang="en">thine for time and eternity.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Jungfrau, Mutter Gottes mein,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Virgin, Mother of God of mine,</v>{/if}
{#if urlLang === 'de'}<v lang="de">lass mich ganz dein Eigen sein!</v>{/if}
{#if urlLang === 'en'}<v lang="en">let me be wholly thine own!</v>{/if}
</p>
{/snippet}
</Prayer>
@@ -0,0 +1,32 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Meménto, o piíssima Virgo María, non esse audítum a sǽculo,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gedenke, o gütigste Jungfrau Maria, es ist noch nie gehört worden,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Remember, O most gracious Virgin Mary, that never was it known</v>{/if}
{#if showLatin}<v lang="la">quemquam ad tua curréntem præsídia, tua implorántem auxília,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dass jemand, der zu dir seine Zuflucht nahm, deine Hilfe anrief</v>{/if}
{#if urlLang === 'en'}<v lang="en">that anyone who fled to thy protection, implored thy help,</v>{/if}
{#if showLatin}<v lang="la">tua peténtem suffrágia, esse derelíctum.</v>{/if}
{#if urlLang === 'de'}<v lang="de">und um deine Fürbitte flehte, von dir verlassen worden ist.</v>{/if}
{#if urlLang === 'en'}<v lang="en">or sought thine intercession, was left unaided.</v>{/if}
{#if showLatin}<v lang="la">Ego tali animátus confidéntia, ad te, Virgo Vírginum, Mater, curro,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Von diesem Vertrauen beseelt, nehme ich meine Zuflucht zu dir, o Jungfrau der Jungfrauen, meine Mutter.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Inspired by this confidence, I fly unto thee, O Virgin of virgins, my Mother;</v>{/if}
{#if showLatin}<v lang="la">ad te vénio, coram te gemens peccátor assísto.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Zu dir komme ich, vor dir stehe ich als ein sündiger Mensch.</v>{/if}
{#if urlLang === 'en'}<v lang="en">to thee do I come, before thee I stand, sinful and sorrowful.</v>{/if}
{#if showLatin}<v lang="la">Noli, Mater Verbi, verba mea despícere;</v>{/if}
{#if urlLang === 'de'}<v lang="de">O Mutter des Ewigen Wortes, verschmähe nicht meine Worte,</v>{/if}
{#if urlLang === 'en'}<v lang="en">O Mother of the Word Incarnate, despise not my petitions,</v>{/if}
{#if showLatin}<v lang="la">sed audi propítia et exáudi.</v>{/if}
{#if urlLang === 'de'}<v lang="de">sondern höre sie gnädig an und erhöre mich.</v>{/if}
{#if urlLang === 'en'}<v lang="en">but in thy mercy hear and answer me.</v>{/if}
<v lang="und">Amen.</v>
</p>
{/snippet}
</Prayer>
@@ -0,0 +1,29 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">O Dómina mea, o Mater mea, tibi me totum óffero,</v>{/if}
{#if urlLang === 'de'}<v lang="de">O meine Gebieterin, o meine Mutter! Dir bringe ich mich ganz dar;</v>{/if}
{#if urlLang === 'en'}<v lang="en">O my Queen, O my Mother, I give myself entirely to thee;</v>{/if}
{#if showLatin}<v lang="la">atque, ut me tibi probem devótum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">und um dir meine Hingabe zu bezeigen,</v>{/if}
{#if urlLang === 'en'}<v lang="en">and to show my devotion to thee,</v>{/if}
{#if showLatin}<v lang="la">cónsecro tibi hódie óculos meos, aures meas, os meum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">weihe ich dir heute meine Augen, meine Ohren, meinen Mund,</v>{/if}
{#if urlLang === 'en'}<v lang="en">I consecrate to thee this day my eyes, my ears, my mouth,</v>{/if}
{#if showLatin}<v lang="la">cor meum, plane me totum.</v>{/if}
{#if urlLang === 'de'}<v lang="de">mein Herz, mich selber ganz und gar.</v>{/if}
{#if urlLang === 'en'}<v lang="en">my heart, my whole being without reserve.</v>{/if}
{#if showLatin}<v lang="la">Quóniam ítaque tuus sum, o bona Mater,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Weil ich also dir gehöre, o gute Mutter,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Wherefore, good Mother, as I am thine own,</v>{/if}
{#if showLatin}<v lang="la">serva me, defénde me, ut rem ac possessiónem tuam.</v>{/if}
{#if urlLang === 'de'}<v lang="de">bewahre mich und beschütze mich als dein Gut und dein Eigentum.</v>{/if}
{#if urlLang === 'en'}<v lang="en">keep me, defend me, as thy property and possession.</v>{/if}
<v lang="und">Amen.</v>
</p>
{/snippet}
</Prayer>
@@ -16,7 +16,7 @@
langStore = null;
}
let showLatin = $derived(showLatinStore ? $showLatinStore : true);
let showLatin = $derived(hasLatin === false ? false : (showLatinStore ? $showLatinStore : true));
let urlLang = $derived(langStore ? $langStore : 'de');
</script>
@@ -0,0 +1,65 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer hasLatin={false}>
{#snippet children(showLatin, urlLang)}
<p>
{#if urlLang === 'de'}<v lang="de">Dir sei, o Gott, für Speis' und Trank,</v>{/if}
{#if urlLang === 'en'}<v lang="en">To Thee, O God, for food and drink,</v>{/if}
{#if urlLang === 'de'}<v lang="de">für alles Gute Lob und Dank.</v>{/if}
{#if urlLang === 'en'}<v lang="en">for all good things, be praise and thanks.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du gabst, Du wirst auch ferner geben,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou hast given, Thou wilt give still more,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Dich preise unser ganzes Leben! Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">may our whole life praise Thee! Amen.</v>{/if}
</p>
{/snippet}
</Prayer>
<p class="prayer-divider"><i>· · ·</i></p>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Ágimus tibi grátias, omnípotens Deus,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Wir danken Dir, allmächtiger Gott,</v>{/if}
{#if urlLang === 'en'}<v lang="en">We give Thee thanks, almighty God,</v>{/if}
{#if showLatin}<v lang="la">pro univérsis benefíciis tuis:</v>{/if}
{#if urlLang === 'de'}<v lang="de">für all Deine Wohltaten,</v>{/if}
{#if urlLang === 'en'}<v lang="en">for all Thy benefits:</v>{/if}
{#if showLatin}<v lang="la">Qui vivis et regnas in sǽcula sæculórum.</v>{/if}
{#if urlLang === 'de'}<v lang="de">der Du lebst und herrschest von Ewigkeit zu Ewigkeit.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Who livest and reignest, world without end.</v>{/if}
<v lang="und">Amen.</v>
</p>
{/snippet}
</Prayer>
<p class="prayer-divider"><i>· · ·</i></p>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Retribúere dignáre, Dómine,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Herr, vergilt allen,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Vouchsafe, O Lord, to reward</v>{/if}
{#if showLatin}<v lang="la">ómnibus, nobis bona faciéntibus propter nomen tuum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">die uns um Deines Namens willen Gutes getan haben,</v>{/if}
{#if urlLang === 'en'}<v lang="en">all those who do good to us for Thy name's sake</v>{/if}
{#if showLatin}<v lang="la">vitam ætérnam.</v>{/if}
{#if urlLang === 'de'}<v lang="de">mit dem ewigen Leben.</v>{/if}
{#if urlLang === 'en'}<v lang="en">with eternal life.</v>{/if}
<v lang="und">Amen.</v>
</p>
{/snippet}
</Prayer>
<style>
.prayer-divider {
text-align: center;
color: grey;
margin: 0.5em 0;
letter-spacing: 0.5em;
}
</style>
@@ -0,0 +1,83 @@
<script>
import Prayer from './Prayer.svelte';
import GloriaPatri from './GloriaPatri.svelte';
</script>
<Prayer hasLatin={false}>
{#snippet children(showLatin, urlLang)}
<p>
{#if urlLang === 'de'}<v lang="de">O Gott, von dem wir alles haben,</v>{/if}
{#if urlLang === 'en'}<v lang="en">O God, from whom we have all things,</v>{/if}
{#if urlLang === 'de'}<v lang="de">wir preisen Dich für Deine Gaben!</v>{/if}
{#if urlLang === 'en'}<v lang="en">we praise Thee for Thy gifts!</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du speisest uns, weil Du uns liebst,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou feedest us because Thou lovest us;</v>{/if}
{#if urlLang === 'de'}<v lang="de">drum segne auch, was Du uns gibst. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">so bless what Thou hast given us. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>
<p class="prayer-divider"><i>· · ·</i></p>
<Prayer hasLatin={false}>
{#snippet children(showLatin, urlLang)}
<p>
{#if urlLang === 'de'}<v lang="de">Komm, Herr Jesus, sei unser Gast,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Come, Lord Jesus, be our guest,</v>{/if}
{#if urlLang === 'de'}<v lang="de">und segne, was Du uns bescheret hast. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and let these gifts to us be blessed. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>
<p class="prayer-divider"><i>· · ·</i></p>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Óculi ómnium in te sperant, Dómine,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Aller Augen warten auf Dich, o Herr,</v>{/if}
{#if urlLang === 'en'}<v lang="en">The eyes of all hope in Thee, O Lord,</v>{/if}
{#if showLatin}<v lang="la">et tu das escam illórum in témpore opportúno.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du gibst ihnen Speise zur rechten Zeit.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and Thou givest them meat in due season.</v>{/if}
{#if showLatin}<v lang="la">Áperis tu manum tuam,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du öffnest Deine milde Hand</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou openest Thy hand,</v>{/if}
{#if showLatin}<v lang="la">et imples omne ánimal benedictióne.</v>{/if}
{#if urlLang === 'de'}<v lang="de">und erfüllest alles, was da lebt, mit Segen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and fillest every living creature with blessing.</v>{/if}
</p>
{/snippet}
</Prayer>
<GloriaPatri />
<p class="prayer-divider"><i>· · ·</i></p>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Bénedic, Dómine, nos et hæc tua dona,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Herr, segne uns und diese Deine Gaben,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Bless us, O Lord, and these Thy gifts,</v>{/if}
{#if showLatin}<v lang="la">quæ de tua largitáte sumus sumptúri.</v>{/if}
{#if urlLang === 'de'}<v lang="de">die wir von Deiner Güte nun empfangen werden,</v>{/if}
{#if urlLang === 'en'}<v lang="en">which we are about to receive from Thy bounty.</v>{/if}
{#if showLatin}<v lang="la">Per Christum Dóminum nostrum.</v>{/if}
{#if urlLang === 'de'}<v lang="de">durch Christus, unseren Herrn.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Through Christ our Lord.</v>{/if}
<v lang="und">Amen.</v>
</p>
{/snippet}
</Prayer>
<style>
.prayer-divider {
text-align: center;
color: grey;
margin: 0.5em 0;
letter-spacing: 0.5em;
}
</style>
+6
View File
@@ -21,4 +21,10 @@ export const validPrayerSlugs = new Set([
'tantum-ergo',
'angelus',
'regina-caeli',
'jungfrau-mutter-gottes-mein', 'virgin-mother-of-god',
'o-meine-gebieterin', 'o-my-queen',
'gedenke-o-guetigste-jungfrau-maria', 'memorare',
'hilf-maria-es-ist-zeit', 'help-mary',
'tischgebet-vor-dem-essen', 'grace-before-meals',
'tischgebet-nach-dem-essen', 'grace-after-meals',
]);
+7
View File
@@ -56,6 +56,13 @@ export const de = {
prayer_before_crucifix: 'Gebet vor einem Kruzifix',
guardian_angel_prayer: 'Schutzengel-Gebet',
apostles_creed: 'Apostolisches Glaubensbekenntnis',
jungfrau_mutter_prayer: 'Jungfrau, Mutter Gottes mein',
o_my_queen_prayer: 'O meine Gebieterin',
memorare_prayer: 'Gedenke, o gütigste Jungfrau Maria',
hilf_maria_prayer: 'Hilf, Maria, es ist Zeit',
grace_before_meals: 'Tischgebet vor dem Essen',
grace_after_meals: 'Tischgebet nach dem Essen',
category_meal: 'Tischgebete',
search_prayers: 'Gebete suchen…',
clear_search: 'Suche löschen',
text_match: 'Treffer im Gebetstext',
+7
View File
@@ -56,6 +56,13 @@ export const en = {
prayer_before_crucifix: 'Prayer Before a Crucifix',
guardian_angel_prayer: 'Guardian Angel Prayer',
apostles_creed: "Apostles' Creed",
jungfrau_mutter_prayer: 'Virgin, Mother of God',
o_my_queen_prayer: 'O My Queen',
memorare_prayer: 'Memorare',
hilf_maria_prayer: 'Help, Mary, it is Time',
grace_before_meals: 'Grace Before Meals',
grace_after_meals: 'Grace After Meals',
category_meal: 'Meal Prayers',
search_prayers: 'Search prayers…',
clear_search: 'Clear search',
text_match: 'Match in prayer text',
+7
View File
@@ -56,6 +56,13 @@ export const la = {
prayer_before_crucifix: 'Oratio ante Crucifixum',
guardian_angel_prayer: 'Angele Dei',
apostles_creed: 'Symbolum Apostolorum',
jungfrau_mutter_prayer: 'Jungfrau, Mutter Gottes mein',
o_my_queen_prayer: 'O Domina Mea',
memorare_prayer: 'Memorare',
hilf_maria_prayer: 'Hilf, Maria, es ist Zeit',
grace_before_meals: 'Benedictio Mensae',
grace_after_meals: 'Gratiarum Actio post Mensam',
category_meal: 'Mensae',
search_prayers: 'Orationes quaerere…',
clear_search: 'Quaestionem delere',
text_match: 'In textu orationis',
+5 -4
View File
@@ -2,7 +2,7 @@ import { browser } from '$app/environment';
import { isOfflineDataAvailable, getLastSync, clearOfflineData } from '$lib/offline/db';
import { downloadAllRecipes, type SyncResult, type SyncProgress } from '$lib/offline/sync';
const AUTO_SYNC_INTERVAL = 30 * 60 * 1000; // 30 minutes
const AUTO_SYNC_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 1 week
const LAST_SYNC_KEY = 'bocken-last-sync-time';
type PWAState = {
@@ -152,12 +152,13 @@ function createPWAStore() {
startAutoSync() {
if (autoSyncInterval) return; // Already running
// Check every 5 minutes if we should sync
// Check hourly if we should sync — actual sync only fires once
// AUTO_SYNC_INTERVAL has elapsed since the last sync.
autoSyncInterval = setInterval(() => {
autoSync();
}, 5 * 60 * 1000); // Check every 5 minutes
}, 60 * 60 * 1000);
console.log('[PWA] Auto-sync enabled (every 30 minutes)');
console.log('[PWA] Auto-sync enabled (weekly)');
},
stopAutoSync() {
+18
View File
@@ -2,6 +2,8 @@
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import OfflineSyncIndicator from '$lib/components/OfflineSyncIndicator.svelte';
import { languageStore } from '$lib/stores/language.svelte';
let { data, children } = $props();
let user = $derived(data.session?.user);
@@ -16,9 +18,25 @@ let user = $derived(data.session?.user);
<LanguageSelector />
{/snippet}
{#snippet logo_overlay()}
<div class="logo-pip">
<OfflineSyncIndicator lang={languageStore.value} />
</div>
{/snippet}
{#snippet right_side()}
<UserHeader {user}></UserHeader>
{/snippet}
{@render children()}
</Header>
<style>
:global(.logo-pip) {
position: absolute;
top: -8px;
right: -7px;
z-index: 2;
pointer-events: auto;
}
</style>
+3
View File
@@ -2,6 +2,7 @@
import { resolve } from '$app/paths';
import LinksGrid from "$lib/components/LinksGrid.svelte";
import Seo from '$lib/components/Seo.svelte';
import OfflineSyncBanner from '$lib/components/OfflineSyncBanner.svelte';
import { onMount } from 'svelte';
let { data } = $props();
@@ -143,6 +144,8 @@ section h2{
</section>
{/if}
<OfflineSyncBanner {lang} />
<section>
<h2>{labels.pages}</h2>
@@ -91,7 +91,7 @@ const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref));
<li style="--active-fill: var(--nord12)"><a href={prayersHref} class:active={prayersActive} title={t.prayers}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 640 512" fill="currentColor"><path d="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg><span class="nav-label">{t.prayers}</span></a></li>
<li style="--active-fill: var(--nord11)"><a href={rosaryHref} class:active={isActive(rosaryHref)} title={t.rosary}><svg class="nav-icon" width="16" height="16" viewBox="0 0 512 512" fill="currentColor"><path d="M241.251,145.056c-39.203-17.423-91.472,17.423-104.54,60.982c-13.068,43.558,8.712,117.608,65.337,143.742 c56.626,26.135,108.896-8.712,87.117-39.202c-74.049-8.712-121.963-87.117-100.184-126.319S280.453,162.479,241.251,145.056z"/><path d="M337.079,271.375c47.914-39.202,21.779-126.319-17.423-135.031c-39.202-8.712-56.626,13.068-26.135,39.202 c39.203,30.491-8.712,91.472-39.202,87.117C254.318,262.663,289.165,310.577,337.079,271.375z"/><path d="M254.318,119.788c43.558-17.423,74.049-9.579,100.184,16.556c13.068-39.202-30.491-104.54-108.896-113.252 S93.153,118.921,127.999,171.191C136.711,153.767,188.981,106.721,254.318,119.788z"/><path d="M110.576,245.24C36.527,262.663,28.87,335.248,45.239,380.27c17.423,47.914,4.356,82.761,26.135,91.472 c20.622,8.253,91.472,13.068,152.454,17.423c60.982,4.356,108.896-47.914,91.472-108.896 C141.067,410.761,110.576,284.442,110.576,245.24z"/><path d="M93.883,235.796c0,0,2.178-28.313,10.89-43.558c-4.356-4.356-8.712-21.779-8.712-21.779 s-4.356-19.601-4.356-34.846c-32.669-6.534-89.295,34.846-91.472,41.38c-2.178,6.534,10.889,80.583,39.202,82.761 C69.927,235.796,93.883,235.796,93.883,235.796z"/><path d="M489.533,175.546c-39.202-82.761-113.252-65.337-113.252-65.337s4.356,21.779-4.356,34.846 c43.558,47.914,13.067,146.643-24.681,158.265c130.675,56.626,159.712-58.081,164.068-75.504 C515.668,210.393,498.245,197.326,489.533,175.546z"/><path d="M454.108,332.076c-22.359,15.841-85.663,11.613-121.964-7.265c1.446,14.514-13.067,37.756-20.325,39.202 c27.59,11.621,53.725,62.436,7.265,116.161c18.878,18.87,95.828,4.356,140.842-24.689c7.325-4.722,18.869-52.27,21.779-79.851 C485.56,339.103,488.963,307.387,454.108,332.076z"/><path d="M257.227,213.294c-18.928,5.164-30.439-6.27-23.234-18.869c5.811-10.167,5.266-20.69-8.712-13.068 c-29.044,17.423-11.612,66.784,24.689,62.428c49.36-17.423,27.581-62.428,14.514-60.982 C251.417,184.249,273.196,208.938,257.227,213.294z"/></svg><span class="nav-label">{t.rosary}</span></a></li>
{#if eastertide}
<li style="--active-fill: var(--nord14)"><a href={angelusHref} class:active={isActive(angelusHref)} title={angelusLabel}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-10 -274 532 548" fill="currentColor"><path d="M256-168c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zM6-63l122 199-56 70c-5 7-8 14-8 23 0 19 16 35 36 35h312c20 0 36-16 36-35 0-9-3-16-8-23l-56-70L507-63c3-6 5-13 5-20 0-20-16-37-37-37-7 0-14 2-20 6l-17 12c-13 8-30 6-40-4l-35-35c-7-7-17-11-27-11s-20 4-27 11l-30 30c-13 13-33 13-46 0l-30-30c-7-7-17-11-27-11s-20 4-27 11l-34 34c-11 11-28 13-41 4l-17-11c-6-4-13-6-20-6-20 0-37 17-37 37 0 7 2 14 6 20z"/></svg><span class="nav-label">{angelusLabel}</span></a></li>
<li style="--active-fill: var(--nord14)"><a href={angelusHref} class:active={isActive(angelusHref)} title={angelusLabel} class="regina-nav"><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-10 -274 532 548" fill="currentColor"><path d="M256-168c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zM6-63l122 199-56 70c-5 7-8 14-8 23 0 19 16 35 36 35h312c20 0 36-16 36-35 0-9-3-16-8-23l-56-70L507-63c3-6 5-13 5-20 0-20-16-37-37-37-7 0-14 2-20 6l-17 12c-13 8-30 6-40-4l-35-35c-7-7-17-11-27-11s-20 4-27 11l-30 30c-13 13-33 13-46 0l-30-30c-7-7-17-11-27-11s-20 4-27 11l-34 34c-11 11-28 13-41 4l-17-11c-6-4-13-6-20-6-20 0-37 17-37 37 0 7 2 14 6 20z"/></svg><span class="nav-label">{angelusLabel}</span><span class="season-pip" aria-hidden="true"></span></a></li>
{:else}
<li style="--active-fill: var(--nord14)"><a href={angelusHref} class:active={isActive(angelusHref)} title={angelusLabel}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="6 -274 564 548" fill="currentColor"><path d="M392-162c-4-10-9-18-15-26 5-4 7-8 7-12 0-18-43-32-96-32s-96 14-96 32c0 4 3 8 7 12-6 8-11 16-15 26-15-11-24-24-24-38 0-35 57-64 128-64s128 29 128 64c0 14-9 27-24 38zm-104-22c35 0 64 29 64 64s-29 64-64 64-64-29-64-64 29-64 64-64zM82 159c3-22-3-48-20-64C34 68 16 30 16-12v-64c0-42 34-76 76-76 23 0 44 10 59 27l65 78c-21 16-37 40-43 67l-43 195c-4 17-2 34 5 49h-21c-26 0-46-24-42-50l10-55zm364 56L403 20c-6-27-21-51-42-67l64-77c15-18 36-28 59-28 42 0 76 34 76 76v64c0 42-18 80-46 107-17 16-23 42-20 64l10 56c4 26-16 49-42 49h-20c6-15 8-32 4-49zM220 31c7-32 35-55 68-55s61 23 68 55l43 194c5 20-11 39-31 39H208c-21 0-36-19-31-39l43-194z"/></svg><span class="nav-label">{angelusLabel}</span></a></li>
{/if}
@@ -115,3 +115,49 @@ const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref));
{@render children()}
</Header>
<style>
/* Pulsating white pip on the Regina Cæli nav link during eastertide.
Mirrors the recipe header's sync indicator dot: 8px circle, double
box-shadow halo, slow breathe. White instead of green because Tempus
Paschale's liturgical color is white. */
:global(.site_header li > a.regina-nav) {
position: relative;
}
:global(.regina-nav .season-pip) {
position: absolute;
top: 2px;
right: 2px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #ffffff;
/* Dark mode default: bright white halo against the dark nav bar. */
box-shadow:
0 0 0 2px rgba(255, 255, 255, 0.45),
0 0 6px rgba(255, 255, 255, 0.85);
animation: regina-season-pulse 4s ease-in-out infinite;
pointer-events: none;
}
/* Light mode: white pip blends into the light nav bar — swap to a dark
drop shadow so the dot still reads. */
@media (prefers-color-scheme: light) {
:global(:root:not([data-theme]) .regina-nav .season-pip) {
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.18),
0 1px 4px rgba(0, 0, 0, 0.45);
}
}
:global(:root[data-theme="light"] .regina-nav .season-pip) {
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.18),
0 1px 4px rgba(0, 0, 0, 0.45);
}
@keyframes regina-season-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.55; transform: scale(0.85); }
}
@media (prefers-reduced-motion: reduce) {
:global(.regina-nav .season-pip) { animation: none; }
}
</style>
@@ -28,6 +28,12 @@
import AnimaChristi from "$lib/components/faith/prayers/AnimaChristi.svelte";
import PrayerBeforeACrucifix from "$lib/components/faith/prayers/PrayerBeforeACrucifix.svelte";
import Postcommunio from "$lib/components/faith/prayers/Postcommunio.svelte";
import JungfrauMutterGottes from "$lib/components/faith/prayers/JungfrauMutterGottes.svelte";
import ODominaMea from "$lib/components/faith/prayers/ODominaMea.svelte";
import Memorare from "$lib/components/faith/prayers/Memorare.svelte";
import HilfMaria from "$lib/components/faith/prayers/HilfMaria.svelte";
import TischgebetVor from "$lib/components/faith/prayers/TischgebetVor.svelte";
import TischgebetNach from "$lib/components/faith/prayers/TischgebetNach.svelte";
import Prayer from "$lib/components/faith/prayers/Prayer.svelte";
import { isEastertide as checkEastertide } from "$lib/js/easter.svelte";
@@ -79,17 +85,24 @@
apostlesCreed: t.apostles_creed,
tantumErgo: 'Tantum Ergo',
angelus: 'Angelus',
reginaCaeli: 'Regína Cæli'
reginaCaeli: 'Regína Cæli',
jungfrauMutter: t.jungfrau_mutter_prayer,
oMyQueen: t.o_my_queen_prayer,
memorare: t.memorare_prayer,
hilfMaria: t.hilf_maria_prayer,
tischgebetVor: t.grace_before_meals,
tischgebetNach: t.grace_after_meals
});
// TODO: Add categories: 'meal' (Tischgebete/Meal) and 'morning_evening' (Morgen-/Abendgebete/Morning & Evening)
// when corresponding prayers are added to the collection
// TODO: Add 'morning_evening' (Morgen-/Abendgebete/Morning & Evening)
// category when corresponding prayers are added to the collection
const categories = [
{ id: 'essential', de: 'Grundgebete', en: 'Essential', la: 'Fundamentales' },
{ id: 'marian', de: 'Marianisch', en: 'Marian', la: 'Mariana' },
{ id: 'saints', de: 'Heilige', en: 'Saints', la: 'Sancti' },
{ id: 'eucharistic', de: 'Eucharistie', en: 'Eucharistic', la: 'Eucharistica' },
{ id: 'meal', de: 'Tischgebete', en: 'Meal', la: 'Mensae' },
{ id: 'praise', de: 'Lobpreis', en: 'Praise', la: 'Laudatio' },
{ id: 'penitential', de: 'Busse', en: 'Penitential', la: 'Paenitentialia' },
];
@@ -115,6 +128,12 @@
animachristi: ['eucharistic'],
prayerbeforeacrucifix: ['eucharistic', 'penitential'],
postcommunio: ['eucharistic'],
jungfrauMutter: ['marian'],
oMyQueen: ['marian'],
memorare: ['marian'],
hilfMaria: ['marian'],
tischgebetVor: ['meal'],
tischgebetNach: ['meal'],
};
// svelte-ignore state_referenced_locally
@@ -170,7 +189,13 @@
{ id: 'reginaCaeli', searchTerms: ['regina caeli', 'regina coeli', 'himmelskönigin', 'queen of heaven'], slug: 'regina-caeli' },
{ id: 'animachristi', searchTerms: ['anima christi', 'seele christi', 'soul of christ'], slug: 'anima-christi' },
{ id: 'prayerbeforeacrucifix', searchTerms: ['kruzifix', 'crucifix', 'kreuz', 'cross', 'en ego'], slug: isEnglish ? 'prayer-before-a-crucifix' : 'gebet-vor-einem-kruzifix' },
{ id: 'postcommunio', searchTerms: ['postcommunio', 'nachkommunion', 'kommunion', 'communion'], slug: 'postcommunio' }
{ id: 'postcommunio', searchTerms: ['postcommunio', 'nachkommunion', 'kommunion', 'communion'], slug: 'postcommunio' },
{ id: 'jungfrauMutter', searchTerms: ['jungfrau mutter gottes', 'maria hilft immer', 'virgin mother of god'], slug: isEnglish ? 'virgin-mother-of-god' : 'jungfrau-mutter-gottes-mein' },
{ id: 'oMyQueen', searchTerms: ['o domina mea', 'gebieterin', 'o my queen', 'queen mother'], slug: isEnglish ? 'o-my-queen' : 'o-meine-gebieterin' },
{ id: 'memorare', searchTerms: ['memorare', 'gedenke', 'remember o most gracious', 'bernard'], slug: isEnglish ? 'memorare' : 'gedenke-o-guetigste-jungfrau-maria' },
{ id: 'hilfMaria', searchTerms: ['hilf maria', 'help mary', 'mutter der barmherzigkeit'], slug: isEnglish ? 'help-mary' : 'hilf-maria-es-ist-zeit' },
{ id: 'tischgebetVor', searchTerms: ['tischgebet vor', 'grace before meals', 'benedic domine', 'aller augen', 'komm herr jesus'], slug: isEnglish ? 'grace-before-meals' : 'tischgebet-vor-dem-essen' },
{ id: 'tischgebetNach', searchTerms: ['tischgebet nach', 'grace after meals', 'agimus tibi gratias', 'wir danken dir'], slug: isEnglish ? 'grace-after-meals' : 'tischgebet-nach-dem-essen' }
]);
// Base URL for prayer links
@@ -202,7 +227,13 @@
reginaCaeli: labels.reginaCaeli,
animachristi: labels.animachristi,
prayerbeforeacrucifix: labels.prayerbeforeacrucifix,
postcommunio: labels.postcommunio
postcommunio: labels.postcommunio,
jungfrauMutter: labels.jungfrauMutter,
oMyQueen: labels.oMyQueen,
memorare: labels.memorare,
hilfMaria: labels.hilfMaria,
tischgebetVor: labels.tischgebetVor,
tischgebetNach: labels.tischgebetNach
};
return /** @type {Record<string, string>} */(nameMap)[id] || id;
}
@@ -325,7 +356,13 @@
reginaCaeli: { bilingue: true },
animachristi: { bilingue: true },
prayerbeforeacrucifix: { bilingue: true },
postcommunio: { bilingue: true }
postcommunio: { bilingue: true },
jungfrauMutter: { bilingue: false },
oMyQueen: { bilingue: true },
memorare: { bilingue: true },
hilfMaria: { bilingue: false },
tischgebetVor: { bilingue: true },
tischgebetNach: { bilingue: true }
};
const isEastertide = $derived(checkEastertide());
@@ -471,15 +508,23 @@ h1{
:global(:root[data-theme="light"]) .postcommunio-section {
background-color: var(--nord5);
}
/* Seasonal badge */
/* Anchor for the absolute-positioned seasonal-badge below */
:global(.prayer-wrapper .gebet_wrapper) {
position: relative;
}
/* Seasonal badge — pinned top-right of the prayer card to match the
placement of the same badge on the rosary mystery cards */
.seasonal-badge {
display: inline-block;
margin-top: 0.5em;
padding: 0.2em 0.7em;
position: absolute;
top: 0.75rem;
right: 0.75rem;
padding: 0.25em 0.7em;
font-size: 0.75em;
border-radius: 999px;
font-weight: 700;
border-radius: var(--radius-sm);
background-color: var(--nord14);
color: var(--nord0);
z-index: 1;
}
/* Search is hidden without JS */
@@ -575,6 +620,18 @@ h1{
<Gloria intro={true} />
{:else if prayer.id === 'postcommunio'}
<Postcommunio onlyIntro={true} />
{:else if prayer.id === 'jungfrauMutter'}
<JungfrauMutterGottes />
{:else if prayer.id === 'oMyQueen'}
<ODominaMea />
{:else if prayer.id === 'memorare'}
<Memorare />
{:else if prayer.id === 'hilfMaria'}
<HilfMaria />
{:else if prayer.id === 'tischgebetVor'}
<TischgebetVor />
{:else if prayer.id === 'tischgebetNach'}
<TischgebetNach />
{/if}
{#if prayer.id === 'reginaCaeli' && isEastertide}
<span class="seasonal-badge">{t.eastertide_badge}</span>
@@ -22,6 +22,12 @@
import TantumErgo from "$lib/components/faith/prayers/TantumErgo.svelte";
import AngelusComponent from "$lib/components/faith/prayers/Angelus.svelte";
import ReginaCaeli from "$lib/components/faith/prayers/ReginaCaeli.svelte";
import JungfrauMutterGottes from "$lib/components/faith/prayers/JungfrauMutterGottes.svelte";
import ODominaMea from "$lib/components/faith/prayers/ODominaMea.svelte";
import Memorare from "$lib/components/faith/prayers/Memorare.svelte";
import HilfMaria from "$lib/components/faith/prayers/HilfMaria.svelte";
import TischgebetVor from "$lib/components/faith/prayers/TischgebetVor.svelte";
import TischgebetNach from "$lib/components/faith/prayers/TischgebetNach.svelte";
import StickyImage from "$lib/components/faith/StickyImage.svelte";
import AngelusStreakCounter from "$lib/components/faith/AngelusStreakCounter.svelte";
import { m, faithSlugFromLang, prayersSlug } from '$lib/js/faithI18n';
@@ -79,7 +85,19 @@
'apostles-creed': { id: 'apostlesCreed', name: t.apostles_creed, bilingue: true },
'tantum-ergo': { id: 'tantumErgo', name: 'Tantum Ergo', bilingue: true },
'angelus': { id: 'angelus', name: 'Angelus', bilingue: true },
'regina-caeli': { id: 'reginaCaeli', name: 'Regína Cæli', bilingue: true }
'regina-caeli': { id: 'reginaCaeli', name: 'Regína Cæli', bilingue: true },
'jungfrau-mutter-gottes-mein': { id: 'jungfrauMutter', name: t.jungfrau_mutter_prayer, bilingue: false },
'virgin-mother-of-god': { id: 'jungfrauMutter', name: t.jungfrau_mutter_prayer, bilingue: false },
'o-meine-gebieterin': { id: 'oMyQueen', name: t.o_my_queen_prayer, bilingue: true },
'o-my-queen': { id: 'oMyQueen', name: t.o_my_queen_prayer, bilingue: true },
'gedenke-o-guetigste-jungfrau-maria': { id: 'memorare', name: t.memorare_prayer, bilingue: true },
'memorare': { id: 'memorare', name: t.memorare_prayer, bilingue: true },
'hilf-maria-es-ist-zeit': { id: 'hilfMaria', name: t.hilf_maria_prayer, bilingue: false },
'help-mary': { id: 'hilfMaria', name: t.hilf_maria_prayer, bilingue: false },
'tischgebet-vor-dem-essen': { id: 'tischgebetVor', name: t.grace_before_meals, bilingue: true },
'grace-before-meals': { id: 'tischgebetVor', name: t.grace_before_meals, bilingue: true },
'tischgebet-nach-dem-essen': { id: 'tischgebetNach', name: t.grace_after_meals, bilingue: true },
'grace-after-meals': { id: 'tischgebetNach', name: t.grace_after_meals, bilingue: true }
});
const prayer = $derived(/** @type {Record<string, {id: string, name: string, bilingue: boolean}>} */(prayerDefs)[data.prayer]);
@@ -184,7 +202,7 @@ h1 {
<h1>{prayerName}</h1>
{#if !isLatin}
{#if !isLatin && isBilingue}
<div class="toggle-controls">
<LanguageToggle
initialLatin={data.initialLatin}
@@ -225,7 +243,7 @@ h1 {
<div class="container">
<h1>{prayerName}</h1>
{#if !isLatin}
{#if !isLatin && isBilingue}
<div class="toggle-controls">
<LanguageToggle
initialLatin={data.initialLatin}
@@ -279,6 +297,18 @@ h1 {
<AngelusComponent verbose={true} />
{:else if prayerId === 'reginaCaeli'}
<ReginaCaeli />
{:else if prayerId === 'jungfrauMutter'}
<JungfrauMutterGottes />
{:else if prayerId === 'oMyQueen'}
<ODominaMea />
{:else if prayerId === 'memorare'}
<Memorare />
{:else if prayerId === 'hilfMaria'}
<HilfMaria />
{:else if prayerId === 'tischgebetVor'}
<TischgebetVor />
{:else if prayerId === 'tischgebetNach'}
<TischgebetNach />
{/if}
</div>
</div>
@@ -187,8 +187,11 @@
}
.season-eastertide .season-badge {
background: var(--color-bg-elevated);
color: var(--color-text-primary);
/* Liturgical white — fixed across themes so the badge stays white in
dark mode (was rendering as muted grey via --color-bg-elevated).
Pure #ffffff also pops harder against the off-white page bg in light. */
background: #ffffff;
color: var(--nord0);
}
</style>
@@ -45,7 +45,6 @@ onNavigate((navigation) => {
});
import UserHeader from '$lib/components/UserHeader.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import OfflineSyncIndicator from '$lib/components/OfflineSyncIndicator.svelte';
import BookOpen from '@lucide/svelte/icons/book-open';
import Heart from '@lucide/svelte/icons/heart';
import Leaf from '@lucide/svelte/icons/leaf';
@@ -134,12 +133,6 @@ const recipeCanonicalPath = $derived(recipeAltPath(page.url.pathname, /** @type
<LanguageSelector lang={data.lang} />
{/snippet}
{#snippet logo_overlay()}
<div class="logo-pip">
<OfflineSyncIndicator lang={data.lang} />
</div>
{/snippet}
{#snippet right_side()}
<UserHeader {user} recipeLang={data.recipeLang} lang={data.lang}></UserHeader>
{/snippet}
@@ -147,12 +140,3 @@ const recipeCanonicalPath = $derived(recipeAltPath(page.url.pathname, /** @type
{@render children()}
</Header>
<style>
:global(.logo-pip) {
position: absolute;
top: -8px;
right: -7px;
z-index: 2;
pointer-events: auto;
}
</style>
@@ -4,7 +4,6 @@
import AddButton from '$lib/components/AddButton.svelte';
import Seo from '$lib/components/Seo.svelte';
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
import OfflineSyncBanner from '$lib/components/OfflineSyncBanner.svelte';
import Search from '$lib/components/recipes/Search.svelte';
import { getCategories } from '$lib/js/categories';
import { m, type RecipesLang } from '$lib/js/recipesI18n';
@@ -315,19 +314,6 @@
z-index: 10;
}
/* ─── Offline sync banner — between search and recipe grid ─── */
.banner-wrap {
max-width: 1200px;
margin: 0 auto;
padding: 0 2em;
position: relative;
z-index: 9;
}
.banner-wrap.fallback {
padding: 0 2em;
margin: 0 auto;
}
.sentinel {
height: 1px;
}
@@ -442,9 +428,6 @@
<div class="hero-search-wrap">
<Search lang={data.lang} recipes={data.all_brief} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
</div>
<div class="banner-wrap">
<OfflineSyncBanner lang={data.lang} />
</div>
<div class="recipe-grid">
{#each visibleRecipes as recipe, i (recipe._id)}
<CompactCard
@@ -472,9 +455,6 @@
<h1>{labels.title}</h1>
<p class="subheading">{labels.subheading}</p>
</div>
<div class="banner-wrap fallback">
<OfflineSyncBanner lang={data.lang} />
</div>
<Search lang={data.lang} recipes={data.all_brief} isLoggedIn={!!data.session?.user} onSearchResults={handleSearchResults}></Search>
<div class="recipe-grid">