152 Commits

Author SHA1 Message Date
b255fc01e9 recipes: use emoji font for favorites button
Some checks failed
CI / update (push) Failing after 20s
2026-02-18 21:06:51 +01:00
b90a42b1aa recipes: add shared "to try" list for external recipes
Some checks failed
CI / update (push) Failing after 20s
Household-shared list of external recipes to try, with name, multiple
links, and optional notes. Includes add/edit/delete with confirmation.
Linked from the favorites page via a styled pill button.
2026-02-18 21:01:24 +01:00
7ba0995bf8 recipes: hide image-wrap background color during view transition morph
Some checks failed
CI / update (push) Failing after 21s
2026-02-18 20:37:22 +01:00
9177164ddf recipes: hero image view transition, skip transitions for recipe-to-recipe
Some checks failed
CI / update (push) Failing after 20s
2026-02-18 10:07:42 +01:00
207efcc38e recipes: view transitions for recipe detail navigation
All checks were successful
CI / update (push) Successful in 1m31s
Image morphs between CompactCard thumbnail and hero, title block
slides up from bottom, header persists across transitions. Only
activates for recipe detail navigations, not between list pages.
2026-02-17 18:59:24 +01:00
f074c0af08 recipes: drop opacity transition from TitleImgParallax hero image
All checks were successful
CI / update (push) Successful in 1m30s
Remove the opacity 0→1 fade-in transition — it's annoying when the
image is already cached. The dominant color background handles the
loading state, so no transition needed.
2026-02-17 18:34:58 +01:00
d0a01a75e7 recipes: sharpen Gaussian kernel for dominant color extraction
All checks were successful
CI / update (push) Successful in 1m36s
Reduce sigma from 0.3 to 0.15 * dimension so edge pixels contribute
under 1% weight, heavily biasing the color toward the image center.
2026-02-17 18:25:24 +01:00
53da9ad26d recipes: replace placeholder images with OKLAB dominant color backgrounds
Instead of generating/serving 20px placeholder images with blur CSS, extract
a perceptually accurate dominant color (Gaussian-weighted OKLAB average) and
use it as a solid background-color while the full image loads. Removes
placeholder image generation, blur CSS/JS, and placeholder directory references
across upload flows, API routes, service worker, and all card/hero components.
Adds admin bulk tool to backfill colors for existing recipes.
2026-02-17 18:25:17 +01:00
0ea09e424e recipes: two-column card grid on mobile, compact card sizing
All checks were successful
CI / update (push) Successful in 1m29s
2026-02-17 16:11:57 +01:00
716c6cc6e6 fix: use python3 for emoji codepoint extraction in font subsetting
All checks were successful
CI / update (push) Successful in 1m39s
grep -oP '.' splits multi-byte emoji into individual bytes when the
locale is not UTF-8 (e.g. CI runners with LANG=C), causing pyftsubset
to fail on invalid codepoints.
2026-02-17 16:05:55 +01:00
eeb3030186 fix: emoji font on recipe hero link, orange OR toggle for better contrast
All checks were successful
CI / update (push) Successful in 8s
2026-02-17 16:02:22 +01:00
16d891fc2f fix: render desktop nav at all widths when no links, fix profile menu positioning
All checks were successful
CI / update (push) Successful in 8s
Skip mobile sidebar/hamburger entirely when no links snippet is provided.
The nav with .no-links class stays in desktop layout at all screen widths.
Override UserHeader mobile styles from .no-links context to keep dropdown
opening downward with tail centered below the profile picture.
2026-02-17 15:59:13 +01:00
cf73e6b62f fix: language selector speech bubble, profile menu on mobile, hide redundant hamburger
All checks were successful
CI / update (push) Successful in 8s
- LanguageSelector: add speech bubble tail, replace green active with
  nord8 blue + dark text, remove floating gap
- Header: hide hamburger menu on mobile when no links, show profile
  picture directly in top bar instead
- UserHeader: center mobile dropdown, fix tail color/position, add
  profile picture overlay to tuck tail behind, add drop shadow
- Main layout: stop passing empty links snippet
2026-02-17 13:22:20 +01:00
8db7ca6bcc fix: LinksGrid lock icons use muted color, shrink on mobile, keep images larger
All checks were successful
CI / update (push) Successful in 9s
Decouple lock-icon fill from nth-child color cycling via :not(.lock-icon),
use subtle --nord3 fill in both themes, add responsive lock sizing, and
bump mobile image heights (72→90px, 48→64px).
2026-02-17 13:06:52 +01:00
13fd2143d9 recipes: compact tag/category pills with fluid scaling, add tag search
All checks were successful
CI / update (push) Successful in 8s
Shrink TagBall font/padding and TagCloud gap using clamp() for
fluid sizing across viewports. Add search input on the tags page
to filter through keywords.
2026-02-17 13:01:12 +01:00
a074fdc7e3 refactor: slimmer header, JS-less hamburger menu, bottom-aligned mobile nav
All checks were successful
CI / update (push) Successful in 9s
- Reduce header height to 3rem with CSS variable --header-h
- Scale logo via --symbol-size variable, decrease nav link font sizes
- Replace JS-driven sidebar toggle with checkbox hack (:has selector)
- Separate drop shadow into own element for correct z-index layering
  (top bar > sidebar > shadow)
- Bottom-align mobile nav links via ::before flex spacer
- Slide-in transition scoped to :has(:checked) to prevent resize artifacts
2026-02-17 10:32:02 +01:00
dbf6744479 fix: footer hidden behind recipe hero parallax section
The hero-section's scaleY transform created a stacking context that
painted over the footer, and margin-bottom: -20vh over-compensated
for the parallax gap, pulling the footer into the recipe cards.
Derive margin-bottom from actual parallax parameters and make the
footer position: relative so it paints above the transform layer.
2026-02-17 09:03:15 +01:00
28057e88d5 fix: LinksGrid shows 2 columns on mobile, scale down icons/text
Use CSS min() in grid minmax to guarantee 2 tiles side-by-side at
any viewport width. Add responsive breakpoints (560px, 410px) to
progressively shrink SVG height, font size, and spacing.
2026-02-17 08:44:04 +01:00
e58c8e46ef fonts: consolidate font-family to global stack, self-host subset emoji font
All checks were successful
CI / update (push) Successful in 8s
Remove redundant `font-family: sans-serif` from 18 component-level
declarations — they now inherit the Helvetica/Arial/Noto Sans stack
from the global `*` selector in app.css.

Add self-hosted NotoColorEmoji subset (56 KB, down from 11 MB) as
fallback for systems without the Noto Color Emoji font installed.
The subset is generated at prebuild time via pyftsubset with a fixed
list of the ~32 emojis actually used on the site.
2026-02-16 21:34:12 +01:00
2024551e0e fix: remove build warnings (unused CSS, a11y labels, npmrc)
All checks were successful
CI / update (push) Successful in 1m30s
- settings: remove unused form p/h4 CSS selectors
- prayers: remove unused .postcommunio-section/links CSS selectors
- RosarySvg: add aria-label to all bead hitbox anchors
- .npmrc: remove pnpm-only resolution-mode setting
2026-02-16 18:54:10 +01:00
c53aee7123 recipes: replace Card with CompactCard + CSS grid on all sub-pages
Migrate all recipe sub-pages from the old fixed-size Card component
inside flex-wrap Recipes wrapper to CompactCard with responsive CSS
grid for visual consistency with the main recipes page.
2026-02-16 18:47:12 +01:00
7e94758b23 recipes: CompactCard with larger icon and anchor
All checks were successful
CI / update (push) Successful in 1m31s
2026-02-16 17:51:31 +01:00
10dd3158fe recipes: filter panel does not create page overflow
All checks were successful
CI / update (push) Successful in 1m29s
2026-02-16 15:23:02 +01:00
7922563c4d fix: prevent hero image flash by aligning server/client random seed
All checks were successful
CI / update (push) Successful in 1m28s
Generate heroIndex on the server and pass it to the client so SSR and
hydration pick the same hero recipe, eliminating the image swap on
first interaction.
2026-02-16 14:43:37 +01:00
c855cdd25c recipes: fix compact card tag styling and add filter placeholder for CLS
All checks were successful
CI / update (push) Successful in 1m30s
- Fix g-tag dark mode hover text disappearing (explicit background-color)
- Scope compact card tag styles to avoid global/scoped CSS flash on load
- Add placeholder div to prevent layout shift when FilterPanel hydrates
- Improve LogicModeToggle contrast in light mode (nord4 → nord3/nord1)
- Bump compact card recipe name font-size to 1.1rem
2026-02-16 14:30:32 +01:00
f900d4217d recipes: Swissmilk-inspired hero redesign with parallax and card refresh
- Full-bleed hero image with CSS parallax (scaleY technique matching TitleImgParallax)
- Hero picks random seasonal recipe with hashed image on each visit
- Left-aligned title, subheading, and featured recipe link over the hero
- Category chips with ellipsis collapse on small screens (<600px)
- Search bar anchored to hero/grid boundary regardless of chip count
- CompactCard redesign: 3/2 aspect ratio, rounded corners, subtle hover zoom
- Search component margin adjusted to sit flush at hero boundary
2026-02-16 13:53:52 +01:00
0e9daf296d css: replace hardcoded values with design tokens
All checks were successful
CI / update (push) Successful in 1m34s
Replace 30 border-radius: 1000px → var(--radius-pill), 6 border-radius:
20px → var(--radius-card), 21 transition: 100ms → var(--transition-fast),
and 32 transition: 200ms → var(--transition-normal) across the codebase.
2026-02-16 09:45:56 +01:00
4191012cf1 css: consolidate stylesheets into single source of truth
All checks were successful
CI / update (push) Successful in 1m29s
Merge nordtheme.css tokens and utility classes into app.css, import
app.css once in root layout, delete redundant files (nordtheme.css,
form.css, rosenkranz.css), move domain CSS to layouts, fix broken
shake keyframe in action_button.css, and scope form styles to the
two pages that need them. 10 CSS files → 6, 41 redundant imports removed.
2026-02-15 22:26:27 +01:00
a435a1142f fix: streak counter showing zero on second device due to stale localStorage
All checks were successful
CI / update (push) Successful in 1m30s
The RosaryStreakStore singleton survives client-side navigation but
the first mount. Also reorder onMount to merge server data before
assigning to streak, preventing a frame of stale localStorage values.
2026-02-15 22:06:36 +01:00
8c984f3064 rosary: update flagellation mystery image to rubens
All checks were successful
CI / update (push) Successful in 1m33s
2026-02-14 21:30:45 +01:00
044fddd1c9 faith: remove angelus from header
All checks were successful
CI / update (push) Successful in 1m30s
2026-02-14 11:55:15 +01:00
db28629c7d rosenkranz: von "Heute" -> "Wochentag" gewechselt
All checks were successful
CI / update (push) Successful in 1m28s
2026-02-13 19:24:03 +01:00
0e0af55ce7 prayers: add plenary indulgence explanation to postcommunio
All checks were successful
CI / update (push) Successful in 1m29s
2026-02-13 15:34:20 +01:00
601e2f6513 prayers: add 5 new prayers, move Angelus route, liturgical seasons
Add Guardian Angel, Apostles' Creed, Tantum Ergo, Angelus, and Regina
Caeli to the prayers collection. Move standalone Angelus route into the
prayers system with a 301 redirect from the old path. Extract Easter
computation into shared utility ($lib/js/easter.svelte.ts) and use it
for liturgical season awareness: during Eastertide the rosary defaults
to Glorious mysteries and swaps Salve Regina for Regina Caeli; during
Lent it defaults to Sorrowful mysteries. Seasonal badges shown on both
the mystery selector and prayer sections.
2026-02-13 14:56:12 +01:00
aa64cd8306 rosary: make scroll-to-top button work without JS
All checks were successful
CI / update (push) Successful in 1m27s
Replace <button> with <a href="#top"> anchor link. JS intercepts for
smooth scrolling, no-JS falls back to hash navigation.
2026-02-13 13:18:38 +01:00
96a91ed8dd rosary: progressive enhancement for no-JS browsers
All checks were successful
CI / update (push) Successful in 1m33s
SVG beads are now anchor links to prayer sections, with CSS :has(:target)
highlighting the active bead. Inline mystery images render in each decade
by default and hide when JS takes over. StreakCounter uses a form action
fallback for logged-in users and hides entirely for anonymous no-JS users.
Show images toggle now works via ?images= URL param like the other toggles.
2026-02-13 13:04:11 +01:00
a0146927b6 prayers: fix minor issues
All checks were successful
CI / update (push) Successful in 1m28s
2026-02-12 21:17:07 +01:00
443e3300a1 fix: sync payments page state on URL param changes
All checks were successful
CI / update (push) Successful in 1m26s
$state() only captured initial data prop values, so navigating to
different offset/limit params always showed the first page results.
2026-02-12 17:44:58 +01:00
2f711c66b0 rosary: snap mystery images instantly at edges
All checks were successful
CI / update (push) Successful in 1m27s
When jumping to the top or bottom of the rosary, snap the mystery
image column instantly instead of smooth scrolling.
2026-02-12 17:37:30 +01:00
7901d56b5b rosary: prevent manual scrolling on mystery image column
All checks were successful
CI / update (push) Successful in 1m29s
Change overflow-y from auto to hidden so the image column only
scrolls programmatically in sync with prayers, not via user input.
2026-02-12 16:48:34 +01:00
2c364ed351 rosary: remove scroll polyfill and optimize SVGs
All checks were successful
CI / update (push) Successful in 1m32s
Drop the smooth-scroll polyfill (now universally supported) and run
SVGO on benedictus.svg (35KB→19KB) and the cross glyph path.
2026-02-12 08:32:03 +01:00
904c5c0df0 rosary: only show embers after 14 days, not flame
All checks were successful
CI / update (push) Successful in 1m29s
2026-02-11 20:43:12 +01:00
091c23a0bd refactor: extract sub-components and modules from rosary +page.svelte
All checks were successful
CI / update (push) Successful in 3m9s
Break the 1889-line rosary page into focused modules:
- rosaryData.js: mystery data, labels, weekday schedule, SVG positions
- rosaryScrollSync.js: bidirectional scroll sync (prayers ↔ SVG ↔ images)
- RosarySvg.svelte: SVG bead visualization
- MysterySelector.svelte: mystery picker grid
- MysteryImageColumn.svelte: desktop image column

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:33:37 +01:00
7bdc62489c add forgotten routes in to build
All checks were successful
CI / update (push) Successful in 1m28s
2026-02-11 10:19:11 +01:00
01ccc705ee add Douay-Rheims Bible to the project for english bible references
All checks were successful
CI / update (push) Successful in 19s
2026-02-11 10:15:54 +01:00
35ea60e637 refactor: replace all any types in translation.ts with proper interfaces 2026-02-11 09:55:07 +01:00
ac4c00a082 refactor: merge api/recipes and api/rezepte into unified recipeLang route
Consolidate duplicate recipe API routes into a single
api/[recipeLang=recipeLang]/ structure. Both /api/recipes/ and
/api/rezepte/ URLs continue to work via the param matcher. Shared
read endpoints now serve both languages with caching for both.

Also: remove dead code (5 unused components, cookie.js) and the
redundant cron-execute recurring payment route.
2026-02-11 09:49:50 +01:00
8560077759 refactor: reorganize components into domain subfolders and replace relative imports
Move components from flat src/lib/components/ into recipes/, faith/, and
cospend/ subdirectories. Replace ~144 relative imports across API routes
and lib files with $models, $utils, $types, and $lib aliases. Add $types
alias to svelte.config.js. Remove unused EditRecipe.svelte.
2026-02-11 09:49:11 +01:00
b3c3f34e50 refactor: extract PipImage component from inline PiP markup
Deduplicates mobile PiP image code shared between the rosary page and
StickyImage. Adds fullscreen support to StickyImage and fixes hidden PiP
elements capturing pointer events via pointer-events: none default.
2026-02-10 21:16:05 +01:00
6eaf0bb4f4 rosary: add mystery images for all four mystery types with PiP fullscreen
All checks were successful
CI / update (push) Successful in 1m29s
Generalize mystery images from sorrowful-only to all mystery types (joyful,
sorrowful, glorious, luminous). Add PiP fullscreen mode with tap-to-show
controls and double-tap to toggle enlarged/fullscreen.
2026-02-09 23:02:34 +01:00
07554f16df rosary: fix mystery image column clipping at edges
All checks were successful
CI / update (push) Successful in 1m26s
Use taller edge pads (100vh) for before/after targets so images don't
peek at viewport top or bottom. Scroll to edge pads with zero offset
so previous/next images hide fully behind the sticky header.
2026-02-09 15:18:14 +01:00
bf3014337e fix: PiP show/hide on resize and skip observer at page top
All checks were successful
CI / update (push) Successful in 1m28s
- Resize handler calls pip.show()/hide() instead of just reposition(),
  fixing PiP not appearing when resizing from desktop to mobile
- IntersectionObserver skips all updates when scrollY < 50, preventing
  stale activeSection from re-scrolling SVG after jump-to-top
2026-02-09 14:32:39 +01:00
6182b8f943 rosary: add all 5 sorrowful mystery images with artist captions
- One image per mystery (garden, flagellation, mocking, carry, crucifixion)
- Desktop: figcaption with artist, title (translated DE/EN), and year
- Fix Map.has vs `in` operator bug preventing PiP from showing
- Reposition PiP on image load to prevent off-screen positioning
- Mystery image column clips behind header (top: 0 + padding-top: 6rem)
- Snap SVG and images instantly to top; reset activeSection to cross
2026-02-09 14:16:28 +01:00
8246906a76 fix: make hasLoadedFromStorage reactive so localStorage saves trigger
All checks were successful
CI / update (push) Successful in 1m28s
2026-02-09 09:18:17 +01:00
a5e119f976 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
2026-02-09 09:12:34 +01:00
a4738134fe extract PiP drag/snap/enlarge logic into shared createPip() utility
All checks were successful
CI / update (push) Successful in 1m32s
Both StickyImage and rosary page now use the same pip.svelte.ts factory
for mobile drag-to-corner, snap, and double-tap enlarge behavior.
2026-02-09 08:48:20 +01:00
6433576b28 rosary: add mystery images with scrollable sticky column and mobile PiP
Add a third grid column for sorrowful mystery images (mocking for
mysteries 2-3, crucifixion for mystery 5). Desktop uses a scrollable
sticky sidebar synced to prayer scroll position. Mobile shows a
floating PiP thumbnail. Extract prayer page PiP logic into reusable
StickyImage component.
2026-02-09 08:47:53 +01:00
ea6d2cab5c ablassgebete: add prayer page with sticky crucifix and draggable PiP
All checks were successful
CI / update (push) Successful in 1m31s
- Add AblassGebete component and crucifix images
- Desktop: sticky crucifix on right, centered prayer with balanced spacing
- Mobile: draggable picture-in-picture thumbnail that snaps to corners
- Double-tap to enlarge/shrink with directional animation
- Monolingual sections override bilingual grey styling
- 404 for English route /faith/prayers/ablassgebete (German only)
- Reposition on window resize including desktop/mobile breakpoint crossing
2026-02-08 22:13:23 +01:00
cda8fe0885 rosary: show broken streak reset on page visit, not only after clicking "prayed"
All checks were successful
CI / update (push) Successful in 1m37s
2026-02-05 20:47:19 +01:00
83de5fed34 cospend: filter recent activity by chart category selection
All checks were successful
CI / update (push) Successful in 1m27s
Clicking a category on the bar chart now filters the recent activity
list to show only payments in that category. Includes a clear filter
button and empty state message. Also increases recent splits from 10
to 30 for better coverage when filtering.
2026-02-04 16:57:49 +01:00
8776ab894b fix: remove Svelte 4 object reassignment causing $effect infinite loop
The splitAmounts = { ...splitAmounts } pattern created a circular
dependency inside $effect blocks—reading and writing the same reactive
value—which Svelte 5 killed via loop protection, leaving the split
method selector non-reactive when selecting "50/50 + personal".
2026-02-04 16:52:12 +01:00
7d6a80442a faith: progressive enhancement for all faith pages without JS
All checks were successful
CI / update (push) Successful in 1m29s
- Rosary: mystery selection, luminous toggle, and latin toggle fall back
  to URL params (?mystery=, ?luminous=, ?latin=) for no-JS navigation
- Prayers/Angelus: latin toggle uses URL param fallback
- Search on prayers page hidden without JS (requires DOM queries)
- Toggle component supports href prop for link-based no-JS self-submit
- LanguageSelector uses <a> links with computed paths and :focus-within
  dropdown for no-JS; displays correct language via server-provided prop
- Recipe language links use translated slugs from $page.data
- URL params cleaned via replaceState after hydration to avoid clutter
2026-02-04 14:14:13 +01:00
1c100a4534 fix: use accent-dark with nord5 light override for prayer backgrounds
All checks were successful
CI / update (push) Successful in 1m32s
var(--color-bg-secondary) from app.css is not available since app.css
is never imported. Use var(--accent-dark) from nordtheme.css with
explicit light mode overrides using var(--nord5).
2026-02-04 13:05:08 +01:00
d65886b4e7 fix: use semantic color for prayer section backgrounds in light mode
All checks were successful
CI / update (push) Successful in 1m27s
Replace var(--accent-dark) with var(--color-bg-secondary) which maps
to the correct color in both modes, removing dead @media overrides
that referenced the undefined var(--accent-light). Also match rosary
cross fill to Benedictus medal color in light mode.
2026-02-04 13:01:54 +01:00
9da0a2740d rosary: inline cross glyph as SVG path for consistent rendering
All checks were successful
CI / update (push) Successful in 1m27s
Replace <text> elements using the crosses web font with inlined SVG
paths extracted from the font file. Web fonts in SVG <text> elements
don't load reliably on Android, causing fallback rendering.
2026-02-04 12:09:00 +01:00
7411160a23 rosary: derive SVG bead positions from sectionPositions dictionary
Use sectionPositions as single source of truth for all bead coordinates.
Compute transition bead positions as midpoints between decades, generate
decade beads and hitboxes via loops, and adjust bead spacing.
2026-02-04 12:04:08 +01:00
767b43e2ff 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.
2026-02-04 11:18:54 +01:00
3831cd17de rosary: progressively shrink mystery selectors instead of stacking on small screens
All checks were successful
CI / update (push) Successful in 1m32s
2026-02-04 10:03:58 +01:00
7954d57cf1 rosary: reduce bundle size and improve responsive layout
All checks were successful
CI / update (push) Successful in 1m28s
Remove redundant CSS already handled by Prayer.svelte, drop unused
rosenkranz.css import, and replace inline BenedictusMedal component
(34KB, ~52 DOM elements) with a static SVG referenced via <image>.
Use fluid sidebar width (clamp) for smoother desktop/mobile transition.
2026-02-03 20:51:32 +01:00
14f54e6623 rosary: fix mystery selection lost when excluding luminous
All checks were successful
CI / update (push) Successful in 1m32s
The on:change handler on the Toggle component was silently ignored
since Toggle is a Svelte 5 component that doesn't support the Svelte 4
event directive. Replace with a reactive $effect that reverts to
today's mystery when luminous is excluded while selected.
2026-02-03 15:01:53 +01:00
e8b0fb7d9d rosary: fix today badge z-index
All checks were successful
CI / update (push) Successful in 1m27s
2026-02-03 14:48:47 +01:00
b28154564f perf: pre-generate Bible verse data and reduce DOM via conditional rendering
All checks were successful
CI / update (push) Successful in 1m34s
- Extract Bible lookup logic into shared src/lib/server/bible.ts module
- Add build script to pre-generate all 20 mystery verse lookups as static data,
  eliminating runtime API calls on rosary page load
- Update Prayer.svelte to pass showLatin/urlLang as snippet parameters; all 14
  prayer components now conditionally render only visible language elements
  instead of hiding via CSS
- Extract 4 inline mystery selector SVGs into MysteryIcon.svelte component
- Remove unused CSS selectors from angelus page
2026-02-03 14:28:09 +01:00
3ff3d0dc8a fix: remove default figure margin shifting parallax hero image
All checks were successful
CI / update (push) Successful in 1m30s
2026-02-03 08:27:43 +01:00
c0eb49158c fix: apply border-radius directly on card images for older WebKit
All checks were successful
CI / update (push) Successful in 1m24s
2026-02-03 08:21:55 +01:00
43378bfec0 fix: center parallax hero image cross-browser
Replace Firefox-specific @supports hack with explicit absolute
centering (left/right/margin-inline: auto) on the image container.
2026-02-03 08:18:36 +01:00
650c061137 rosary: add SVG hitboxes for easier bead tapping on mobile 2026-02-03 08:14:50 +01:00
649bd19287 streak: replace burst flame with rising particle effect
All checks were successful
CI / update (push) Successful in 1m23s
Rework the burst mode in FireEffect to use 24 data-driven particles
instead of the old scale-and-pop flame. Each particle has unique
position, size, delay, and duration for an organic rising effect.
Latch burst state in StreakAura so the animation plays its full
duration regardless of when the parent resets the prop.
2026-02-03 08:01:00 +01:00
52ae9659b8 angelus: align styling with other faith routes
All checks were successful
CI / update (push) Successful in 1m22s
2026-02-02 23:13:02 +01:00
58a0496ff3 rosary: add missing saint Michael prayer
All checks were successful
CI / update (push) Successful in 1m22s
2026-02-02 23:08:20 +01:00
0d2e33d84d angelus: add missing Ave Maria
All checks were successful
CI / update (push) Successful in 1m23s
2026-02-02 22:59:11 +01:00
b94a6bbb87 prayers: add angelus link
All checks were successful
CI / update (push) Successful in 1m22s
2026-02-02 22:51:51 +01:00
660a1b0539 prayers: add search and individual prayer pages
All checks were successful
CI / update (push) Successful in 1m22s
- Add SearchInput component for reusable search UI
- Add search functionality to prayers list with two-tier results:
  - Primary matches (name/searchTerms) shown first
  - Secondary matches (text content) shown after with reduced opacity
- Add individual prayer pages with language-appropriate slugs
  (e.g., /glaube/gebete/ave-maria, /faith/prayers/hail-mary)
- Make prayer cards clickable to navigate to individual pages
- Fix language visibility for prayers without Latin (BruderKlaus, Joseph)
- Add Prayer wrapper to MichaelGebet for consistent styling
- Use CSS columns for masonry layout with dynamic reordering
2026-02-02 22:22:56 +01:00
8699bef209 move glaube higher up in main LinksGrid
All checks were successful
CI / update (push) Successful in 1m21s
2026-02-02 20:47:10 +01:00
95d3be8b82 prayers: bow-emphasis only for main language 2026-02-02 20:39:11 +01:00
82db2c9e50 angelus: cleaner and now in Header
All checks were successful
CI / update (push) Successful in 1m25s
2026-02-02 16:59:25 +01:00
69293c39f9 prayers: add English translations for all prayer components
Add official Catholic English translations to all prayer components
for /faith/* routes. Prayer names on /faith/prayers are now displayed
in English. Remove unused Angelus.svelte component.
2026-02-02 16:39:40 +01:00
1a5117e8d0 faith: add bilingual routes /glaube ↔ /faith
Add language toggle support for faith pages similar to recipes.
Routes now work in both German and English:
- /glaube ↔ /faith
- /glaube/gebete ↔ /faith/prayers
- /glaube/rosenkranz ↔ /faith/rosary
- /glaube/angelus ↔ /faith/angelus
2026-02-02 16:15:51 +01:00
87d5e9cbc0 rosary: clean up unused CSS, fix unclosed tag
All checks were successful
CI / update (push) Successful in 1m24s
- FireEffect now only contains fire-related styles
- StreakAura now only contains aura, number, halo, wing styles
- Fix unclosed <i> tags in JosephGebet.svelte
2026-02-01 14:12:18 +01:00
924ce386d5 rosary: server-side streak fetch, remove aggressive polling
All checks were successful
CI / update (push) Successful in 1m29s
- Fetch streak data in +page.server.ts for logged-in users via API
- Initialize store once with server data, sync only runs once
- Only poll for reconnection in PWA mode when offline with pending changes
- Extract FireEffect to separate component with burst animation
- Convert StreakAura/StreakCounter to Svelte 5 runes ($props, $state, $derived)
- Fix SSR flash by using server data for initial render
2026-02-01 13:50:33 +01:00
01fd067eda rosary: cleaner light mode
All checks were successful
CI / update (push) Successful in 1m20s
2026-01-31 16:12:18 +01:00
ee2bee6ac5 rosary: light/dark mode benedicturs medal 2026-01-31 16:09:21 +01:00
58fd2b89d2 rosary: fade-in shadow removed
All checks were successful
CI / update (push) Successful in 1m21s
2026-01-31 15:53:59 +01:00
d3f0d8a24c glaube: add ⚬ to mary and jesus in prayers
All checks were successful
CI / update (push) Successful in 1m21s
2026-01-31 15:15:49 +01:00
6028373093 glaube: gone woke
All checks were successful
CI / update (push) Successful in 1m27s
2026-01-31 14:59:07 +01:00
1cce6a56ac rosary: glow animation earlier, embers before full fire
All checks were successful
CI / update (push) Successful in 1m22s
2026-01-31 12:20:35 +01:00
71dd5b6285 rosary: stylized StreakCounter dependant on length
All checks were successful
CI / update (push) Successful in 1m31s
2026-01-31 10:56:29 +01:00
17b0fb9275 remove jukit garbage 2026-01-31 10:12:45 +01:00
45b0f5f2a6 rosary: less colourful mystery selector
All checks were successful
CI / update (push) Successful in 1m28s
2026-01-30 15:47:58 +01:00
92963f4451 rosary: StreakCounter singular Tag for length==1 2026-01-30 15:41:20 +01:00
7f4a8eec8b rosary: cleanup
All checks were successful
CI / update (push) Successful in 1m28s
2026-01-30 15:36:13 +01:00
daa801e14a fix logged in state broken on rosary due to prerendering
All checks were successful
CI / update (push) Successful in 1m21s
2026-01-30 12:47:28 +01:00
a58e52c891 feat: add server persistence for rosary streak
All checks were successful
CI / update (push) Successful in 1m21s
- Add RosaryStreak MongoDB model for logged-in users
- Add /api/glaube/rosary-streak GET/POST endpoints
- Sync streak to server when logged in, merge local/server data
- Auto-sync when coming back online (PWA offline support)
- Falls back to localStorage for guests
2026-01-30 12:37:01 +01:00
06477d656a feat: add streak counter to rosary page
Track daily rosary prayer streaks using localStorage. Shows consecutive
days prayed and disables button if already prayed today.
2026-01-30 12:36:55 +01:00
bff86041fd prayers: add Confiteor
All checks were successful
CI / update (push) Successful in 1m24s
2026-01-30 08:40:29 +01:00
53ad393541 refactor: reduce all_brief payload to first image's alt and mediapath
All checks were successful
CI / update (push) Successful in 1m20s
Only include the necessary image fields for Card.svelte instead of
the entire images array to reduce API response size.
2026-01-29 14:22:57 +01:00
408bc42784 ci: clear Redis recipe cache on deploy
All checks were successful
CI / update (push) Successful in 1m23s
Ensures fresh data is fetched after deployments when API projections
or data structures change.
2026-01-29 14:04:36 +01:00
308d9c6dac fix: include images in all_brief API endpoints
All checks were successful
CI / update (push) Successful in 1m23s
Card.svelte uses recipe.images[0].mediapath for the hashed image path,
but the all_brief endpoints weren't fetching the images field, causing
new recipes to fall back to short_name.webp instead of the correct path.
2026-01-29 13:40:33 +01:00
763edb35c3 fix: include images and translations in offline-db brief recipes
All checks were successful
CI / update (push) Successful in 1m26s
The offline sync wasn't caching thumbnails because the images field
was missing from the MongoDB projection. Also add translations for
caching English recipe __data.json URLs.
2026-01-29 10:18:23 +01:00
d9e9ae049e feat: add sync progress tracking with image download status
- Service worker reports image caching progress back to main thread
- Sync progress shows current phase (recipes, pages, data, images)
- Display progress bar for image downloads in sync tooltip
- Use mediapath for thumbnail URLs (with hash for cache busting)
- Serve cached thumbnails as fallback for full/placeholder when offline
2026-01-29 10:18:02 +01:00
86f28fa1b7 feat: auto-sync recipes and show sync button only in PWA mode
- Auto-sync recipes every 30 minutes when online in PWA mode
- Only show offline sync button when running as installed PWA
- Detect standalone mode via display-mode media query and iOS check
- Trigger initial sync on PWA install (appinstalled event)
- Listen for online event to sync when coming back online
- Store last sync time in localStorage to track sync intervals
2026-01-29 09:58:03 +01:00
c86a734da0 feat: extend PWA offline support to all recipe routes and glaube pages
- Add offline support for category, tag, icon list pages
- Add offline support for favorites page (stores locally for offline)
- Add offline support for season list page
- Cache root page and glaube pages for offline access
- Dynamically discover glaube routes at build time using Vite glob
- Add db functions for getAllCategories, getAllTags, getAllIcons
- Pre-cache __data.json for all category, tag, icon, season subroutes
- Update service worker to cache glaube and root page responses
2026-01-29 09:57:58 +01:00
9ff30b28cd feat: add PWA offline support for recipe pages
- Add service worker with caching for build assets, static files, images, and pages
- Add IndexedDB storage for recipes (brief and full data)
- Add offline-db API endpoint for bulk recipe download
- Add offline sync button component in header
- Add offline-shell page for direct navigation fallback
- Pre-cache __data.json for client-side navigation
- Add +page.ts universal load functions with IndexedDB fallback
- Add PWA manifest and icons for installability
- Update recipe page to handle missing data gracefully
2026-01-28 21:38:33 +01:00
9db2009777 fix: update DeepL API to header-based authentication
All checks were successful
CI / update (push) Successful in 1m30s
DeepL deprecated form-body authentication (auth_key in request body).
Now using Authorization header as required by the API.
2026-01-28 15:09:54 +01:00
baee021869 fix latin Fatima to P. Ramm's Ordo Missæ
All checks were successful
CI / update (push) Successful in 1m22s
2026-01-26 21:40:25 +01:00
ee2aff46a1 perf: preload crosses font on glaube pages
All checks were successful
CI / update (push) Successful in 1m29s
Add preload hint to fetch the crosses.woff2 font early,
improving First Contentful Paint on /glaube routes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 10:35:04 +01:00
8398bc9b14 refactor: remove duplicate crosses font-face declaration
The crosses font is already defined in app.css which loads globally,
so the duplicate in rosenkranz.css is unnecessary.
2026-01-26 10:32:33 +01:00
31c9f43114 refactor: replace shadow span with box-shadow on button_wrapper
All checks were successful
CI / update (push) Successful in 1m21s
Remove separate .button_wrapper_shadow element and apply box-shadow
directly to .button_wrapper in mobile view. Reduces DOM by 1 node.
2026-01-25 20:50:43 +01:00
3cc962f454 refactor: reduce DOM nesting and simplify templates
- Remove nested .wrapper div in recipe page using CSS Grid with full-bleed background
- Consolidate multiplier forms in IngredientsPage into single form
- Simplify fermentation conditionals in InstructionsPage with optional chaining
- Use conditional rendering instead of visibility wrapper in Search
- Remove unnecessary dialog wrapper in TitleImgParallax
2026-01-25 20:24:48 +01:00
5824993b18 refactor: simplify Card HTML and extract search filter composable
All checks were successful
CI / update (push) Successful in 1m23s
- Remove unnecessary wrapper divs in Card component (.card_anchor, .div_div_image)
- Flatten Card HTML from 4 levels to 2 levels of nesting
- Create reusable createSearchFilter composable in $lib/js/searchFilter.svelte.ts
- Apply search filter composable to category, tag, and favorites pages
2026-01-25 14:48:55 +01:00
940f9f14a2 fix tag styling on Cards
All checks were successful
CI / update (push) Successful in 1m19s
2026-01-23 16:14:34 +01:00
63e9b3de21 Move more CSS styling to a global css files to reduce bundle size
All checks were successful
CI / update (push) Successful in 2m53s
2026-01-23 15:37:32 +01:00
f3b92e8b1a refactor: clean up recipe routes and reduce bundle size
- Eliminate duplicate API fetch in recipe page by passing item from
  server load to universal load instead of fetching twice
- Replace cheerio with simple regex in stripHtmlTags, removing ~200KB
  dependency
- Refactor multiplier buttons in IngredientsPage to use loop instead
  of 5 repeated form elements
- Move /rezepte/untranslated to /[recipeLang]/admin/untranslated and
  delete legacy /rezepte/ layout files
2026-01-23 15:04:58 +01:00
ab2a6c9158 feat: add page titles to recipe and glaube routes
All checks were successful
CI / update (push) Successful in 1m20s
- Add titles to category, tag, icon, season routes
- Add bilingual support (German/English) for recipe route titles
- Use consistent "Bocken Recipes" / "Bocken Rezepte" branding
- Change English tagline from "Bocken's Recipes" to "Bocken Recipes"
- Add titles to /glaube and /glaube/gebete pages
- Make tips-and-tricks page language-aware
2026-01-20 19:54:33 +01:00
e366b44bba fix: include server load data in universal load for recipe page title
The +page.server.ts fetches recipe data and strips HTML tags server-side
to avoid bundling cheerio in the client. However, the universal load in
+page.ts wasn't including this data in its return value.

Fixed by:
1. Having +page.server.ts fetch the recipe directly (since it runs before
   +page.ts and can't access its data via parent())
2. Adding the `data` parameter to +page.ts and spreading it in the return

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 19:44:52 +01:00
1f5b342d8e cleanup 2026-01-20 19:37:49 +01:00
2b3aae8087 fix: append image to FormData before submission in use:enhance
The image upload broke because formData.append() was being called in the
async callback of use:enhance, which runs AFTER the form submission.
Moved the append call to the outer function which runs BEFORE submission.

Also cleaned up debug console.log statements from CardAdd.svelte.
2026-01-20 19:37:16 +01:00
0459ef26bc fix: use simple event handler for file selection instead of $effect
All checks were successful
CI / update (push) Successful in 1m21s
Replace $effect + bind:files approach with straightforward onchange handler:
- Use event.currentTarget.files[0] to get selected file
- Avoid reactive complexity that caused infinite loops
- Add bind:this reference to file input for clearing
- Clean implementation that works reliably in Svelte 5
2026-01-20 17:29:41 +01:00
3fb0a72014 fix: rewrite CardAdd image upload using Svelte 5 best practices
All checks were successful
CI / update (push) Successful in 1m20s
Replace event handler approach with bind:files and $effect:
- Use bind:files on file input for reactive FileList binding
- Use $effect to react to file selection and handle validation
- Properly clean up blob URLs to prevent memory leaks
- Remove exported functions that aren't used externally
- Add key to each block for tags
- Fix self-assignment warning in tag handling

The previous implementation used onchange with this.files which doesn't
work in Svelte 5. The new approach uses the idiomatic bind:files pattern.
2026-01-20 17:18:25 +01:00
b5fbedd13b fix: use event parameter in CardAdd image selection handler
All checks were successful
CI / update (push) Successful in 1m21s
The show_local_image function was using `this.files[0]` which doesn't work
in Svelte 5's onchange handlers. Changed to use `event.target.files[0]`
to properly access the selected file.

This fixes recipe image uploads not working because the file was never
being captured from the input element.
2026-01-20 17:09:28 +01:00
1bf4f4cbcd debug: add client-side logging for recipe image upload
All checks were successful
CI / update (push) Successful in 1m22s
Add console logging in the browser to track image selection and form
submission:
- Log when user selects an image file in CardAdd component
- Log file validation steps (MIME type, size)
- Log form submission and FormData preparation
- Log whether image is being appended to form

This helps diagnose if the issue is client-side (image not selected/sent)
or server-side (image not received/processed).
2026-01-20 17:04:33 +01:00
a5e8edf5f1 fix: remove unused CSS selectors from payments edit page
Remove .split-details and .personal-amount CSS selectors that were not
being used in the markup, eliminating build warnings.
2026-01-20 12:30:52 +01:00
d62315ad01 debug: add comprehensive logging to recipe image upload flow
All checks were successful
CI / update (push) Successful in 1m23s
Add detailed console logging throughout the image upload pipeline to help
diagnose upload issues:
- Log file metadata and validation steps in imageValidation.ts
- Log image processing and file saving operations in imageProcessing.ts
- Log form data and processing steps in recipe add page action
- Log API request details and upload progress in img/add endpoint

All logs are prefixed with component name ([ImageValidation], [ImageProcessing],
[RecipeAdd], [API:ImgAdd]) for easy filtering and debugging.
2026-01-20 12:27:03 +01:00
8af2fc3f3b build: keep console.error/warn/info in production builds
All checks were successful
CI / update (push) Successful in 1m22s
Changed drop_console from true to ['log', 'debug'] to retain
error and warning logs for debugging while reducing bundle size.
2026-01-20 12:16:14 +01:00
180bdb4aaa fix: restore rosary counter functionality with Svelte 5 reactivity
The Ave Maria counter was not updating the visualization when clicked.
Fixed by wrapping decadeCounters in $state() for proper reactivity tracking
and correcting data-section attributes to use template literals instead of
string literals in the decade loop.
2026-01-20 12:03:23 +01:00
50b0e7a70e fix: handle undefined/null input in stripHtmlTags function
All checks were successful
CI / update (push) Successful in 1m19s
Adds null check to prevent crash when recipe name or description fields are undefined.
2026-01-19 21:55:32 +01:00
5598b19ec9 perf: optimize bundle size and add build optimizations
All checks were successful
CI / update (push) Successful in 1m23s
- Move HTML stripping to server-side to remove cheerio from client bundle (247KB reduction)
- Add terser minification with console/debugger removal
- Enable manual code chunking for chart.js and auth libraries
- Convert TTF fonts to WOFF2 format (~900KB savings)
- Enable brotli/gzip precompression in adapter
- Update CSS to prefer WOFF2 with TTF fallback
2026-01-19 21:46:10 +01:00
a48ae3ff3c refactor: defer recipe image upload until form submission
Changed recipe image upload behavior to only process images when the
form is submitted, rather than immediately on file selection. This
prevents orphaned image files when users abandon the form.

Changes:
- CardAdd.svelte: Preview only, store File object instead of uploading
- Created imageProcessing.ts: Shared utility for image processing
- Add/edit page clients: Use selected_image_file instead of filename
- Add/edit page servers: Process and save images during form submission
- Images are validated, hashed, and saved in multiple formats on submit

Benefits:
- No orphaned files from abandoned forms
- Faster initial file selection experience
- Server-side image processing ensures security validation
- Cleaner architecture with shared processing logic
2026-01-15 14:18:52 +01:00
5fae01c06d feat: use checksummed filenames for recipe images and clean up old files
All checks were successful
CI / update (push) Successful in 1m13s
Updated recipe image handling to use checksummed filenames for proper
cache busting. When uploading a new image during recipe edit, old image
files (both hashed and unhashed versions) are now properly deleted from
all directories (full, thumb, placeholder).

Changes:
- CardAdd.svelte: Use checksummed filename from upload response
- Edit page server: Add deleteRecipeImage() helper to remove old images
- Edit page server: Delete old images when new image is uploaded
2026-01-15 13:57:06 +01:00
ae32db7dfd feat: improve settlement display and add split recalculation to payment edit
All checks were successful
CI / update (push) Successful in 1m13s
Settlement Display Improvements:
- Redesigned settlement cards with distinct visual style
- Added gradient background and colored top border stripe
- Centered layout with prominent amount display
- Added settlement badge with icon
- Responsive vertical layout on mobile devices
- Fixed overflow issues on small screens

Payment Edit Enhancements:
- Added automatic split recalculation when amount changes
- Implemented editable personal amounts for personal_equal split method
- Real-time validation showing total personal and remainder
- Live split preview with automatic updates
- Support for all split methods: equal, full, personal_equal, proportional
- Foreign currency support with exchange rate recalculation
- Safeguards against infinite recalculation loops
- Improved UI with split method info display
- Responsive design for mobile devices
2026-01-13 20:03:13 +01:00
baa3f3e533 feat: add Redis caching to cospend API endpoints
All checks were successful
CI / update (push) Successful in 1m21s
Implement comprehensive caching for all cospend routes to improve performance:

Cache Implementation:
- Balance API: 30-minute TTL for user balances and global balances
- Debts API: 15-minute TTL for debt breakdown calculations
- Payments List: 10-minute TTL with pagination support
- Individual Payment: 30-minute TTL for payment details

Cache Invalidation:
- Created invalidateCospendCaches() helper function
- Invalidates user balances, debts, and payment lists on mutations
- Applied to payment create, update, and delete operations
- Applied to recurring payment execution (manual and cron)
2026-01-13 19:45:11 +01:00
041d415525 fix: allow arbitrary decimal values for base multiplier 2026-01-13 19:30:35 +01:00
296201eee5 fix: improve UI elements in recipe editor
All checks were successful
CI / update (push) Successful in 1m14s
- Center isBaseRecipe toggle by changing display to inline-flex
- Fix note field editing by adding textarea with bindable value
- Clear instruction step input after submission instead of restoring placeholder
- Style note textarea with transparent background and lighter placeholder text

The instruction field now properly clears on submission, while ingredient fields retain their previous values.
2026-01-13 19:14:26 +01:00
b43b45dac2 feat: add base multiplier support for recipe references
Add optional baseMultiplier field to ingredient and instruction references, allowing base recipes to be included at scaled amounts (e.g., 0.5 for half the recipe).

- Add baseMultiplier field to Recipe schema with default value of 1
- Update TypeScript types to include baseMultiplier
- Add multiplier input field to BaseRecipeSelector modal
- Apply baseMultiplier to ingredient amounts during flattening
- Combine baseMultiplier with recipe multiplier in links
- Display and allow editing baseMultiplier in recipe editor

The multiplier cascades through nested references and works alongside the standard recipe multiplier for compound scaling.
2026-01-13 19:14:10 +01:00
db5d326fa2 fix: ensure isBaseRecipe checkbox submits correctly and use styled Toggle component
All checks were successful
CI / update (push) Successful in 1m12s
- Add hidden input to properly serialize isBaseRecipe boolean as "true"/"false" string
- Replace plain HTML checkbox with Toggle component for consistent styling
- Checkbox values don't submit when unchecked; hidden input ensures value is always sent
2026-01-13 15:33:22 +01:00
5ca044c79f fix: ensure recipe translations save properly by awaiting DOM updates before form submission
All checks were successful
CI / update (push) Successful in 1m13s
Previously, when users approved or skipped translations in the recipe forms, the translation data wasn't being saved to the database. This was caused by a timing issue where the form was submitted before Svelte had updated the DOM with the hidden inputs containing the translation data.

Fixed by using tick() to wait for pending state changes to be applied before submitting the form.
2026-01-13 15:24:37 +01:00
0dc950c824 fix: prevent infinite effect loop in recipe translation workflow
All checks were successful
CI / update (push) Successful in 1m14s
Convert recipe data functions to $derived reactive variables to prevent
infinite $effect loops. Previously, calling functions inline in component
props created new objects on every reactive check, causing the
TranslationApproval component's syncBaseRecipeReferences $effect to run
continuously, resulting in the translation workflow hanging.
2026-01-13 15:12:16 +01:00
0a49e20c02 refactor: migrate recipe forms to SvelteKit actions with secure image upload
Refactor recipe add/edit routes from client-side fetch to proper SvelteKit
form actions with progressive enhancement and comprehensive security improvements.

**Security Enhancements:**
- Implement 5-layer image validation (file size, MIME type, extension, magic bytes, Sharp structure)
- Replace insecure base64 JSON encoding with FormData for file uploads
- Add file-type@19 dependency for magic bytes validation
- Validate actual file type via magic bytes to prevent file type spoofing

**Progressive Enhancement:**
- Forms now work without JavaScript using native browser submission
- Add use:enhance for improved client-side UX when JS is available
- Serialize complex nested data (ingredients/instructions) via JSON in hidden fields
- Translation workflow integrated via programmatic form submission

**Bug Fixes:**
- Add type="button" to all interactive buttons in CreateIngredientList and CreateStepList
  to prevent premature form submission when clicking on ingredients/steps
- Fix SSR errors by using season_local state instead of get_season() DOM query
- Fix redirect handling in form actions (redirects were being caught as errors)
- Fix TranslationApproval to handle recipes without images using null-safe checks
- Add reactive effect to sync editableEnglish.images with germanData.images length
- Detect and hide 150x150 placeholder images in CardAdd component

**Features:**
- Make image uploads optional for recipe creation (use placeholder based on short_name)
- Handle three image scenarios in edit: keep existing, upload new, rename on short_name change
- Automatic image file renaming across full/thumb/placeholder directories when short_name changes
- Change detection for partial translation updates in edit mode

**Technical Changes:**
- Create imageValidation.ts utility with comprehensive file validation
- Create recipeFormHelpers.ts for data extraction, validation, and serialization
- Refactor /api/rezepte/img/add endpoint to use FormData instead of base64
- Update CardAdd component to upload via FormData immediately with proper error handling
- Use Image API for placeholder detection (avoids CORS issues with fetch)
2026-01-13 15:12:07 +01:00
deac9e3d1f fix: update event handlers to Svelte 5 syntax in add page and rosary counter
Fixes issues where translation buttons and rosary bead counter were not working
due to incomplete Svelte 5 migration. Updated parent components to use new
callback prop syntax (onapproved/onskipped/oncancelled) and lowercase onclick
handlers to match child component expectations.

- Fix TranslationApproval event handlers in recipe add page
- Fix CounterButton onclick prop in rosary page
2026-01-13 12:59:06 +01:00
f4783bea0a chore: update CI workflow to use pnpm instead of npm
All checks were successful
CI / update (push) Successful in 1m20s
Changed deployment workflow to use pnpm with frozen lockfile to ensure
consistent dependency installation matching local development environment.
2026-01-10 17:52:02 +01:00
a822ea738c chore: update @auth/sveltekit to v1.11.1 and remove direct @auth/core dependency
Updated authentication packages to latest versions for security fixes:
- @auth/sveltekit: 1.10.0 → 1.11.1 (includes nodemailer security fix)
- @auth/core: removed from devDependencies (transitively pulled as 0.41.1)

Changed imports to use @auth/sveltekit/providers instead of @auth/core/providers
and removed unused imports from hooks.server.ts.
2026-01-10 17:49:51 +01:00
a936b736de feat: add AND/OR logic mode toggle to filter panel
All checks were successful
CI / update (push) Successful in 1m15s
- Add LogicModeToggle component to switch between AND and OR filter logic
- Enable multi-select for category and icon filters in OR mode
- Update Search component to handle both AND and OR filtering logic
- Resize Toggle component to match LogicModeToggle size (44px x 24px)
- Position logic mode toggle on the left side of filter panel
- Auto-convert arrays to single values when switching from OR to AND mode
- In OR mode: recipes match if they satisfy ANY active filter
- In AND mode: recipes must satisfy ALL active filters
2026-01-10 17:34:13 +01:00
bc170abcdf fix: add category and favorites filters to all recipe pages
- Move categories logic into Search component for centralization
- Add isLoggedIn prop to SeasonLayout and IconLayout components
- Fix FilterPanel CSS to properly handle hidden favorites filter
- Fix FavoritesFilter to trigger onToggle when checkbox changes
- Update Search effect to track all filter states (category, tags, icon, season, favorites)
- Hide favorites filter on favorites page while maintaining proper grid layout
- All filters now work consistently across entire site
2026-01-10 17:20:00 +01:00
360 changed files with 53418 additions and 9285 deletions

View File

@@ -31,7 +31,9 @@ jobs:
git remote set-url origin https://Alexander:${{ secrets.homepage_gitea_token }}@git.bocken.org/Alexander/homepage
git fetch origin
git reset --hard origin/master
npm run build
pnpm install --frozen-lockfile
pnpm run build
redis-cli KEYS 'recipes:*' | xargs -r redis-cli DEL
sudo systemctl stop homepage.service
mkdir -p dist
rm -rf dist/*

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
.DS_Store
*/.jukit
*/.jukit/*
node_modules
/build
/.svelte-kit

1
.npmrc
View File

@@ -1,2 +1 @@
engine-strict=true
resolution-mode=highest

View File

@@ -1,300 +0,0 @@
# Formatter Replacement Summary
**Date:** 2025-11-18
**Status:** ✅ Complete
## Overview
Successfully replaced all inline formatting functions (65+ occurrences across 12 files) with shared formatter utilities from `$lib/utils/formatters.ts`.
---
## Files Modified
### Components (3 files)
1. **DebtBreakdown.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 4 calls to use `formatCurrency(amount, 'CHF', 'de-CH')`
2. **EnhancedBalance.svelte**
- ✅ Replaced inline `formatCurrency` with utility (kept wrapper for Math.abs)
- ✅ Added import: `import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters'`
- ✅ Wrapper function: `formatCurrency(amount) => formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH')`
3. **PaymentModal.svelte**
- ✅ Replaced inline `formatCurrency` with utility (kept wrapper for Math.abs)
- ✅ Added import: `import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters'`
- ✅ Wrapper function: `formatCurrency(amount) => formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH')`
### Cospend Pages (5 files)
4. **routes/cospend/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 5 calls to include CHF and de-CH parameters
5. **routes/cospend/payments/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 6 calls to include CHF and de-CH parameters
6. **routes/cospend/payments/view/[id]/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 7 calls to include CHF and de-CH parameters
7. **routes/cospend/recurring/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 5 calls to include CHF and de-CH parameters
8. **routes/cospend/settle/+page.svelte**
- ✅ Removed inline `formatCurrency` function
- ✅ Added import: `import { formatCurrency } from '$lib/utils/formatters'`
- ✅ Updated 4 calls to include CHF and de-CH parameters
### Configuration (1 file)
9. **svelte.config.js**
- ✅ Added `$utils` alias for `src/utils` directory
- ✅ Enables clean imports: `import { formatCurrency } from '$lib/utils/formatters'`
---
## Changes Summary
### Before Refactoring
**Problem:** Duplicate `formatCurrency` functions in 8 files:
```typescript
// Repeated 8 times across codebase
function formatCurrency(amount) {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
}
// Usage
{formatCurrency(payment.amount)}
```
### After Refactoring
**Solution:** Single shared utility with consistent usage:
```typescript
// Once in $lib/utils/formatters.ts
export function formatCurrency(
amount: number,
currency: string = 'EUR',
locale: string = 'de-DE'
): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(amount);
}
// Usage in components/pages
import { formatCurrency } from '$lib/utils/formatters';
{formatCurrency(payment.amount, 'CHF', 'de-CH')}
```
---
## Impact
### Code Duplication Eliminated
- **Before:** 8 duplicate `formatCurrency` functions
- **After:** 1 shared utility function
- **Reduction:** ~88% less formatting code
### Function Calls Updated
- **Total calls updated:** 31 formatCurrency calls
- **Parameters added:** CHF and de-CH to all calls
- **Consistency:** 100% of currency formatting now uses shared utility
### Lines of Code Removed
Approximately **40-50 lines** of duplicate code removed across 8 files.
---
## Benefits
### 1. Maintainability ✅
- ✅ Single source of truth for currency formatting
- ✅ Future changes only need to update one file
- ✅ Consistent formatting across entire application
### 2. Consistency ✅
- ✅ All currency displayed with same format
- ✅ Locale-aware formatting (de-CH)
- ✅ Proper currency symbol placement
### 3. Testability ✅
- ✅ Formatting logic has comprehensive unit tests (29 tests)
- ✅ Easy to test edge cases centrally
- ✅ Regression testing in one location
### 4. Type Safety ✅
- ✅ TypeScript types for all formatter functions
- ✅ JSDoc comments with examples
- ✅ IDE auto-completion support
### 5. Extensibility ✅
- ✅ Easy to add new formatters (date, number, etc.)
- ✅ Support for multiple locales
- ✅ Support for multiple currencies
---
## Remaining Inline Formatting (Optional Future Work)
### Files Still Using Inline `.toFixed()`
These files use `.toFixed()` for specific formatting needs. Could be replaced with `formatNumber()` if desired:
1. **SplitMethodSelector.svelte**
- Uses `.toFixed(2)` for split calculations
- Could use: `formatNumber(amount, 2, 'de-CH')`
2. **BarChart.svelte**
- Uses `.toFixed(0)` and `.toFixed(2)` for chart labels
- Could use: `formatNumber(amount, decimals, 'de-CH')`
3. **payments/add/+page.svelte** & **payments/edit/[id]/+page.svelte**
- Uses `.toFixed(2)` and `.toFixed(4)` for currency conversions
- Could use: `formatNumber(amount, decimals, 'de-CH')`
4. **recurring/edit/[id]/+page.svelte**
- Uses `.toFixed(2)` and `.toFixed(4)` for exchange rates
- Could use: `formatNumber(rate, 4, 'de-CH')`
5. **IngredientsPage.svelte**
- Uses `.toFixed(3)` for recipe ingredient calculations
- This is domain-specific logic, probably best left as-is
### Files Using `.toLocaleString()`
These files use `.toLocaleString()` for date formatting:
1. **payments/add/+page.svelte**
- Uses `.toLocaleString('de-CH', options)` for next execution date
- Could use: `formatDateTime(date, 'de-CH', options)`
2. **recurring/edit/[id]/+page.svelte**
- Uses `.toLocaleString('de-CH', options)` for next execution date
- Could use: `formatDateTime(date, 'de-CH', options)`
**Recommendation:** These are lower priority since they're used less frequently and the pattern is consistent.
---
## Testing Results
### Unit Tests ✅
```bash
Test Files: 2 passed (2)
Tests: 38 passed, 1 skipped (39)
Duration: ~500ms
```
**Test Coverage:**
- ✅ formatCurrency function (5 tests)
- ✅ formatDate function (5 tests)
- ✅ formatDateTime function (2 tests)
- ✅ formatNumber function (4 tests)
- ✅ formatRelativeTime function (2 tests)
- ✅ formatFileSize function (6 tests)
- ✅ formatPercentage function (5 tests)
- ✅ Auth middleware (9 tests)
### Build Status ✅
```bash
149 modules transformed
✔ Build completed successfully
```
**No breaking changes:** All existing functionality preserved.
---
## Migration Notes
### For Future Developers
**When adding new currency displays:**
```typescript
// ✅ DO: Use shared formatter
import { formatCurrency } from '$lib/utils/formatters';
{formatCurrency(amount, 'CHF', 'de-CH')}
// ❌ DON'T: Create new inline formatters
function formatCurrency(amount) {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
}
```
**When adding new number/date formatting:**
```typescript
// Numbers
import { formatNumber } from '$lib/utils/formatters';
{formatNumber(value, 2, 'de-CH')} // 2 decimal places
// Dates
import { formatDate, formatDateTime } from '$lib/utils/formatters';
{formatDate(date, 'de-CH')}
{formatDateTime(date, 'de-CH', { dateStyle: 'long', timeStyle: 'short' })}
```
---
## Files Created/Modified
### Created
- `scripts/replace_formatters.py` - Automated replacement script
- `scripts/update_formatter_calls.py` - Update formatter call parameters
- `scripts/replace-formatters.md` - Progress tracking
- `FORMATTER_REPLACEMENT_SUMMARY.md` - This document
### Modified
- 8 Svelte components/pages (formatCurrency replaced)
- 1 configuration file (svelte.config.js - added alias)
### Scripts Used
- Python automation for consistent replacements
- Bash scripts for verification
- Manual cleanup for edge cases
---
## Conclusion
**Successfully eliminated all duplicate formatCurrency functions**
**31 function calls updated to use shared utility**
**All tests passing (38/38)**
**Build successful with no breaking changes**
**~40-50 lines of duplicate code removed**
**Single source of truth for currency formatting**
**Result:** Cleaner, more maintainable codebase with consistent formatting across the entire application. Future changes to currency formatting only require updating one file instead of 8.
**Next Steps (Optional):**
1. Replace remaining `.toFixed()` calls with `formatNumber()` (8 files)
2. Replace `.toLocaleString()` calls with `formatDateTime()` (2 files)
3. Add more formatter utilities as needed (file size, percentages, etc.)

12
TODO.md Normal file
View File

@@ -0,0 +1,12 @@
# TODO
## Refactor Recipe Search Component
Refactor `src/lib/components/Search.svelte` to use the new `SearchInput.svelte` component for the visual input part. This will:
- Reduce code duplication between recipe search and prayer search
- Keep the visual styling consistent across the site
- Separate concerns: SearchInput handles the UI, Search.svelte handles recipe-specific filtering logic
Files involved:
- `src/lib/components/Search.svelte` - refactor to use SearchInput
- `src/lib/components/SearchInput.svelte` - the reusable input component

View File

@@ -1,138 +0,0 @@
# TypeScript Error Cleanup TODO
Generated from `pnpm check` output. Total errors found: 1239
## Categories
### 1. Implicit 'any' Types (High Priority - Easy Fixes)
Files with missing type annotations for function parameters
### 2. Mongoose Type Issues
Property access and type compatibility issues with Mongoose models
### 3. Null/Undefined Safety
Properties that may be null or undefined
### 4. Type Mismatches
Arguments and assignments with incompatible types
### 5. Missing Imports/Definitions
Cannot find name/namespace errors
---
## Detailed Errors
### Category 1: Implicit 'any' Types
#### src/lib/components/do_on_key.js
- [x] Line 1:27 - Parameter 'event' implicitly has an 'any' type ✅
- [x] Line 1:34 - Parameter 'key' implicitly has an 'any' type ✅
- [x] Line 1:39 - Parameter 'needsctrl' implicitly has an 'any' type ✅
- [x] Line 1:50 - Parameter 'fn' implicitly has an 'any' type ✅
#### src/lib/js/randomize.js
- [x] Line 2:21 - Parameter 'a' implicitly has an 'any' type ✅
- [x] Line 11:28 - Parameter 'array' implicitly has an 'any' type ✅
#### src/utils/cookie.js
- [x] Line 2:35 - Parameter 'request' implicitly has an 'any' type ✅
- [x] Line 4:45 - Parameter 'cookie' implicitly has an 'any' type ✅
#### src/hooks.server.ts
- [ ] Line 26:32 - Binding element 'event' implicitly has an 'any' type
- [ ] Line 26:39 - Binding element 'resolve' implicitly has an 'any' type
#### src/lib/js/stripHtmlTags.ts
- [x] Line 4:31 - Parameter 'input' implicitly has an 'any' type ✅
#### src/lib/utils/settlements.ts
- [ ] Line 51:40 - Parameter 'split' implicitly has an 'any' type
- [ ] Line 57:41 - Parameter 'split' implicitly has an 'any' type
#### src/routes/[recipeLang=recipeLang]/favorites/+page.server.ts
- [ ] Line 25:49 - Parameter 'recipe' implicitly has an 'any' type
#### src/routes/api/cospend/payments/+server.ts
- [ ] Line 135:48 - Parameter 'split' implicitly has an 'any' type
### Category 2: Mongoose/Model Type Issues
#### src/models/RecurringPayment.ts
- [ ] Line 98:20 - Property 'frequency' does not exist on type
#### src/routes/api/cospend/debts/+server.ts
- [ ] Line 36:64 - Property '_id' does not exist on FlattenMaps type
- [ ] Line 54:21 - Property '_id' does not exist on FlattenMaps type
- [ ] Line 54:56 - Property '_id' does not exist on FlattenMaps type
#### src/routes/api/generate-alt-text/+server.ts
- [ ] Line 60:34 - Type 'never[]' is missing properties (DocumentArray)
- [ ] Lines 62-75 - Multiple 'possibly null/undefined' errors for recipe.translations
#### src/routes/api/generate-alt-text-bulk/+server.ts
- [ ] Lines 93-108 - Similar DocumentArray and null/undefined issues
### Category 3: Null/Undefined Safety
#### src/lib/server/middleware/auth.ts
- [ ] Lines 42-44 - Type 'string | null | undefined' not assignable to 'string | undefined' (3 instances)
- [ ] Lines 77-79 - Same issue (3 more instances)
### Category 4: Error Handling (unknown type)
#### src/routes/api/cospend/payments/+server.ts
- [ ] Line 91:70 - 'e' is of type 'unknown'
#### src/routes/api/cospend/payments/[id]/+server.ts
- [ ] Line 26:9 - 'e' is of type 'unknown'
- [ ] Line 88:9 - 'e' is of type 'unknown'
- [ ] Line 121:9 - 'e' is of type 'unknown'
#### src/routes/api/cospend/upload/+server.ts
- [ ] Line 59:9 - 'err' is of type 'unknown'
### Category 5: Missing Definitions
#### src/lib/server/scheduler.ts
- [ ] Line 10:17 - Cannot find namespace 'cron'
- [ ] Line 30:7 - Invalid argument for TaskOptions
#### src/routes/(main)/settings/+page.server.ts
- [ ] Line 6:22 - Cannot find name 'authenticateUser'
#### src/hooks.server.ts
- [ ] Lines 39, 51, 61 - error() function doesn't accept custom object format
### Category 6: Aggregate Pipeline Issues
#### src/routes/api/cospend/monthly-expenses/+server.ts
- [ ] Line 79:45 - No matching overload for aggregate pipeline
- [ ] Line 115:62 - 'value' is of type 'unknown'
- [ ] Line 125:39 - Type incompatibility in map function
### Category 7: Svelte Component Props
#### Various .svelte files
- [ ] Multiple implicit 'any' types in event handlers and derived state
- [ ] Missing prop type definitions
- [ ] Element binding type issues
---
## Quick Wins (Start Here)
1. **do_on_key.js** - Add type annotations (4 parameters)
2. **randomize.js** - Add type annotations (2 parameters)
3. **cookie.js** - Add type annotations (2 parameters)
4. **stripHtmlTags.ts** - Add input parameter type
5. **Error handling** - Type catch blocks properly (`catch (e: unknown)`)
## Progress Tracking
- Total Errors: 1239
- Fixed: 12
- Remaining: 1227
- Categories Completed: Quick Wins (Category 1 - Partial)
Last Updated: Mon Jan 5 11:48:00 PM CET 2026

View File

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="-10 0 2058 2048"
id="svg1"
sodipodi:docname="dove.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.20494477"
inkscape:cx="875.84571"
inkscape:cy="3122.7925"
inkscape:window-width="1436"
inkscape:window-height="1749"
inkscape:window-x="1440"
inkscape:window-y="47"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
fill="currentColor"
d="M1935 90q0 32 -38 91q-21 29 -56 90q-20 55 -63 164q-35 86 -95 143q-22 -21 -43 -45q51 -49 85 -139q49 -130 61 -152q-126 48 -152 63q-76 46 -95 128q-27 -18 -58 -25q28 -104 97 -149q31 -20 138 -52q90 -28 137 -74l29 -39q22 -30 32 -30q21 0 21 26zM1714 653 q-90 30 -113 43q-65 36 -65 90q0 19 20 119q23 116 23 247q0 169 -103 299q-111 141 -275 141q-254 0 -283 87q-16 104 -31 207q-27 162 -76 162q-21 0 -41 -20q-16 -19 -32 -37q-10 3 -33 22q-18 15 -39 15q-28 0 -50 -44.5t-30 -44.5q-10 0 -35.5 11.5t-41.5 11.5 q-47 0 -58.5 -45.5t-21.5 -45.5t-29.5 2.5t-29.5 2.5q-46 0 -46 -30q0 -16 14 -44.5t14 -44.5q0 -8 -46.5 -25.5t-46.5 -48.5q0 -34 35.5 -52t99.5 -31q91 -19 103 -22q113 -32 171 -93q37 -39 105 -165q34 -64 43 -82q26 -53 31 -85q-129 -67 -224 -76q-33 0 -96 -11 q-36 -13 -36 -41q0 -7 2 -19.5t2 -19.5q0 -20 -67.5 -42t-67.5 -64q0 -11 8.5 -30t8.5 -30q0 -15 -79 -39t-79 -63q0 -16 9 -45t9 -45q0 -20 -29 -43q-23 -17 -46 -33q-49 -44 -49 -215q0 -8 1 -15q91 53 194 68l282 16q202 12 304 59q143 65 143 210q0 15 -2 44t-2 44 q0 122 78 122q73 0 108 -133q16 -70 32 -139q21 -81 57 -119q46 -51 130 -51q71 0 122 61q90 107 154 149zM1597 636q-25 -22 -77 -91q-30 -40 -75 -40q-91 0 -131 115q-30 106 -59 213q-44 115 -144 115q-146 0 -146 -180q0 -16 2.5 -46.5t2.5 -46.5q0 -62 -19 -87 q-70 -92 -303 -115q-173 -9 -347 -18q-55 -6 -116 -30v34q0 27 57.5 73.5t57.5 91.5q0 16 -10.5 45t-10.5 44q1 1 7 1q3 0 7 1q146 36 146 105q0 13 -8.5 32.5t-8.5 27.5h10q5 0 9 1q61 15 86 36q32 28 28 85q173 15 372 107q-7 77 -80 215q-67 128 -127 195 q-67 74 -169 104q-96 24 -193 47q-10 3 -29 13q86 18 86 70q0 19 -19 62q15 -5 33 -5q42 0 59 26q8 11 22 61l-1 3q10 0 34.5 -11.5t42.5 -11.5q55 0 88 84q38 -32 64 -32q37 0 66 41q25 -53 33 -151q10 -112 23 -154q43 -136 337 -136q116 0 215 -108q105 -114 105 -277 q0 -23 -12 -112l-28 -207q-4 -30 -4 -42q0 -97 124 -147zM1506 605q0 38 -38 38q-39 0 -39 -38t39 -38q38 0 38 38z"
id="path1" />
<path
d="m 1724.44,1054.6641 c -31.1769,-18 -37.7653,-42.5884 -19.7653,-73.76528 5.3333,-9.2376 12.354,-16.7312 21.0621,-22.4808 6.2201,-4.1068 44.7886,-7.2427 115.7055,-9.4077 70.9168,-2.1649 110.128,-1.0807 117.6336,3.2526 30.0222,17.3334 35.5333,42.45448 16.5333,75.36348 -7.3333,12.7017 -16.1754,20.6833 -26.5263,23.9448 -24.5645,1.2137 -56.7805,3.0135 -96.648,5.3994 -72.6282,5.7957 -115.2931,5.0269 -127.9949,-2.3065 z"
style="fill:currentColor"
id="path1-2"
sodipodi:nodetypes="ssssssccs" />
<path
d="m 386.57764,1262.0569 c 53.44793,-14.3214 85.17574,-2.8075 95.18337,34.5417 9.83517,36.7052 -12.29319,62.3047 -66.38503,76.7986 l -82.1037,21.9996 c -54.09184,14.4939 -86.05533,3.3882 -95.89047,-33.317 -10.00766,-37.3491 12.67841,-63.4432 68.05807,-78.2821 z"
style="fill:currentColor"
id="path1-7" />
<path
d="m 1115.7599,372.22724 c 14.3213,53.44793 2.8073,85.17581 -34.5418,95.18323 -36.705,9.83527 -62.3047,-12.29323 -76.7986,-66.38485 l -21.99962,-82.10394 c -14.4939,-54.09162 -3.3882,-86.05531 33.31712,-95.89019 37.349,-10.00765 63.4431,12.67818 78.2821,68.05802 z"
style="fill:currentColor"
id="path1-7-6" />
<path
d="m 1184.6228,1956.284 c -4.807,-8.0003 -6.8298,-42.7561 -6.0684,-104.2674 0.7614,-61.5113 2.7093,-100.0139 5.8437,-115.508 3.1343,-15.4941 11.8445,-27.5329 26.1306,-36.117 30.2866,-18.198 54.7006,-11.868 73.242,18.99 5.4937,9.1432 8.145,43.3269 7.9537,102.5512 -0.081,52.9359 -1.4296,89.5231 -4.0464,109.7617 -2.276,16.9226 -11.1284,30.0192 -26.5575,39.29 -33.1439,19.9148 -58.643,15.0146 -76.4977,-14.7005 z"
style="fill:currentColor"
id="path1-6" />
<path
d="m 1773.3127,1737.6952 c -9.0153,-2.4157 -34.6139,-26.0118 -76.7955,-70.7882 -42.1816,-44.7764 -67.5266,-73.826 -76.035,-87.1489 -8.5084,-13.3228 -10.6057,-28.0334 -6.2922,-44.1323 9.145,-34.1293 31.1041,-46.5353 65.8774,-37.2179 10.3033,2.7609 35.9565,25.5088 76.9595,68.2441 36.7142,38.1352 61.1596,65.3907 73.3362,81.7668 10.1182,13.7541 12.8479,29.3245 8.1892,46.7113 -10.0077,37.3492 -31.7542,51.5375 -65.2396,42.5651 z"
style="fill:currentColor"
id="path1-6-9" />
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python3
"""Extract crown emoji from Symbola font as SVG."""
import fontforge
import sys
# Path to Symbola font
font_path = "/usr/share/fonts/TTF/Symbola.ttf"
# Dove emoji Unicode codepoint
dove_codepoint = 0x1F54A # U+1F54A 🕊️
# Output SVG file
output_path = "dove.svg"
try:
# Open the font
font = fontforge.open(font_path)
# Select the dove glyph by Unicode codepoint
if dove_codepoint in font:
glyph = font[dove_codepoint]
# Export as SVG
glyph.export(output_path)
print(f"✓ Successfully extracted dove emoji to {output_path}")
print(f" Glyph name: {glyph.glyphname}")
print(f" Unicode: U+{dove_codepoint:04X}")
else:
print(f"✗ Dove emoji (U+{dove_codepoint:04X}) not found in font")
sys.exit(1)
font.close()
except Exception as e:
print(f"✗ Error: {e}")
sys.exit(1)

View File

@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite dev",
"prebuild": "bash scripts/subset-emoji-font.sh && npx vite-node scripts/generate-mystery-verses.ts",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -14,12 +15,15 @@
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
"test:e2e:ui": "playwright test --ui",
"test:e2e:docker:up": "docker compose -f docker-compose.test.yml up -d",
"test:e2e:docker:down": "docker compose -f docker-compose.test.yml down -v",
"test:e2e:docker": "docker compose -f docker-compose.test.yml up -d && playwright test; docker compose -f docker-compose.test.yml down -v",
"test:e2e:docker:run": "docker run --rm --network host -v $(pwd):/app -w /app -e CI=true mcr.microsoft.com/playwright:v1.56.1-noble /bin/bash -c 'npm install -g pnpm@9.0.0 && pnpm install --frozen-lockfile && pnpm run build && pnpm exec playwright test --project=chromium'"
},
"packageManager": "pnpm@9.0.0",
"devDependencies": {
"@auth/core": "^0.40.0",
"@playwright/test": "^1.56.1",
"@playwright/test": "1.56.1",
"@sveltejs/adapter-auto": "^6.1.0",
"@sveltejs/kit": "^2.37.0",
"@sveltejs/vite-plugin-svelte": "^6.1.3",
@@ -31,16 +35,17 @@
"jsdom": "^27.2.0",
"svelte": "^5.38.6",
"svelte-check": "^4.0.0",
"terser": "^5.46.0",
"tslib": "^2.6.0",
"typescript": "^5.1.6",
"vite": "^7.1.3",
"vitest": "^4.0.10"
},
"dependencies": {
"@auth/sveltekit": "^1.10.0",
"@auth/sveltekit": "^1.11.1",
"@sveltejs/adapter-node": "^5.0.0",
"chart.js": "^4.5.0",
"cheerio": "1.0.0-rc.12",
"file-type": "^19.0.0",
"ioredis": "^5.9.0",
"mongoose": "^8.0.0",
"node-cron": "^4.2.1",

343
pnpm-lock.yaml generated
View File

@@ -9,17 +9,17 @@ importers:
.:
dependencies:
'@auth/sveltekit':
specifier: ^1.10.0
version: 1.10.0(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)
specifier: ^1.11.1
version: 1.11.1(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)
'@sveltejs/adapter-node':
specifier: ^5.0.0
version: 5.3.1(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))
version: 5.3.1(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))
chart.js:
specifier: ^4.5.0
version: 4.5.0
cheerio:
specifier: 1.0.0-rc.12
version: 1.0.0-rc.12
file-type:
specifier: ^19.0.0
version: 19.6.0
ioredis:
specifier: ^5.9.0
version: 5.9.0
@@ -33,27 +33,24 @@ importers:
specifier: ^0.33.0
version: 0.33.5
devDependencies:
'@auth/core':
specifier: ^0.40.0
version: 0.40.0
'@playwright/test':
specifier: ^1.56.1
specifier: 1.56.1
version: 1.56.1
'@sveltejs/adapter-auto':
specifier: ^6.1.0
version: 6.1.0(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))
version: 6.1.0(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))
'@sveltejs/kit':
specifier: ^2.37.0
version: 2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))
version: 2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
'@sveltejs/vite-plugin-svelte':
specifier: ^6.1.3
version: 6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))
version: 6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
'@testing-library/jest-dom':
specifier: ^6.9.1
version: 6.9.1
'@testing-library/svelte':
specifier: ^5.2.9
version: 5.2.9(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))(vitest@4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0))
version: 5.2.9(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))(vitest@4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0)(terser@5.46.0))
'@types/node':
specifier: ^22.12.0
version: 22.18.0
@@ -72,6 +69,9 @@ importers:
svelte-check:
specifier: ^4.0.0
version: 4.3.1(picomatch@4.0.3)(svelte@5.38.6)(typescript@5.1.6)
terser:
specifier: ^5.46.0
version: 5.46.0
tslib:
specifier: ^2.6.0
version: 2.6.0
@@ -80,10 +80,10 @@ importers:
version: 5.1.6
vite:
specifier: ^7.1.3
version: 7.1.3(@types/node@22.18.0)
version: 7.1.3(@types/node@22.18.0)(terser@5.46.0)
vitest:
specifier: ^4.0.10
version: 4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0)
version: 4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0)(terser@5.46.0)
packages:
@@ -102,12 +102,12 @@ packages:
'@asamuzakjp/nwsapi@2.3.9':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
'@auth/core@0.40.0':
resolution: {integrity: sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==}
'@auth/core@0.41.1':
resolution: {integrity: sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==}
peerDependencies:
'@simplewebauthn/browser': ^9.0.1
'@simplewebauthn/server': ^9.0.2
nodemailer: ^6.8.0
nodemailer: ^7.0.7
peerDependenciesMeta:
'@simplewebauthn/browser':
optional: true
@@ -116,13 +116,13 @@ packages:
nodemailer:
optional: true
'@auth/sveltekit@1.10.0':
resolution: {integrity: sha512-nTKS3FoFvgdqUwb7a8HZpLxDlx+pHndygcodM16J/iFHbe/0wha0MUCuTkVeUYZuKwL63L2ujmMAC1WEoki2+g==}
'@auth/sveltekit@1.11.1':
resolution: {integrity: sha512-cWNfXcKrNIVtJYOY1tq7H7m03j89Wg7xrTvOJALu18fZdYulzYCPIAdTw8XSEzOp6KyhOGo7tmW7VtzRNtr/8Q==}
peerDependencies:
'@simplewebauthn/browser': ^9.0.1
'@simplewebauthn/server': ^9.0.3
'@sveltejs/kit': ^1.0.0 || ^2.0.0
nodemailer: ^6.6.5
nodemailer: ^7.0.7
svelte: ^3.54.0 || ^4.0.0 || ^5.0.0-0
peerDependenciesMeta:
'@simplewebauthn/browser':
@@ -144,6 +144,9 @@ packages:
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@borewit/text-codec@0.2.1':
resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
@@ -453,6 +456,9 @@ packages:
resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.11':
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
'@jridgewell/sourcemap-codec@1.4.15':
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
@@ -629,6 +635,9 @@ packages:
cpu: [x64]
os: [win32]
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
@@ -696,6 +705,9 @@ packages:
vitest:
optional: true
'@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@@ -798,13 +810,13 @@ packages:
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
bson@6.10.4:
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
engines: {node: '>=16.20.1'}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
chai@6.2.1:
resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==}
engines: {node: '>=18'}
@@ -813,13 +825,6 @@ packages:
resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==}
engines: {pnpm: '>=8'}
cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
cheerio@1.0.0-rc.12:
resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
engines: {node: '>= 6'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -846,6 +851,9 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
@@ -853,17 +861,10 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
css-tree@3.1.0:
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
@@ -930,23 +931,6 @@ packages:
dom-accessibility-api@0.6.3:
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@@ -987,6 +971,10 @@ packages:
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
file-type@19.6.0:
resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==}
engines: {node: '>=18'}
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
@@ -1003,6 +991,10 @@ packages:
function-bind@1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
get-stream@9.0.1:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
has@1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
@@ -1011,9 +1003,6 @@ packages:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
@@ -1026,6 +1015,9 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
indent-string@4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
@@ -1055,6 +1047,10 @@ packages:
is-reference@3.0.3:
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
is-stream@4.0.1:
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
engines: {node: '>=18'}
jose@6.1.0:
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
@@ -1176,18 +1172,9 @@ packages:
resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
engines: {node: '>=6.0.0'}
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
oauth4webapi@3.8.1:
resolution: {integrity: sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA==}
parse5-htmlparser2-tree-adapter@7.0.0:
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
parse5@7.1.2:
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
parse5@8.0.0:
resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
@@ -1197,6 +1184,10 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
peek-readable@5.4.2:
resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==}
engines: {node: '>=14.16'}
picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@@ -1328,6 +1319,13 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
sparse-bitfield@3.0.3:
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
@@ -1344,6 +1342,10 @@ packages:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
strtok3@9.1.1:
resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==}
engines: {node: '>=16'}
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
@@ -1363,6 +1365,11 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
terser@5.46.0:
resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==}
engines: {node: '>=10'}
hasBin: true
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1388,6 +1395,10 @@ packages:
resolution: {integrity: sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw==}
hasBin: true
token-types@6.1.2:
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
engines: {node: '>=14.16'}
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -1412,6 +1423,10 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uint8array-extras@1.5.0:
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
engines: {node: '>=18'}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -1576,7 +1591,7 @@ snapshots:
'@asamuzakjp/nwsapi@2.3.9': {}
'@auth/core@0.40.0':
'@auth/core@0.41.1':
dependencies:
'@panva/hkdf': 1.2.1
jose: 6.1.0
@@ -1584,10 +1599,10 @@ snapshots:
preact: 10.24.3
preact-render-to-string: 6.5.11(preact@10.24.3)
'@auth/sveltekit@1.10.0(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)':
'@auth/sveltekit@1.11.1(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)':
dependencies:
'@auth/core': 0.40.0
'@sveltejs/kit': 2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))
'@auth/core': 0.41.1
'@sveltejs/kit': 2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
set-cookie-parser: 2.7.1
svelte: 5.38.6
@@ -1601,6 +1616,8 @@ snapshots:
'@babel/runtime@7.28.4': {}
'@borewit/text-codec@0.2.1': {}
'@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
@@ -1795,6 +1812,11 @@ snapshots:
'@jridgewell/resolve-uri@3.1.0': {}
'@jridgewell/source-map@0.3.11':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.30
'@jridgewell/sourcemap-codec@1.4.15': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
@@ -1925,29 +1947,31 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.50.0':
optional: true
'@sec-ant/readable-stream@0.4.1': {}
'@standard-schema/spec@1.0.0': {}
'@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)':
dependencies:
acorn: 8.15.0
'@sveltejs/adapter-auto@6.1.0(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))':
'@sveltejs/adapter-auto@6.1.0(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))':
dependencies:
'@sveltejs/kit': 2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))
'@sveltejs/kit': 2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
'@sveltejs/adapter-node@5.3.1(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))':
'@sveltejs/adapter-node@5.3.1(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))':
dependencies:
'@rollup/plugin-commonjs': 28.0.6(rollup@4.50.0)
'@rollup/plugin-json': 6.1.0(rollup@4.50.0)
'@rollup/plugin-node-resolve': 16.0.1(rollup@4.50.0)
'@sveltejs/kit': 2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))
'@sveltejs/kit': 2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
rollup: 4.50.0
'@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))':
'@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))':
dependencies:
'@standard-schema/spec': 1.0.0
'@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0)
'@sveltejs/vite-plugin-svelte': 6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))
'@sveltejs/vite-plugin-svelte': 6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
'@types/cookie': 0.6.0
acorn: 8.15.0
cookie: 0.6.0
@@ -1960,27 +1984,27 @@ snapshots:
set-cookie-parser: 2.6.0
sirv: 3.0.1
svelte: 5.38.6
vite: 7.1.3(@types/node@22.18.0)
vite: 7.1.3(@types/node@22.18.0)(terser@5.46.0)
'@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))':
'@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))':
dependencies:
'@sveltejs/vite-plugin-svelte': 6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))
'@sveltejs/vite-plugin-svelte': 6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
debug: 4.4.1
svelte: 5.38.6
vite: 7.1.3(@types/node@22.18.0)
vite: 7.1.3(@types/node@22.18.0)(terser@5.46.0)
transitivePeerDependencies:
- supports-color
'@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))':
'@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))
'@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
debug: 4.4.1
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.18
svelte: 5.38.6
vite: 7.1.3(@types/node@22.18.0)
vitefu: 1.1.1(vite@7.1.3(@types/node@22.18.0))
vite: 7.1.3(@types/node@22.18.0)(terser@5.46.0)
vitefu: 1.1.1(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
transitivePeerDependencies:
- supports-color
@@ -2004,13 +2028,15 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
'@testing-library/svelte@5.2.9(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0))(vitest@4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0))':
'@testing-library/svelte@5.2.9(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))(vitest@4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0)(terser@5.46.0))':
dependencies:
'@testing-library/dom': 10.4.1
svelte: 5.38.6
optionalDependencies:
vite: 7.1.3(@types/node@22.18.0)
vitest: 4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0)
vite: 7.1.3(@types/node@22.18.0)(terser@5.46.0)
vitest: 4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0)(terser@5.46.0)
'@tokenizer/token@0.3.0': {}
'@types/aria-query@5.0.4': {}
@@ -2050,13 +2076,13 @@ snapshots:
chai: 6.2.1
tinyrainbow: 3.0.3
'@vitest/mocker@4.0.10(vite@7.1.3(@types/node@22.18.0))':
'@vitest/mocker@4.0.10(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))':
dependencies:
'@vitest/spy': 4.0.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.1.3(@types/node@22.18.0)
vite: 7.1.3(@types/node@22.18.0)(terser@5.46.0)
'@vitest/pretty-format@4.0.10':
dependencies:
@@ -2084,7 +2110,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vitest: 4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0)
vitest: 4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0)(terser@5.46.0)
'@vitest/utils@4.0.10':
dependencies:
@@ -2113,35 +2139,16 @@ snapshots:
dependencies:
require-from-string: 2.0.2
boolbase@1.0.0: {}
bson@6.10.4: {}
buffer-from@1.1.2: {}
chai@6.2.1: {}
chart.js@4.5.0:
dependencies:
'@kurkle/color': 0.3.4
cheerio-select@2.1.0:
dependencies:
boolbase: 1.0.0
css-select: 5.1.0
css-what: 6.1.0
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
cheerio@1.0.0-rc.12:
dependencies:
cheerio-select: 2.1.0
dom-serializer: 2.0.0
domhandler: 5.0.3
domutils: 3.1.0
htmlparser2: 8.0.2
parse5: 7.1.2
parse5-htmlparser2-tree-adapter: 7.0.0
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -2166,25 +2173,17 @@ snapshots:
color-convert: 2.0.1
color-string: 1.9.1
commander@2.20.3: {}
commondir@1.0.1: {}
cookie@0.6.0: {}
css-select@5.1.0:
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 5.0.3
domutils: 3.1.0
nth-check: 2.1.1
css-tree@3.1.0:
dependencies:
mdn-data: 2.12.2
source-map-js: 1.2.1
css-what@6.1.0: {}
css.escape@1.5.1: {}
cssstyle@5.3.3:
@@ -2226,26 +2225,6 @@ snapshots:
dom-accessibility-api@0.6.3: {}
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
domelementtype@2.3.0: {}
domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
domutils@3.1.0:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
entities@4.5.0: {}
entities@6.0.1: {}
es-module-lexer@1.7.0: {}
@@ -2299,6 +2278,13 @@ snapshots:
fflate@0.8.2: {}
file-type@19.6.0:
dependencies:
get-stream: 9.0.1
strtok3: 9.1.1
token-types: 6.1.2
uint8array-extras: 1.5.0
flatted@3.3.3: {}
fsevents@2.3.2:
@@ -2309,6 +2295,11 @@ snapshots:
function-bind@1.1.1: {}
get-stream@9.0.1:
dependencies:
'@sec-ant/readable-stream': 0.4.1
is-stream: 4.0.1
has@1.0.3:
dependencies:
function-bind: 1.1.1
@@ -2317,13 +2308,6 @@ snapshots:
dependencies:
whatwg-encoding: 3.1.1
htmlparser2@8.0.2:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
entities: 4.5.0
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
@@ -2342,6 +2326,8 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
indent-string@4.0.0: {}
ioredis@5.9.0:
@@ -2379,6 +2365,8 @@ snapshots:
dependencies:
'@types/estree': 1.0.8
is-stream@4.0.1: {}
jose@6.1.0: {}
js-tokens@4.0.0: {}
@@ -2490,21 +2478,8 @@ snapshots:
node-cron@4.2.1: {}
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
oauth4webapi@3.8.1: {}
parse5-htmlparser2-tree-adapter@7.0.0:
dependencies:
domhandler: 5.0.3
parse5: 7.1.2
parse5@7.1.2:
dependencies:
entities: 4.5.0
parse5@8.0.0:
dependencies:
entities: 6.0.1
@@ -2513,6 +2488,8 @@ snapshots:
pathe@2.0.3: {}
peek-readable@5.4.2: {}
picocolors@1.0.0: {}
picocolors@1.1.1: {}
@@ -2672,6 +2649,13 @@ snapshots:
source-map-js@1.2.1: {}
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
source-map@0.6.1: {}
sparse-bitfield@3.0.3:
dependencies:
memory-pager: 1.5.0
@@ -2686,6 +2670,11 @@ snapshots:
dependencies:
min-indent: 1.0.1
strtok3@9.1.1:
dependencies:
'@tokenizer/token': 0.3.0
peek-readable: 5.4.2
supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@4.3.1(picomatch@4.0.3)(svelte@5.38.6)(typescript@5.1.6):
@@ -2719,6 +2708,13 @@ snapshots:
symbol-tree@3.2.4: {}
terser@5.46.0:
dependencies:
'@jridgewell/source-map': 0.3.11
acorn: 8.15.0
commander: 2.20.3
source-map-support: 0.5.21
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@@ -2741,6 +2737,12 @@ snapshots:
dependencies:
tldts-core: 7.0.18
token-types@6.1.2:
dependencies:
'@borewit/text-codec': 0.2.1
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
totalist@3.0.1: {}
tough-cookie@6.0.0:
@@ -2759,9 +2761,11 @@ snapshots:
typescript@5.1.6: {}
uint8array-extras@1.5.0: {}
undici-types@6.21.0: {}
vite@7.1.3(@types/node@22.18.0):
vite@7.1.3(@types/node@22.18.0)(terser@5.46.0):
dependencies:
esbuild: 0.25.9
fdir: 6.5.0(picomatch@4.0.3)
@@ -2772,15 +2776,16 @@ snapshots:
optionalDependencies:
'@types/node': 22.18.0
fsevents: 2.3.3
terser: 5.46.0
vitefu@1.1.1(vite@7.1.3(@types/node@22.18.0)):
vitefu@1.1.1(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0)):
optionalDependencies:
vite: 7.1.3(@types/node@22.18.0)
vite: 7.1.3(@types/node@22.18.0)(terser@5.46.0)
vitest@4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0):
vitest@4.0.10(@types/node@22.18.0)(@vitest/ui@4.0.10)(jsdom@27.2.0)(terser@5.46.0):
dependencies:
'@vitest/expect': 4.0.10
'@vitest/mocker': 4.0.10(vite@7.1.3(@types/node@22.18.0))
'@vitest/mocker': 4.0.10(vite@7.1.3(@types/node@22.18.0)(terser@5.46.0))
'@vitest/pretty-format': 4.0.10
'@vitest/runner': 4.0.10
'@vitest/snapshot': 4.0.10
@@ -2797,7 +2802,7 @@ snapshots:
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 7.1.3(@types/node@22.18.0)
vite: 7.1.3(@types/node@22.18.0)(terser@5.46.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.18.0

View File

@@ -0,0 +1,88 @@
/**
* Pre-generates Bible verse data for all rosary mystery references.
* Run with: npx vite-node scripts/generate-mystery-verses.ts
*/
import { writeFileSync } from 'fs';
import { resolve } from 'path';
import { lookupReference } from '../src/lib/server/bible';
import { mysteryReferences, mysteryReferencesEnglish, theologicalVirtueReference, theologicalVirtueReferenceEnglish } from '../src/lib/data/mysteryDescriptions';
import type { MysteryDescription, VerseData } from '../src/lib/data/mysteryDescriptions';
function generateVerseData(
references: Record<string, readonly { title: string; reference: string }[]>,
tsvPath: string
): Record<string, MysteryDescription[]> {
const result: Record<string, MysteryDescription[]> = {};
for (const [mysteryType, refs] of Object.entries(references)) {
const descriptions: MysteryDescription[] = [];
for (const ref of refs) {
const lookup = lookupReference(ref.reference, tsvPath);
let text = '';
let verseData: VerseData | null = null;
if (lookup && lookup.verses.length > 0) {
text = `«${lookup.verses.map((v) => v.text).join(' ')}»`;
verseData = {
book: lookup.book,
chapter: lookup.chapter,
verses: lookup.verses
};
} else {
console.warn(`No verses found for: ${ref.reference} in ${tsvPath}`);
}
descriptions.push({
title: ref.title,
reference: ref.reference,
text,
verseData
});
}
result[mysteryType] = descriptions;
}
return result;
}
const dePath = resolve('static/allioli.tsv');
const enPath = resolve('static/drb.tsv');
const mysteryVerseDataDe = generateVerseData(mysteryReferences, dePath);
const mysteryVerseDataEn = generateVerseData(mysteryReferencesEnglish, enPath);
// Generate theological virtue (1 Cor 13) verse data
function generateSingleRef(ref: { title: string; reference: string }, tsvPath: string): MysteryDescription {
const lookup = lookupReference(ref.reference, tsvPath);
let text = '';
let verseData: VerseData | null = null;
if (lookup && lookup.verses.length > 0) {
text = `«${lookup.verses.map((v) => v.text).join(' ')}»`;
verseData = { book: lookup.book, chapter: lookup.chapter, verses: lookup.verses };
} else {
console.warn(`No verses found for: ${ref.reference} in ${tsvPath}`);
}
return { title: ref.title, reference: ref.reference, text, verseData };
}
const theologicalVirtueDataDe = generateSingleRef(theologicalVirtueReference, dePath);
const theologicalVirtueDataEn = generateSingleRef(theologicalVirtueReferenceEnglish, enPath);
const output = `// Auto-generated by scripts/generate-mystery-verses.ts — do not edit manually
import type { MysteryDescription } from './mysteryDescriptions';
export const mysteryVerseDataDe: Record<string, MysteryDescription[]> = ${JSON.stringify(mysteryVerseDataDe, null, '\t')};
export const mysteryVerseDataEn: Record<string, MysteryDescription[]> = ${JSON.stringify(mysteryVerseDataEn, null, '\t')};
export const theologicalVirtueVerseDataDe: MysteryDescription = ${JSON.stringify(theologicalVirtueDataDe, null, '\t')};
export const theologicalVirtueVerseDataEn: MysteryDescription = ${JSON.stringify(theologicalVirtueDataEn, null, '\t')};
`;
const outPath = resolve('src/lib/data/mysteryVerseData.ts');
writeFileSync(outPath, output, 'utf-8');
console.log(`Wrote mystery verse data to ${outPath}`);

54
scripts/subset-emoji-font.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Subset NotoColorEmoji to only the emojis we actually use.
# Requires: fonttools (provides pyftsubset) and woff2 (provides woff2_compress)
#
# Source font: system-installed NotoColorEmoji.ttf
# Output: static/fonts/NotoColorEmoji.woff2 + .ttf
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
OUT_DIR="$PROJECT_ROOT/static/fonts"
SRC_FONT="/usr/share/fonts/noto/NotoColorEmoji.ttf"
if [ ! -f "$SRC_FONT" ]; then
echo "Error: Source font not found at $SRC_FONT" >&2
exit 1
fi
# ─── Fixed list of emojis to include ────────────────────────────────
# Recipe icons (from database + hardcoded)
# Season/liturgical: ☀️ ✝️ ❄️ 🌷 🍂 🎄 🐇
# Food/recipe: 🍽️ 🥫
# UI/cospend categories: 🛒 🛍️ 🚆 ⚡ 🎉 🤝 💸
# Status/feedback: ❤️ 🖤 ✅ ❌ 🚀 ⚠️ ✨ 🔄
# Features: 📋 🖼️ 📖 🤖 🌐 🔐 🔍 🚫
EMOJIS="☀✝❄🌷🍂🎄🐇🍽🥫🛒🛍🚆⚡🎉🤝💸❤🖤✅❌🚀⚠✨🔄📋🖼📖🤖🌐🔐🔍🚫"
# ────────────────────────────────────────────────────────────────────
# Build Unicode codepoint list from the emoji string (Python for reliable Unicode handling)
UNICODES=$(python3 -c "print(','.join(f'U+{ord(c):04X}' for c in '$EMOJIS'))")
GLYPH_COUNT=$(python3 -c "print(len('$EMOJIS'))")
echo "Subsetting NotoColorEmoji with $GLYPH_COUNT glyphs..."
# Subset to TTF
pyftsubset "$SRC_FONT" \
--unicodes="$UNICODES" \
--output-file="$OUT_DIR/NotoColorEmoji.ttf" \
--no-ignore-missing-unicodes
# Convert to WOFF2
woff2_compress "$OUT_DIR/NotoColorEmoji.ttf"
ORIG_SIZE=$(stat -c%s "$SRC_FONT" 2>/dev/null || stat -f%z "$SRC_FONT")
TTF_SIZE=$(stat -c%s "$OUT_DIR/NotoColorEmoji.ttf" 2>/dev/null || stat -f%z "$OUT_DIR/NotoColorEmoji.ttf")
WOFF2_SIZE=$(stat -c%s "$OUT_DIR/NotoColorEmoji.woff2" 2>/dev/null || stat -f%z "$OUT_DIR/NotoColorEmoji.woff2")
echo "Done!"
echo " Original: $(numfmt --to=iec "$ORIG_SIZE")"
echo " TTF: $(numfmt --to=iec "$TTF_SIZE")"
echo " WOFF2: $(numfmt --to=iec "$WOFF2_SIZE")"

View File

@@ -15,6 +15,15 @@
url(/fonts/crosses.ttf) format('truetype');
}
@font-face {
font-family: 'Noto Color Emoji Subset';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/NotoColorEmoji.woff2) format('woff2'),
url(/fonts/NotoColorEmoji.ttf) format('truetype');
}
/* ============================================
COLOR SYSTEM
Based on Nord Theme with semantic naming
@@ -109,6 +118,35 @@
--color-warning: var(--nord13);
--color-error: var(--nord11);
--color-info: var(--nord10);
/* Shared transitions & shadows */
--transition-fast: 100ms;
--transition-normal: 200ms;
--shadow-sm: 0 0 0.4em 0.05em rgba(0,0,0,0.2);
--shadow-md: 0 0 0.5em 0.1em rgba(0,0,0,0.3);
--shadow-lg: 0 0 1em 0.1em rgba(0,0,0,0.4);
--shadow-hover: 0.1em 0.1em 0.5em 0.1em rgba(0,0,0,0.3);
--radius-pill: 1000px;
--radius-card: 20px;
--radius-sm: 0.3rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
/* Spacing scale */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-2xl: 3rem;
/* Font size scale */
--text-sm: 0.85rem;
--text-base: 1rem;
--text-lg: 1.1rem;
--text-xl: 1.5rem;
--text-2xl: 2rem;
--text-3xl: 3rem;
}
/* ============================================
@@ -208,65 +246,142 @@ a:focus-visible {
color: var(--color-link-hover);
}
/* ============================================
FORM STYLES
GLOBAL UTILITY CLASSES
============================================ */
form {
background-color: var(--color-bg-secondary);
display: flex;
flex-direction: column;
max-width: 600px;
gap: 0.5em;
margin-inline: auto;
justify-content: center;
align-items: center;
padding-block: 2rem;
margin-block: 2rem;
}
form label {
font-size: 1.2em;
}
form input {
display: block;
font-size: 1.2rem;
}
form:not(.search) button {
background-color: var(--color-accent);
color: var(--color-text-on-accent);
/* Pill-shaped element base */
.g-pill {
border-radius: var(--radius-pill);
border: none;
padding: 0.5em 1em;
font-size: 1.3em;
border-radius: 1000px;
margin-top: 1em;
transition: 100ms;
cursor: pointer;
display: inline-block;
text-decoration: none;
transition: var(--transition-fast);
}
form:not(.search) button:hover,
form:not(.search) button:focus-visible {
background-color: var(--color-accent-hover);
scale: 1.1;
/* Interactive hover/focus effects */
.g-interactive {
transition: var(--transition-fast);
}
.g-interactive:hover,
.g-interactive:focus-visible {
transform: scale(1.05);
box-shadow: var(--shadow-hover);
}
.g-interactive:focus {
scale: 0.9;
}
form:not(.search) button:active {
background-color: var(--color-accent-active);
/* Light background button (with dark mode) */
.g-btn-light {
background-color: var(--nord5);
color: var(--nord0);
box-shadow: var(--shadow-sm);
}
form p {
max-width: 400px;
margin-top: 0;
}
form h4 {
margin-bottom: 0;
}
@media screen and (max-width: 600px) {
form {
margin-top: 0;
@media (prefers-color-scheme: dark) {
.g-btn-light {
background-color: var(--nord0);
color: white;
}
}
/* Dark background button */
.g-btn-dark,
.g-btn-dark:visited,
.g-btn-dark:link {
background-color: var(--nord0);
color: var(--nord6);
box-shadow: var(--shadow-lg);
}
.g-btn-dark:hover,
.g-btn-dark:focus-visible {
background-color: var(--nord1);
color: var(--nord6);
}
/* Icon badge (circular icon container) */
.g-icon-badge {
font-family: "Noto Color Emoji", "Noto Color Emoji Subset", emoji, sans-serif;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
text-decoration: none;
transition: var(--transition-fast);
box-shadow: var(--shadow-lg);
}
.g-icon-badge:hover,
.g-icon-badge:focus-visible {
transform: scale(1.1);
box-shadow: var(--shadow-hover);
}
/* Tag/chip styling */
.g-tag,
.g-tag:visited,
.g-tag:link {
padding: 0.25em 1em;
border-radius: var(--radius-pill);
background-color: var(--nord5);
color: var(--nord0);
text-decoration: none;
cursor: pointer;
transition: transform var(--transition-fast), background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast);
box-shadow: var(--shadow-sm);
border: none;
display: inline-block;
}
.g-tag:hover,
.g-tag:focus-visible {
transform: scale(1.05);
background-color: var(--nord8);
box-shadow: var(--shadow-hover);
color: var(--nord0);
}
@media (prefers-color-scheme: dark) {
.g-tag,
.g-tag:visited,
.g-tag:link {
background-color: var(--nord0);
color: white;
}
.g-tag:hover,
.g-tag:focus-visible {
background-color: var(--nord8);
color: var(--nord0);
}
}
/* ============================================
RECIPE GRID
Responsive card grid used across recipe pages
============================================ */
.recipe-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.8em;
padding: 0 0.8em;
max-width: 1400px;
margin: 0 auto 2em;
}
@media (max-width: 250px) {
.recipe-grid {
grid-template-columns: 1fr;
}
}
@media (min-width: 600px) {
.recipe-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5em;
padding: 0 1.5em;
}
}
@media (min-width: 1024px) {
.recipe-grid {
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.8em;
}
}

View File

@@ -4,6 +4,9 @@
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#5E81AC" />
<link rel="apple-touch-icon" href="/favicon-192.png" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View File

@@ -1,5 +1,5 @@
import { SvelteKitAuth } from "@auth/sveltekit"
import Authentik from "@auth/core/providers/authentik"
import Authentik from "@auth/sveltekit/providers/authentik"
import { AUTHENTIK_ID, AUTHENTIK_SECRET, AUTHENTIK_ISSUER } from "$env/static/private";
export const { handle, signIn, signOut } = SvelteKitAuth({

View File

@@ -1,15 +1,10 @@
import type { Handle, HandleServerError } from "@sveltejs/kit"
import { redirect } from "@sveltejs/kit"
import { error } from "@sveltejs/kit"
import { SvelteKitAuth } from "@auth/sveltekit"
import Authentik from "@auth/core/providers/authentik"
import { AUTHENTIK_ID, AUTHENTIK_SECRET, AUTHENTIK_ISSUER } from "$env/static/private";
import { sequence } from "@sveltejs/kit/hooks"
import * as auth from "./auth"
import { initializeScheduler } from "./lib/server/scheduler"
import { dbConnect } from "./utils/db"
import fs from 'fs'
import path from 'path'
// Initialize database connection on server startup
console.log('🚀 Server starting - initializing database connection...');
@@ -68,9 +63,11 @@ async function authorization({ event, resolve }) {
}
// Bible verse functionality for error pages
async function getRandomVerse(fetch: typeof globalThis.fetch): Promise<any> {
async function getRandomVerse(fetch: typeof globalThis.fetch, pathname: string): Promise<any> {
const isEnglish = pathname.startsWith('/faith/') || pathname.startsWith('/recipes/');
const endpoint = isEnglish ? '/api/faith/bibel/zufallszitat' : '/api/glaube/bibel/zufallszitat';
try {
const response = await fetch('/api/glaube/bibel/zufallszitat');
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -85,11 +82,14 @@ export const handleError: HandleServerError = async ({ error, event, status, mes
console.error('Error occurred:', { error, status, message, url: event.url.pathname });
// Add Bible verse to error context
const bibleQuote = await getRandomVerse(event.fetch);
const bibleQuote = await getRandomVerse(event.fetch, event.url.pathname);
const isEnglish = event.url.pathname.startsWith('/faith/') || event.url.pathname.startsWith('/recipes/');
return {
message: message,
bibleQuote
bibleQuote,
lang: isEnglish ? 'en' : 'de'
};
};

View File

@@ -1 +0,0 @@
{"terminal": "nvimterm"}

View File

@@ -2,7 +2,6 @@
import type { Snippet } from 'svelte';
let { href, ariaLabel = undefined, children } = $props<{ href: string, ariaLabel?: string, children?: Snippet }>();
import "$lib/css/nordtheme.css"
import "$lib/css/action_button.css"
</script>
@@ -14,9 +13,9 @@ right:0;
width: 1rem;
height: 1rem;
padding: 2rem;
border-radius: 1000px;
border-radius: var(--radius-pill);
margin: 2rem;
transition: 200ms;
transition: var(--transition-normal);
background-color: var(--red);
display: grid;
justify-content: center;

View File

@@ -1,70 +0,0 @@
<script>
let { x = 0, y = 0, size = 40 } = $props();
</script>
<svg {x} {y} width={size} height={size} viewBox="0 0 334 326" xmlns="http://www.w3.org/2000/svg">
<path id="path2987" style="fill:#fff" d="m168.72 17.281c-80.677 0-146.06 65.386-146.06 146.06 0 80.677 65.386 146.09 146.06 146.09 80.677 0 146.09-65.417 146.09-146.09 0-80.677-65.417-146.06-146.09-146.06zm2.9062 37.812c21.086 0.35166 41.858 7.6091 59.156 19.688 40.942 26.772 56.481 83.354 38.875 128.22-16.916 45.3-67.116 74.143-114.72 67.844-50.947-5.2807-92.379-52.101-94.563-102.72-4.0889-58.654 48.31-113.56 107.03-113 1.4077-0.03846 2.813-0.05469 4.2188-0.03125z"/>
<path id="rect3812" style="fill:#fff" d="m166.45 51.969c-11.386 0.159-21.538 7.2129-24 12.25 3.2629 3.3685 6.337 8.536 7.375 19.5v159.78c-1.0775 10.727-4.1463 15.792-7.375 19.125 2.4156 4.9422 12.251 11.811 23.375 12.219v0.0312h4.8124c11.386-0.159 21.538-7.2129 24-12.25-3.2629-3.3685-6.337-8.536-7.375-19.5v-159.78c1.0775-10.727 4.1463-15.792 7.375-19.125-2.41-4.938-12.25-11.807-23.37-12.215v-0.03125h-4.8124z"/>
<path id="path3846" style="fill:#fff" d="m280 161.33c-0.159-11.386-7.2129-21.538-12.25-24-3.3685 3.2629-8.536 6.337-19.5 7.375h-159.78c-10.727-1.0775-15.792-4.1463-19.125-7.375-4.9422 2.4156-11.811 12.251-12.219 23.375h-0.0312v4.8124c0.159 11.386 7.2129 21.538 12.25 24 3.3685-3.2629 8.536-6.337 19.5-7.375h159.78c10.727 1.0775 15.792 4.1463 19.125 7.375 4.9422-2.4156 11.811-12.251 12.219-23.375h0.0312v-4.8124z"/>
<path id="path3848" style="fill:#fff" transform="matrix(.86578 0 0 .86578 78.719 48.374)" d="m69.839 72.529c0 14.565-11.807 26.373-26.373 26.373-14.565 0-26.373-11.807-26.373-26.373 0-14.565 11.807-26.373 26.373-26.373 14.565 0 26.373 11.807 26.373 26.373z"/>
<path id="path3848-4" style="fill:#fff" transform="matrix(.86578 0 0 .86578 182.94 48.396)" d="m69.839 72.529c0 14.565-11.807 26.373-26.373 26.373-14.565 0-26.373-11.807-26.373-26.373 0-14.565 11.807-26.373 26.373-26.373 14.565 0 26.373 11.807 26.373 26.373z"/>
<path id="path3848-0" style="fill:#fff" transform="matrix(.86578 0 0 .86578 78.848 152.7)" d="m69.839 72.529c0 14.565-11.807 26.373-26.373 26.373-14.565 0-26.373-11.807-26.373-26.373 0-14.565 11.807-26.373 26.373-26.373 14.565 0 26.373 11.807 26.373 26.373z"/>
<path id="path3848-9" style="fill:#fff" transform="matrix(.86578 0 0 .86578 183.14 152.6)" d="m69.839 72.529c0 14.565-11.807 26.373-26.373 26.373-14.565 0-26.373-11.807-26.373-26.373 0-14.565 11.807-26.373 26.373-26.373 14.565 0 26.373 11.807 26.373 26.373z"/>
<g id="text3882" stroke-linejoin="round" transform="matrix(.99979 .020664 -.020664 .99979 2.2515 -4.8909)" stroke="var(--nord2)" stroke-linecap="round" fill="var(--nord2)">
<path id="path3054" d="m125.64 31.621-0.0722-0.2536 7.1008-2.0212c2.5247-0.71861 4.4393-0.95897 5.7437-0.72108 1.3044 0.23795 2.3382 0.70216 3.1013 1.3926 0.76311 0.69054 1.3404 1.7233 1.7318 3.0984 0.59992 2.1077 0.32369 3.8982-0.82869 5.3715-1.1524 1.4734-2.9805 2.542-5.4842 3.2059l-0.0674-0.23669c1.5779-0.44913 2.7221-1.2987 3.4324-2.5488 0.71031-1.25 0.84089-2.664 0.39175-4.242-0.34971-1.2285-0.89961-2.2508-1.6497-3.0669-0.75012-0.81603-1.6297-1.3485-2.6387-1.5974-1.009-0.24887-2.404-0.11987-4.1848 0.387l-1.7752 0.50529 6.0683 21.319c0.34327 1.206 0.68305 1.886 1.0193 2.0401 0.33626 0.15406 0.88198 0.12362 1.6372-0.09134l0.0674 0.23669-6.5767 1.872-0.0674-0.23669c1.1722-0.33365 1.6059-1.0359 1.3011-2.1066l-5.871-20.626c-0.25024-0.87912-0.52897-1.4303-0.8362-1.6536-0.30723-0.22322-0.82152-0.23218-1.5429-0.02688z"/>
<path id="path3056" d="m169.99 38.101-7.5573 0.14169-3.7357 9.8628c-0.2563 0.70806-0.38292 1.1441-0.37984 1.3081 0.007 0.38665 0.29793 0.57459 0.87205 0.56383l0.005 0.24605-3.8489 0.07216-0.005-0.24605c0.51554-0.0097 0.94669-0.14375 1.2935-0.40225 0.34678-0.2585 0.72118-0.91895 1.1232-1.9814l8.9409-23.867 0.43938-0.0082 9.6735 22.709c0.002 0.11717 0.24981 0.66634 0.74293 1.6475 0.49306 0.98116 1.2258 1.4626 2.1984 1.4444l0.005 0.24605-6.0458 0.11335-0.005-0.24605c0.55067-0.01032 0.82195-0.23224 0.81384-0.66576-0.006-0.29292-0.14904-0.75906-0.43059-1.3984-0.0478-0.04599-0.0902-0.12138-0.12731-0.22617-0.0257-0.11672-0.0443-0.17498-0.056-0.17476zm-7.3247-0.5835 6.9949-0.13115-3.6628-8.7571z"/>
<path id="path3058" d="m215.05 32.098-0.0754 0.25265c-0.67276-0.28643-1.3926-0.12832-2.1595 0.47432-0.0112-0.0033-0.0331 0.0085-0.0656 0.03545-0.0213 0.03035-0.0465 0.0534-0.0757 0.06913l-0.19006 0.14505c-0.0763 0.05063-0.11777 0.08716-0.12445 0.10961l-0.21872 0.11815-8.9764 7.0246 4.3093 13.156c0.45546 1.5057 0.85105 2.4952 1.1868 2.9684 0.33566 0.47322 0.71066 0.75333 1.125 0.84033l-0.0704 0.23581-6.1647-1.8404 0.0704-0.23581c0.51768 0.19124 0.83688 0.08474 0.95758-0.3195 0.0972-0.32565 0.0472-0.79308-0.15004-1.4023l-3.7548-11.449-9.4239 7.2945c-0.003 0.01123-0.19115 0.16919-0.56339 0.47387-0.41047 0.26882-0.6576 0.54359-0.7414 0.82431-0.10057 0.33687 0.13489 0.57227 0.70641 0.7062l-0.0704 0.23581-3.7056-1.1063 0.0704-0.23581c0.53899 0.16091 1.0726 0.09397 1.6009-0.20082l0.37685-0.2177 11.393-8.8529-4.2181-12.908c0.0469-0.15718-0.0793-0.51283-0.37858-1.0669-0.29932-0.55407-0.71956-0.88743-1.2607-1.0001l0.0754-0.25265 6.249 1.8656-0.0754 0.25265c-0.67376-0.20112-1.0659-0.11641-1.1766 0.25413-0.0805 0.26952-0.002 0.76385 0.23602 1.483l3.0954 9.4177 8.2667-6.4293c0.0145-0.0079 0.14851-0.13908 0.40187-0.39368 0.25331-0.25456 0.39171-0.42114 0.4152-0.49977 0.057-0.19088 0.034-0.32919-0.0688-0.41494-0.10284-0.08571-0.32269-0.17886-0.65953-0.27945l0.0754-0.25265z"/>
</g>
<path id="path3892" d="m131.41 57.101c22.962-9.0656 53.003-10.067 77.513 0.96671" fill="none"/>
<g id="text3882-4" stroke-linejoin="round" stroke="var(--nord2)" stroke-linecap="round" fill="var(--nord2)">
<path id="path3038" d="m235.06 72.827-0.26589-0.20212 6.4642-23.724c0.44911-1.6752 0.58908-2.7501 0.4199-3.2247-0.16922-0.47452-0.43107-0.84654-0.78557-1.116l0.15956-0.20991 4.758 3.6168-0.15956 0.20991c-0.39857-0.3471-0.73258-0.3434-1.002 0.01109-0.10639 0.13996-0.19187 0.3105-0.25644 0.51163l-5.6785 20.745 18.416-11.04c0.24704-0.15074 0.42022-0.29142 0.51954-0.42204 0.24109-0.31719 0.10378-0.64972-0.41192-0.99761l0.15957-0.20991 3.0927 2.3509-0.15957 0.20991c-0.21461-0.1631-0.46389-0.30476-0.74785-0.42496-0.28403-0.12019-0.71153-0.06612-1.2825 0.16221-0.57105 0.22835-0.87187 0.35297-0.90244 0.37386z"/>
<path id="path3040" d="m278.77 79.16 0.22305-0.14062 3.9185 6.2156c0.99367 1.5762 1.6777 2.7381 2.052 3.4857 0.37431 0.74758 0.59849 1.6141 0.67255 2.5995 0.074 0.98539-0.10481 1.8774-0.53644 2.6759-0.43168 0.79855-1.1332 1.5041-2.1047 2.1165-1.1103 0.69996-2.3795 1.0325-3.8075 0.99774-1.4281-0.0348-2.8734-0.44312-4.336-1.225l-1.4042 3.0464c-0.38231 0.71202-1.1219 2.4285-2.2186 5.1495-1.0968 2.721-1.6158 4.617-1.5569 5.6882 0.0588 1.0711 0.3226 1.9785 0.79134 2.722 0.46246 0.73355 1.2762 1.3981 2.4412 1.9935l-0.24115 0.27671c-1.7215-0.57713-3.1822-1.8174-4.3821-3.7207-0.79997-1.2689-1.1132-2.5953-0.93972-3.9791 0.17346-1.3839 1.233-4.2683 3.1785-8.6534 0.007-0.03233 0.0209-0.05474 0.0407-0.06724l2.0029-4.3381-1.2875-1.9104-1.1156-1.7695-8.3865 5.2872c-0.6741 0.42497-1.1011 0.81886-1.2811 1.1817-0.17994 0.3628-0.0543 0.8862 0.37693 1.5702l-0.20818 0.13124-3.5717-5.6654 0.20818-0.13124c0.47346 0.68508 0.89871 1.03 1.2757 1.0347 0.37702 0.0047 0.97693-0.25225 1.7997-0.77097l17.412-10.978c0.56503-0.35622 0.98655-0.72586 1.2646-1.1089 0.27798-0.38305 0.18445-0.9544-0.28059-1.7141zm2.2305 4.329-10.275 6.4778 1.4437 2.2899c1.1374 1.8042 2.4074 2.8737 3.8099 3.2086 1.4025 0.33488 2.7977 0.06485 4.1855-0.8101 1.3482-0.84996 2.3542-2.0833 3.0181-3.7002 0.66383-1.6168 0.31454-3.5058-1.0478-5.6668z"/>
<path id="path3043" d="m304.97 139.01-2.8793 1.9196-0.0812-0.19782c0.0114-0.003 0.27603-0.39661 0.7939-1.182 0.51781-0.78539 0.8634-1.6276 1.0368-2.5266 0.17331-0.89905 0.14258-1.8627-0.0922-2.8909-0.39397-1.7251-1.2005-2.9804-2.4196-3.7658-1.2191-0.78541-2.5256-1.019-3.9194-0.7007-0.92542 0.21132-1.6796 0.63296-2.2625 1.2649-0.58294 0.63196-0.99926 1.7698-1.249 3.4135-0.27608 2.7917-0.52511 4.7147-0.7471 5.7691-0.22203 1.0544-0.75747 2.1443-1.6063 3.2697-0.84889 1.1254-2.0959 1.876-3.741 2.2517-0.68549 0.15652-1.3578 0.22591-2.017 0.20816-0.65917-0.0178-1.3177-0.13787-1.9755-0.36027-0.65783-0.22243-1.2805-0.52799-1.8681-0.91668-0.58762-0.38872-1.0629-0.81208-1.426-1.2701-0.36304-0.45803-0.73392-1.2268-1.1126-2.3063-0.37873-1.0795-0.60853-1.7963-0.68941-2.1505l-1.5379-6.7348 7.3346-1.6749 0.0587 0.25706c-2.4282 0.57854-3.9944 1.5372-4.6984 2.876-0.70401 1.3388-0.78599 3.1906-0.24595 5.5555 0.49047 2.1478 1.3713 3.6626 2.6424 4.5443 1.2712 0.8817 2.6208 1.1595 4.0489 0.8334 0.86826-0.19828 1.5637-0.56143 2.0862-1.0894 0.5225-0.52802 0.93554-1.1933 1.2391-1.9959 0.30354-0.80258 0.56488-2.2506 0.78402-4.3441 0.23314-2.0847 0.51456-3.5764 0.84426-4.4751 0.32968-0.89869 0.94577-1.7275 1.8483-2.4866 0.90249-0.75903 1.885-1.2599 2.9475-1.5025 1.9536-0.44612 3.7463-0.14929 5.3781 0.89047s2.6968 2.6507 3.1952 4.8328c0.19303 0.84542 0.30463 1.7816 0.3348 2.8084 0.0301 1.0269 0.0297 1.604-0.001 1.7312-0.0124 0.0509-0.0134 0.0992-0.003 0.14492z"/>
<path id="path3045" d="m305.53 190.02-0.62918 4.9701-0.26159-0.0331c0.0724-0.75866-0.0212-1.3021-0.28088-1.6302-0.25969-0.32821-0.86619-0.55264-1.8195-0.6733l-23.386-2.9605 24.41-17.729-19.008-2.4064c-0.60455-0.0765-1.058-0.0867-1.3605-0.0305-0.30242 0.0562-0.51699 0.16489-0.6437 0.32604-0.12671 0.16113-0.28653 0.58386-0.47947 1.2682l-0.24415-0.0309 0.62477-4.9352 0.24414 0.0309c-0.11185 0.88357-0.0244 1.4528 0.26223 1.7076 0.28667 0.25481 1.0229 0.45728 2.2088 0.6074l17.387 2.201c1.523 0.1928 2.7938 0.1381 3.8125-0.16408 1.0186-0.3022 1.5902-1.225 1.7148-2.7685l0.26159 0.0331-0.61153 4.8306-22.945 16.656 18.136 2.296c0.97656 0.1236 1.5945 0.0246 1.8537-0.29688 0.25922-0.32158 0.42342-0.75557 0.49261-1.302z"/>
<path id="path3047" d="m289.19 232.48-3.433-0.43565 0.0682-0.20268c0.0103 0.005 0.46837-0.11886 1.3741-0.37304 0.90573-0.25423 1.7186-0.66421 2.4385-1.2299 0.71988-0.56577 1.3279-1.314 1.824-2.2447 0.83238-1.5615 1.0453-3.0383 0.63863-4.4303s-1.2408-2.4243-2.5024-3.0969c-0.83765-0.44653-1.6837-0.62197-2.5381-0.52631-0.85443 0.0956-1.9143 0.68265-3.1797 1.761-2.0373 1.9285-3.4852 3.2184-4.3436 3.8696-0.85845 0.65123-1.977 1.124-3.3556 1.4183-1.3786 0.29426-2.8125 0.0445-4.3016-0.7493-0.62047-0.33077-1.1739-0.71876-1.6603-1.164-0.48641-0.44522-0.90529-0.96731-1.2566-1.5663-0.35134-0.59898-0.62169-1.2378-0.81106-1.9164-0.18935-0.67863-0.27117-1.3099-0.24544-1.8937 0.0257-0.58389 0.24909-1.4077 0.67005-2.4714 0.42097-1.0637 0.7169-1.7559 0.88779-2.0765l3.2497-6.0961 6.639 3.5391-0.12403 0.23268c-2.2137-1.1535-4.025-1.4551-5.4339-0.90469-1.4089 0.55038-2.6839 1.8959-3.825 4.0365-1.0364 1.9441-1.3631 3.6656-0.98022 5.1646 0.38289 1.4989 1.2207 2.5929 2.5133 3.282 0.78593 0.41894 1.5492 0.60008 2.2899 0.54342 0.74068-0.0567 1.4886-0.28881 2.2436-0.69635 0.75509-0.40756 1.9011-1.3305 3.438-2.7687 1.5418-1.4224 2.7315-2.3652 3.5693-2.8282 0.83779-0.46308 1.8462-0.68576 3.0254-0.66807 1.1791 0.0177 2.2495 0.28285 3.2113 0.79553 1.7683 0.94264 2.9284 2.3412 3.4803 4.1958 0.55182 1.8545 0.30129 3.7694-0.75159 5.7446-0.40795 0.76523-0.93686 1.5457-1.5867 2.3413-0.6499 0.7956-1.0282 1.2313-1.1351 1.3072-0.0428 0.0303-0.0751 0.0662-0.0972 0.10756z"/>
<path id="path3049" d="m253.71 273.01-3.0849 2.6673-0.17245-0.19946c0.99118-0.94999 1.0384-1.9435 0.14161-2.9807-0.19161-0.22165-0.48263-0.50449-0.87307-0.84851l-14.878-13.069c-0.76791-0.63734-1.3195-0.9931-1.6548-1.0673-0.33522-0.0742-0.76579 0.0773-1.2917 0.45457l-0.16096-0.18616 4.4013-3.8055 0.16096 0.18616c-0.59151 0.48045-0.63435 1.0132-0.1285 1.5983 0.11499 0.13295 0.36769 0.37146 0.75811 0.71554l14.434 12.663-6.994-22.279 0.13297-0.11497 21.053 10.078-10.527-16.18c-0.15615-0.25228-0.35687-0.52025-0.60214-0.80391-0.44455-0.51416-0.96379-0.51447-1.5577-0.00093l-0.16096-0.18616 2.9918-2.5868 0.16096 0.18616c-0.4521 0.39089-0.69259 0.69953-0.72149 0.92591s0.0266 0.49211 0.16644 0.79721c0.13987 0.30509 0.32237 0.62367 0.54751 0.95573l11.448 17.755c1.1222 0.56333 1.9417 0.77653 2.4585 0.63959 0.51673-0.13698 0.94353-0.35109 1.2804-0.64232l0.17246 0.19945-3.6966 3.1962-20.742-10.067z"/>
<path id="path3051" d="m205.18 269.68 0.31132-0.12093 16.841 17.917c1.193 1.2589 2.036 1.9403 2.529 2.0443 0.49295 0.10393 0.94698 0.0753 1.3621-0.0859l0.0955 0.24578-5.571 2.164-0.0955-0.24578c0.50429-0.1582 0.67581-0.44484 0.51457-0.85991-0.0636-0.16387-0.16431-0.32592-0.30198-0.48614l-14.712-15.689-0.2212 21.471c-0.0007 0.2894 0.0286 0.51058 0.088 0.66354 0.14428 0.37137 0.49953 0.42824 1.0657 0.17061l0.0955 0.24578-3.6212 1.4066-0.0955-0.24578c0.25125-0.0976 0.50236-0.23603 0.75332-0.4152 0.25098-0.17924 0.42846-0.57191 0.53244-1.178 0.104-0.60616 0.15509-0.92773 0.15327-0.96471z"/>
</g>
<path id="path3042" d="m224.89 65.361c81.253 49.938 78.324 173.23-27.662 207.57" fill="none"/>
<g id="text3882-4-4" stroke-linejoin="round" stroke="var(--nord2)" stroke-linecap="round" fill="var(--nord2)">
<path id="path3023" d="m113.7 291.67 0.12516-3.4583 0.20799 0.0497c-0.005 0.0108 0.1605 0.45578 0.49511 1.335 0.33465 0.87919 0.81606 1.6518 1.4442 2.318 0.62822 0.66608 1.4281 1.2043 2.3996 1.6148 1.6301 0.68858 3.12 0.76779 4.4698 0.23762 1.3498-0.53021 2.3029-1.4538 2.8593-2.7708 0.3694-0.87442 0.46804-1.7328 0.29593-2.5751-0.17209-0.84236-0.85204-1.8452-2.0399-3.0085-2.1039-1.8556-3.5187-3.1816-4.2446-3.978-0.7258-0.7964-1.2972-1.8679-1.7143-3.2144-0.41705-1.3466-0.29725-2.7971 0.35942-4.3516 0.27363-0.6477 0.61027-1.2338 1.0099-1.7583 0.39968-0.52448 0.88198-0.98862 1.4469-1.3924 0.56495-0.40378 1.1768-0.73048 1.8357-0.98011 0.65885-0.24961 1.2802-0.38787 1.864-0.41475 0.58383-0.0269 1.4244 0.12148 2.5217 0.44508s1.8132 0.55609 2.1479 0.69745l6.3637 2.6883-2.9277 6.9304-0.24288-0.1026c0.94975-2.3085 1.0872-4.1396 0.41234-5.4932-0.67485-1.3537-2.1296-2.5025-4.3641-3.4465-2.0295-0.85733-3.7734-1.0279-5.2318-0.5118-1.4584 0.51614-2.4726 1.4489-3.0426 2.7983-0.34656 0.82043-0.45832 1.5969-0.33528 2.3295 0.12307 0.73258 0.4215 1.4566 0.89529 2.1719 0.47383 0.71537 1.4961 1.7737 3.0667 3.1751 1.5553 1.4076 2.6012 2.5078 3.1378 3.3005 0.53654 0.79274 0.84901 1.7771 0.93743 2.953 0.0884 1.1759-0.0794 2.2658-0.50351 3.2698-0.7798 1.8459-2.0684 3.1271-3.8658 3.8435-1.7974 0.71637-3.727 0.63906-5.7889-0.23193-0.79882-0.33748-1.6237-0.79406-2.4745-1.3697-0.85083-0.57572-1.3188-0.91336-1.404-1.0129-0.034-0.0398-0.0726-0.0689-0.11586-0.0871z"/>
<path id="path3025" d="m68.299 260.77-3.0403-2.718 0.17573-0.19658c1.0691 0.86139 2.0605 0.78098 2.9743-0.2412 0.19529-0.21842 0.43854-0.54326 0.72973-0.97453l11.056-16.429c0.53378-0.8432 0.81598-1.4358 0.8466-1.7778 0.03067-0.34197-0.17473-0.74959-0.61622-1.2229l0.16402-0.18347 4.3377 3.8778-0.16402 0.18347c-0.55224-0.52512-1.0861-0.49938-1.6016 0.0772-0.11713 0.13106-0.32133 0.41222-0.61258 0.84348l-10.71 15.937 21.201-9.7892 0.13105 0.11715-7.2989 22.17 14.699-12.513c0.23021-0.18718 0.47027-0.42055 0.7202-0.70012 0.453-0.50672 0.38682-1.0217-0.19854-1.545l0.16402-0.18347 2.9486 2.636-0.16402 0.18347c-0.44556-0.39832-0.78245-0.59732-1.0107-0.597-0.22821 0.00033-0.48466 0.0894-0.76934 0.26716-0.28467 0.17777-0.57725 0.39956-0.87775 0.66536l-16.14 13.63c-0.415 1.1851-0.52151 2.0252-0.31953 2.5202 0.20202 0.49494 0.46901 0.89081 0.80098 1.1876l-0.17573 0.19657-3.6432-3.2569 7.3287-21.86z"/>
<path id="path3027" d="m39.292 218.38c-1.4886-3.3138-1.6984-6.4762-0.62946-9.4873 1.069-3.011 3.1108-5.1937 6.1252-6.5479 3.8804-1.7431 7.6935-1.9915 11.439-0.74522 3.7459 1.2464 6.5241 3.8845 8.3344 7.9145 1.4694 3.271 1.6034 6.429 0.40185 9.4739-1.2015 3.0449-3.2988 5.2396-6.292 6.5842-2.0203 0.90759-4.3198 1.3368-6.8985 1.2876-2.5786-0.0492-4.9925-0.78268-7.2417-2.2004-2.2491-1.4177-3.9956-3.5108-5.2393-6.2795zm23.133-10.276c-0.7299-1.6248-1.8858-3.0326-3.4677-4.2233s-3.3413-1.897-5.2781-2.119c-1.9368-0.22191-3.7123 0.0297-5.3264 0.75478-2.2876 1.0277-4.0918 2.5736-5.4127 4.638-1.3208 2.0644-2.0722 4.3096-2.2541 6.7359-0.18187 2.4263 0.09934 4.4679 0.84363 6.1248 1.0517 2.341 2.888 4.0694 5.5089 5.1852 2.621 1.1158 5.241 1.0854 7.8599-0.0911 3.3459-1.503 5.7621-3.9888 7.2487-7.4572s1.5792-6.6511 0.27787-9.548z"/>
<path id="path3029" d="m54.305 176.72-0.24585 0.0109c-0.03577-0.8078-0.21116-1.325-0.52618-1.5515-0.31502-0.22652-0.8413-0.32345-1.5789-0.29079l-21.178 0.93782c-0.74924 0.0332-1.2866 0.15082-1.6119 0.35291-0.32534 0.20209-0.47899 0.77194-0.46096 1.7096l-0.26341 0.0117-0.30716-6.9366 0.26341-0.0117c0.02854 0.64391 0.13045 1.091 0.30573 1.3413 0.17533 0.25031 0.37602 0.41151 0.60206 0.48361 0.22609 0.0721 0.73132 0.0908 1.5157 0.056l18.158-0.80407c1.5571-0.0689 2.638-0.49218 3.2429-1.2697 0.60487-0.77752 0.86712-2.0736 0.78677-3.8882l-0.141-3.16c-0.06739-1.5219-0.4914-2.6205-1.272-3.2956-0.78063-0.67509-2.2088-1.0077-4.2847-0.99795l-0.01089-0.24585 6.2166-0.27528z"/>
<path id="path3031" d="m32.995 125.27 0.25545 0.0653c-0.11822 0.32055-0.16075 0.69977-0.12759 1.1376 0.03321 0.4379 0.17094 0.72712 0.41317 0.86767 0.24228 0.14058 0.85161 0.33569 1.828 0.58533l19.261 4.9248c1.0445 0.26708 1.694 0.38779 1.9486 0.36214 0.25452-0.0256 0.48962-0.14091 0.70531-0.34582 0.21568-0.20491 0.40336-0.61958 0.56302-1.244l0.23842 0.061-1.7113 6.6929-0.23842-0.061c0.21482-0.84016 0.18656-1.4038-0.08476-1.6909-0.27132-0.28709-0.98033-0.57724-2.127-0.87044l-19.074-4.8769c-1.1921-0.3048-2.0017-0.39084-2.4287-0.25812-0.42702 0.13273-0.71944 0.60227-0.87726 1.4086l-0.25545-0.0653z"/>
<path id="path3033" d="m71.729 102.86-0.18056 0.28098-24.16-4.5763c-1.7054-0.31583-2.788-0.37074-3.2477-0.16472-0.45973 0.20605-0.80997 0.49638-1.0507 0.871l-0.22182-0.14254 3.231-5.0279 0.22182 0.14254c-0.31464 0.42465-0.28466 0.75734 0.08995 0.99806 0.1479 0.09505 0.32464 0.16684 0.53023 0.21536l21.127 4.0277-12.455-17.49c-0.16972-0.23441-0.3236-0.39597-0.46163-0.4847-0.33518-0.21537-0.65588-0.05231-0.96208 0.48918l-0.22182-0.14254 2.1001-3.2682 0.22182 0.14254c-0.1457 0.22678-0.26729 0.48645-0.36477 0.77899-0.09746 0.29261-0.0099 0.71453 0.26268 1.2658 0.2726 0.5513 0.42051 0.84137 0.44374 0.8702z"/>
<path id="path3035" d="m76.563 57.963-0.15835-0.21082 5.383-4.0433c1.5742-1.1823 2.7385-1.9543 3.4929-2.3158 0.75443-0.36145 1.5705-0.58234 2.4482-0.66269 0.87768-0.08029 1.7895 0.07023 2.7355 0.45156 0.94598 0.38138 1.7533 1.0171 2.4219 1.9073 1.3161 1.7522 1.4922 3.7818 0.52818 6.0887 1.6798-0.86602 3.267-1.0945 4.7614-0.68544 1.4944 0.40911 2.766 1.3117 3.8146 2.7078 0.97826 1.3024 1.543 2.7323 1.6941 4.2896s-0.0607 2.8266-0.63536 3.8079c-0.5747 0.98128-1.6163 2.0385-3.1249 3.1716l-7.9973 6.0069-0.1478-0.19677c0.63716-0.47858 0.94798-0.91357 0.93246-1.305-0.01552-0.39139-0.42092-1.1165-1.2162-2.1753l-11.718-15.602c-0.56302-0.74958-0.96767-1.2444-1.214-1.4845-0.24625-0.24004-0.53578-0.33768-0.86857-0.29293-0.33277 0.04479-0.70998 0.22553-1.1316 0.54222zm4.02-2.6677 6.8303 9.0936 2.5158-1.8897c1.7053-1.2809 2.4708-2.618 2.2964-4.0112-0.17444-1.3932-0.6241-2.5724-1.349-3.5375-0.78121-1.04-1.6495-1.7472-2.6048-2.1216-0.95534-0.37429-1.9814-0.42805-3.078-0.16128-1.0967 0.26683-2.4508 1.0055-4.0625 2.216zm8.7739 8.3152-1.6163 1.214 4.9301 6.5637c0.68268 0.90889 1.1612 1.4728 1.4356 1.6917 0.27437 0.21895 0.64832 0.38509 1.1218 0.49842 0.47351 0.11334 1.0553 0.05374 1.7454-0.17879 0.69006-0.23252 1.5551-0.73939 2.5952-1.5206 1.6116-1.2105 2.4703-2.4381 2.5761-3.6827 0.10574-1.2446-0.39035-2.5978-1.4883-4.0595-1.1402-1.5179-2.3643-2.5331-3.6725-3.0454-1.3082-0.51233-2.4121-0.59181-3.3119-0.23845-0.89975 0.3534-2.3382 1.2726-4.3152 2.7576z"/>
</g>
<path id="path3087" d="m136.47 273.26c-103.66-36.68-111.66-168.69-13.78-214.23" fill="none"/>
<g id="text3882-7-5" stroke-linejoin="round" stroke="var(--nord2)" stroke-linecap="round" fill="var(--nord2)">
<path id="path3072" d="m177.99 64.817 0.96679 2.7422h-0.24609c-2.5078-2.4961-5.461-3.7441-8.8594-3.7441-3.5274 0.000026-6.3779 1.0489-8.5518 3.1465-2.1738 2.0977-3.2608 4.6348-3.2607 7.6113-0.00001 1.8399 0.48339 3.9551 1.4502 6.3457 0.96679 2.3906 2.4932 4.3564 4.5791 5.8975 2.0859 1.541 4.6113 2.3115 7.5762 2.3115 1.9453 0 3.8525-0.31348 5.7217-0.94043 1.8691-0.62695 3.2607-1.4678 4.1748-2.5225l0.22851 0.07031-1.8105 2.7773c-2.9297 0.63281-4.8311 1.0078-5.7041 1.125-0.87307 0.11719-2.042 0.17578-3.5068 0.17578-5.4258 0-9.3076-1.2568-11.646-3.7705-2.3379-2.5137-3.5068-5.5225-3.5068-9.0264-0.00001-2.3672 0.60058-4.6435 1.8018-6.8291 1.2012-2.1855 2.9326-3.9082 5.1943-5.168 2.2617-1.2597 4.8457-1.8896 7.752-1.8896 1.4531 0.000026 2.7099 0.13186 3.7705 0.39551 1.0605 0.2637 1.9424 0.53616 2.6455 0.81738l1.1602 0.43945c0.0351 0.01174 0.0586 0.02346 0.0703 0.03516z"/>
<path id="path3074" d="m173.39 106.17 1.2305 3.2344-0.21094 0.0352c-0.00002-0.0117-0.32521-0.3574-0.97559-1.0371-0.6504-0.67966-1.3945-1.2041-2.2324-1.5732-0.8379-0.36911-1.7842-0.55369-2.8389-0.55371-1.7695 0.00002-3.1728 0.50686-4.21 1.5205-1.0371 1.0137-1.5557 2.2354-1.5557 3.665 0 0.94923 0.24316 1.7783 0.72949 2.4873s1.5029 1.3682 3.0498 1.9775c2.6601 0.89064 4.4795 1.5615 5.458 2.0127 0.97851 0.45118 1.9219 1.2158 2.8301 2.2939 0.90819 1.0781 1.3623 2.461 1.3623 4.1484-0.00002 0.70313-0.0821 1.374-0.2461 2.0127-0.16408 0.63868-0.42775 1.2539-0.79101 1.8457-0.3633 0.5918-0.79982 1.1309-1.3096 1.6172-0.50978 0.48633-1.0283 0.85547-1.5557 1.1074-0.52735 0.25195-1.3594 0.44238-2.4961 0.57129-1.1367 0.1289-1.8867 0.19335-2.25 0.19335h-6.9082v-7.5234h0.26367c0.0234 2.4961 0.60937 4.2363 1.7578 5.2207 1.1484 0.98438 2.9355 1.4766 5.3613 1.4766 2.2031 0 3.876-0.52148 5.0186-1.5644 1.1426-1.043 1.7139-2.2969 1.7139-3.7617-0.00001-0.89062-0.19923-1.6494-0.59766-2.2764-0.39845-0.62694-0.95509-1.1777-1.6699-1.6523-0.71485-0.4746-2.0684-1.0518-4.0605-1.7314-1.9805-0.69139-3.3721-1.2978-4.1748-1.8193-0.80274-0.52147-1.4736-1.3066-2.0127-2.3555-0.53907-1.0488-0.8086-2.1182-0.8086-3.208 0-2.0039 0.68848-3.6855 2.0654-5.0449 1.377-1.3594 3.1846-2.039 5.4228-2.0391 0.86718 0.00002 1.8047 0.0996 2.8125 0.29883 1.0078 0.19924 1.5703 0.32815 1.6875 0.38671 0.0469 0.0235 0.0937 0.0352 0.14063 0.0352z"/>
<path id="path3076" d="m173.39 148.48 1.2305 3.2344-0.21094 0.0352c-0.00002-0.0117-0.32521-0.3574-0.97559-1.0371-0.6504-0.67966-1.3945-1.2041-2.2324-1.5732-0.8379-0.36911-1.7842-0.55368-2.8389-0.55371-1.7695 0.00003-3.1728 0.50686-4.21 1.5205-1.0371 1.0137-1.5557 2.2354-1.5557 3.665 0 0.94924 0.24316 1.7783 0.72949 2.4873s1.5029 1.3682 3.0498 1.9775c2.6601 0.89064 4.4795 1.5615 5.458 2.0127 0.97851 0.45118 1.9219 1.2158 2.8301 2.2939 0.90819 1.0781 1.3623 2.461 1.3623 4.1484-0.00002 0.70313-0.0821 1.374-0.2461 2.0127-0.16408 0.63867-0.42775 1.2539-0.79101 1.8457-0.3633 0.5918-0.79982 1.1309-1.3096 1.6172-0.50978 0.48633-1.0283 0.85547-1.5557 1.1074-0.52735 0.25195-1.3594 0.44238-2.4961 0.57129-1.1367 0.1289-1.8867 0.19336-2.25 0.19336h-6.9082v-7.5234h0.26367c0.0234 2.4961 0.60937 4.2363 1.7578 5.2207 1.1484 0.98438 2.9355 1.4766 5.3613 1.4766 2.2031 0.00001 3.876-0.52148 5.0186-1.5644 1.1426-1.043 1.7139-2.2969 1.7139-3.7617-0.00001-0.89062-0.19923-1.6494-0.59766-2.2764-0.39845-0.62695-0.95509-1.1777-1.6699-1.6524-0.71485-0.4746-2.0684-1.0517-4.0605-1.7314-1.9805-0.6914-3.3721-1.2978-4.1748-1.8193-0.80274-0.52147-1.4736-1.3066-2.0127-2.3555-0.53907-1.0488-0.8086-2.1181-0.8086-3.208 0-2.0039 0.68848-3.6855 2.0654-5.0449 1.377-1.3594 3.1846-2.039 5.4228-2.0391 0.86718 0.00003 1.8047 0.0996 2.8125 0.29883 1.0078 0.19924 1.5703 0.32815 1.6875 0.38672 0.0469 0.0235 0.0937 0.0352 0.14063 0.0352z"/>
<path id="path3078" d="m177.34 190.47h4.0781v0.26367c-1.3711 0.0703-2.0567 0.79104-2.0566 2.1621-0.00003 0.29299 0.0351 0.69729 0.10547 1.2129l2.707 19.617c0.16403 0.98438 0.3486 1.6143 0.55371 1.8896 0.20505 0.27539 0.62985 0.44238 1.2744 0.50097v0.2461h-5.8184v-0.2461c0.76169 0.0234 1.1426-0.35156 1.1426-1.125-0.00002-0.17577-0.0352-0.52148-0.10546-1.0371l-2.6367-19.02-9.2812 21.428h-0.17578l-9.334-21.393-2.6191 19.125c-0.0469 0.29297-0.0703 0.62696-0.0703 1.002 0 0.67969 0.39257 1.0195 1.1777 1.0195v0.2461h-3.9551v-0.2461c0.59766 0.00001 0.98145-0.0762 1.1514-0.22851 0.16992-0.15234 0.30176-0.38965 0.39551-0.71191 0.0937-0.32227 0.16406-0.68262 0.21094-1.0811l2.9531-20.918c-0.48047-1.1601-0.96094-1.8574-1.4414-2.0918-0.48048-0.23434-0.94337-0.35153-1.3887-0.35156v-0.26367h4.8867l9.1055 21.182z"/>
<path id="path3080" d="m158.85 258.68v-0.24609c0.80859 0 1.333-0.15234 1.5732-0.45703 0.24023-0.30469 0.36035-0.82617 0.36035-1.5645v-21.199c0-0.74998-0.0937-1.292-0.28125-1.626-0.1875-0.33396-0.75-0.51267-1.6875-0.53613v-0.26368h6.9434v0.26368c-0.64454 0.00002-1.0957 0.0821-1.3535 0.24609-0.25782 0.16409-0.42774 0.35745-0.50977 0.58008-0.082 0.22268-0.12305 0.72659-0.12305 1.5117v18.176c0 1.5586 0.375 2.6572 1.125 3.2959 0.75 0.63867 2.0332 0.95801 3.8496 0.958h3.1641c1.5234 0.00001 2.6396-0.37499 3.3486-1.125 0.70897-0.74999 1.1045-2.1621 1.1865-4.2363h0.2461v6.2226z"/>
</g>
<rect id="rect4001" style="fill:#fff" height="33.325" width="33.325" y="146.77" x="151.78"/>
<g id="text3882-7" stroke-linejoin="round" stroke="var(--nord2)" stroke-linecap="round" fill="var(--nord2)">
<path id="path3061" d="m95.864 150.69h5.0098v0.26367c-0.76174 0.0235-1.2891 0.18459-1.582 0.4834-0.293 0.29885-0.43948 0.92873-0.43945 1.8896v23.572l-20.654-21.99v19.16c-0.000005 0.60938 0.04687 1.0606 0.14062 1.3535 0.09374 0.29297 0.22851 0.49219 0.4043 0.59765 0.17578 0.10547 0.61523 0.21094 1.3184 0.31641v0.24609h-4.9746v-0.24609c0.89062 0 1.4443-0.1582 1.6611-0.47461 0.21679-0.3164 0.32519-1.0723 0.3252-2.2676v-17.525c-0.000004-1.5351-0.21387-2.789-0.6416-3.7617-0.42774-0.97263-1.415-1.4238-2.9619-1.3535v-0.26367h4.8691l19.406 20.672v-18.281c-0.000025-0.98435-0.17581-1.5849-0.52734-1.8018-0.35159-0.21677-0.80276-0.32517-1.3535-0.32519z"/>
<path id="path3063" d="m116.49 150.96v-0.26367h11.883c3.0234 0.00002 5.4111 0.23733 7.1631 0.71191 1.7519 0.47463 3.2783 1.2217 4.5791 2.2412 1.3008 1.0196 2.332 2.3233 3.0938 3.9111 0.76169 1.5879 1.1425 3.3604 1.1426 5.3174-0.00003 1.8867-0.39553 3.7412-1.1865 5.5635-0.79104 1.8223-1.8662 3.3926-3.2256 4.7109-1.3594 1.3184-2.792 2.2207-4.2978 2.707-1.5059 0.48633-3.835 0.72949-6.9873 0.72949h-12.164v-0.24609h0.3164c0.63281 0 1.0488-0.25488 1.248-0.76465 0.19921-0.50976 0.29882-1.4326 0.29883-2.7686v-18.861c-0.00001-1.4062-0.12012-2.2558-0.36035-2.5488-0.24024-0.29294-0.74122-0.43943-1.5029-0.43945zm8.8242 0.28125h-3.9726v20.092c-0.00001 1.2656 0.21386 2.2061 0.6416 2.8213 0.42773 0.61524 1.1572 1.0606 2.1885 1.3359 1.0312 0.27539 2.4199 0.41309 4.166 0.41309 4.8516 0 8.2588-1.2451 10.222-3.7354 1.9629-2.4902 2.9443-5.206 2.9443-8.1475-0.00003-3.457-1.3448-6.4512-4.0342-8.9824-2.6895-2.5312-6.7412-3.7968-12.155-3.7969z"/>
<path id="path3065" d="m172.31 151.03 1.2305 3.2344-0.21094 0.0352c-0.00002-0.0117-0.32521-0.3574-0.97559-1.0371-0.6504-0.67966-1.3945-1.2041-2.2324-1.5732-0.8379-0.36912-1.7842-0.55369-2.8389-0.55371-1.7695 0.00002-3.1728 0.50686-4.21 1.5205-1.0371 1.0137-1.5557 2.2354-1.5557 3.665 0 0.94923 0.24316 1.7783 0.72949 2.4873s1.5029 1.3682 3.0498 1.9775c2.6601 0.89064 4.4795 1.5615 5.458 2.0127 0.97851 0.45119 1.9219 1.2158 2.8301 2.294 0.90819 1.0781 1.3623 2.461 1.3623 4.1484-0.00002 0.70313-0.082 1.374-0.2461 2.0127-0.16408 0.63868-0.42775 1.2539-0.79101 1.8457-0.3633 0.5918-0.79982 1.1309-1.3096 1.6172-0.50978 0.48633-1.0283 0.85547-1.5557 1.1074-0.52735 0.25196-1.3594 0.44239-2.4961 0.57129-1.1367 0.12891-1.8867 0.19336-2.25 0.19336h-6.9082v-7.5234h0.26367c0.0234 2.4961 0.60937 4.2363 1.7578 5.2207 1.1484 0.98438 2.9355 1.4766 5.3613 1.4766 2.2031 0 3.876-0.52148 5.0186-1.5644 1.1426-1.043 1.7139-2.2969 1.7139-3.7617-0.00001-0.89062-0.19923-1.6494-0.59766-2.2764-0.39845-0.62694-0.95509-1.1777-1.6699-1.6523-0.71485-0.4746-2.0684-1.0518-4.0605-1.7314-1.9805-0.69139-3.3721-1.2978-4.1748-1.8193-0.80274-0.52147-1.4736-1.3066-2.0127-2.3555-0.53907-1.0488-0.8086-2.1182-0.8086-3.208 0-2.0039 0.68848-3.6855 2.0654-5.0449 1.377-1.3594 3.1846-2.039 5.4228-2.0391 0.86718 0.00002 1.8047 0.0996 2.8125 0.29882 1.0078 0.19925 1.5703 0.32816 1.6875 0.38672 0.0469 0.0235 0.0937 0.0352 0.14063 0.0352z"/>
<path id="path3067" d="m213.81 150.69h4.0781v0.26367c-1.3711 0.0703-2.0567 0.79104-2.0566 2.1621-0.00002 0.29299 0.0351 0.69728 0.10547 1.2129l2.707 19.617c0.16404 0.98438 0.34861 1.6143 0.55371 1.8896 0.20505 0.27539 0.62986 0.44239 1.2744 0.50098v0.24609h-5.8184v-0.24609c0.76169 0.0234 1.1426-0.35156 1.1426-1.125-0.00003-0.17578-0.0352-0.52148-0.10547-1.0371l-2.6367-19.02-9.2812 21.428h-0.17578l-9.334-21.393-2.6191 19.125c-0.0469 0.29297-0.0703 0.62695-0.0703 1.002 0 0.67969 0.39258 1.0195 1.1777 1.0195v0.24609h-3.9551v-0.24609c0.59765 0 0.98144-0.0762 1.1514-0.22852 0.16992-0.15234 0.30176-0.38964 0.39551-0.71191 0.0937-0.32226 0.16406-0.68262 0.21094-1.081l2.9531-20.918c-0.48047-1.1601-0.96094-1.8574-1.4414-2.0918-0.48047-0.23435-0.94336-0.35154-1.3887-0.35156v-0.26367h4.8867l9.1055 21.182z"/>
<path id="path3069" d="m234.95 150.96v-0.26367h11.883c3.0234 0.00002 5.4111 0.23733 7.1631 0.71191 1.7519 0.47463 3.2783 1.2217 4.5791 2.2412 1.3008 1.0196 2.332 2.3233 3.0938 3.9111 0.76169 1.5879 1.1426 3.3604 1.1426 5.3174-0.00003 1.8867-0.39554 3.7412-1.1865 5.5635-0.79105 1.8223-1.8662 3.3926-3.2256 4.7109-1.3594 1.3184-2.792 2.2207-4.2978 2.707-1.5059 0.48633-3.835 0.72949-6.9873 0.72949h-12.164v-0.24609h0.31641c0.63281 0 1.0488-0.25488 1.248-0.76465 0.19922-0.50976 0.29883-1.4326 0.29883-2.7686v-18.861c0-1.4062-0.12012-2.2558-0.36035-2.5488-0.24024-0.29294-0.74121-0.43943-1.5029-0.43945zm8.8242 0.28125h-3.9727v20.092c0 1.2656 0.21386 2.2061 0.6416 2.8213 0.42773 0.61524 1.1572 1.0606 2.1885 1.3359 1.0312 0.27539 2.4199 0.41309 4.166 0.41309 4.8515 0 8.2588-1.2451 10.222-3.7354 1.9629-2.4902 2.9443-5.206 2.9443-8.1475-0.00002-3.457-1.3448-6.4512-4.0342-8.9824-2.6895-2.5312-6.7412-3.7968-12.155-3.7969z"/>
</g>
<path id="path3083" stroke-linejoin="round" d="m124.76 99.299 0.9668 2.7422h-0.2461c-2.5078-2.4961-5.461-3.7441-8.8594-3.7441-3.5274 0.000025-6.3779 1.0489-8.5518 3.1465-2.1738 2.0977-3.2607 4.6348-3.2607 7.6113 0 1.8399 0.48339 3.9551 1.4502 6.3457 0.96679 2.3906 2.4932 4.3564 4.5791 5.8975 2.0859 1.541 4.6113 2.3115 7.5762 2.3115 1.9453 0 3.8525-0.31348 5.7217-0.94043 1.8691-0.62695 3.2607-1.4678 4.1748-2.5225l0.22852 0.0703-1.8106 2.7773c-2.9297 0.63282-4.8311 1.0078-5.7041 1.125-0.87307 0.11719-2.042 0.17578-3.5068 0.17578-5.4258 0-9.3076-1.2568-11.646-3.7705-2.3379-2.5137-3.5068-5.5225-3.5068-9.0264 0-2.3672 0.60058-4.6435 1.8018-6.8291 1.2012-2.1855 2.9326-3.9082 5.1943-5.168 2.2617-1.2597 4.8457-1.8896 7.752-1.8896 1.4531 0.000026 2.7099 0.13186 3.7705 0.39551 1.0605 0.2637 1.9424 0.53616 2.6455 0.81738l1.1602 0.43945c0.0351 0.01174 0.0586 0.02346 0.0703 0.03516z" stroke="var(--nord2)" stroke-linecap="round" fill="var(--nord2)"/>
<g id="text3882-7-7-1" stroke-linejoin="round" stroke="var(--nord2)" stroke-linecap="round" fill="var(--nord2)">
<path id="path3086" d="m226.63 97.821 1.2305 3.2344-0.21093 0.0352c-0.00002-0.0117-0.32521-0.3574-0.97559-1.0371-0.6504-0.67966-1.3945-1.2041-2.2324-1.5732-0.8379-0.36912-1.7842-0.55369-2.8389-0.55371-1.7695 0.000025-3.1729 0.50686-4.21 1.5205-1.0371 1.0137-1.5557 2.2354-1.5557 3.665-0.00001 0.94923 0.24316 1.7783 0.72949 2.4873 0.48632 0.709 1.5029 1.3682 3.0498 1.9775 2.6602 0.89064 4.4795 1.5615 5.458 2.0127 0.9785 0.45119 1.9219 1.2158 2.8301 2.294 0.90819 1.0781 1.3623 2.461 1.3623 4.1484-0.00001 0.70313-0.082 1.374-0.24609 2.0127-0.16408 0.63868-0.42775 1.2539-0.79102 1.8457-0.36329 0.5918-0.79981 1.1309-1.3096 1.6172-0.50977 0.48633-1.0283 0.85547-1.5557 1.1074-0.52736 0.25195-1.3594 0.44238-2.4961 0.57128-1.1367 0.12891-1.8867 0.19336-2.25 0.19336h-6.9082v-7.5234h0.26368c0.0234 2.4961 0.60937 4.2363 1.7578 5.2207 1.1484 0.98438 2.9355 1.4766 5.3613 1.4766 2.2031 0 3.876-0.52148 5.0186-1.5644 1.1426-1.043 1.7138-2.2969 1.7139-3.7617-0.00002-0.89062-0.19924-1.6494-0.59766-2.2764-0.39845-0.62694-0.95509-1.1777-1.6699-1.6523-0.71486-0.4746-2.0684-1.0518-4.0606-1.7314-1.9805-0.69139-3.3721-1.2978-4.1748-1.8193-0.80274-0.52147-1.4736-1.3066-2.0127-2.3555-0.53906-1.0488-0.80859-2.1182-0.80859-3.208 0-2.0039 0.68847-3.6855 2.0654-5.0449 1.377-1.3593 3.1846-2.039 5.4228-2.0391 0.86718 0.000026 1.8047 0.09963 2.8125 0.29883 1.0078 0.19924 1.5703 0.32815 1.6875 0.38672 0.0469 0.02346 0.0937 0.03518 0.14063 0.03516z" stroke="var(--nord2)" fill="var(--nord2)"/>
</g>
<g id="text3882-7-7-2" stroke-linejoin="round" stroke="var(--nord2)" stroke-linecap="round" fill="var(--nord2)">
<path id="path3089" d="m212.54 202.63v-0.26367h6.7324c1.9687 0.00003 3.3633 0.0821 4.1836 0.24609 0.8203 0.16409 1.6055 0.47757 2.3555 0.94043 0.74999 0.46292 1.3887 1.1309 1.916 2.0039 0.52732 0.87307 0.791 1.8662 0.79101 2.9795-0.00001 2.1914-1.0781 3.9199-3.2344 5.1856 1.8633 0.31642 3.2695 1.0869 4.2188 2.3115 0.9492 1.2246 1.4238 2.71 1.4238 4.4561-0.00002 1.6289-0.40725 3.1113-1.2217 4.4473-0.81447 1.3359-1.7461 2.2236-2.7949 2.6631-1.0488 0.43945-2.5166 0.65918-4.4033 0.65918h-10.002v-0.24609c0.79687 0 1.3066-0.16114 1.5293-0.4834 0.22265-0.32227 0.33398-1.1455 0.33398-2.4697v-19.512c0-0.93747-0.0264-1.5762-0.0791-1.916-0.0527-0.33982-0.22559-0.59178-0.51855-0.75586-0.29297-0.16404-0.70313-0.24607-1.2305-0.2461zm4.8164 0.28125v11.373h3.1465c2.1328 0.00001 3.5478-0.60936 4.2451-1.8281 0.69725-1.2187 1.0459-2.4316 1.0459-3.6387-0.00001-1.3008-0.26954-2.3877-0.80859-3.2607-0.53908-0.87302-1.3272-1.5322-2.3643-1.9775-1.0371-0.44529-2.5635-0.66794-4.5791-0.66797zm2.0215 11.918h-2.0215v8.209c0 1.1367 0.0439 1.875 0.13184 2.2148 0.0879 0.33985 0.2871 0.69727 0.59766 1.0723 0.31054 0.375 0.81151 0.67675 1.5029 0.90527s1.6875 0.34277 2.9883 0.34277c2.0156 0 3.4394-0.46582 4.2715-1.3975 0.83202-0.93164 1.248-2.3115 1.248-4.1396-0.00002-1.8984-0.36916-3.4453-1.1074-4.6406-0.7383-1.1953-1.5733-1.9219-2.5049-2.1797-0.93165-0.2578-2.6338-0.3867-5.1064-0.38672z" stroke="var(--nord2)" fill="var(--nord2)"/>
</g>
<g id="text3882-7-7-22" stroke-linejoin="round" stroke="var(--nord2)" stroke-linecap="round" fill="var(--nord2)">
<path id="path3092" d="m108.68 203.42v-0.26367h7.3828c2.625 0.00002 4.5322 0.29299 5.7217 0.8789 1.1894 0.58597 2.0566 1.3155 2.6016 2.1885 0.5449 0.87307 0.81736 2.0244 0.81738 3.4541-0.00002 2.1914-0.75588 3.8379-2.2676 4.9394-1.5117 1.1016-3.5625 1.6289-6.1523 1.582v-0.2461c1.6406 0.00002 2.9736-0.50389 3.999-1.5117s1.5381-2.332 1.5381-3.9726c-0.00002-1.2773-0.24904-2.4111-0.74707-3.4014-0.49806-0.99021-1.1983-1.7431-2.1006-2.2588-0.90235-0.5156-2.2793-0.77341-4.1309-0.77344h-1.8457v22.166c-0.00001 1.2539 0.14062 2.001 0.42187 2.2412 0.28125 0.24023 0.81445 0.36035 1.5996 0.36035v0.2461h-6.8379v-0.2461c1.2188 0 1.8281-0.55664 1.8281-1.6699v-21.445c-0.00001-0.91404-0.11719-1.5205-0.35156-1.8193-0.23438-0.2988-0.72657-0.44822-1.4766-0.44824z" stroke="var(--nord2)" fill="var(--nord2)"/>
</g>
<path id="path3888" d="m139.19 59.343c0 9.9799-8.0903 18.07-18.07 18.07-9.9799 0-18.07-8.0903-18.07-18.07 0-9.9799 8.0903-18.07 18.07-18.07 9.9799 0 18.07 8.0903 18.07 18.07z" transform="matrix(.15488 0 0 .15488 96.011 40.384)" fill="var(--nord2)"/>
<path id="path3888-1" d="m139.19 59.343c0 9.9799-8.0903 18.07-18.07 18.07-9.9799 0-18.07-8.0903-18.07-18.07 0-9.9799 8.0903-18.07 18.07-18.07 9.9799 0 18.07 8.0903 18.07 18.07z" transform="matrix(.15488 0 0 .15488 203.55 40.283)" fill="var(--nord2)"/>
<path id="path3888-7" d="m139.19 59.343c0 9.9799-8.0903 18.07-18.07 18.07-9.9799 0-18.07-8.0903-18.07-18.07 0-9.9799 8.0903-18.07 18.07-18.07 9.9799 0 18.07 8.0903 18.07 18.07z" transform="matrix(.17299 0 0 .17299 147.99 279.77)" fill="var(--nord2)"/>
<path id="path3888-4" d="m139.19 59.343c0 9.9799-8.0903 18.07-18.07 18.07-9.9799 0-18.07-8.0903-18.07-18.07 0-9.9799 8.0903-18.07 18.07-18.07 9.9799 0 18.07 8.0903 18.07 18.07z" transform="matrix(.15488 0 0 .15488 131.66 277.84)" fill="var(--nord2)"/>
<path id="path3888-0" d="m139.19 59.343c0 9.9799-8.0903 18.07-18.07 18.07-9.9799 0-18.07-8.0903-18.07-18.07 0-9.9799 8.0903-18.07 18.07-18.07 9.9799 0 18.07 8.0903 18.07 18.07z" transform="matrix(.15488 0 0 .15488 168.54 277.98)" fill="var(--nord2)"/>
<path id="path3021" stroke-linejoin="round" d="m61.398 206.07c0.38726-6.2993 0.78765-12.891-3.9191-17.556 2.2141 1.3159 3.7733 2.2888 5.016 5.4372 1.2085 3.0616 2.4354 10.148 0.93876 15.254-0.47418-1.2005-1.5449-2.5682-2.0357-3.1354z" stroke="var(--nord2)" stroke-linecap="round" stroke-width=".81607" fill="var(--nord2)"/>
</svg>

View File

@@ -1,326 +0,0 @@
<script lang='ts'>
import Check from '$lib/assets/icons/Check.svelte';
import Cross from '$lib/assets/icons/Cross.svelte';
import SeasonSelect from '$lib/components/SeasonSelect.svelte';
import '$lib/css/action_button.css'
import '$lib/css/nordtheme.css'
import '$lib/css/shake.css'
import { redirect } from '@sveltejs/kit';
import { RecipeModelType } from '../../types/types';
import type { PageData } from './$types';
import CardAdd from '$lib/components/CardAdd.svelte';
import CreateIngredientList from '$lib/components/CreateIngredientList.svelte';
import CreateStepList from '$lib/components/CreateStepList.svelte';
let {
data,
actions,
title,
card_data = $bindable({
icon: data.icon,
category: data.category,
name: data.name,
description: data.description,
tags: data.tags,
}),
add_info = $bindable({
preparation: data.preparation,
fermentation: {
bulk: data.fermentation.bulk,
final: data.fermentation.final,
},
baking: {
length: data.baking.length,
temperature: data.baking.temperature,
mode: data.baking.mode,
},
total_time: data.total_time,
}),
portions = $bindable(data.portions),
ingredients = $bindable(data.ingredients),
instructions = $bindable(data.instructions)
}: {
data: PageData,
actions: [String],
title: string,
card_data?: any,
add_info?: any,
portions?: any,
ingredients?: any,
instructions?: any
} = $props();
let preamble = $state(data.preamble);
let addendum = $state(data.addendum);
import { season } from '$lib/js/season_store';
season.update(() => data.season)
let season_local = $state();
season.subscribe((s) => {
season_local = s
});
let old_short_name = $state(data.short_name);
let images = $state(data.images);
let short_name = $state(data.short_name);
let password = $state();
let datecreated = $state(data.datecreated);
let datemodified = $state(new Date());
function get_season(){
let season = []
const el = document.getElementById("labels");
for(var i = 0; i < el.children.length; i++){
if(el.children[i].children[0].children[0].checked){
season.push(i+1)
}
}
return season
}
function write_season(season){
const el = document.getElementById("labels");
for(var i = 0; i < season.length; i++){
el.children[i].children[0].children[0].checked = true
}
}
async function doDelete(){
const response = confirm("Bist du dir sicher, dass du das Rezept löschen willst?")
if(!response){
return
}
const res = await fetch('/api/delete', {
method: 'POST',
body: JSON.stringify({
old_short_name,
headers: {
'content-type': 'application/json',
bearer: password,
}
})
})
}
async function doEdit() {
const res = await fetch('/api/edit', {
method: 'POST',
body: JSON.stringify({
: {
...card_data,
...add_info,
images, // TODO
season: season_local,
short_name,
datecreated,
datemodified,
instructions,
ingredients,
addendum,
preamble
},
old_short_name,
headers: {
'content-type': 'application/json',
bearer: password,
}
})
})
const item = await res.json();
}
async function doAdd () {
const res = await fetch('/api/add', {
method: 'POST',
body: JSON.stringify({
recipe: {
...card_data,
...add_info,
images: {mediapath: short_name + '.webp', alt: "", caption: ""}, // TODO
season: season_local,
short_name,
datecreated,
datemodified,
instructions,
ingredients,
preamble,
addendum,
},
headers: {
'content-type': 'application/json',
bearer: password,
}
})
})
}
</script>
<style>
input{
display: block;
border: unset;
margin: 1rem auto;
padding: 0.5em 1em;
border-radius: 1000px;
background-color: var(--nord4);
font-size: 1.1rem;
transition: 100ms;
}
input:hover,
input:focus-visible
{
scale: 1.05 1.05;
}
.list_wrapper{
margin-inline: auto;
display: flex;
flex-direction: row;
max-width: 1000px;
gap: 2rem;
justify-content: center;
}
@media screen and (max-width: 700px){
.list_wrapper{
flex-direction: column;
}
}
input[type=password]{
box-sizing: border-box;
font-size: 1.5rem;
padding-block: 0.5em;
display: inline;
width: 100%;
}
.submit_wrapper{
position: relative;
margin-inline: auto;
width: max(300px, 50vw)
}
.submit_wrapper button{
position: absolute;
right:-1em;
bottom: -0.5em;
}
.submit_wrapper h2{
margin-bottom: 0;
}
h1{
text-align: center;
margin-bottom: 2rem;
}
.title_container{
max-width: 1000px;
display: flex;
flex-direction: column;
margin-inline: auto;
}
.title{
position: relative;
width: min(800px, 80vw);
margin-block: 2rem;
margin-inline: auto;
background-color: var(--nord6);
background-color: red;
padding: 1rem 2rem;
}
.title p{
border: 2px solid var(--nord1);
border-radius: 10000px;
padding: 0.5em 1em;
font-size: 1.1rem;
transition: 200ms;
}
.title p:hover,
.title p:focus-within{
scale: 1.02 1.02;
}
.addendum{
font-size: 1.1rem;
max-width: 90%;
margin-inline: auto;
border: 2px solid var(--nord1);
border-radius: 45px;
padding: 1em 1em;
transition: 100ms;
}
.addendum:hover,
.addendum:focus-within
{
scale: 1.02 1.02;
}
.addendum_wrapper{
max-width: 1000px;
margin-inline: auto;
}
h3{
text-align: center;
}
.delete{
position: fixed;
right: 0;
bottom: 0;
margin: 2rem;
}
@media (prefers-color-scheme: dark){
.title{
background-color: var(--nord6-dark);
background-color: green;
}
}
</style>
<h1>{title}</h1>
<CardAdd {card_data}></CardAdd>
<h3>Kurzname (für URL):</h3>
<input bind:value={short_name} placeholder="Kurzname"/>
<div class=title_container>
<div class=title>
<h4>Eine etwas längere Beschreibung:</h4>
<p bind:innerText={preamble} contenteditable></p>
<div class=tags>
<h4>Saison:</h4>
<SeasonSelect></SeasonSelect>
</div>
</div>
</div>
<div class=list_wrapper>
<div>
<CreateIngredientList {ingredients} {portions}></CreateIngredientList>
</div>
<div>
<CreateStepList {instructions} {add_info}></CreateStepList>
</div>
</div>
<div class=addendum_wrapper>
<h3>Nachtrag:</h3>
<div class=addendum bind:innerText={addendum} contenteditable></div>
</div>
{#if actions.includes('add')}
<div class=submit_wrapper>
<h2>Neues Rezept hinzufügen:</h2>
<input type="password" placeholder=Passwort bind:value={password}>
<button class=action_button onclick={doAdd}><Check fill=white width=2rem height=2rem></Check></button>
</div>
{/if}
{#if actions.includes('edit')}
<div class=submit_wrapper>
<h2>Editiertes Rezept abspeichern:</h2>
<input type="password" placeholder=Passwort bind:value={password}>
<button class=action_button onclick={doEdit}><Check fill=white width=2rem height=2rem></Check></button>
</div>
{/if}
{#if actions.includes('delete')}
<div class=submit_wrapper>
<h2>Rezept löschen:</h2>
<input type="password" placeholder=Passwort bind:value={password}>
<button class=action_button onclick={doDelete}><Cross fill=white width=2rem height=2rem></Cross></button>
</div>
{/if}

View File

@@ -1,22 +0,0 @@
<script lang="ts">
</script>
<style>
div{
background-color: var(--red);
color: white;
padding: 1em;
font-size: 1.1rem;
max-width: 400px;
margin-inline: auto;
box-shadow: 0.2em 0.2em 0.5em 0.1em rgba(0, 0, 0, 0.3);
margin-bottom: 1em;
}
h3{
margin-block: 0;
}
</style>
<div>
<h3>Notiz:</h3>
<slot></slot>
</div>

View File

@@ -1,305 +0,0 @@
<script lang="ts">
let {
ingredients = $bindable([]),
translationMetadata = null,
onchange
}: {
ingredients?: any[],
translationMetadata?: any[] | null | undefined,
onchange?: (detail: { ingredients: any[] }) => void
} = $props();
function handleChange() {
onchange?.({ ingredients });
}
function updateIngredientGroupName(groupIndex: number, event: Event) {
const target = event.target as HTMLInputElement;
ingredients[groupIndex].name = target.value;
handleChange();
}
function updateIngredientItem(groupIndex: number, itemIndex: number, field: string, event: Event) {
const target = event.target as HTMLInputElement;
ingredients[groupIndex].list[itemIndex][field] = target.value;
handleChange();
}
// Base recipe reference handlers
function updateLabelOverride(groupIndex: number, event: Event) {
const target = event.target as HTMLInputElement;
ingredients[groupIndex].labelOverride = target.value;
handleChange();
}
function updateItemBefore(groupIndex: number, itemIndex: number, field: string, event: Event) {
const target = event.target as HTMLInputElement;
if (!ingredients[groupIndex].itemsBefore) {
ingredients[groupIndex].itemsBefore = [];
}
ingredients[groupIndex].itemsBefore[itemIndex][field] = target.value;
handleChange();
}
function updateItemAfter(groupIndex: number, itemIndex: number, field: string, event: Event) {
const target = event.target as HTMLInputElement;
if (!ingredients[groupIndex].itemsAfter) {
ingredients[groupIndex].itemsAfter = [];
}
ingredients[groupIndex].itemsAfter[itemIndex][field] = target.value;
handleChange();
}
// Check if a group name was re-translated
function isGroupNameTranslated(groupIndex: number): boolean {
return translationMetadata?.[groupIndex]?.nameTranslated ?? false;
}
// Check if a specific item was re-translated
function isItemTranslated(groupIndex: number, itemIndex: number): boolean {
return translationMetadata?.[groupIndex]?.itemsTranslated?.[itemIndex] ?? false;
}
</script>
<style>
.ingredients-editor {
background: var(--nord0);
border: 1px solid var(--nord3);
border-radius: 4px;
padding: 0.75rem;
}
@media(prefers-color-scheme: light) {
.ingredients-editor {
background: var(--nord5);
border-color: var(--nord3);
}
}
.ingredient-group {
margin-bottom: 1.5rem;
}
.ingredient-group:last-child {
margin-bottom: 0;
}
.group-name {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.5rem;
background: var(--nord1);
border: 1px solid var(--nord3);
border-radius: 4px;
color: var(--nord6);
font-weight: 600;
font-size: 0.95rem;
}
@media(prefers-color-scheme: light) {
.group-name {
background: var(--nord6);
color: var(--nord0);
}
}
.ingredient-item {
display: grid;
grid-template-columns: 60px 60px 1fr;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.ingredient-item input {
padding: 0.4rem;
background: var(--nord1);
border: 1px solid var(--nord3);
border-radius: 4px;
color: var(--nord6);
font-size: 0.9rem;
}
@media(prefers-color-scheme: light) {
.ingredient-item input {
background: var(--nord6);
color: var(--nord0);
}
}
.ingredient-item input:focus {
outline: 2px solid var(--nord14);
border-color: var(--nord14);
}
.ingredient-item input.amount {
text-align: right;
}
/* Highlight re-translated items with red border */
.retranslated {
border: 2px solid var(--nord11) !important;
animation: highlight-flash 0.6s ease-out;
}
@keyframes highlight-flash {
0% {
box-shadow: 0 0 10px var(--nord11);
}
100% {
box-shadow: 0 0 0 transparent;
}
}
.reference-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: var(--nord9);
color: var(--nord6);
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.reference-section {
padding: 0.5rem;
background: var(--nord2);
border-radius: 4px;
margin-bottom: 0.5rem;
}
@media(prefers-color-scheme: light) {
.reference-section {
background: var(--nord4);
}
}
.reference-section-label {
font-size: 0.8rem;
font-weight: 600;
color: var(--nord8);
margin-bottom: 0.25rem;
}
</style>
<div class="ingredients-editor">
{#each ingredients as group, groupIndex}
<div class="ingredient-group">
{#if group.type === 'reference'}
<span class="reference-badge">🔗 Base Recipe Reference</span>
{#if group.labelOverride !== undefined}
<input
type="text"
class="group-name"
value={group.labelOverride || ''}
on:input={(e) => updateLabelOverride(groupIndex, e)}
placeholder="Label override (optional)"
/>
{/if}
{#if group.itemsBefore && group.itemsBefore.length > 0}
<div class="reference-section">
<div class="reference-section-label">Items Before Base Recipe:</div>
{#each group.itemsBefore as item, itemIndex}
<div class="ingredient-item">
<input
type="text"
class="amount"
value={item.amount || ''}
on:input={(e) => updateItemBefore(groupIndex, itemIndex, 'amount', e)}
placeholder="Amt"
/>
<input
type="text"
class="unit"
class:retranslated={isItemTranslated(groupIndex, itemIndex)}
value={item.unit || ''}
on:input={(e) => updateItemBefore(groupIndex, itemIndex, 'unit', e)}
placeholder="Unit"
/>
<input
type="text"
class="name"
class:retranslated={isItemTranslated(groupIndex, itemIndex)}
value={item.name || ''}
on:input={(e) => updateItemBefore(groupIndex, itemIndex, 'name', e)}
placeholder="Ingredient name"
/>
</div>
{/each}
</div>
{/if}
{#if group.itemsAfter && group.itemsAfter.length > 0}
<div class="reference-section">
<div class="reference-section-label">Items After Base Recipe:</div>
{#each group.itemsAfter as item, itemIndex}
<div class="ingredient-item">
<input
type="text"
class="amount"
value={item.amount || ''}
on:input={(e) => updateItemAfter(groupIndex, itemIndex, 'amount', e)}
placeholder="Amt"
/>
<input
type="text"
class="unit"
class:retranslated={isItemTranslated(groupIndex, itemIndex)}
value={item.unit || ''}
on:input={(e) => updateItemAfter(groupIndex, itemIndex, 'unit', e)}
placeholder="Unit"
/>
<input
type="text"
class="name"
class:retranslated={isItemTranslated(groupIndex, itemIndex)}
value={item.name || ''}
on:input={(e) => updateItemAfter(groupIndex, itemIndex, 'name', e)}
placeholder="Ingredient name"
/>
</div>
{/each}
</div>
{/if}
{:else}
<input
type="text"
class="group-name"
class:retranslated={isGroupNameTranslated(groupIndex)}
value={group.name || ''}
on:input={(e) => updateIngredientGroupName(groupIndex, e)}
placeholder="Ingredient group name"
/>
{#each group.list as item, itemIndex}
<div class="ingredient-item">
<input
type="text"
class="amount"
value={item.amount || ''}
on:input={(e) => updateIngredientItem(groupIndex, itemIndex, 'amount', e)}
placeholder="Amt"
/>
<input
type="text"
class="unit"
class:retranslated={isItemTranslated(groupIndex, itemIndex)}
value={item.unit || ''}
on:input={(e) => updateIngredientItem(groupIndex, itemIndex, 'unit', e)}
placeholder="Unit"
/>
<input
type="text"
class="name"
class:retranslated={isItemTranslated(groupIndex, itemIndex)}
value={item.name || ''}
on:input={(e) => updateIngredientItem(groupIndex, itemIndex, 'name', e)}
placeholder="Ingredient name"
/>
</div>
{/each}
{/if}
</div>
{/each}
</div>

View File

@@ -1,275 +0,0 @@
<script lang="ts">
let {
instructions = $bindable([]),
translationMetadata = null,
onchange
}: {
instructions?: any[],
translationMetadata?: any[] | null | undefined,
onchange?: (detail: { instructions: any[] }) => void
} = $props();
function handleChange() {
onchange?.({ instructions });
}
function updateInstructionGroupName(groupIndex: number, event: Event) {
const target = event.target as HTMLInputElement;
instructions[groupIndex].name = target.value;
handleChange();
}
function updateStep(groupIndex: number, stepIndex: number, event: Event) {
const target = event.target as HTMLTextAreaElement;
instructions[groupIndex].steps[stepIndex] = target.value;
handleChange();
}
// Base recipe reference handlers
function updateLabelOverride(groupIndex: number, event: Event) {
const target = event.target as HTMLInputElement;
instructions[groupIndex].labelOverride = target.value;
handleChange();
}
function updateStepBefore(groupIndex: number, stepIndex: number, event: Event) {
const target = event.target as HTMLTextAreaElement;
if (!instructions[groupIndex].stepsBefore) {
instructions[groupIndex].stepsBefore = [];
}
instructions[groupIndex].stepsBefore[stepIndex] = target.value;
handleChange();
}
function updateStepAfter(groupIndex: number, stepIndex: number, event: Event) {
const target = event.target as HTMLTextAreaElement;
if (!instructions[groupIndex].stepsAfter) {
instructions[groupIndex].stepsAfter = [];
}
instructions[groupIndex].stepsAfter[stepIndex] = target.value;
handleChange();
}
// Check if a group name was re-translated
function isGroupNameTranslated(groupIndex: number): boolean {
return translationMetadata?.[groupIndex]?.nameTranslated ?? false;
}
// Check if a specific step was re-translated
function isStepTranslated(groupIndex: number, stepIndex: number): boolean {
return translationMetadata?.[groupIndex]?.stepsTranslated?.[stepIndex] ?? false;
}
</script>
<style>
.instructions-editor {
background: var(--nord0);
border: 1px solid var(--nord3);
border-radius: 4px;
padding: 0.75rem;
}
@media(prefers-color-scheme: light) {
.instructions-editor {
background: var(--nord5);
border-color: var(--nord3);
}
}
.instruction-group {
margin-bottom: 1.5rem;
}
.instruction-group:last-child {
margin-bottom: 0;
}
.group-name {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.5rem;
background: var(--nord1);
border: 1px solid var(--nord3);
border-radius: 4px;
color: var(--nord6);
font-weight: 600;
font-size: 0.95rem;
}
@media(prefers-color-scheme: light) {
.group-name {
background: var(--nord6);
color: var(--nord0);
}
}
.step-item {
margin-bottom: 0.75rem;
display: flex;
gap: 0.5rem;
align-items: flex-start;
}
.step-number {
min-width: 2rem;
padding: 0.4rem 0.5rem;
background: var(--nord3);
border-radius: 4px;
text-align: center;
color: var(--nord6);
font-weight: 600;
font-size: 0.9rem;
}
@media(prefers-color-scheme: light) {
.step-number {
background: var(--nord4);
color: var(--nord0);
}
}
.step-item textarea {
flex: 1;
padding: 0.5rem;
background: var(--nord1);
border: 1px solid var(--nord3);
border-radius: 4px;
color: var(--nord6);
font-size: 0.9rem;
font-family: inherit;
resize: vertical;
min-height: 3rem;
}
@media(prefers-color-scheme: light) {
.step-item textarea {
background: var(--nord6);
color: var(--nord0);
}
}
.step-item textarea:focus {
outline: 2px solid var(--nord14);
border-color: var(--nord14);
}
/* Highlight re-translated items with red border */
.retranslated {
border: 2px solid var(--nord11) !important;
animation: highlight-flash 0.6s ease-out;
}
@keyframes highlight-flash {
0% {
box-shadow: 0 0 10px var(--nord11);
}
100% {
box-shadow: 0 0 0 transparent;
}
}
.reference-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: var(--nord9);
color: var(--nord6);
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.reference-section {
padding: 0.5rem;
background: var(--nord2);
border-radius: 4px;
margin-bottom: 0.5rem;
}
@media(prefers-color-scheme: light) {
.reference-section {
background: var(--nord4);
}
}
.reference-section-label {
font-size: 0.8rem;
font-weight: 600;
color: var(--nord8);
margin-bottom: 0.25rem;
}
</style>
<div class="instructions-editor">
{#each instructions as group, groupIndex}
<div class="instruction-group">
{#if group.type === 'reference'}
<span class="reference-badge">🔗 Base Recipe Reference</span>
{#if group.labelOverride !== undefined}
<input
type="text"
class="group-name"
value={group.labelOverride || ''}
on:input={(e) => updateLabelOverride(groupIndex, e)}
placeholder="Label override (optional)"
/>
{/if}
{#if group.stepsBefore && group.stepsBefore.length > 0}
<div class="reference-section">
<div class="reference-section-label">Steps Before Base Recipe:</div>
{#each group.stepsBefore as step, stepIndex}
<div class="step-item">
<div class="step-number">{stepIndex + 1}</div>
<textarea
class:retranslated={isStepTranslated(groupIndex, stepIndex)}
value={step || ''}
on:input={(e) => updateStepBefore(groupIndex, stepIndex, e)}
placeholder="Step description"
/>
</div>
{/each}
</div>
{/if}
{#if group.stepsAfter && group.stepsAfter.length > 0}
<div class="reference-section">
<div class="reference-section-label">Steps After Base Recipe:</div>
{#each group.stepsAfter as step, stepIndex}
<div class="step-item">
<div class="step-number">{stepIndex + 1}</div>
<textarea
class:retranslated={isStepTranslated(groupIndex, stepIndex)}
value={step || ''}
on:input={(e) => updateStepAfter(groupIndex, stepIndex, e)}
placeholder="Step description"
/>
</div>
{/each}
</div>
{/if}
{:else}
<input
type="text"
class="group-name"
class:retranslated={isGroupNameTranslated(groupIndex)}
value={group.name || ''}
on:input={(e) => updateInstructionGroupName(groupIndex, e)}
placeholder="Instruction section name"
/>
{#each group.steps as step, stepIndex}
<div class="step-item">
<div class="step-number">{stepIndex + 1}</div>
<textarea
class:retranslated={isStepTranslated(groupIndex, stepIndex)}
value={step || ''}
on:input={(e) => updateStep(groupIndex, stepIndex, e)}
placeholder="Step description"
/>
</div>
{/each}
{/if}
</div>
{/each}
</div>

View File

@@ -41,9 +41,10 @@
<style>
.favorite-button {
all: unset;
font-family: "Noto Color Emoji", "Noto Color Emoji Subset", emoji, sans-serif;
font-size: 1.5rem;
cursor: pointer;
transition: 100ms;
transition: var(--transition-fast);
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
position: absolute;
bottom: 0.5em;

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import "$lib/css/nordtheme.css"
import { onMount } from "svelte";
import { page } from '$app/stores';
import Symbol from "./Symbol.svelte"
@@ -24,10 +23,9 @@ let underlineWidth = $state(0);
let disableTransition = $state(false);
function toggle_sidebar(state){
// state: force hidden state (optional)
const nav_el = document.querySelector("nav")
if(state === undefined) nav_el.hidden = !nav_el.hidden
else nav_el.hidden = state
const checkbox = document.getElementById('nav-toggle')
if(state === undefined) checkbox.checked = !checkbox.checked
else checkbox.checked = !state
}
function updateUnderline() {
@@ -94,16 +92,17 @@ nav{
background-color: var(--nord0);
top: 0;
z-index: 10;
display: flex !important;
display: flex;
flex-direction: row;
justify-content: space-between !important;
align-items: center;
box-shadow: 0 1em 1rem 0rem rgba(0,0,0,0.4);
height: 4rem;
height: var(--header-h);
padding-left: 0.5rem;
view-transition-name: site-header;
}
nav[hidden]{
display:block;
.nav-toggle{
display: none;
}
:global(.site_header li),
@@ -117,22 +116,20 @@ nav[hidden]{
:global(.site_header li>a)
{
text-decoration: none;
font-family: sans-serif;
font-size: 1.2rem;
font-size: 1rem;
color: inherit;
border-radius: 1000px;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-pill);
padding: 0.4rem 0.6rem;
}
:global(a.entry),
:global(a.entry:link),
:global(a.entry:visited)
{
text-decoration: none;
font-family: sans-serif;
font-size: 1.2rem;
font-size: 1rem;
color: white !important;
border-radius: 1000px;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-pill);
padding: 0.4rem 0.6rem;
}
:global(.site_header li:hover),
@@ -179,6 +176,9 @@ nav[hidden]{
display: none;
padding-inline: 0.5rem;
}
.header-shadow{
display: none;
}
.right-buttons{
display: flex;
align-items: center;
@@ -190,9 +190,10 @@ nav[hidden]{
gap: 0.5rem;
}
:global(svg.symbol){
height: 4rem;
width: 4rem;
--symbol-size: calc(var(--header-h) - 1rem);
width: var(--symbol-size);
border-radius: 10000px;
margin: 0.25rem;
}
/*:global(a:has(svg.symbol)){
padding: 0 !important;
@@ -201,6 +202,8 @@ nav[hidden]{
margin-left: 1rem;
}*/
.wrapper{
--header-h: 3rem;
--symbol-size: calc(var(--header-h) - 1rem);
display:flex;
flex-direction: column;
min-height: 100svh;
@@ -209,19 +212,10 @@ footer{
padding-block: 1rem;
text-align: center;
margin-top: auto;
position: relative;
}
@media screen and (max-width: 800px) {
.button_wrapper_shadow{
box-shadow: 0 1em 1rem 0rem rgba(0,0,0,0.4);
position: fixed;
width: 100%;
height: 4rem;
top: 0;
left: 0;
z-index: 9;
pointer-events: none;
}
.button_wrapper{
display: flex;
justify-content: space-between;
@@ -229,84 +223,100 @@ footer{
position: sticky;
background-color: var(--nord0);
width: 100%;
height: 4rem;
height: var(--header-h);
top: 0;
z-index: 9999;
}
.nav_button{
border: unset;
background-color: unset;
.header-shadow{
display: block;
position: sticky;
top: 0;
width: 100%;
height: var(--header-h);
margin-top: calc(-1 * var(--header-h));
box-shadow: 0 1em 1rem 0rem rgba(0,0,0,0.4);
z-index: 9997;
pointer-events: none;
}
.nav_button{
display: inline-flex;
align-items: center;
justify-content: center;
fill: white;
margin-inline: 0.5rem;
width: 2rem;
aspect-ratio: 1;
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
}
.nav_button svg{
width: 100%;
height: 100%;
transition: 100ms;
transition: var(--transition-fast);
}
.nav_button:focus{
fill: var(--red);
.nav_button:hover,
.nav_button:active,
.nav-toggle:focus-visible + .nav_button{
fill: var(--nord8);
scale: 0.9;
}
.nav_site{
.nav_site:not(.no-links){
position: fixed;
top: 0;
right: 0;
height: 100vh; /* dvh does not work, breaks because of transition and only being applied after scroll ends*/
margin-bottom: 50vh;
width: min(95svw, 25em);
transition: transform 100ms;
z-index: 10;
z-index: 9998;
flex-direction: column;
justify-content: flex-start !important;
align-items: left;
justify-content: space-between!important;
padding-inline: 0.5rem;
}
:global(.nav_site ul){
.nav_site:not(.no-links)::before{
content: '';
flex: 1;
}
:global(.nav_site:not(.no-links) ul){
width: 100% ;
}
.nav_site :first-child{
.nav_site:not(.no-links) :first-child{
display:none;
}
.nav_site[hidden]{
.nav_site:not(.no-links){
transform: translateX(100%);
}
:global(.nav_site a:last-child){
.wrapper:has(.nav-toggle:checked) .nav_site:not(.no-links){
transform: translateX(0);
transition: transform 100ms;
}
:global(.nav_site:not(.no-links) a:last-child){
margin-bottom: 2rem;
}
.nav_site .links-wrapper {
align-self: flex-start;
.nav_site:not(.no-links) .links-wrapper {
width: 100%;
margin: 2rem;
padding: 0 2rem;
}
:global(.site_header){
flex-direction: column;
padding-top: min(10rem, 10vh);
align-items: flex-start;
}
:global(.site_header li, .site_header a){
font-size: 4rem;
font-size: 1.5rem;
}
:global(.site_header li > a, .site_header a){
font-size: 2rem;
font-size: 1.3rem;
}
:global(.site_header li:hover),
:global(.site_header li:focus-within){
transform: unset;
}
.nav_site .header-right{
.nav_site:not(.no-links) .header-right{
flex-direction: column;
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
}
.language-selector-desktop{
.nav_site:not(.no-links) .language-selector-desktop{
display: none;
}
.active-underline {
@@ -319,18 +329,44 @@ footer{
text-underline-offset: 0.3rem;
}
}
.no-links :global(button) {
margin-bottom: 0 !important;
}
.no-links :global(#options) {
top: calc(100% + 10px) !important;
bottom: unset !important;
right: 0 !important;
left: unset !important;
transform: none !important;
}
.no-links :global(.top.speech::after) {
border: 20px solid transparent !important;
border-bottom-color: var(--nord3) !important;
border-top: 0 !important;
top: -10px !important;
bottom: unset !important;
left: unset !important;
right: 0.25rem !important;
margin-left: 0 !important;
}
.no-links :global(button::before) {
display: none;
}
</style>
<div class=wrapper lang=de>
<div>
<span class=button_wrapper_shadow></span>
{#if links}
<div class=button_wrapper>
<a href="/" aria-label="Home"><Symbol></Symbol></a>
<div class="right-buttons">
{@render language_selector_mobile?.()}
<button class=nav_button onclick={() => {toggle_sidebar()}} aria-label="Toggle navigation menu"><svg xmlns="http://www.w3.org/2000/svg" height="0.5em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></button>
<input type="checkbox" id="nav-toggle" class="nav-toggle" aria-label="Toggle navigation menu" />
<label for="nav-toggle" class=nav_button aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" height="0.5em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></label>
</div>
</div>
<nav hidden class=nav_site>
<div class="header-shadow"></div>
{/if}
<nav class=nav_site class:no-links={!links}>
<a href="/" aria-label="Home"><Symbol></Symbol></a>
<div class="links-wrapper">
{@render links?.()}

View File

@@ -1,564 +0,0 @@
<script lang='ts'>
import { onMount } from 'svelte';
import Pen from '$lib/assets/icons/Pen.svelte'
import Cross from '$lib/assets/icons/Cross.svelte'
import Plus from '$lib/assets/icons/Plus.svelte'
import Check from '$lib/assets/icons/Check.svelte'
import "$lib/css/action_button.css"
let { list = $bindable(), list_index } = $props<{ list: any, list_index: number }>();
let edit_ingredient = $state({
amount: "",
unit: "",
name: "",
sublist: "",
list_index: "",
ingredient_index: "",
});
let edit_heading = $state({
name:"",
list_index: "",
});
function get_sublist_index(sublist_name, list){
for(var i =0; i < list.length; i++){
if(list[i].name == sublist_name){
return i
}
}
return -1
}
export function show_modal_edit_subheading_ingredient(list_index){
edit_heading.name = ingredients[list_index].name
edit_heading.list_index = list_index
const el = document.querySelector('#edit_subheading_ingredient_modal')
el.showModal()
}
export function edit_subheading_and_close_modal(){
ingredients[edit_heading.list_index].name = edit_heading.name
const el = document.querySelector('#edit_subheading_ingredient_modal')
el.close()
}
export function add_new_ingredient(){
if(!new_ingredient.name){
return
}
let list_index = get_sublist_index(new_ingredient.sublist, ingredients)
if(list_index == -1){
ingredients.push({
name: new_ingredient.sublist,
list: [],
})
list_index = ingredients.length - 1
}
ingredients[list_index].list.push({ ...new_ingredient})
ingredients = ingredients //tells svelte to update dom
}
export function remove_list(list_index){
if(ingredients[list_index].list.length > 1){
const response = confirm("Bist du dir sicher, dass du diese Liste löschen möchtest? Alle Zutaten der Liste werden hiermit auch gelöscht.");
if(!response){
return
}
}
ingredients.splice(list_index, 1);
ingredients = ingredients //tells svelte to update dom
}
export function remove_ingredient(list_index, ingredient_index){
ingredients[list_index].list.splice(ingredient_index, 1)
ingredients = ingredients //tells svelte to update dom
}
export function show_modal_edit_ingredient(list_index, ingredient_index){
edit_ingredient = {...ingredients[list_index].list[ingredient_index]}
edit_ingredient.list_index = list_index
edit_ingredient.ingredient_index = ingredient_index
edit_ingredient.sublist = ingredients[list_index].name
const modal_el = document.querySelector("#edit_ingredient_modal");
modal_el.showModal();
}
export function edit_ingredient_and_close_modal(){
ingredients[edit_ingredient.list_index].list[edit_ingredient.ingredient_index] = {
amount: edit_ingredient.amount,
unit: edit_ingredient.unit,
name: edit_ingredient.name,
}
ingredients[edit_ingredient.list_index].name = edit_ingredient.sublist
const modal_el = document.querySelector("#edit_ingredient_modal");
modal_el.close();
}
let ghost;
let grabbed;
let lastTarget;
let mouseY = 0; // pointer y coordinate within client
let offsetY = 0; // y distance from top of grabbed element to pointer
let layerY = 0; // distance from top of list to top of client
function grab(clientY, element) {
// modify grabbed element
grabbed = element;
grabbed.dataset.grabY = clientY;
// modify ghost element (which is actually dragged)
ghost.innerHTML = grabbed.innerHTML;
// record offset from cursor to top of element
// (used for positioning ghost)
offsetY = grabbed.getBoundingClientRect().y - clientY;
drag(clientY);
}
// drag handler updates cursor position
function drag(clientY) {
if (grabbed) {
mouseY = clientY;
layerY = ghost.parentNode.getBoundingClientRect().y;
}
}
// touchEnter handler emulates the mouseenter event for touch input
// (more or less)
function touchEnter(ev) {
drag(ev.clientY);
// trigger dragEnter the first time the cursor moves over a list item
let target = document.elementFromPoint(ev.clientX, ev.clientY).closest(".item");
if (target && target != lastTarget) {
lastTarget = target;
dragEnter(ev, target);
}
}
function dragEnter(ev, target) {
// swap items in data
if (grabbed && target != grabbed && target.classList.contains("item")) {
moveDatum(parseInt(grabbed.dataset.index), parseInt(target.dataset.index));
}
}
// does the actual moving of items in data
function moveDatum(from, to) {
let temp = list[0].list[from];
list[0].list = [...list[0].list.slice(0, from), ...list[0].list.slice(from + 1)];
list[0].list= [...list[0].list.slice(0, to), temp, ...list[0].list.slice(to)];
}
function release(ev) {
grabbed = null;
}
function removeDatum(index) {
list= [...list.slice(0, index), ...list.slice(index + 1)];
}
</script>
<style>
input::placeholder{
color: inherit;
}
.drag_handle{
cursor: grab;
display:flex;
justify-content: flex-start;
align-items: center;
}
.drag_handle_header{
padding-right: 0.5em;
}
input{
color: unset;
font-size: unset;
padding: unset;
background-color: unset;
}
input.heading{
all: unset;
box-sizing: border-box;
background-color: var(--nord0);
padding: 1rem;
padding-inline: 2rem;
font-size: 1.5rem;
width: 100%;
border-radius: 1000px;
color: white;
justify-content: center;
align-items: center;
transition: 200ms;
}
input.heading:hover{
background-color: var(--nord1);
}
.heading_wrapper{
position: relative;
width: 300px;
margin-inline: auto;
transition: 200ms;
}
.heading_wrapper:hover
{
transform:scale(1.1,1.1);
}
.heading_wrapper button{
position: absolute;
bottom: -1.5rem;
right: -2rem;
}
.adder{
box-sizing: border-box;
margin-inline: auto;
position: relative;
margin-block: 3rem;
width: 90%;
border-radius: 20px;
transition: 200ms;
}
.adder button{
position: absolute;
right: -1.5rem;
bottom: -1.5rem;
}
.category{
border: none;
position: absolute;
--font_size: 1.5rem;
top: -1em;
left: -1em;
font-family: sans-serif;
font-size: 1.5rem;
background-color: var(--nord0);
color: var(--nord4);
border-radius: 1000000px;
width: 23ch;
padding: 0.5em 1em;
transition: 100ms;
box-shadow: 0.5em 0.5em 1em 0.4em rgba(0,0,0,0.3);
}
.category:hover{
background-color: var(--nord1);
transform: scale(1.05,1.05);
}
.adder:hover,
.adder:focus-within
{
transform: scale(1.05, 1.05);
}
.add_ingredient{
font-family: sans-serif;
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-items: center;
font-size: 1.2rem;
padding: 2rem;
padding-top: 2.5rem;
border-radius: 20px;
background-color: var(--blue);
color: #bbb;
transition: 200ms;
gap: 0.5rem;
}
.add_ingredient input{
border: 2px solid var(--nord4);
color: var(--nord4);
border-radius: 1000px;
padding: 0.5em 1em;
transition: 100ms;
}
.add_ingredient input:hover,
.add_ingredient input:focus-visible
{
border-color: white;
color: white;
transform: scale(1.02, 1.02);
}
.add_ingredient input:nth-of-type(1){
max-width: 8ch;
}
.add_ingredient input:nth-of-type(2){
max-width: 8ch;
}
.add_ingredient input:nth-of-type(3){
max-width: 30ch;
}
dialog{
box-sizing: content-box;
width: 100%;
height: 100%;
background-color: transparent;
border: unset;
margin: 0;
transition: 500ms;
}
dialog[open]::backdrop{
animation: show 200ms ease forwards;
}
@keyframes show{
from {
backdrop-filter: blur(0px);
}
to {
backdrop-filter: blur(10px);
}
}
dialog .adder{
margin-top: 5rem;
}
dialog h2{
font-size: 3rem;
font-family: sans-serif;
color: white;
text-align: center;
margin-top: 30vh;
margin-top: 30dvh;
filter: drop-shadow(0 0 0.4em black)
drop-shadow(0 0 1em black)
;
}
.mod_icons{
display: flex;
flex-direction: row;
margin-left: 2rem;
}
.button_subtle{
padding: 0em;
animation: unset;
margin: 0.2em 0.1em;
background-color: transparent;
box-shadow: unset;
}
.button_subtle:hover{
scale: 1.2 1.2;
}
h3{
width: fit-content;
display: flex;
flex-direction: row;
max-width: 1000px;
justify-content: space-between;
user-select: none;
cursor: pointer;
}
.ingredients_grid > span{
box-sizing: border-box;
display: grid;
font-size: 1.1em;
grid-template-columns: 1em 2fr 3fr 2em;
grid-template-rows: auto;
grid-auto-flow: row;
align-items: center;
row-gap: 0.5em;
column-gap: 0.5em;
}
.ingredients_grid > *{
cursor: pointer;
user-select: none;
}
.ingredients_grid>*:nth-child(3n+1){
min-width: 5ch;
}
.list_wrapper{
padding-inline: 2em;
padding-block: 1em;
}
.list_wrapper p[contenteditable]{
border: 2px solid grey;
border-radius: 1000px;
padding: 0.25em 1em;
background-color: white;
transition: 200ms;
}
@media screen and (max-width: 500px){
dialog h2{
margin-top: 2rem;
}
dialog .heading_wrapper{
width: 80%;
}
.ingredients_grid .mod_icons{
margin-left: 0;
}
}
.list {
cursor: grab;
z-index: 5;
display: flex;
flex-direction: column;
}
.item {
min-height: 3em;
margin-bottom: 0.5em;
border-radius: 2px;
user-select: none;
}
.item:last-child {
margin-bottom: 0;
}
.item:not(#grabbed):not(#ghost) {
z-index: 10;
}
.item > * {
margin: auto;
}
.buttons {
width: 32px;
min-width: 32px;
margin: auto 0;
display: flex;
flex-direction: column;
}
.buttons button {
cursor: pointer;
width: 18px;
height: 18px;
margin: 0 auto;
padding: 0;
border: 1px solid rgba(0, 0, 0, 0);
background-color: inherit;
}
.buttons button:focus {
border: 1px solid black;
}
.delete {
width: 32px;
}
#grabbed {
opacity: 0.0;
}
#ghost {
pointer-events: none;
z-index: -5;
position: absolute;
top: 0;
left: 0;
opacity: 0.0;
}
#ghost * {
pointer-events: none;
}
#ghost.haunting {
z-index: 20;
opacity: 1.0;
}
main {
position: relative;
}
</style>
<main>
<div class=dragdroplist>
<div
bind:this={ghost}
id="ghost"
class={grabbed ? "item haunting" : "item"}
style={"top: " + (mouseY + offsetY - layerY) + "px"}><p></p>
</div>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<h3 onclick={() => show_modal_edit_subheading_ingredient(list_index)}>
<div class="drag_handle drag_handle_header"><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></div>
<div>
{#if list.name }
{list.name}
{:else}
Leer
{/if}
</div>
<div class=mod_icons>
<button class="action_button button_subtle" onclick={() => show_modal_edit_subheading_ingredient(list_index)}>
<Pen fill=var(--nord1)></Pen> </button>
<button class="action_button button_subtle" onclick={() => remove_list(list_index)}>
<Cross fill=var(--nord1)></Cross></button>
</div>
</h3>
<div class="ingredients_grid list"
on:mousemove={function(ev) {ev.stopPropagation(); drag(ev.clientY);}}
on:touchmove={function(ev) {ev.stopPropagation(); drag(ev.touches[0].clientY);}}
on:mouseup={function(ev) {ev.stopPropagation(); release(ev);}}
on:touchend={function(ev) {ev.stopPropagation(); release(ev.touches[0]);}}
>
{#each list.list as ingredient, ingredient_index}
<span
id={(grabbed && (ingredient.id ? ingredient.id : JSON.stringify(ingredient)) == grabbed.dataset.id) ? "grabbed" : ""}
class="item"
data-index={ingredient_index}
data-id={(ingredient.id ? ingredient.id : JSON.stringify(ingredient))}
data-grabY="0"
on:mousedown={function(ev) {grab(ev.clientY, this);}}
on:touchstart={function(ev) {grab(ev.touches[0].clientY, this);}}
on:mouseenter={function(ev) {ev.stopPropagation(); dragEnter(ev, ev.target);}}
on:touchmove={function(ev) {ev.stopPropagation(); ev.preventDefault(); touchEnter(ev.touches[0]);}}
>
<div class=drag_handle><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></div>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div onclick={() => show_modal_edit_ingredient(list_index, ingredient_index)} >{ingredient.amount} {ingredient.unit}</div>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div onclick={() => show_modal_edit_ingredient(list_index, ingredient_index)} >{@html ingredient.name}</div>
<div class=mod_icons><button class="action_button button_subtle" onclick={() => show_modal_edit_ingredient(list_index, ingredient_index)}>
<Pen fill=var(--nord1) height=1em width=1em></Pen></button>
<button class="action_button button_subtle" onclick="{() => remove_ingredient(list_index, ingredient_index)}"><Cross fill=var(--nord1) height=1em width=1em></Cross></button></div>
</span>
{/each}
</div>
</div>
</main>
<dialog id=edit_ingredient_modal>
<h2>Zutat verändern</h2>
<div class=adder>
<input class=category type="text" bind:value={edit_ingredient.sublist} placeholder="Kategorie (optional)">
<div class=add_ingredient onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<input type="text" placeholder="250..." bind:value={edit_ingredient.amount} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<input type="text" placeholder="mL..." bind:value={edit_ingredient.unit} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<input type="text" placeholder="Milch..." bind:value={edit_ingredient.name} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<button class=action_button onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)} onclick={edit_ingredient_and_close_modal}>
<Check fill=white style="width: 2rem; height: 2rem;"></Check>
</button>
</div>
</div>
</dialog>
<dialog id=edit_subheading_ingredient_modal>
<h2>Kategorie umbenennen</h2>
<div class=heading_wrapper>
<input class=heading type="text" bind:value={edit_heading.name} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} >
<button class=action_button onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} onclick={edit_subheading_and_close_modal}>
<Check fill=white style="width:2rem; height:2rem;"></Check>
</button>
</div>
</dialog>

View File

@@ -5,18 +5,29 @@
import { languageStore } from '$lib/stores/language';
import { onMount } from 'svelte';
let { lang = undefined }: { lang?: 'de' | 'en' } = $props();
// Use prop for display if provided (SSR-safe), otherwise fall back to store
const displayLang = $derived(lang ?? $languageStore);
let currentPath = $state('');
let langButton: HTMLButtonElement;
let langOptions: HTMLDivElement;
let isOpen = $state(false);
// Faith subroute mappings
const faithSubroutes: Record<string, Record<string, string>> = {
en: { gebete: 'prayers', rosenkranz: 'rosary' },
de: { prayers: 'gebete', rosary: 'rosenkranz' }
};
$effect(() => {
// Update current language and path when page changes (reactive to browser navigation)
const path = $page.url.pathname;
currentPath = path;
if (path.startsWith('/recipes')) {
if (path.startsWith('/recipes') || path.startsWith('/faith')) {
languageStore.set('en');
} else if (path.startsWith('/rezepte')) {
} else if (path.startsWith('/rezepte') || path.startsWith('/glaube')) {
languageStore.set('de');
} else if (path === '/') {
// On main page, read from localStorage
@@ -28,12 +39,58 @@
});
function toggle_language_options(){
if (langOptions) {
langOptions.hidden = !langOptions.hidden;
}
isOpen = !isOpen;
}
function convertFaithPath(path: string, targetLang: 'de' | 'en'): string {
const faithMatch = path.match(/^\/(glaube|faith)(\/(.+))?$/);
if (!faithMatch) return path;
const targetBase = targetLang === 'en' ? 'faith' : 'glaube';
const rest = faithMatch[3]; // e.g., "gebete", "rosenkranz/sub", "angelus"
if (!rest) {
return `/${targetBase}`;
}
// Split on / to convert just the first segment (gebete→prayers, etc.)
const parts = rest.split('/');
parts[0] = faithSubroutes[targetLang][parts[0]] || parts[0];
return `/${targetBase}/${parts.join('/')}`;
}
// Compute target paths for each language (used as href for no-JS)
function computeTargetPath(targetLang: 'de' | 'en'): string {
const path = currentPath || $page.url.pathname;
if (path.startsWith('/glaube') || path.startsWith('/faith')) {
return convertFaithPath(path, targetLang);
}
// Use translated recipe slugs from page data when available (works during SSR)
const pageData = $page.data;
if (targetLang === 'en' && path.startsWith('/rezepte')) {
if (pageData?.englishShortName) {
return `/recipes/${pageData.englishShortName}`;
}
return path.replace('/rezepte', '/recipes');
}
if (targetLang === 'de' && path.startsWith('/recipes')) {
if (pageData?.germanShortName) {
return `/rezepte/${pageData.germanShortName}`;
}
return path.replace('/recipes', '/rezepte');
}
return path;
}
const dePath = $derived(computeTargetPath('de'));
const enPath = $derived(computeTargetPath('en'));
async function switchLanguage(lang: 'de' | 'en') {
isOpen = false;
// Update the shared language store immediately
languageStore.set(lang);
@@ -51,6 +108,13 @@
return;
}
// Handle faith pages
if (path.startsWith('/glaube') || path.startsWith('/faith')) {
const newPath = convertFaithPath(path, lang);
await goto(newPath);
return;
}
// If we have recipe translation data from store, use the correct short names
const recipeData = $recipeTranslationStore;
if (recipeData) {
@@ -76,7 +140,7 @@
} else if (lang === 'de' && path.startsWith('/recipes')) {
newPath = path.replace('/recipes', '/rezepte');
} else if (!path.startsWith('/rezepte') && !path.startsWith('/recipes')) {
// On other pages (glaube, cospend, etc), go to recipe home
// On other pages (cospend, etc), go to recipe home
newPath = lang === 'en' ? '/recipes' : '/rezepte';
}
@@ -86,7 +150,7 @@
onMount(() => {
const handleClick = (e: MouseEvent) => {
if(langButton && !langButton.contains(e.target as Node)){
if (langOptions) langOptions.hidden = true;
isOpen = false;
}
};
@@ -128,8 +192,27 @@
width: 10ch;
padding: 0.5rem;
z-index: 1000;
display: none;
}
.language-options button{
.language-options::after {
content: "";
border: 10px solid transparent;
border-bottom-color: var(--bg_color);
border-top: 0;
position: absolute;
top: -10px;
right: 1rem;
}
/* Show via JS toggle */
.language-options.open {
display: block;
}
/* Show via CSS focus-within (no-JS fallback) */
.language-selector:focus-within .language-options {
display: block;
}
.language-options a{
display: block;
width: 100%;
background-color: transparent;
color: white;
@@ -140,32 +223,38 @@
cursor: pointer;
font-size: 1rem;
text-align: left;
text-decoration: none;
transition: background-color 100ms;
box-sizing: border-box;
}
.language-options button:hover{
.language-options a:hover{
background-color: var(--nord2);
}
.language-options button.active{
background-color: var(--nord14);
.language-options a.active{
background-color: var(--nord8);
color: var(--nord0);
font-weight: 700;
}
</style>
<div class="language-selector">
<button bind:this={langButton} onclick={toggle_language_options} class="language-button">
{$languageStore.toUpperCase()}
{displayLang.toUpperCase()}
</button>
<div bind:this={langOptions} class="language-options" hidden>
<button
class:active={$languageStore === 'de'}
onclick={() => switchLanguage('de')}
<div class="language-options" class:open={isOpen}>
<a
href={dePath}
class:active={displayLang === 'de'}
onclick={(e) => { e.preventDefault(); switchLanguage('de'); }}
>
DE
</button>
<button
class:active={$languageStore === 'en'}
onclick={() => switchLanguage('en')}
</a>
<a
href={enPath}
class:active={displayLang === 'en'}
onclick={(e) => { e.preventDefault(); switchLanguage('en'); }}
>
EN
</button>
</a>
</div>
</div>

View File

@@ -1,12 +1,12 @@
<style>
:global(.links_grid a:nth-child(4n)),
:global(.links_grid a:nth-child(4n) svg){
:global(.links_grid a:nth-child(4n) svg:not(.lock-icon)){
background-color: var(--nord4);
fill: var(--nord11);
}
:global(.links_grid a:nth-child(4n+1)),
:global(.links_grid a:nth-child(4n+1) svg){
:global(.links_grid a:nth-child(4n+1) svg:not(.lock-icon)){
background-color: var(--nord6);
fill: var(--nord10);
}
@@ -20,7 +20,7 @@
:global(a){
text-decoration: unset;
color: var(--nord0);
transition: 200ms;
transition: var(--transition-normal);
}
:global(.links_grid a:hover){
box-shadow: 1em 1em 2em 1em rgba(0,0,0, 0.3);
@@ -30,7 +30,7 @@
}
.links_grid{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(min(250px, calc(50% - 1rem)), 1fr));
gap: 2rem;
max-width: 1000px;
margin-inline: auto;
@@ -43,7 +43,7 @@
justify-content: center;
text-decoration: unset;
color: var(--nord0);
transition: 200ms;
transition: var(--transition-normal);
width: 100%;
padding: 1rem;
position: relative;
@@ -64,8 +64,50 @@
right: 0.5rem;
width: 1.5rem;
height: 1.5rem;
fill: var(--nord0);
opacity: 0.6;
fill: var(--nord3);
opacity: 0.5;
}
@media (max-width: 560px) {
.links_grid {
gap: 1rem;
padding: 1.5rem 0.75rem;
}
:global(.links_grid a :is(svg, img)) {
height: 90px;
}
:global(.links_grid h3) {
font-size: 1.2rem;
}
:global(.links_grid a) {
padding: 0.75rem;
}
:global(.links_grid a .lock-icon) {
width: 1.2rem;
height: 1.2rem;
}
}
@media (max-width: 410px) {
.links_grid {
gap: 0.5rem;
padding: 1rem 0.5rem;
}
:global(.links_grid a :is(svg, img)) {
height: 64px;
}
:global(.links_grid h3) {
font-size: 0.95rem;
}
:global(.links_grid a) {
padding: 0.5rem;
}
:global(.links_grid a .lock-icon) {
width: 1rem;
height: 1rem;
top: 0.3rem;
right: 0.3rem;
}
}
@media (prefers-color-scheme: dark){
@@ -73,26 +115,25 @@
color: white;
}
:global(.links_grid a .lock-icon){
fill: white;
fill: var(--nord3);
}
:global(.links_grid a:nth-child(4n)),
:global(.links_grid a:nth-child(4n) svg){
:global(.links_grid a:nth-child(4n) svg:not(.lock-icon)){
background-color: var(--nord6-dark);
fill: var(--nord11);
}
:global(.links_grid a:nth-child(4n+1)),
:global(.links_grid a:nth-child(4n+1) svg){
:global(.links_grid a:nth-child(4n+1) svg:not(.lock-icon)){
background-color: var(--accent-dark);
fill: var(--nord9);
}
:global(.links_grid a:nth-child(4n+2)),
:global(.links_grid a:nth-child(4n+2) svg){
:global(.links_grid a:nth-child(4n+2) svg:not(.lock-icon)){
background-color: var(--nord1);
fill: var(--nord8);
}
:global(.links_grid a:nth-child(4n+3)),
:global(.links_grid a:nth-child(4n+3) svg){
:global(.links_grid a:nth-child(4n+3) svg:not(.lock-icon)){
background-color: var(--background-dark);
fill: var(--nord7);
}

View File

@@ -0,0 +1,255 @@
<script lang="ts">
import { onMount } from 'svelte';
import { pwaStore } from '$lib/stores/pwa.svelte';
let { lang = 'de' }: { lang?: string } = $props();
let showTooltip = $state(false);
let mounted = $state(false);
const labels = $derived({
syncForOffline: lang === 'en' ? 'Save for offline' : 'Offline speichern',
syncing: lang === 'en' ? 'Syncing...' : 'Synchronisiere...',
offlineReady: lang === 'en' ? 'Offline ready' : 'Offline bereit',
lastSync: lang === 'en' ? 'Last sync' : 'Letzte Sync',
recipes: lang === 'en' ? 'recipes' : 'Rezepte',
syncNow: lang === 'en' ? 'Sync now' : 'Jetzt synchronisieren',
clearData: lang === 'en' ? 'Clear offline data' : 'Offline-Daten löschen'
});
onMount(async () => {
mounted = true;
// Initialize PWA store (checks standalone mode, starts auto-sync if needed)
await pwaStore.initialize();
});
async function handleSync() {
await pwaStore.syncForOffline();
}
async function handleClear() {
await pwaStore.clearOfflineData();
showTooltip = false;
}
function formatDate(isoString: string | null): string {
if (!isoString) return '';
const date = new Date(isoString);
return date.toLocaleDateString(lang === 'en' ? 'en-US' : 'de-DE', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
}
</script>
<style>
.offline-sync {
position: relative;
}
.sync-button {
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
color: white;
transition: color 100ms;
}
.sync-button:hover,
.sync-button:focus {
color: var(--nord8);
}
.sync-button.syncing {
animation: pulse 1s infinite;
}
.sync-button.available {
color: var(--nord14);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.sync-icon {
width: 1.5rem;
height: 1.5rem;
fill: currentColor;
}
.tooltip {
position: absolute;
top: 100%;
right: 0;
margin-top: 0.5rem;
background: var(--nord0);
border: 1px solid var(--nord3);
border-radius: 0.5rem;
padding: 1rem;
min-width: 200px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 100;
}
.tooltip-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.status {
font-size: 0.875rem;
color: var(--nord4);
}
.status.ready {
color: var(--nord14);
}
.tooltip-button {
background: var(--nord3);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
transition: background 100ms;
}
.tooltip-button:hover {
background: var(--nord2);
}
.tooltip-button.clear {
background: var(--nord11);
}
.tooltip-button.clear:hover {
background: #c04040;
}
.tooltip-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.meta {
font-size: 0.75rem;
color: var(--nord4);
}
.progress-container {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.progress-text {
font-size: 0.75rem;
color: var(--nord4);
}
.progress-bar {
width: 100%;
height: 4px;
background: var(--nord3);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--nord14);
transition: width 150ms ease-out;
}
</style>
{#if mounted && pwaStore.isStandalone}
<div class="offline-sync">
<button
class="sync-button"
class:syncing={pwaStore.isSyncing}
class:available={pwaStore.isOfflineAvailable}
onclick={() => showTooltip = !showTooltip}
title={pwaStore.isOfflineAvailable ? labels.offlineReady : labels.syncForOffline}
>
<svg class="sync-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
{#if pwaStore.isOfflineAvailable}
<!-- Checkmark icon when offline data is available -->
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
{:else}
<!-- Download icon when no offline data -->
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
{/if}
</svg>
</button>
{#if showTooltip}
<div class="tooltip">
<div class="tooltip-content">
{#if pwaStore.isOfflineAvailable}
<div class="status ready">{labels.offlineReady}</div>
<div class="meta">
{pwaStore.recipeCount} {labels.recipes}
{#if pwaStore.lastSyncDate}
<br>{labels.lastSync}: {formatDate(pwaStore.lastSyncDate)}
{/if}
</div>
<button
class="tooltip-button"
onclick={handleSync}
disabled={pwaStore.isSyncing}
>
{pwaStore.isSyncing ? labels.syncing : labels.syncNow}
</button>
<button
class="tooltip-button clear"
onclick={handleClear}
disabled={pwaStore.isSyncing}
>
{labels.clearData}
</button>
{:else}
<div class="status">{labels.syncForOffline}</div>
<button
class="tooltip-button"
onclick={handleSync}
disabled={pwaStore.isSyncing}
>
{pwaStore.isSyncing ? labels.syncing : labels.syncForOffline}
</button>
{/if}
{#if pwaStore.isSyncing && pwaStore.syncProgress}
<div class="progress-container">
<div class="progress-text">{pwaStore.syncProgress.message}</div>
{#if pwaStore.syncProgress.imageProgress}
<div class="progress-bar">
<div
class="progress-fill"
style="width: {(pwaStore.syncProgress.imageProgress.completed / pwaStore.syncProgress.imageProgress.total) * 100}%"
></div>
</div>
{/if}
</div>
{/if}
{#if pwaStore.error}
<div class="status" style="color: var(--nord11);">
{pwaStore.error}
</div>
{/if}
</div>
</div>
{/if}
</div>
{/if}

View File

@@ -1,142 +0,0 @@
<script lang="ts">
let {
germanUrl,
englishUrl,
currentLang = 'de',
hasTranslation = true
}: {
germanUrl: string,
englishUrl: string,
currentLang?: 'de' | 'en',
hasTranslation?: boolean
} = $props();
function setLanguagePreference(lang: 'de' | 'en') {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('preferredLanguage', lang);
}
}
</script>
<style>
.language-switcher {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
display: flex;
gap: 0.5rem;
background: var(--nord0);
padding: 0.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
@media(prefers-color-scheme: light) {
.language-switcher {
background: var(--nord6);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.language-switcher a {
padding: 0.5rem 1rem;
border-radius: 4px;
text-decoration: none;
color: var(--nord4);
font-weight: 600;
font-size: 0.9rem;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
@media(prefers-color-scheme: light) {
.language-switcher a {
color: var(--nord2);
}
}
.language-switcher a:hover {
background: var(--nord3);
color: var(--nord6);
}
@media(prefers-color-scheme: light) {
.language-switcher a:hover {
background: var(--nord4);
color: var(--nord0);
}
}
.language-switcher a.active {
background: var(--nord14);
color: var(--nord0);
}
.language-switcher a.active:hover {
background: var(--nord15);
}
.language-switcher a.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.flag {
font-size: 1.2rem;
line-height: 1;
}
@media (max-width: 600px) {
.language-switcher {
top: 0.5rem;
right: 0.5rem;
padding: 0.25rem;
gap: 0.25rem;
}
.language-switcher a {
padding: 0.4rem 0.7rem;
font-size: 0.85rem;
}
.flag {
font-size: 1rem;
}
}
</style>
<div class="language-switcher">
<a
href={germanUrl}
class:active={currentLang === 'de'}
aria-label="Switch to German"
onclick={() => setLanguagePreference('de')}
>
<span class="flag">🇩🇪</span>
<span class="label">DE</span>
</a>
{#if hasTranslation}
<a
href={englishUrl}
class:active={currentLang === 'en'}
aria-label="Switch to English"
onclick={() => setLanguagePreference('en')}
>
<span class="flag">🇬🇧</span>
<span class="label">EN</span>
</a>
{:else}
<span
class="disabled"
title="English translation not available"
aria-label="English translation not available"
>
<span class="flag">🇬🇧</span>
<span class="label">EN</span>
</span>
{/if}
</div>

View File

@@ -0,0 +1,85 @@
<script>
let {
value = $bindable(''),
placeholder = 'Search...',
clearTitle = 'Clear search',
onClear = () => {}
} = $props();
function handleClear() {
value = '';
onClear();
}
</script>
<style>
input {
all: unset;
box-sizing: border-box;
background: var(--nord0);
color: #fff;
padding: 0.7rem 2rem;
border-radius: var(--radius-pill);
width: 100%;
}
input::placeholder {
color: var(--nord6);
}
.search {
width: 500px;
max-width: 85vw;
position: relative;
margin: 2.5rem auto 1.2rem;
font-size: 1.6rem;
display: flex;
align-items: center;
transition: var(--transition-fast);
filter: drop-shadow(0.4em 0.5em 0.4em rgba(0,0,0,0.4));
}
.search:hover,
.search:focus-within {
scale: 1.02 1.02;
filter: drop-shadow(0.4em 0.5em 1em rgba(0,0,0,0.6));
}
.search-button {
all: unset;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: 0.5em;
width: 1.5em;
height: 1.5em;
color: var(--nord6);
cursor: pointer;
transition: color 180ms ease-in-out;
}
.search-button:hover {
color: white;
scale: 1.1 1.1;
}
.search-button:active {
transition: 50ms;
scale: 0.8 0.8;
}
.search-button svg {
width: 100%;
height: 100%;
}
</style>
<div class="search">
<input type="text" {placeholder} bind:value>
{#if value}
<button type="button" class="search-button" onclick={handleClear}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<title>{clearTitle}</title>
<path d="M135.19 390.14a28.79 28.79 0 0021.68 9.86h246.26A29 29 0 00432 371.13V140.87A29 29 0 00403.13 112H156.87a28.84 28.84 0 00-21.67 9.84v0L46.33 256l88.86 134.11z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M336.67 192.33L206.66 322.34M336.67 322.34L206.66 192.33"/>
</svg>
</button>
{/if}
</div>

View File

@@ -1,13 +1,12 @@
<script>
import "$lib/css/nordtheme.css";
</script>
<style>
:root{
--icon_fill: var(--nord4);
}
svg{
transition: 100ms;
height: 3em;
transition: var(--transition-fast);
height: var(--symbol-size, 3em);
}
svg:hover,
svg:focus-visible

View File

@@ -1,15 +1,14 @@
<script lang="ts">
let { tag, ref } = $props<{ tag: string, ref: string }>();
import '$lib/css/nordtheme.css'
</script>
<style>
a{
background-color: var(--blue);
text-decoration: none;
padding: 2rem;
padding: clamp(0.4rem, 0.8vw, 0.8rem) clamp(0.8rem, 1.5vw, 1.5rem);
border-radius: 1000000px;
transition: 100ms;
font-size: 2rem;
transition: var(--transition-fast);
font-size: clamp(0.85rem, 1.8vw, 1.5rem);
color: white;
}
a:hover{

View File

@@ -5,9 +5,8 @@ div{
flex-direction: row;
flex-wrap: wrap;
margin-inline:auto;
gap: 1rem;
gap: clamp(0.4rem, 1vw, 1rem);
justify-content: space-evenly;
font-family: sans-serif;
}
</style>

View File

@@ -1,23 +1,26 @@
<script lang="ts">
let { checked = $bindable(false), label = "", accentColor = "var(--nord14)" } = $props<{ checked?: boolean, label?: string, accentColor?: string }>();
let { checked = $bindable(false), label = "", accentColor = "var(--nord14)", href = undefined as string | undefined } = $props<{ checked?: boolean, label?: string, accentColor?: string, href?: string }>();
</script>
<style>
.toggle-wrapper {
display: flex;
display: inline-flex;
}
.toggle-wrapper label {
.toggle-wrapper label,
.toggle-wrapper a {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
font-size: 0.95rem;
color: var(--nord4);
text-decoration: none;
}
@media(prefers-color-scheme: light) {
.toggle-wrapper label {
.toggle-wrapper label,
.toggle-wrapper a {
color: var(--nord2);
}
}
@@ -26,37 +29,43 @@
user-select: none;
}
/* iOS-style toggle switch */
/* iOS-style toggle switch — shared by checkbox and link variants */
.toggle-track,
.toggle-wrapper input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 51px;
height: 31px;
margin: 0;
width: 44px;
height: 24px;
background: var(--nord2);
border-radius: 31px;
border-radius: 24px;
position: relative;
cursor: pointer;
transition: background 0.3s ease;
outline: none;
border: none;
flex-shrink: 0;
display: inline-block;
}
@media(prefers-color-scheme: light) {
.toggle-track,
.toggle-wrapper input[type="checkbox"] {
background: var(--nord4);
}
}
.toggle-track.checked,
.toggle-wrapper input[type="checkbox"]:checked {
background: var(--accent-color);
}
.toggle-track::before,
.toggle-wrapper input[type="checkbox"]::before {
content: '';
position: absolute;
width: 27px;
height: 27px;
width: 20px;
height: 20px;
border-radius: 50%;
top: 2px;
left: 2px;
@@ -65,14 +74,22 @@
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.toggle-track.checked::before,
.toggle-wrapper input[type="checkbox"]:checked::before {
transform: translateX(20px);
}
</style>
<div class="toggle-wrapper" style="--accent-color: {accentColor}">
<label>
<input type="checkbox" bind:checked />
<span>{label}</span>
</label>
{#if href}
<a {href} onclick={(e) => { e.preventDefault(); checked = !checked; }}>
<span class="toggle-track" class:checked></span>
<span>{label}</span>
</a>
{:else}
<label>
<input type="checkbox" bind:checked />
<span>{label}</span>
</label>
{/if}
</div>

View File

@@ -81,6 +81,7 @@
background-color: var(--bg_color);
width: 30ch;
padding: 1rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
#options ul{
color: white;
@@ -97,7 +98,7 @@
text-decoration: none;
color: white;
text-align: left;
transition: 100ms;
transition: var(--transition-fast);
}
#options li:hover a{
color: var(--red);
@@ -116,22 +117,31 @@ h2 + p{
#options{
top: unset;
bottom: calc(100% + 15px);
right: -200%;
z-index: 99999999999999999999;
left: 50%;
right: unset;
transform: translateX(-50%);
z-index: 10;
}
.top.speech::after {
/* (B2-1) DOWN TRIANGLE */
border-top-color: #a53d38;
border-bottom: 0;
z-index: 99999999999999999999;
/* (B2-2) POSITION AT BOTTOM */
bottom: -20px; left: 50%;
border: 20px solid transparent;
border-top-color: var(--bg_color);
border-bottom-width: 0;
top: unset;
bottom: -20px;
left: 50%;
margin-left: -20px;
}
button{
margin-bottom: 2rem;
}
button::before{
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: inherit;
z-index: 20;
}
}
</style>

View File

@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { Chart, registerables } from 'chart.js';
let { data = { labels: [], datasets: [] }, title = '', height = '400px' } = $props<{ data?: any, title?: string, height?: string }>();
let { data = { labels: [], datasets: [] }, title = '', height = '400px', onFilterChange = null } = $props<{ data?: any, title?: string, height?: string, onFilterChange?: ((categories: string[] | null) => void) | null }>();
let canvas = $state();
let chart = $state();
@@ -42,6 +42,19 @@
return categoryColorMap[category] || nordColors[index % nordColors.length];
}
function emitFilter() {
if (!onFilterChange || !chart) return;
const allVisible = chart.data.datasets.every((_, idx) => !chart.getDatasetMeta(idx).hidden);
if (allVisible) {
onFilterChange(null);
} else {
const visible = chart.data.datasets
.filter((_, idx) => !chart.getDatasetMeta(idx).hidden)
.map(ds => ds.label.toLowerCase());
onFilterChange(visible);
}
}
function createChart() {
if (!canvas || !data.datasets) return;
@@ -135,7 +148,6 @@
},
onClick: (event, legendItem, legend) => {
const datasetIndex = legendItem.datasetIndex;
const clickedMeta = chart.getDatasetMeta(datasetIndex);
// Check if only this dataset is currently visible
const onlyThisVisible = chart.data.datasets.every((dataset, idx) => {
@@ -156,6 +168,7 @@
}
chart.update();
emitFilter();
}
},
title: {
@@ -229,6 +242,7 @@
}
chart.update();
emitFilter();
}
}
},

View File

@@ -3,7 +3,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import ProfilePicture from './ProfilePicture.svelte';
import EditButton from './EditButton.svelte';
import EditButton from '$lib/components/EditButton.svelte';
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters';

View File

@@ -58,7 +58,6 @@
splitAmounts[user] = splitAmount;
}
});
splitAmounts = { ...splitAmounts };
}
function calculateFullPayment() {
@@ -75,7 +74,6 @@
splitAmounts[user] = amountPerOtherUser;
}
});
splitAmounts = { ...splitAmounts };
}
function calculatePersonalEqualSplit() {
@@ -100,7 +98,6 @@
splitAmounts[user] = totalOwed;
}
});
splitAmounts = { ...splitAmounts };
}
function handleSplitMethodChange() {
@@ -116,7 +113,6 @@
splitAmounts[user] = 0;
}
});
splitAmounts = { ...splitAmounts };
}
}

View File

@@ -5,19 +5,23 @@
reference = '',
title = '',
verseData = null,
lang = 'de',
onClose
}: {
reference?: string,
title?: string,
verseData?: VerseData | null,
lang?: string,
onClose: () => void
} = $props();
const isEnglish = $derived(lang === 'en');
let book: string = $state(verseData?.book || '');
let chapter: number = $state(verseData?.chapter || 0);
let verses: Array<{ verse: number; text: string }> = $state(verseData?.verses || []);
let loading = $state(false);
let error = $state(verseData ? '' : 'Keine Versdaten verfügbar');
let error = $state(verseData ? '' : (lang === 'en' ? 'No verse data available' : 'Keine Versdaten verfügbar'));
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget) {
@@ -49,7 +53,7 @@
{/if}
<p class="modal-reference">{reference}</p>
</div>
<button class="close-button" onclick={onClose} aria-label="Schließen">
<button class="close-button" onclick={onClose} aria-label={isEnglish ? 'Close' : 'Schließen'}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
@@ -59,7 +63,7 @@
<div class="modal-body">
{#if loading}
<p class="loading">Lädt...</p>
<p class="loading">{isEnglish ? 'Loading...' : 'Lädt...'}</p>
{:else if error}
<p class="error">{error}</p>
{:else if verses.length > 0}
@@ -72,7 +76,7 @@
{/each}
</div>
{:else}
<p class="error">Keine Verse gefunden</p>
<p class="error">{isEnglish ? 'No verses found' : 'Keine Verse gefunden'}</p>
{/if}
</div>
</div>
@@ -184,9 +188,9 @@
border: none;
cursor: pointer;
padding: 1rem;
border-radius: 1000px;
border-radius: var(--radius-pill);
color: white;
transition: 200ms;
transition: var(--transition-normal);
box-shadow: 0 0 1em 0.2em rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
@@ -244,6 +248,7 @@
gap: 0.75rem;
line-height: 1.6;
color: var(--nord4);
margin: 0;
}
@media(prefers-color-scheme: light) {

View File

@@ -0,0 +1,276 @@
<!-- FireEffect.svelte -->
<script lang="ts">
interface Props {
holy?: boolean;
burst?: boolean;
fire ?: boolean;
}
let { holy = false, burst = false, fire = false}: Props = $props();
const burstParticles = [
{ x: 10, y: 0, size: 8, delay: 0, dur: 1.6 },
{ x: 25, y: 5, size: 10, delay: 0.05, dur: 1.8 },
{ x: 40, y: 10, size: 12, delay: 0.02, dur: 2.0 },
{ x: 55, y: 3, size: 7, delay: 0.1, dur: 1.7 },
{ x: 70, y: 8, size: 9, delay: 0.08, dur: 1.9 },
{ x: 85, y: 2, size: 11, delay: 0.12, dur: 1.6 },
{ x: 15, y: 15, size: 6, delay: 0.15, dur: 1.5 },
{ x: 35, y: 20, size: 10, delay: 0.18, dur: 1.8 },
{ x: 50, y: 12, size: 8, delay: 0.07, dur: 2.0 },
{ x: 65, y: 18, size: 7, delay: 0.22, dur: 1.7 },
{ x: 80, y: 25, size: 9, delay: 0.1, dur: 1.9 },
{ x: 20, y: 30, size: 11, delay: 0.25, dur: 1.6 },
{ x: 45, y: 22, size: 6, delay: 0.03, dur: 1.8 },
{ x: 60, y: 28, size: 10, delay: 0.2, dur: 2.0 },
{ x: 75, y: 15, size: 8, delay: 0.14, dur: 1.5 },
{ x: 30, y: 35, size: 12, delay: 0.28, dur: 1.7 },
{ x: 5, y: 10, size: 7, delay: 0.06, dur: 1.9 },
{ x: 90, y: 20, size: 9, delay: 0.16, dur: 1.6 },
{ x: 48, y: 32, size: 8, delay: 0.3, dur: 2.0 },
{ x: 22, y: 8, size: 10, delay: 0.11, dur: 1.8 },
{ x: 68, y: 35, size: 6, delay: 0.23, dur: 1.5 },
{ x: 38, y: 5, size: 11, delay: 0.04, dur: 1.7 },
{ x: 82, y: 30, size: 7, delay: 0.26, dur: 1.9 },
{ x: 52, y: 18, size: 9, delay: 0.09, dur: 1.6 },
];
</script>
{#if burst}
<div class="burst-particles" class:holy-fire={holy}>
{#each burstParticles as p}
<div
class="bp"
style:left="{p.x}%"
style:bottom="{p.y}%"
style:width="{p.size}px"
style:height="{p.size}px"
style:animation-delay="{p.delay}s"
style:animation-duration="{p.dur}s"
></div>
{/each}
</div>
{:else}
<div class="fire" class:holy-fire={holy}>
<div class="fire-left">
{#if fire}<div class="main-fire"></div>{/if}
<div class="particle-fire"></div>
</div>
<div class="fire-center">
{#if fire}<div class="main-fire"></div>{/if}
<div class="particle-fire"></div>
</div>
<div class="fire-right">
{#if fire}<div class="main-fire"></div>{/if}
<div class="particle-fire"></div>
</div>
<div class="fire-bottom">
{#if fire}<div class="main-fire"></div>{/if}
</div>
</div>
{/if}
<style>
/* =====================
PURE CSS FIRE (SCALED + RISING)
===================== */
.fire {
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%) scale(0.55);
width: 100px;
height: 100px;
pointer-events: none;
z-index: 4;
}
/* ---------- animations ---------- */
@keyframes scaleUpDown {
0%,100% { transform: scaleY(1) scaleX(1); }
50%,90% { transform: scaleY(1.1); }
75% { transform: scaleY(0.95); }
80% { transform: scaleX(0.95); }
}
@keyframes shake {
0%,100% { transform: skewX(0) scale(1); }
50% { transform: skewX(5deg) scale(0.9); }
}
@keyframes particleUp {
0% { opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% {
opacity: 0;
top: -100%;
transform: scale(0.5);
}
}
@keyframes glow {
0%,100% { background-color: #ef5a00; }
50% { background-color: #ff7800; }
}
/* ---------- fire structure ---------- */
.fire-center,
.fire-left,
.fire-right {
position: absolute;
width: 100%;
height: 100%;
}
.fire-center {
animation: scaleUpDown 3s ease-out infinite;
}
.fire-left {
animation: shake 3s ease-out infinite;
}
.fire-right {
animation: shake 2s ease-out infinite;
}
.main-fire {
position: absolute;
width: 100%;
height: 100%;
background-image: radial-gradient(
farthest-corner at 10px 0,
color-mix(in srgb, var(--nord11) 70%, transparent),
color-mix(in srgb, var(--nord12) 70%, transparent) 60%,
color-mix(in srgb, var(--nord13) 85%, transparent) 95%
);
filter: drop-shadow(0 0 6px var(--nord12));
transform: scaleX(0.8) rotate(45deg);
border-radius: 0 40% 60% 40%;
}
.fire-left .main-fire {
top: 15%;
left: -20%;
width: 80%;
height: 80%;
}
.fire-right .main-fire {
top: 15%;
right: -25%;
width: 80%;
height: 80%;
}
.particle-fire {
position: absolute;
width: 10px;
height: 10px;
background-color: var(--nord13);
filter: drop-shadow(0 0 4px var(--nord12));
border-radius: 50%;
animation: particleUp 2.5s ease-out infinite;
}
.fire-center .particle-fire {
top: 60%;
left: 45%;
}
.fire-left .particle-fire {
top: 20%;
left: 20%;
animation-duration: 3s;
}
.fire-right .particle-fire {
top: 45%;
left: 50%;
width: 15px;
height: 15px;
}
.fire-bottom .main-fire {
position: absolute;
top: 30%;
left: 20%;
width: 75%;
height: 75%;
background-color: #ff7800;
transform: scaleX(0.8) rotate(45deg);
border-radius: 0 40% 100% 40%;
filter: blur(10px);
animation: glow 2s ease-out infinite;
}
/* =====================
HOLY (BLUE-WHITE) FIRE
===================== */
.holy-fire .main-fire {
background-image: radial-gradient(
farthest-corner at 10px 0,
#9fd4ff 0%,
#eaf6ff 95%
);
filter: drop-shadow(0 0 12px rgba(180,220,255,.9));
}
.holy-fire .particle-fire {
background-color: #eaf6ff;
filter: drop-shadow(0 0 12px rgba(180,220,255,.9));
}
.holy-fire .fire-bottom .main-fire {
background-color: #d6ecff;
}
/* =====================
BURST particles only
===================== */
.burst-particles {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 80px;
pointer-events: none;
z-index: 6;
}
.burst-particles .bp {
position: absolute;
background-color: var(--nord13);
filter: drop-shadow(0 0 4px var(--nord12));
border-radius: 50%;
opacity: 0;
animation: burstParticleUp 2s ease-out forwards;
}
.burst-particles.holy-fire .bp {
background-color: #eaf6ff;
filter: drop-shadow(0 0 12px rgba(180,220,255,.9));
}
@keyframes burstParticleUp {
0% {
transform: translateY(0) scale(1);
opacity: 0;
}
10% {
opacity: 1;
}
60% {
opacity: 0.8;
}
100% {
transform: translateY(-80px) scale(0.3);
opacity: 0;
}
}
</style>

View File

@@ -1,13 +1,17 @@
<script>
import { onMount } from 'svelte';
import { getLanguageContext } from '$lib/contexts/languageContext.js';
import Toggle from './Toggle.svelte';
import Toggle from '$lib/components/Toggle.svelte';
export let initialLatin = undefined;
export let hasUrlLatin = false;
export let href = undefined;
// Get the language context (must be created by parent page)
const { showLatin } = getLanguageContext();
const { showLatin, lang } = getLanguageContext();
// Local state for the checkbox
let showBilingual = true;
let showBilingual = initialLatin !== undefined ? initialLatin : true;
// Flag to prevent saving before we've loaded from localStorage
let hasLoadedFromStorage = false;
@@ -20,11 +24,18 @@
localStorage.setItem('rosary_showBilingual', showBilingual.toString());
}
// Dynamic label based on URL language
$: label = $lang === 'en'
? 'Show Latin and English'
: 'Lateinisch und Deutsch anzeigen';
onMount(() => {
// Load from localStorage
const saved = localStorage.getItem('rosary_showBilingual');
if (saved !== null) {
showBilingual = saved === 'true';
// Only load from localStorage if no URL param was set
if (!hasUrlLatin) {
const saved = localStorage.getItem('rosary_showBilingual');
if (saved !== null) {
showBilingual = saved === 'true';
}
}
// Now allow saving
@@ -34,6 +45,7 @@
<Toggle
bind:checked={showBilingual}
label="Lateinisch und Deutsch anzeigen"
{label}
{href}
accentColor="var(--nord14)"
/>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
let { type }: { type: 'joyful' | 'sorrowful' | 'glorious' | 'luminous' } = $props();
</script>
{#if type === 'joyful'}
<svg viewBox="-10 0 2058 2048">
<path d="M1935 90q0 32 -38 91q-21 29 -56 90q-20 55 -63 164q-35 86 -95 143q-22 -21 -43 -45q51 -49 85 -139q49 -130 61 -152q-126 48 -152 63q-76 46 -95 128q-27 -18 -58 -25q28 -104 97 -149q31 -20 138 -52q90 -28 137 -74l29 -39q22 -30 32 -30q21 0 21 26zM1714 653 q-90 30 -113 43q-65 36 -65 90q0 19 20 119q23 116 23 247q0 169 -103 299q-111 141 -275 141q-254 0 -283 87q-16 104 -31 207q-27 162 -76 162q-21 0 -41 -20q-16 -19 -32 -37q-10 3 -33 22q-18 15 -39 15q-28 0 -50 -44.5t-30 -44.5q-10 0 -35.5 11.5t-41.5 11.5 q-47 0 -58.5 -45.5t-21.5 -45.5t-29.5 2.5t-29.5 2.5q-46 0 -46 -30q0 -16 14 -44.5t14 -44.5q0 -8 -46.5 -25.5t-46.5 -48.5q0 -34 35.5 -52t99.5 -31q91 -19 103 -22q113 -32 171 -93q37 -39 105 -165q34 -64 43 -82q26 -53 31 -85q-129 -67 -224 -76q-33 0 -96 -11 q-36 -13 -36 -41q0 -7 2 -19.5t2 -19.5q0 -20 -67.5 -42t-67.5 -64q0 -11 8.5 -30t8.5 -30q0 -15 -79 -39t-79 -63q0 -16 9 -45t9 -45q0 -20 -29 -43q-23 -17 -46 -33q-49 -44 -49 -215q0 -8 1 -15q91 53 194 68l282 16q202 12 304 59q143 65 143 210q0 15 -2 44t-2 44 q0 122 78 122q73 0 108 -133q16 -70 32 -139q21 -81 57 -119q46 -51 130 -51q71 0 122 61q90 107 154 149zM1597 636q-25 -22 -77 -91q-30 -40 -75 -40q-91 0 -131 115q-30 106 -59 213q-44 115 -144 115q-146 0 -146 -180q0 -16 2.5 -46.5t2.5 -46.5q0 -62 -19 -87 q-70 -92 -303 -115q-173 -9 -347 -18q-55 -6 -116 -30v34q0 27 57.5 73.5t57.5 91.5q0 16 -10.5 45t-10.5 44q1 1 7 1q3 0 7 1q146 36 146 105q0 13 -8.5 32.5t-8.5 27.5h10q5 0 9 1q61 15 86 36q32 28 28 85q173 15 372 107q-7 77 -80 215q-67 128 -127 195 q-67 74 -169 104q-96 24 -193 47q-10 3 -29 13q86 18 86 70q0 19 -19 62q15 -5 33 -5q42 0 59 26q8 11 22 61l-1 3q10 0 34.5 -11.5t42.5 -11.5q55 0 88 84q38 -32 64 -32q37 0 66 41q25 -53 33 -151q10 -112 23 -154q43 -136 337 -136q116 0 215 -108q105 -114 105 -277 q0 -23 -12 -112l-28 -207q-4 -30 -4 -42q0 -97 124 -147zM1506 605q0 38 -38 38q-39 0 -39 -38t39 -38q38 0 38 38z" />
<path d="m 1724.44,1054.6641 c -31.1769,-18 -37.7653,-42.5884 -19.7653,-73.76528 5.3333,-9.2376 12.354,-16.7312 21.0621,-22.4808 6.2201,-4.1068 44.7886,-7.2427 115.7055,-9.4077 70.9168,-2.1649 110.128,-1.0807 117.6336,3.2526 30.0222,17.3334 35.5333,42.45448 16.5333,75.36348 -7.3333,12.7017 -16.1754,20.6833 -26.5263,23.9448 -24.5645,1.2137 -56.7805,3.0135 -96.648,5.3994 -72.6282,5.7957 -115.2931,5.0269 -127.9949,-2.3065 z" />
<path d="m 386.57764,1262.0569 c 53.44793,-14.3214 85.17574,-2.8075 95.18337,34.5417 9.83517,36.7052 -12.29319,62.3047 -66.38503,76.7986 l -82.1037,21.9996 c -54.09184,14.4939 -86.05533,3.3882 -95.89047,-33.317 -10.00766,-37.3491 12.67841,-63.4432 68.05807,-78.2821 z"/>
<path d="m 1115.7599,372.22724 c 14.3213,53.44793 2.8073,85.17581 -34.5418,95.18323 -36.705,9.83527 -62.3047,-12.29323 -76.7986,-66.38485 l -21.99962,-82.10394 c -14.4939,-54.09162 -3.3882,-86.05531 33.31712,-95.89019 37.349,-10.00765 63.4431,12.67818 78.2821,68.05802 z" />
<path d="m 1184.6228,1956.284 c -4.807,-8.0003 -6.8298,-42.7561 -6.0684,-104.2674 0.7614,-61.5113 2.7093,-100.0139 5.8437,-115.508 3.1343,-15.4941 11.8445,-27.5329 26.1306,-36.117 30.2866,-18.198 54.7006,-11.868 73.242,18.99 5.4937,9.1432 8.145,43.3269 7.9537,102.5512 -0.081,52.9359 -1.4296,89.5231 -4.0464,109.7617 -2.276,16.9226 -11.1284,30.0192 -26.5575,39.29 -33.1439,19.9148 -58.643,15.0146 -76.4977,-14.7005 z" />
<path d="m 1773.3127,1737.6952 c -9.0153,-2.4157 -34.6139,-26.0118 -76.7955,-70.7882 -42.1816,-44.7764 -67.5266,-73.826 -76.035,-87.1489 -8.5084,-13.3228 -10.6057,-28.0334 -6.2922,-44.1323 9.145,-34.1293 31.1041,-46.5353 65.8774,-37.2179 10.3033,2.7609 35.9565,25.5088 76.9595,68.2441 36.7142,38.1352 61.1596,65.3907 73.3362,81.7668 10.1182,13.7541 12.8479,29.3245 8.1892,46.7113 -10.0077,37.3492 -31.7542,51.5375 -65.2396,42.5651 z" />
</svg>
{:else if type === 'sorrowful'}
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 512 512"><path d="M255.094 24.875c-16.73 9.388-34.47 42.043-41.688 59.47-14.608-2.407-28.87-3.664-42.562-3.75-11.446-.074-22.49.68-33.03 2.218-16.34-8.284-34.766-29.065-42.626-50-9.324 15.704-9.558 42.313-5.782 64.593-19.443 9.72-35.107 23.633-45.53 41.688-7.262 12.577-11.5 26.34-12.97 40.875 13.294-25.904 35-46.957 65.656-54.345-34.99 31.783-59.85 87.186-51.5 129.406-1.2 22.87-9.48 37.647-24.75 44.595 16.335 4.59 35.497 3.343 49.438-1.28 24.94 34.82 60.818 67.882 105.063 94.342-6.952 17.613-16.677 49.21-16.47 66.032 10.846-13.178 37.433-40.585 61.72-42.783 23.656 10.27 47.35 17.698 70.312 22.313 12.423 17.25 12.895 38.867 7.375 53.594 16.402-9.2 33.82-33.187 39.938-48 47.1 1.423 88.046-10.534 114.718-35.563 17.536 5.52 30.744 15.707 39.813 30.5.243-19.578-8.05-44.353-18-60.31 13.42-28.268 12.786-61.81.5-96.158l.405.47c9.976-11.804 18.304-33.19 18.063-52.907-8.535 10.373-20.727 15.14-36.75 14.188-13.56-22.597-31.81-44.812-54.032-65.375 10.56-19.27 30.402-36.43 44.156-47.97-18.985-5.337-67.794 5.2-80.78 17.782l5.906 8.5c5.637 11.99 9.503 24.423 11.093 37.063-26.323-37.275-70.72-74.72-114.905-95.625-15.894-25.424-19.322-56.118-12.78-73.563zm-82.875 97.063c1.13-.015 2.258-.008 3.405 0 31.56.2 68.888 8.842 107 25.656-8.8 20.095-14.74 44.482-10 61.344 13.33-18.637 37.313-34.22 55.406-37.5 55.904 34.315 96.215 78.718 111.658 118.718l.093.22c16.088 37.88 13.36 85.186-26.56 117.312 4.79-11.41 7.986-23.828 9.5-36.438-14.078 10.012-33.524 15.304-56.314 15.97-1.954-17.242-9.117-52.874-22.28-65.72 1.565 16.122-8.11 46.272-26.22 61.063-31.916-6.495-66.794-19.67-101.03-39.438-9.538-5.506-18.65-11.307-27.314-17.344-3.444-23.614 7.842-53.562 20.563-64.03-18.967-.234-46.71 22.156-59.313 32.75-40.974-38.47-64.14-81.11-61.25-115 16.275-1.708 36.144.927 51.72 8-3.92-15.382-18.553-31.733-34.407-44.344 14.757-13.826 37.7-20.852 65.344-21.22z"/></svg>
</svg>
{:else if type === 'glorious'}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 0 2060 2048">
<path d="M1968 505l-119 632q101 61 101 163q0 149 -228 212q-171 47 -356 47h-682q-47 0 -111 -8q-210 -26 -293 -55q-180 -62 -180 -196q0 -124 101 -163l-119 -632h37q87 0 170 43q-18 85 -18 103q0 116 75 130q31 -47 77 -129l40 147q49 -37 95 -37t100 37q9 -38 31 -113 q34 29 68 57q47 38 75 38q34 0 60 -27.5t26 -61.5q0 -26 -31 -74l-46 -72q46 -13 91 -26q55 -15 93 -15t93 15q45 13 91 26l-46 72q-31 51 -31 74q0 34 26 61.5t60 27.5q26 0 75 -38q34 -28 68 -57l31 113q66 -37 97 -37q56 0 95 37q14 -48 43 -145q39 66 77 127 q75 -14 75 -130q0 17 -18 -103q89 -43 207 -43zM1889 557h-29q-10 0 -17 7q0 94 -9 130q-14 63 -67 110q-33 29 -63 29q-28 0 -59 -41q-31 115 -31 169q57 -36 77 -36q75 0 75 119q0 78 -32 126h-183q-54 -79 -54 -198v-5q64 -28 64 -80q0 -30 -20 -52.5t-50 -22.5 q-33 0 -55 22.5t-22 55.5q0 53 46 74q-10 44 -21 86.5t-45.5 81t-39.5 38.5h-271q-21 -52 -21 -81q0 -65 47 -114.5t112 -49.5q29 0 106 36q7 -33 7 -82q0 -26 -7 -89q-42 43 -106 43q-65 0 -112 -49.5t-47 -114.5q0 -40 33 -105q-26 -7 -70 -7q-48 0 -70 7q33 63 33 105 q0 65 -47 114.5t-112 49.5q-60 0 -106 -43q-7 63 -7 87q0 53 7 84q70 -36 106 -36q65 0 112 49.5t47 114.5q0 32 -21 81h-271q-16 0 -57 -58q-21 -30 -32 -72q-8 -38 -17 -76q46 -14 46 -74q0 -78 -77 -78q-30 0 -50 22t-20 53q0 48 64 80v4q0 125 -54 199h-183 q-32 -54 -32 -124q0 -121 75 -121q19 0 77 36v-20q0 -27 -31 -151q-27 43 -59 43q-19 0 -51 -19q-40 -24 -67 -87q-24 -57 -24 -109q0 -10 1 -29t1 -28q-18 -1 -23 -1q-13 0 -22 1l46 241q64 17 64 101q0 51 -30 51q-3 0 -6 -1q19 83 39 212l-2 4q-102 20 -102 110 q0 141 342 175q132 13 150 13h726q-9 0 55 -5q437 -34 437 -183q0 -88 -105 -111l40 -215q-2 0 -5 1q-31 0 -31 -51q0 -32 16 -62q19 -34 48 -39zM1518 888q0 34 -30 34q-34 0 -34 -34t32 -34t32 34zM1099 880q0 30 -22 51t-52 21q-29 0 -51.5 -21.5t-22.5 -50.5 q0 -31 22 -54.5t52 -23.5q31 0 52.5 23.5t21.5 54.5zM596 888q0 34 -34 34q-30 0 -30 -34t32 -34t32 34z" />
</svg>
{:else if type === 'luminous'}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 0 2156 2048">
<path d="M1668 383q0 14 -48.5 92.5t-64.5 96t-41 17.5q-53 0 -53 -54q0 -16 46 -92q41 -68 60 -92q16 -20 43 -20q58 0 58 52zM688 535q0 54 -54 54q-16 0 -30 -7q-10 -5 -66 -95.5t-56 -103.5q0 -52 57 -52q22 0 34 11q20 31 53 81q62 90 62 112zM2064 842q0 59 -56 100 q-231 162 -468 342l190 586q1 4 -5 28q-22 84 -110 84q-23 0 -45 -11q-18 -9 -203 -146l-291 -213q-125 89 -328 238q-51 39 -156 114q-28 18 -63 18q-46 0 -78.5 -32t-34.5 -78l194 -589q-76 -58 -197 -144q-81 -57 -163 -114q-126 -91 -147 -118t-21 -65q0 -36 29.5 -75.5 t64.5 -39.5h604q33 -94 126 -375q19 -62 61 -184q29 -73 108 -73t110 83q4 11 58 177l123 372h607q34 0 64 41q27 38 27 74zM1129 1958q0 83 -58 83q-57 0 -57 -84v-85q0 -84 57 -84q58 0 58 86v84zM1943 849h-659l-211 -636l-207 629h-663l541 397l-206 621l537 -386 l536 389l-209 -629zM1671 934l-370 267l150 436l-378 -271l-371 271q8 -34 15 -68q10 -41 28 -62q46 -53 144 -120q80 -53 159 -106l296 210l-112 -344l299 -213h140z" />
</svg>
{/if}
<style>
svg {
width: 80px;
height: 80px;
fill: var(--nord4);
transition: fill 0.3s ease;
}
@media(prefers-color-scheme: light) {
svg {
fill: var(--nord0);
}
}
:global(.mystery-button.selected) svg,
:global(.mystery-button:hover) svg {
fill: var(--nord10);
}
</style>

View File

@@ -0,0 +1,119 @@
<script>
/**
* @param {ReturnType<import('$lib/js/pip.svelte').createPip>} pip - a createPip() instance
* @param {string} src - image source
* @param {string} [alt] - image alt text
* @param {boolean} [visible] - whether the PiP should be shown
* @param {(e: Event) => void} [onload] - callback when image loads
*/
let { pip, src, alt = '', visible = false, onload, el = $bindable(null) } = $props();
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="pip-container"
class:visible
class:enlarged={pip.enlarged}
class:fullscreen={pip.fullscreen}
bind:this={el}
onpointerdown={pip.onpointerdown}
onpointermove={pip.onpointermove}
onpointerup={pip.onpointerup}
>
{#if src}
<img {src} {alt} {onload}>
{/if}
{#if pip.showControls}
<button
class="pip-fullscreen-btn"
aria-label="Fullscreen"
onpointerdown={(e) => e.stopPropagation()}
onclick={(e) => { e.stopPropagation(); pip.toggleFullscreen(); }}
>
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="8 3 3 3 3 8"/>
<polyline points="16 3 21 3 21 8"/>
<polyline points="8 21 3 21 3 16"/>
<polyline points="16 21 21 21 21 16"/>
</svg>
</button>
{/if}
</div>
<style>
.pip-container {
position: fixed;
top: 0;
left: 0;
z-index: 10000;
opacity: 0;
touch-action: none;
cursor: grab;
user-select: none;
transition: opacity 0.25s ease;
pointer-events: none;
}
.pip-container:active {
cursor: grabbing;
}
.pip-container img {
height: 25vh;
width: auto;
object-fit: contain;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
pointer-events: none;
transition: height 0.25s ease;
}
.pip-container.enlarged img {
height: 37.5vh;
}
.pip-container.fullscreen {
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.95);
display: flex;
align-items: center;
justify-content: center;
cursor: default;
}
.pip-container.fullscreen img {
border-radius: 0;
box-shadow: none;
}
.pip-fullscreen-btn {
all: unset;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: transparent;
filter: drop-shadow(0 0 1px black);
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
z-index: 1;
pointer-events: auto;
outline: none;
transition: transform 0.15s ease;
}
.pip-fullscreen-btn:hover,
.pip-fullscreen-btn:active {
transform: translate(-50%, -50%) scale(1.2);
}
.pip-container.fullscreen .pip-fullscreen-btn {
top: auto;
left: auto;
bottom: 10vw;
right: 10vw;
transform: none;
}
.pip-container.fullscreen .pip-fullscreen-btn:hover,
.pip-container.fullscreen .pip-fullscreen-btn:active {
transform: scale(0.85);
}
</style>

View File

@@ -0,0 +1,173 @@
<script>
import { onMount } from 'svelte';
import { createPip } from '$lib/js/pip.svelte';
import PipImage from '$lib/components/faith/PipImage.svelte';
/**
* @param {'layout' | 'overlay'} mode
* - 'layout': flex row on desktop (image sticky right, content left). Use as page-level wrapper.
* - 'overlay': image floats over the page (fixed position, IntersectionObserver show/hide). Use when nested inside existing layouts.
*/
let { src, alt = '', mode = 'layout', children } = $props();
let pipEl = $state(null);
let contentEl = $state(null);
let inView = $state(false);
const pip = createPip({ fullscreenEnabled: true });
function isMobile() {
return !window.matchMedia('(min-width: 1024px)').matches;
}
// PiP drag behavior only on mobile for both modes
function isPipActive() {
return isMobile();
}
function updateVisibility() {
if (!pipEl) return;
if (isPipActive()) {
// Mobile PiP mode
if (inView) {
pip.show(pipEl);
} else {
pip.hide();
}
} else {
// Desktop (both modes): CSS handles everything
pipEl.style.opacity = '';
pipEl.style.transform = '';
}
}
$effect(() => {
inView;
updateVisibility();
});
function onResize() {
if (!pipEl) return;
if (isPipActive() && inView) {
pip.reposition();
} else {
updateVisibility();
}
}
onMount(() => {
updateVisibility();
window.addEventListener('resize', onResize);
let observer;
if (contentEl) {
observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
inView = entry.isIntersecting;
}
},
{ threshold: 0 }
);
observer.observe(contentEl);
}
return () => {
window.removeEventListener('resize', onResize);
observer?.disconnect();
};
});
</script>
<div class="sticky-image-layout" class:overlay={mode === 'overlay'}>
<div class="image-wrap-desktop">
<img {src} {alt}>
</div>
<PipImage {pip} {src} {alt} visible={inView} bind:el={pipEl} />
<div class="content-scroll" bind:this={contentEl}>
{@render children()}
</div>
</div>
<style>
.sticky-image-layout {
display: flex;
flex-direction: column;
align-items: center;
margin: auto;
padding: 0 1em;
}
.sticky-image-layout.overlay {
display: contents;
}
.image-wrap-desktop {
display: none;
}
.content-scroll {
width: 100%;
max-width: 700px;
}
.overlay .content-scroll {
max-width: none;
}
@media (min-width: 1024px) {
.sticky-image-layout.overlay {
display: grid;
grid-template-columns: 1fr auto;
gap: 2rem;
width: calc(100% + 25vw + 2rem);
}
.image-wrap-desktop {
display: block;
position: sticky;
top: 4rem;
align-self: start;
order: 1;
}
.overlay .image-wrap-desktop img {
height: auto;
max-height: calc(100vh - 5rem);
width: auto;
max-width: 25vw;
}
.sticky-image-layout:not(.overlay) {
flex-direction: row;
align-items: flex-start;
gap: 2em;
}
.sticky-image-layout:not(.overlay) .content-scroll {
flex: 0 1 700px;
}
.sticky-image-layout:not(.overlay) .image-wrap-desktop {
display: block;
position: sticky;
top: 4rem;
flex: 1;
order: 1;
}
.sticky-image-layout:not(.overlay) .image-wrap-desktop img {
max-height: calc(100vh - 4rem);
height: auto;
width: 100%;
object-fit: contain;
}
}
@media (prefers-color-scheme: light) {
.sticky-image-layout:not(.overlay) .image-wrap-desktop {
background-color: var(--nord5);
}
}
@media (prefers-color-scheme: light) and (min-width: 1024px) {
.sticky-image-layout:not(.overlay) .image-wrap-desktop {
background-color: transparent;
}
}
@media (min-width: 1400px) {
.sticky-image-layout:not(.overlay)::before {
content: '';
flex: 1;
order: -1;
}
}
</style>

View File

@@ -0,0 +1,224 @@
<script lang="ts">
import FireEffect from './FireEffect.svelte';
interface Props {
value?: number;
burst?: boolean;
}
let { value = 0, burst = false }: Props = $props();
// Latch burst so the FireEffect stays mounted for the full animation
let showBurst = $state(false);
let burstTimer: ReturnType<typeof setTimeout> | undefined;
$effect(() => {
if (burst) {
clearTimeout(burstTimer);
showBurst = true;
burstTimer = setTimeout(() => showBurst = false, 2000);
}
});
const phase = $derived(
value >= 365 ? 6 :
value >= 180 ? 5 :
value >= 90 ? 4 :
value >= 30 ? 3 :
value >= 14 ? 2 :
value >= 7 ? 1 : 0
);
</script>
<div class="aura phase-{phase}" class:holy-fire={phase>=4}>
{#if phase >= 6}
<div class="wing left">
<svg viewBox="0 0 91.871681 77.881462" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-52.477632,-104.97065)">
<path d="m 85.574148,126.32647 c 18.072102,-19.56175 30.274102,-22.98334 39.785082,-20.76506 9.511,2.21826 19.51366,9.15611 18.96878,29.09808 -0.54488,19.94196 -8.19899,32.59335 -9.5936,33.90688 -1.39462,1.31353 -3.57898,1.31075 -6.51179,2.71347 -2.55794,1.22341 -2.94677,4.76843 -7.73616,6.52744 -5.5551,2.04023 -9.62876,-2.264 -13.20665,-3.0632 -5.81575,-1.2991 -6.82149,3.71895 -12.602091,4.60267 -8.390895,1.28278 -9.861661,-5.68162 -14.831326,-3.50879 -4.969644,2.1728 -12.234764,11.49793 -22.596805,4.55731 -17.23226,-11.54237 20.720254,-45.6491 28.32456,-54.0688 z"/>
</g>
</svg>
</div>
<div class="wing right">
<svg viewBox="0 0 91.871681 77.881462" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-52.477632,-104.97065)">
<path d="m 85.574148,126.32647 c 18.072102,-19.56175 30.274102,-22.98334 39.785082,-20.76506 9.511,2.21826 19.51366,9.15611 18.96878,29.09808 -0.54488,19.94196 -8.19899,32.59335 -9.5936,33.90688 -1.39462,1.31353 -3.57898,1.31075 -6.51179,2.71347 -2.55794,1.22341 -2.94677,4.76843 -7.73616,6.52744 -5.5551,2.04023 -9.62876,-2.264 -13.20665,-3.0632 -5.81575,-1.2991 -6.82149,3.71895 -12.602091,4.60267 -8.390895,1.28278 -9.861661,-5.68162 -14.831326,-3.50879 -4.969644,2.1728 -12.234764,11.49793 -22.596805,4.55731 -17.23226,-11.54237 20.720254,-45.6491 28.32456,-54.0688 z"/>
</g>
</svg>
</div>
{/if}
{#if phase >= 5}
<div class="halo" ></div>
{/if}
{#if phase >= 2}
<FireEffect holy={phase>=4} fire={phase>=3}/>
{/if}
{#if showBurst}
<FireEffect holy={phase>=4} burst fire={phase>=3}/>
{/if}
<span class="number">{value}</span>
</div>
<style>
/* =====================
BASE LAYOUT
===================== */
.aura {
position: relative;
width: 88px;
display: grid;
place-items: center;
}
.aura.phase-3,
.aura.phase-4,
.aura.phase-5,
.aura.phase-6
{
height: 88px;
}
.number {
position: relative;
z-index: 5;
font-size: 2.5rem;
font-weight: 700;
color: var(--nord13);
--shadow-outline: 0 0 1px rgba(255,255,255,0.9), 0 0 3px rgba(0,0,0,0.4);
}
/* =====================
PHASE 1 GLOW
===================== */
.phase-1 .number {
text-shadow:
0 0 8px rgba(255,215,100,.5),
0 0 16px rgba(255,215,100,.35);
animation: glow-pulse 2.5s ease-in-out infinite;
}
@keyframes glow-pulse {
0%,100% { text-shadow: 0 0 8px rgba(255,215,100,.4); }
50% { text-shadow: 0 0 16px rgba(255,215,100,.8); }
}
/* =====================
PHASE 3 HALO
===================== */
.halo {
position: absolute;
top: -6px;
width: 70px;
height: 20px;
border-radius: 50%;
border: 4px solid rgba(255,235,180,.9);
box-shadow:
0 0 12px rgba(255,235,180,.8),
0 0 20px rgba(255,235,180,.5);
animation: halo-float 3s ease-in-out infinite;
z-index: 3;
}
@keyframes halo-float {
0%,100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
/* =====================
PHASE 4 WINGS
===================== */
.wing {
position: absolute;
top: 20%;
width: 36px;
height: 64px;
z-index: 0;
pointer-events: none;
transform-origin: top center;
filter: drop-shadow(0 0 3px white);
}
.wing svg {
width: 100%;
height: 100%;
fill: white;
}
.wing.left {
left: -18px;
transform: rotate(-10deg);
animation: wing-slow-fly-left 4s ease-in-out infinite alternate;
}
.wing.right {
right: -18px;
transform: scaleX(-1) rotate(-10deg);
animation: wing-slow-fly-right 4s ease-in-out infinite alternate;
}
/* slow back-and-forth rotation for a gentle flight */
@keyframes wing-slow-fly-left {
0% { transform: rotate(10deg); }
50% { transform: rotate(0deg); }
100% { transform: rotate(10deg); }
}
@keyframes wing-slow-fly-right {
0% { transform: scaleX(-1) rotate(10deg); }
50% { transform: scaleX(-1) rotate(0deg); }
100% { transform: scaleX(-1) rotate(10deg); }
}
/* =====================
EMBER TEXT SHADOW
===================== */
.phase-2 .number,
.phase-3 .number,
.phase-4 .number {
animation: ember-pulse 1.4s infinite alternate;
}
@keyframes ember-pulse {
0% {
text-shadow:
var(--shadow-outline),
0 0 6px rgba(255,140,0,.6),
0 0 12px rgba(255,90,0,.4),
0 0 20px rgba(255,50,0,.2);
}
100% {
text-shadow:
var(--shadow-outline),
0 0 10px rgba(255,180,0,.9),
0 0 18px rgba(255,120,0,.6),
0 0 28px rgba(255,70,0,.35);
}
}
.holy-fire .number {
animation: holy-ember 1.8s infinite alternate;
color: #eaf6ff;
--shadow-outline: 0 0 5px rgba(0,0,0,0.7);
}
@keyframes holy-ember {
0% {
text-shadow:
var(--shadow-outline),
0 0 6px rgba(180,220,255,.6),
0 0 14px rgba(120,180,255,.45),
0 0 24px rgba(80,140,255,.3);
}
100% {
text-shadow:
var(--shadow-outline),
0 0 12px rgba(220,245,255,.95),
0 0 22px rgba(160,210,255,.7),
0 0 36px rgba(100,160,255,.45);
}
}
</style>

View File

@@ -0,0 +1,142 @@
<script lang="ts">
import { browser } from '$app/environment';
import { getRosaryStreak } from '$lib/stores/rosaryStreak.svelte';
import StreakAura from '$lib/components/faith/StreakAura.svelte';
import { tick, onMount } from 'svelte';
let burst = $state(false);
let streak = $state<ReturnType<typeof getRosaryStreak> | null>(null);
interface Props {
streakData?: { length: number; lastPrayed: string | null } | null;
lang?: 'de' | 'en';
isLoggedIn?: boolean;
}
let { streakData = null, lang = 'de', isLoggedIn = false }: Props = $props();
const isEnglish = $derived(lang === 'en');
// Derive display values: use store when available, fall back to server data for SSR
let displayLength = $derived(streak?.length ?? streakData?.length ?? 0);
let prayedToday = $derived(streak?.prayedToday ?? (streakData?.lastPrayed === new Date().toISOString().split('T')[0]));
// Labels need to come after displayLength since they depend on it
const labels = $derived({
days: isEnglish ? (displayLength === 1 ? 'Day' : 'Days') : (displayLength === 1 ? 'Tag' : 'Tage'),
prayed: isEnglish ? 'Prayed' : 'Gebetet',
prayedToday: isEnglish ? 'Prayed today' : 'Heute gebetet',
ariaLabel: isEnglish ? 'Mark prayer as prayed' : 'Gebet als gebetet markieren'
});
// Initialize store on mount (client-side only)
// Init with server data BEFORE assigning to streak, so displayLength
// never sees stale localStorage data from the singleton
onMount(() => {
const s = getRosaryStreak();
s.initWithServerData(streakData, isLoggedIn);
streak = s;
});
async function pray() {
burst = true;
await tick();
setTimeout(() => burst = false, 700);
streak?.recordPrayer();
}
</script>
<div class="streak-container" class:no-js-hidden={!isLoggedIn}>
<div class="streak-display">
<StreakAura value={displayLength} {burst} />
<span class="streak-label">{labels.days}</span>
</div>
<form method="POST" action="?/pray" onsubmit={(e) => { e.preventDefault(); pray(); }}>
<button
class="streak-button"
type="submit"
disabled={prayedToday}
aria-label={labels.ariaLabel}
>
{#if prayedToday}
{labels.prayedToday}
{:else}
{labels.prayed}
{/if}
</button>
</form>
</div>
<style>
.streak-container {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1rem 1.5rem;
background: var(--nord1);
border-radius: 12px;
width: fit-content;
}
@media (prefers-color-scheme: light) {
.streak-container {
background: var(--nord5);
}
}
.streak-display {
display: flex;
flex-direction: column;
align-items: center;
}
.streak-label {
font-size: 0.85rem;
color: var(--nord4);
text-transform: uppercase;
letter-spacing: 0.05em;
}
@media (prefers-color-scheme: light) {
.streak-label {
color: var(--nord3);
}
}
.streak-button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background: var(--nord10);
color: white;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.streak-button:hover:not(:disabled) {
background: var(--nord9);
transform: translateY(-2px);
}
.streak-button:disabled {
background: var(--nord3);
cursor: default;
opacity: 0.7;
}
/* Hide for non-logged-in users without JS (no form action available) */
.no-js-hidden {
display: none;
}
:global(html.js-enabled) .no-js-hidden {
display: flex;
}
@media (prefers-color-scheme: light) {
.streak-button:disabled {
background: var(--nord4);
}
}
</style>

View File

@@ -0,0 +1,64 @@
<script>
import Prayer from './Prayer.svelte';
import Paternoster from './Paternoster.svelte';
import AveMaria from './AveMaria.svelte';
import GloriaPatri from './GloriaPatri.svelte';
export let verbose = false;
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<div class="monolingual">
<p>
<v lang=de>
Seele Christi, heilige mich.
<i></i>
Leib Christi erlöse mich.
<i></i>
Blut Christi, tränke mich.
<i></i>
Wasser der Seite Christi, wasche mich.
<i></i>
Leiden Christi, stärke mich.
<i></i>
O gütiger Jesus, erhöre mich.
<i></i>
Verbirg in Deine Wunden mich.
<i></i>
Von Dir lass nimmer scheiden mich.
<i></i>
In meiner Todesstunde rufe mich,
<i></i>
Und heisse zur Dir kommen mich,
<i></i>
Damit ich möge loben Dich
<i></i>
Mit Deinen Heiligen ewiglich. Amen.
</v>
</p>
</div>
<h3> Vollkommener Ablass</h3>
<h4> Paternoster </h4>
{#if verbose }
<Paternoster />
{/if}
<h4> Ave Maria </h4>
{#if verbose }
<AveMaria />
{/if}
<h4> Gloria Patri </h4>
{#if verbose }
<GloriaPatri />
{/if}
<p>
{#if showLatin}
<v lang=la >En ego, o bone et dulcíssime Jesu, ante contspéctum tuum génibusme provólvo ac máximo ánimi ardóre te oro atque obtéstor, ut meum in in cor vívidos fídei, spei et caritátis sensus, atque veram peccatórum meórum pœniténtiam, éaque emendándi firmíssimam voluntátem velis imprímere; dum magno ánimi afféctu et dolóre tua quinque vúlnera mecum ipse consídero ac mente contémplor, illud præ óculis habens, quod jam in ore ponébat tuo David Prophéta de te, o bone Jesu: Fodérunt manus meas et pedes meos; dinumeravérunt ómnio ossa mea (Ps. 21, 17-18)</v>
{/if}
<v lang=de>
Siehe, o gütiger und milder Jesus, ich werfe mich vor Deinen Augen auf die Knie. Inbrünstig bitte und beschwöre ich Dich: Präge meinem Herzen lebendige Gefühle des Glaubens, der Hoffung und der Liebe ein sowie wahre Reue über meine Sünden und den ganz festen Willen, mich zu bessern. Voll Liebe und Schmerz schaue ich Deine fünf Wunden und betrachte sie in meinem Geiste. Dabei halte ich mir vor Augen, was im Hinblick auf Dich, o guter Jesus, schon der Prophet David Dir in den Mund legte: «Sie haben Meine Hände und Meine Füsse durchbohrt; alle meine Gebeine haben sie gezählt.» (Ps. 21, 17-18)
</v>
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,108 @@
<script>
import Prayer from './Prayer.svelte';
import AveMaria from './AveMaria.svelte';
let { verbose = false } = $props();
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la"><i>℣.</i> Ángelus Dómini nuntiávit Maríæ.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℣.</i> Der Engel des Herrn brachte Maria die Botschaft</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℣.</i> The Angel of the Lord declared unto Mary.</v>{/if}
{#if showLatin}<v lang="la"><i>℟.</i> Et concépit de Spíritu Sancto.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℟.</i> und sie empfing vom Heiligen Geist.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℟.</i> And she conceived of the Holy Spirit.</v>{/if}
</p>
{/snippet}
</Prayer>
{#if verbose}
<AveMaria />
{:else}
<p class="ave-indicator"><i>— Ave Maria —</i></p>
{/if}
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la"><i>℣.</i> Ecce ancílla Dómini,</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℣.</i> Maria sprach: Siehe, ich bin die Magd des Herrn</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℣.</i> Behold the handmaid of the Lord.</v>{/if}
{#if showLatin}<v lang="la"><i>℟.</i> Fiat mihi secúndum verbum tuum.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℟.</i> mir geschehe nach Deinem Wort.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℟.</i> Be it done unto me according to thy word.</v>{/if}
</p>
{/snippet}
</Prayer>
{#if verbose}
<AveMaria />
{:else}
<p class="ave-indicator"><i>— Ave Maria —</i></p>
{/if}
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la"><i>℣.</i> Et Verbum caro factum est,</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℣.</i> Und das Wort ist Fleisch geworden</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℣.</i> And the Word was made flesh.</v>{/if}
{#if showLatin}<v lang="la"><i>℟.</i> Et habitávit in nobis.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℟.</i> und hat unter uns gewohnt.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℟.</i> And dwelt among us.</v>{/if}
</p>
{/snippet}
</Prayer>
{#if verbose}
<AveMaria />
{:else}
<p class="ave-indicator"><i>— Ave Maria —</i></p>
{/if}
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la"><i>℣.</i> Ora pro nobis, sancta Dei Génetrix,</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℣.</i> Bitte für uns, heilige Gottesmutter,</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℣.</i> Pray for us, O holy Mother of God.</v>{/if}
{#if showLatin}<v lang="la"><i>℟.</i> Ut digni efficiámur promissiónibus Christi.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℟.</i> auf dass wir würdig werden der Verheissungen Christi.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℟.</i> That we may be made worthy of the promises of Christ.</v>{/if}
</p>
{/snippet}
</Prayer>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la"><i>℣.</i> Orémus.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℣.</i> Lasset uns beten.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℣.</i> Let us pray:</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Grátiam tuam, quǽsumus, Dómine, méntibus nostris infúnde;</v>{/if}
{#if urlLang === 'de'}<v lang="de">Allmächtiger Gott, giesse deine Gnade in unsere Herzen ein.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Pour forth, we beseech Thee, O Lord, Thy grace into our hearts,</v>{/if}
{#if showLatin}<v lang="la">ut qui, Ángelo nuntiánte, Christi Fílii tui incarnatiónem cognóvimus,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Durch die Botschaft des Engels haben wir die Menschwerdung Christi, deines Sohnes, erkannt.</v>{/if}
{#if urlLang === 'en'}<v lang="en">that we to whom the Incarnation of Christ Thy Son was made known by the message of an angel,</v>{/if}
{#if showLatin}<v lang="la">per passiónem eius et crucem ad resurrectiónis glóriam perducámur.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Lass uns durch sein Leiden und Kreuz zur Herrlichkeit der Auferstehung gelangen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">may by His Passion and Cross be brought to the glory of His Resurrection.</v>{/if}
{#if showLatin}<v lang="la">Per eúmdem Christum Dóminum nostrum. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Darum bitten wir durch Christus, unseren Herrn. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Through the same Christ Our Lord. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>
<style>
.ave-indicator {
text-align: center;
color: grey;
margin: 0.5em 0;
}
</style>

View File

@@ -0,0 +1,53 @@
<script>
import Prayer from './Prayer.svelte';
import Paternoster from './Paternoster.svelte';
import AveMaria from './AveMaria.svelte';
import GloriaPatri from './GloriaPatri.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang=la>Ánima Christi, santífica me.</v>{/if}
{#if urlLang=='de'}<v lang=de>Seele Christi, heilige mich.</v>{/if}
{#if urlLang=='en'}<v lang=en>Soul of Christ, sanctify me.</v>{/if}
{#if showLatin}<v lang=la>Corpus Christi, salva me.</v>{/if}
{#if urlLang=='de'}<v lang=de>Leib Christi, erlöse mich.</v>{/if}
{#if urlLang=='en'}<v lang=en>Body of Christ, save me.</v>{/if}
{#if showLatin}<v lang=la>Sanguis Christi, inébria me.</v>{/if}
{#if urlLang=='de'}<v lang=de>Blut Christi, tränke mich.</v>{/if}
{#if urlLang=='en'}<v lang=en>Blood of Christ, inebriate me.</v>{/if}
{#if showLatin}<v lang=la>Aqua láteris Christi, lava me.</v>{/if}
{#if urlLang=='de'}<v lang=de>Wasser der Seite Christi, wasche mich.</v>{/if}
{#if urlLang=='en'}<v lang=en>Water from the side of Christ, wash me.</v>{/if}
{#if showLatin}<v lang=la>Pássio Christi, confórta me.</v>{/if}
{#if urlLang=='de'}<v lang=de>Leiden Christi, stärke mich.</v>{/if}
{#if urlLang=='en'}<v lang=en>Passion of Christ, strenghten me.</v>{/if}
{#if showLatin}<v lang=la>O bone Iesu, exáudi me.</v>{/if}
{#if urlLang=='de'}<v lang=de>O gütiger Jesus, erhöre mich.</v>{/if}
{#if urlLang=='en'}<v lang=en>O good Jesus, hear me.</v>{/if}
{#if showLatin}<v lang=la>Intra tua vúlnera abscónde me.</v>{/if}
{#if urlLang=='de'}<v lang=de>Verbirg in Deine Wunden mich.</v>{/if}
{#if urlLang=='en'}<v lang=en>Within Thy wounds hide me.</v>{/if}
{#if showLatin}<v lang=la>Ne permíttas me separári a te.</v>{/if}
{#if urlLang=='de'}<v lang=de>Von Dir lass nimmer scheiden mich.</v>{/if}
{#if urlLang=='en'}<v lang=en>Separated from Thee let me never be.</v>{/if}
{#if showLatin}<v lang=la>Ab hoste malígno defénde me.</v>{/if}
{#if urlLang=='de'}<v lang=de>Vor dem bösen Feind beschütze mich.</v>{/if}
{#if urlLang=='en'}<v lang=en>From the malignant enemeny, defend me.</v>{/if}
{#if showLatin}<v lang=la>In hora mortis meæ voca me.</v>{/if}
{#if urlLang=='de'}<v lang=de>In meiner Todesstunde rufe mich,</v>{/if}
{#if urlLang=='en'}<v lang=en>At the hour of death, call me.</v>{/if}
{#if showLatin}<v lang=la>Et iube me veníre ad te,</v>{/if}
{#if urlLang=='de'}<v lang=de>Und heisse zur Dir kommen mich,</v>{/if}
{#if urlLang=='en'}<v lang=en>And bid me come unto Thee</v>{/if}
{#if showLatin}<v lang=la>Ut cum Sanctis tuis laudem te</v>{/if}
{#if urlLang=='de'}<v lang=de>Damit ich möge loben Dich</v>{/if}
{#if urlLang=='en'}<v lang=en>That with Thy Saints I may praise Thee</v>{/if}
{#if showLatin}<v lang=la>in sǽcula sæculórum.</v>{/if}
{#if urlLang=='de'}<v lang=de>Mit Deinen Heiligen ewiglich.</v>{/if}
{#if urlLang=='en'}<v lang=en>forever and ever.</v>{/if}
<v lang=und>Amen.</v>
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,68 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Credo in Deum Patrem omnipoténtem,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ich glaube an Gott, den Vater, den Allmächtigen,</v>{/if}
{#if urlLang === 'en'}<v lang="en">I believe in God, the Father almighty,</v>{/if}
{#if showLatin}<v lang="la">Creatórem cæli et terræ.</v>{/if}
{#if urlLang === 'de'}<v lang="de">den Schöpfer des Himmels und der Erde.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Creator of heaven and earth.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Et in Iesum Christum, Fílium eius únicum, Dóminum nostrum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Und an Jesus Christus, seinen eingeborenen Sohn, unsern Herrn,</v>{/if}
{#if urlLang === 'en'}<v lang="en">And in Jesus Christ, His only Son, our Lord,</v>{/if}
{#if showLatin}<v lang="la">qui concéptus est de Spíritu Sancto,</v>{/if}
{#if urlLang === 'de'}<v lang="de">der empfangen ist vom Heiligen Geist,</v>{/if}
{#if urlLang === 'en'}<v lang="en">who was conceived by the Holy Spirit,</v>{/if}
{#if showLatin}<v lang="la">natus ex María Vírgine,</v>{/if}
{#if urlLang === 'de'}<v lang="de">geboren von der Jungfrau Maria,</v>{/if}
{#if urlLang === 'en'}<v lang="en">born of the Virgin Mary,</v>{/if}
{#if showLatin}<v lang="la">passus sub Póntio Piláto,</v>{/if}
{#if urlLang === 'de'}<v lang="de">gelitten unter Pontius Pilatus,</v>{/if}
{#if urlLang === 'en'}<v lang="en">suffered under Pontius Pilate,</v>{/if}
{#if showLatin}<v lang="la">crucifíxus, mórtuus, et sepúltus,</v>{/if}
{#if urlLang === 'de'}<v lang="de">gekreuzigt, gestorben und begraben,</v>{/if}
{#if urlLang === 'en'}<v lang="en">was crucified, died, and was buried.</v>{/if}
{#if showLatin}<v lang="la">descéndit ad ínferos,</v>{/if}
{#if urlLang === 'de'}<v lang="de">hinabgestiegen in das Reich des Todes,</v>{/if}
{#if urlLang === 'en'}<v lang="en">He descended into hell.</v>{/if}
{#if showLatin}<v lang="la">tértia die resurréxit a mórtuis,</v>{/if}
{#if urlLang === 'de'}<v lang="de">am dritten Tage auferstanden von den Toten,</v>{/if}
{#if urlLang === 'en'}<v lang="en">On the third day He rose again from the dead.</v>{/if}
{#if showLatin}<v lang="la">ascéndit ad cælos,</v>{/if}
{#if urlLang === 'de'}<v lang="de">aufgefahren in den Himmel,</v>{/if}
{#if urlLang === 'en'}<v lang="en">He ascended into heaven,</v>{/if}
{#if showLatin}<v lang="la">sedet ad déxteram Dei Patris omnipoténtis,</v>{/if}
{#if urlLang === 'de'}<v lang="de">er sitzet zur Rechten Gottes, des allmächtigen Vaters;</v>{/if}
{#if urlLang === 'en'}<v lang="en">and sits at the right hand of God the Father almighty.</v>{/if}
{#if showLatin}<v lang="la">inde ventúrus est iudicáre vivos et mórtuos.</v>{/if}
{#if urlLang === 'de'}<v lang="de">von dort wird er kommen, zu richten die Lebenden und die Toten.</v>{/if}
{#if urlLang === 'en'}<v lang="en">From thence He shall come to judge the living and the dead.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Credo in Spíritum Sanctum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ich glaube an den Heiligen Geist,</v>{/if}
{#if urlLang === 'en'}<v lang="en">I believe in the Holy Spirit,</v>{/if}
{#if showLatin}<v lang="la">sanctam Ecclésiam cathólicam,</v>{/if}
{#if urlLang === 'de'}<v lang="de">die heilige katholische Kirche,</v>{/if}
{#if urlLang === 'en'}<v lang="en">the holy catholic Church,</v>{/if}
{#if showLatin}<v lang="la">sanctórum communiónem,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gemeinschaft der Heiligen,</v>{/if}
{#if urlLang === 'en'}<v lang="en">the communion of saints,</v>{/if}
{#if showLatin}<v lang="la">remissiónem peccatórum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Vergebung der Sünden,</v>{/if}
{#if urlLang === 'en'}<v lang="en">the forgiveness of sins,</v>{/if}
{#if showLatin}<v lang="la">carnis resurrectiónem,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Auferstehung der Toten</v>{/if}
{#if urlLang === 'en'}<v lang="en">the resurrection of the body,</v>{/if}
{#if showLatin}<v lang="la">vitam ætérnam. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">und das ewige Leben. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and life everlasting. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import Prayer from './Prayer.svelte';
let { mystery = "", mysteryLatin = "", mysteryEnglish = "" } = $props<{ mystery?: string, mysteryLatin?: string, mysteryEnglish?: string }>();
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Ave <i><sup></sup></i>María, grátia plena. Dóminus tecum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gegrüsset seist du <i><sup></sup></i>Maria, voll der Gnade; der Herr ist mit dir;</v>{/if}
{#if urlLang === 'en'}<v lang="en">Hail <i><sup></sup></i>Mary, full of grace. The Lord is with thee.</v>{/if}
{#if showLatin}<v lang="la">benedícta tu in muliéribus,</v>{/if}
{#if urlLang === 'de'}<v lang="de">du bist gebenedeit unter den Frauen,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Blessed art thou amongst women,</v>{/if}
{#if showLatin}<v lang="la">et benedíctus fructus ventris tui, {#if !mysteryLatin}<i><sup></sup></i>Jesus.{/if}</v>{/if}
{#if urlLang === 'de'}<v lang="de">und gebenedeit ist die Frucht deines Leibes, {#if !mystery}<i><sup></sup></i>Jesus.{/if}</v>{/if}
{#if urlLang === 'en'}<v lang="en">and blessed is the fruit of thy womb, {#if !mysteryEnglish}<i><sup></sup></i>Jesus.{/if}</v>{/if}
{#if showLatin && mysteryLatin}
<v lang="la" class="mystery-text"><i><sup></sup></i>{mysteryLatin}</v>
{/if}
{#if urlLang === 'de' && mystery}
<v lang="de" class="mystery-text"><i><sup></sup></i>{mystery}</v>
{/if}
{#if urlLang === 'en' && mysteryEnglish}
<v lang="en" class="mystery-text"><i><sup></sup></i>{mysteryEnglish}</v>
{/if}
</p>
<p>
{#if showLatin}<v lang="la">Sancta <i><sup></sup></i>María, mater Dei, ora pro nobis peccatóribus,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Heilige <i><sup></sup></i>Maria, Mutter Gottes, bitte für uns Sünder</v>{/if}
{#if urlLang === 'en'}<v lang="en">Holy <i><sup></sup></i>Mary, Mother of God, pray for us sinners,</v>{/if}
{#if showLatin}<v lang="la">nunc, et in hora mortis nostræ. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">jetzt und in der Stunde unseres Todes. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">now and at the hour of our death. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,32 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer hasLatin={false}>
{#snippet children(showLatin, urlLang)}
<p>
{#if urlLang === 'de'}<v lang="de">Mein Herr und mein Gott,</v>{/if}
{#if urlLang === 'en'}<v lang="en">My Lord and my God,</v>{/if}
{#if urlLang === 'de'}<v lang="de">nimm alles von mir,</v>{/if}
{#if urlLang === 'en'}<v lang="en">take from me everything</v>{/if}
{#if urlLang === 'de'}<v lang="de">was mich hindert zu Dir.</v>{/if}
{#if urlLang === 'en'}<v lang="en">that distances me from Thee.</v>{/if}
</p>
<p>
{#if urlLang === 'de'}<v lang="de">Mein Herr und mein Gott,</v>{/if}
{#if urlLang === 'en'}<v lang="en">My Lord and my God,</v>{/if}
{#if urlLang === 'de'}<v lang="de">gib alles mir,</v>{/if}
{#if urlLang === 'en'}<v lang="en">give me everything</v>{/if}
{#if urlLang === 'de'}<v lang="de">was mich führet zu Dir.</v>{/if}
{#if urlLang === 'en'}<v lang="en">that brings me closer to Thee.</v>{/if}
</p>
<p>
{#if urlLang === 'de'}<v lang="de">Mein Herr und mein Gott,</v>{/if}
{#if urlLang === 'en'}<v lang="en">My Lord and my God,</v>{/if}
{#if urlLang === 'de'}<v lang="de">nimm mich mir</v>{/if}
{#if urlLang === 'en'}<v lang="en">detach me from myself</v>{/if}
{#if urlLang === 'de'}<v lang="de">und gib mich ganz zu eigen Dir.</v>{/if}
{#if urlLang === 'en'}<v lang="en">to give my all to Thee.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,55 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Confíteor Deo omnipoténti,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ich bekenne Gott, dem Allmächtigen,</v>{/if}
{#if urlLang === 'en'}<v lang="en">I confess to almighty God,</v>{/if}
{#if showLatin}<v lang="la">beátæ Maríæ semper Vírgini</v>{/if}
{#if urlLang === 'de'}<v lang="de">der seligen, allzeit reinen Jungfrau Maria,</v>{/if}
{#if urlLang === 'en'}<v lang="en">to blessed Mary ever Virgin,</v>{/if}
{#if showLatin}<v lang="la">beáto Michaéli Archángelo,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dem hl. Erzengel Michael,</v>{/if}
{#if urlLang === 'en'}<v lang="en">to blessed Michael the Archangel,</v>{/if}
{#if showLatin}<v lang="la">beáto Ioánni Baptístæ,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dem hl. Johannes dem Täufer,</v>{/if}
{#if urlLang === 'en'}<v lang="en">to blessed John the Baptist,</v>{/if}
{#if showLatin}<v lang="la">sanctis Apóstolis Petro et Paulo,</v>{/if}
{#if urlLang === 'de'}<v lang="de">den hll. Aposteln Petrus und Paulus,</v>{/if}
{#if urlLang === 'en'}<v lang="en">to the holy Apostles Peter and Paul,</v>{/if}
{#if showLatin}<v lang="la">ómnibus Sanctis, et tibi pater:</v>{/if}
{#if urlLang === 'de'}<v lang="de">allen Heiligen und dir, Vater,</v>{/if}
{#if urlLang === 'en'}<v lang="en">to all the Saints, and to you, Father,</v>{/if}
{#if showLatin}<v lang="la">quia paccávi nimis</v>{/if}
{#if urlLang === 'de'}<v lang="de">dass ich viel gesündigt habe</v>{/if}
{#if urlLang === 'en'}<v lang="en">that I have sinned exceedingly</v>{/if}
{#if showLatin}<v lang="la">cogitatióne, verbe et ópere:</v>{/if}
{#if urlLang === 'de'}<v lang="de">in Gedanken, Worten und Werken,</v>{/if}
{#if urlLang === 'en'}<v lang="en">in thought, word, and deed:</v>{/if}
{#if showLatin}<v lang="la">mea culpa, mea culpa, mea máxima cupla.</v>{/if}
{#if urlLang === 'de'}<v lang="de">durch meine Schuld, durch meine Schuld, durch meine übergrosse Schuld.</v>{/if}
{#if urlLang === 'en'}<v lang="en">through my fault, through my fault, through my most grievous fault.</v>{/if}
{#if showLatin}<v lang="la">Ideo precor beátam Maríam semper Vírginem,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Darum bitte ich die selige, allzeit reine Jungfrau Maria,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Therefore I beseech the blessed Mary ever Virgin,</v>{/if}
{#if showLatin}<v lang="la">beátum Michaélem Archángelum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">den hl. Erzengel Michael,</v>{/if}
{#if urlLang === 'en'}<v lang="en">blessed Michael the Archangel,</v>{/if}
{#if showLatin}<v lang="la">beátum Ioánnem Baptístam,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dem hl. Johannes den Täufer,</v>{/if}
{#if urlLang === 'en'}<v lang="en">blessed John the Baptist,</v>{/if}
{#if showLatin}<v lang="la">sanctos Apóstolos Petrum et Paulum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">die hll. Apostel Petrus und Paulus,</v>{/if}
{#if urlLang === 'en'}<v lang="en">the holy Apostles Peter and Paul,</v>{/if}
{#if showLatin}<v lang="la">omnes Sanctos, et te pater,</v>{/if}
{#if urlLang === 'de'}<v lang="de">alle Heiligen und dich, Vater,</v>{/if}
{#if urlLang === 'en'}<v lang="en">all the Saints, and you, Father,</v>{/if}
{#if showLatin}<v lang="la">Oráre pro me ad Dóminum Deum nostrum.</v>{/if}
{#if urlLang === 'de'}<v lang="de">für mich zu beten bei Gott unserem Herrn.</v>{/if}
{#if urlLang === 'en'}<v lang="en">to pray for me to the Lord our God.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,130 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Credo in unum <i><sup></sup></i> Deum, Patrem omnipoténtem,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ich glaub an den einen <i><sup></sup></i> Gott. Den allmächtigen Vater,</v>{/if}
{#if urlLang === 'en'}<v lang="en">I believe in one <i><sup></sup></i> God, the Father almighty,</v>{/if}
{#if showLatin}<v lang="la">factórem cæli et terræ,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Schöpfer des Himmels und der Erde,</v>{/if}
{#if urlLang === 'en'}<v lang="en">maker of heaven and earth,</v>{/if}
{#if showLatin}<v lang="la">visibílium ómnium et invisibílium.</v>{/if}
{#if urlLang === 'de'}<v lang="de">aller sichtbaren und unsichtbaren Dinge.</v>{/if}
{#if urlLang === 'en'}<v lang="en">of all things visible and invisible.</v>{/if}
{#if showLatin}<v lang="la">Et in unum Dóminum <i><sup></sup></i> Jesum Christum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Und an den einen Herrn <i><sup></sup></i> Jesus Christus,</v>{/if}
{#if urlLang === 'en'}<v lang="en">And in one Lord <i><sup></sup></i> Jesus Christ,</v>{/if}
{#if showLatin}<v lang="la">Fílium Dei unigénitum.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gottes eingeborenen Sohn.</v>{/if}
{#if urlLang === 'en'}<v lang="en">the Only Begotten Son of God,</v>{/if}
{#if showLatin}<v lang="la">Et ex Patre natum ante ómnia sǽcula.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Er ist aus dem Vater geboren vor aller Zeit.</v>{/if}
{#if urlLang === 'en'}<v lang="en">born of the Father before all ages.</v>{/if}
{#if showLatin}<v lang="la">Deum de Deo,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gott von Gott,</v>{/if}
{#if urlLang === 'en'}<v lang="en">God from God,</v>{/if}
{#if showLatin}<v lang="la">lumen de lúmine,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Licht vom Lichte,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Light from Light,</v>{/if}
{#if showLatin}<v lang="la">Deum verum de Deo vero.</v>{/if}
{#if urlLang === 'de'}<v lang="de">wahrer Gott vom wahren Gott;</v>{/if}
{#if urlLang === 'en'}<v lang="en">true God from true God,</v>{/if}
{#if showLatin}<v lang="la">Génitum, non factum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gezeugt, nicht geschaffen,</v>{/if}
{#if urlLang === 'en'}<v lang="en">begotten, not made,</v>{/if}
{#if showLatin}<v lang="la">consubstantiálem Patri:</v>{/if}
{#if urlLang === 'de'}<v lang="de">eines Wesens mit dem Vater;</v>{/if}
{#if urlLang === 'en'}<v lang="en">consubstantial with the Father;</v>{/if}
{#if showLatin}<v lang="la">per quem ómnia facta sunt.</v>{/if}
{#if urlLang === 'de'}<v lang="de">durch Ihn ist alles geschaffen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">through Him all things were made.</v>{/if}
{#if showLatin}<v lang="la">Qui propter nos hómines</v>{/if}
{#if urlLang === 'de'}<v lang="de">Für uns Menschen</v>{/if}
{#if urlLang === 'en'}<v lang="en">For us men</v>{/if}
{#if showLatin}<v lang="la">et propter nostram salútem</v>{/if}
{#if urlLang === 'de'}<v lang="de">und um unsres Heiles willen</v>{/if}
{#if urlLang === 'en'}<v lang="en">and for our salvation</v>{/if}
{#if showLatin}<v lang="la">descéndit de cælis.</v>{/if}
{#if urlLang === 'de'}<v lang="de">ist Er vom Himmel herabgestiegen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">He came down from heaven.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Et incarnátus est de Spíritu Sancto</v>{/if}
{#if urlLang === 'de'}<v lang="de">Er hat Fleisch angenommen durch den Hl. Geist</v>{/if}
{#if urlLang === 'en'}<v lang="en">And by the Holy Spirit was incarnate</v>{/if}
{#if showLatin}<v lang="la">ex <i><sup></sup></i> María Vírgine:</v>{/if}
{#if urlLang === 'de'}<v lang="de">aus <i><sup></sup></i> Maria, der Jungfrau</v>{/if}
{#if urlLang === 'en'}<v lang="en">of the Virgin <i><sup></sup></i> Mary,</v>{/if}
{#if showLatin}<v lang="la">Et homo factus est.</v>{/if}
{#if urlLang === 'de'}<v lang="de">und ist Mensch geworden.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and became man.</v>{/if}
{#if showLatin}<v lang="la">Crucifíxus étiam pro nobis:</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gekreuzigt wurde Er sogar für uns;</v>{/if}
{#if urlLang === 'en'}<v lang="en">For our sake He was crucified</v>{/if}
{#if showLatin}<v lang="la">sub Póntio Piláto passus, et sepúltus est.</v>{/if}
{#if urlLang === 'de'}<v lang="de">unter Pontius Pilatus hat Er den Tod erlitten</v>{/if}
{#if urlLang === 'de'}<v lang="de">und ist begraben worden</v>{/if}
{#if urlLang === 'en'}<v lang="en">under Pontius Pilate, He suffered death and was buried.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Et resurréxit tértia die,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Er ist auferstanden am dritten Tage,</v>{/if}
{#if urlLang === 'en'}<v lang="en">And rose again on the third day</v>{/if}
{#if showLatin}<v lang="la">secúndum Scriptúras.</v>{/if}
{#if urlLang === 'de'}<v lang="de">gemäss der Schrift;</v>{/if}
{#if urlLang === 'en'}<v lang="en">in accordance with the Scriptures.</v>{/if}
{#if showLatin}<v lang="la">Et ascéndit in cáelum:</v>{/if}
{#if urlLang === 'de'}<v lang="de">Er ist aufgefahren in den Himmel</v>{/if}
{#if urlLang === 'en'}<v lang="en">He ascended into heaven</v>{/if}
{#if showLatin}<v lang="la">sedet ad déxteram Patris.</v>{/if}
{#if urlLang === 'de'}<v lang="de">und sitzet zur Rechten des Vaters.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and is seated at the right hand of the Father.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Et íterum ventúrus est cum glória</v>{/if}
{#if urlLang === 'de'}<v lang="de">Er wird wiederkommen in Herrlichkeit,</v>{/if}
{#if urlLang === 'en'}<v lang="en">He will come again in glory</v>{/if}
{#if showLatin}<v lang="la">judicáre vivos et mórtuos:</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gericht zu halten über Lebende und Tote:</v>{/if}
{#if urlLang === 'en'}<v lang="en">to judge the living and the dead</v>{/if}
{#if showLatin}<v lang="la">cujus regni non erit finis.</v>{/if}
{#if urlLang === 'de'}<v lang="de">und Seines Reiches wird kein Endes sein.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and His kingdom will have no end.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Et in Spíritum Sanctum,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ich glaube an den Heiligen Geist,</v>{/if}
{#if urlLang === 'en'}<v lang="en">I believe in the Holy Spirit,</v>{/if}
{#if showLatin}<v lang="la">Dóminum et vivificántem:</v>{/if}
{#if urlLang === 'de'}<v lang="de">den Herrn und Lebensspender,</v>{/if}
{#if urlLang === 'en'}<v lang="en">the Lord, the giver of life,</v>{/if}
{#if showLatin}<v lang="la">qui ex Patre Filióque procédit.</v>{/if}
{#if urlLang === 'de'}<v lang="de">der vom Vater und vom Sohne ausgeht.</v>{/if}
{#if urlLang === 'en'}<v lang="en">who proceeds from the Father and the Son,</v>{/if}
{#if showLatin}<v lang="la">Qui cum Patre et Fílio simul <i><sup></sup></i> adorátur et conglorificátur:</v>{/if}
{#if urlLang === 'de'}<v lang="de">zugleich <i><sup></sup></i> angebetet und verherrlicht;</v>{/if}
{#if urlLang === 'en'}<v lang="en">who with the Father and the Son is <i><sup></sup></i> adored and glorified,</v>{/if}
{#if showLatin}<v lang="la">qui locútus est per Prophétas.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Er hat gesprochen durch die Propheten.</v>{/if}
{#if urlLang === 'en'}<v lang="en">who has spoken through the prophets.</v>{/if}
{#if showLatin}<v lang="la">Et unam sanctam cathólicam et apostólicam Ecclésiam.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ich glaube an die eine, heilige, katholische und apostolische Kirche.</v>{/if}
{#if urlLang === 'en'}<v lang="en">I believe in one, holy, catholic and apostolic Church.</v>{/if}
{#if showLatin}<v lang="la">Confíteor unum baptísma</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ich bekenne die eine Taufe</v>{/if}
{#if urlLang === 'en'}<v lang="en">I confess one Baptism</v>{/if}
{#if showLatin}<v lang="la">in remissiónem peccatórum.</v>{/if}
{#if urlLang === 'de'}<v lang="de">zur Vergebung der Sünden.</v>{/if}
{#if urlLang === 'en'}<v lang="en">for the forgiveness of sins</v>{/if}
{#if showLatin}<v lang="la">Et exspécto resurrectiónem mortuórum.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ich erwarte die Auferstehung der Toten.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and I look forward to the resurrection of the dead</v>{/if}
{#if showLatin}<v lang="la"><i></i> Et vitam ventúri sǽculi. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i></i> Und das Leben der zukünftigen Welt. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i></i> and the life of the world to come. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,25 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la"><i><sup></sup></i>Jésú, indúlge peccáta nostra,</v>{/if}
{#if urlLang === 'de'}<v lang="de">O mein <i><sup></sup></i>Jesus, verzeih' uns unsere Sünden,</v>{/if}
{#if urlLang === 'en'}<v lang="en">O my <i><sup></sup></i>Jesus, forgive us our sins,</v>{/if}
{#if showLatin}<v lang="la">præsérva nos ab igne inférni,</v>{/if}
{#if urlLang === 'de'}<v lang="de">bewahre uns vor den Feuern der Hölle</v>{/if}
{#if urlLang === 'en'}<v lang="en">save us from the fires of hell,</v>{/if}
{#if showLatin}<v lang="la">duc omnes ad cæli glóriam, </v>{/if}
{#if urlLang === 'de'}<v lang="de">und führe alle Seelen in den Himmel,</v>{/if}
{#if urlLang === 'en'}<v lang="en">and lead all souls to heaven,</v>{/if}
{#if showLatin}<v lang="la">præcípe tua</v>{/if}
{#if urlLang === 'de'}<v lang="de">besonders jene,</v>{/if}
{#if urlLang === 'en'}<v lang="en">especially those</v>{/if}
{#if showLatin}<v lang="la">misericórdia máxime egéntes. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">die Deiner Barmherzigkeit am meisten bedürfen. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">who are in most need of Thy mercy. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,98 @@
<script>
import Prayer from './Prayer.svelte';
let { intro = false } = $props();
</script>
{#if intro}
<Prayer hasLatin={false}>
{#snippet children(showLatin, urlLang)}
<p class="intro">
{#if urlLang === 'en'}This ancient hymn begins with the words the angels used to celebrate the newborn Savior. It first praises God the Father, then God the Son; it concludes with homage to the Most Holy Trinity, during which one makes the sign of the cross.{/if}
{#if urlLang === 'de'}Der uralte Gesang beginnt mit den Worten, mit denen die Engelscharen den neugeborenen Welterlöser feierten. Er preist zunächst Gott Vater, dann Gott Sohn; er schliesst mit einer Huldigung an die Heiligste Dreifaltigkeit, wobei man sich mit dem grossen Kreuze bezeichnet.{/if}
</p>
{/snippet}
</Prayer>
{/if}
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Glória in excélsis <i><sup></sup></i> Deo.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Ehre sei <i><sup></sup></i> Gott in der Höhe.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Glory to <i><sup></sup></i> God in the highest.</v>{/if}
{#if showLatin}<v lang="la">Et in terra pax homínibus</v>{/if}
{#if urlLang === 'de'}<v lang="de">Und auf Erden Friede den Mesnchen,</v>{/if}
{#if urlLang === 'en'}<v lang="en">And on earth peace to men</v>{/if}
{#if showLatin}<v lang="la">bonæ voluntátis.</v>{/if}
{#if urlLang === 'de'}<v lang="de">die guten Willens sind.</v>{/if}
{#if urlLang === 'en'}<v lang="en">of good will.</v>{/if}
{#if showLatin}<v lang="la">Laudámus te.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Wir loben Dich.</v>{/if}
{#if urlLang === 'en'}<v lang="en">We praise Thee.</v>{/if}
{#if showLatin}<v lang="la">Benedícimus te.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Wir preisen Dich.</v>{/if}
{#if urlLang === 'en'}<v lang="en">We bless Thee.</v>{/if}
{#if showLatin}<v lang="la"><i><sup></sup></i> Adorámus te.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i><sup></sup></i> Wir beten Dich an.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i><sup></sup></i> We adore Thee.</v>{/if}
{#if showLatin}<v lang="la">Glorificámus te.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Wir verherrlichen Dich.</v>{/if}
{#if urlLang === 'en'}<v lang="en">We glorify Thee.</v>{/if}
{#if showLatin}<v lang="la"><i><sup></sup></i> Grátias ágimus tibi</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i><sup></sup></i> Wir sagen Dir Dank</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i><sup></sup></i> We give Thee thanks</v>{/if}
{#if showLatin}<v lang="la">propter magnam glóriam tuam.</v>{/if}
{#if urlLang === 'de'}<v lang="de">ob Deiner grossen Herrlichkeit.</v>{/if}
{#if urlLang === 'en'}<v lang="en">for Thy great glory.</v>{/if}
{#if showLatin}<v lang="la">Dómine Deus, Rex cæléstis,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Herr und Gott, König des Himmels,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Lord God, heavenly King,</v>{/if}
{#if showLatin}<v lang="la">Deus Pater omnípotens.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gott allmächtiger Vater!</v>{/if}
{#if urlLang === 'en'}<v lang="en">God the Father almighty.</v>{/if}
{#if showLatin}<v lang="la">Dómine Fili unigénite, <i><sup></sup></i> Jesu Christe.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Herr <i><sup></sup></i> Jesus Christus, eingeborener Sohn!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Lord <i><sup></sup></i> Jesus Christ, the only-begotten Son.</v>{/if}
{#if showLatin}<v lang="la">Dómine Deus, Agnus Dei,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Herr und Gott, Lamm Gottes,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Lord God, Lamb of God,</v>{/if}
{#if showLatin}<v lang="la">Fílius Patris.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Sohn des Vaters!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Son of the Father.</v>{/if}
{#if showLatin}<v lang="la">Qui tollis peccáta mundi,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du nimmst hinweg die Sünden der Welt:</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou who takest away the sins of the world,</v>{/if}
{#if showLatin}<v lang="la">miserére nobis.</v>{/if}
{#if urlLang === 'de'}<v lang="de">erbarme Dich unser.</v>{/if}
{#if urlLang === 'en'}<v lang="en">have mercy on us.</v>{/if}
{#if showLatin}<v lang="la">Qui tollis peccáta mundi,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du nimmst hinwerg die Sünden der Welt.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou who takest away the sins of the world,</v>{/if}
{#if showLatin}<v lang="la"><i><sup></sup></i> súscipe depreciatiónem nostram.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i><sup></sup></i> nimm unser Flehen gnädig auf.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i><sup></sup></i> receive our prayer.</v>{/if}
{#if showLatin}<v lang="la">Qui sedes ad déxteram Patris,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du sitzt zur Rechten des Vaters:</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou who sittest at the right hand of the Father,</v>{/if}
{#if showLatin}<v lang="la">miserére nobis.</v>{/if}
{#if urlLang === 'de'}<v lang="de">erbarme Dich unser.</v>{/if}
{#if urlLang === 'en'}<v lang="en">have mercy on us.</v>{/if}
{#if showLatin}<v lang="la">Quóniam tu solus Sanctus.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Denn Du allein bist der Heilige.</v>{/if}
{#if urlLang === 'en'}<v lang="en">For Thou alone art holy.</v>{/if}
{#if showLatin}<v lang="la">Tu solus Altíssimus,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du allein der Höchste,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thou alone art the Most High,</v>{/if}
{#if showLatin}<v lang="la"><i><sup></sup></i> Jesu Christe.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i><sup></sup></i> Jesus Christus,</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i><sup></sup></i> Jesus Christ.</v>{/if}
{#if showLatin}<v lang="la">Cum Sancto Spíritu</v>{/if}
{#if urlLang === 'de'}<v lang="de">Mit dem Hl. Geiste,</v>{/if}
{#if urlLang === 'en'}<v lang="en">With the Holy Spirit,</v>{/if}
{#if showLatin}<v lang="la"><i></i> in glória Dei Patris. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i></i> in der Herrlichkeit Gottes des Vaters. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i></i> in the glory of God the Father. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,19 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la"><i><sup></sup></i>Glória Patri, et Fílio, et Spirítui Sancto.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i><sup></sup></i>Ehre sei dem Vater und dem Sohne und dem Hl. Geiste.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i><sup></sup></i>Glory be to the Father, and to the Son, and to the Holy Spirit.</v>{/if}
{#if showLatin}<v lang="la">Sicut erat in princípio, et nunc, et semper:</v>{/if}
{#if urlLang === 'de'}<v lang="de">Wie es war am Anfang, so auch jetzt und allezeit</v>{/if}
{#if urlLang === 'en'}<v lang="en">As it was in the beginning, is now, and ever shall be,</v>{/if}
{#if showLatin}<v lang="la">et in sǽcula sæculórum. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">und in Ewigkeit. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">world without end. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,25 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Ángele Dei,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Engel Gottes,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Angel of God,</v>{/if}
{#if showLatin}<v lang="la">qui custos es mei,</v>{/if}
{#if urlLang === 'de'}<v lang="de">mein Beschützer,</v>{/if}
{#if urlLang === 'en'}<v lang="en">my guardian dear,</v>{/if}
{#if showLatin}<v lang="la">me, tibi commíssum pietáte supérna,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dir hat Gottes Vorsehung mich anvertraut;</v>{/if}
{#if urlLang === 'en'}<v lang="en">to whom God's love commits me here,</v>{/if}
{#if showLatin}<v lang="la">illúmina, custódi, rege et gubérna.</v>{/if}
{#if urlLang === 'de'}<v lang="de">erleuchte, beschütze, leite und führe mich.</v>{/if}
{#if urlLang === 'en'}<v lang="en">ever this day be at my side, to light and guard, to rule and guide.</v>{/if}
{#if showLatin}<v lang="la">Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,22 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer hasLatin={false}>
{#snippet children(showLatin, urlLang)}
<p>
{#if urlLang === 'de'}<v lang="de">Jungfräulicher Vater <i><sup></sup></i>Jesu,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Virgin Father of <i><sup></sup></i>Jesus,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Reinster Bräutigam <i><sup></sup></i>Mariä,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Most pure Spouse of <i><sup></sup></i>Mary,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Sankt Joseph, bitte Tag für Tag bei Jesus, dem Sohn Gottes.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Saint Joseph, pray each day to Jesus, the Son of God.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Seine Kraft und Gnade soll uns stärken,</v>{/if}
{#if urlLang === 'en'}<v lang="en">May His power and grace strengthen us,</v>{/if}
{#if urlLang === 'de'}<v lang="de">dass wir siegreich streiten im Leben</v>{/if}
{#if urlLang === 'en'}<v lang="en">that we may fight victoriously in life</v>{/if}
{#if urlLang === 'de'}<v lang="de">und die Krone von Ihm erhalten im Sterben.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and receive the crown from Him at death.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,13 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">In nómine <i></i> Patris, et Fílii, et Spíritus Sancti. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Im Namen des <i></i> Vaters und des Sohnes und des Heiligen Geistes. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">In the name of the <i></i> Father, and of the Son, and of the Holy Spirit. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,40 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Sáncte Míchael Archángele,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Heiliger Erzengel Michael,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Saint Michael the Archangel,</v>{/if}
{#if showLatin}<v lang="la">defénde nos in proélio,</v>{/if}
{#if urlLang === 'de'}<v lang="de">verteidige uns im Kampfe!</v>{/if}
{#if urlLang === 'en'}<v lang="en">defend us in battle.</v>{/if}
{#if showLatin}<v lang="la">cóntra nequítam et insídias</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gegen die Bosheit und Nachstellungen</v>{/if}
{#if urlLang === 'en'}<v lang="en">Be our protection against the wickedness</v>{/if}
{#if showLatin}<v lang="la">diáboli ésto præsídium.</v>{/if}
{#if urlLang === 'de'}<v lang="de">des Teufels sei unser Schutz. </v>{/if}
{#if urlLang === 'en'}<v lang="en">and snares of the devil.</v>{/if}
{#if showLatin}<v lang="la">Ímperet ílli Déus, súpplices deprecámur:</v>{/if}
{#if urlLang === 'de'}<v lang="de">»Gott gebiete ihm!«, so bitten wir flehentlich.</v>{/if}
{#if urlLang === 'en'}<v lang="en">May God rebuke him, we humbly pray;</v>{/if}
{#if showLatin}<v lang="la">tuque, Prínceps milítæ cæléstis,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Du aber, Fürst der himmlischen Heerscharen,</v>{/if}
{#if urlLang === 'en'}<v lang="en">and do thou, O Prince of the heavenly host,</v>{/if}
{#if showLatin}<v lang="la">Sátanam aliósque spíritus malígnos,</v>{/if}
{#if urlLang === 'de'}<v lang="de">stosse den Satan und die anderen bösen Geister,</v>{/if}
{#if urlLang === 'en'}<v lang="en">by the power of God, thrust into hell Satan</v>{/if}
{#if showLatin}<v lang="la">qui ad perditiónem animárum</v>{/if}
{#if showLatin}<v lang="la">pervagántur in múndo,</v>{/if}
{#if urlLang === 'de'}<v lang="de">die in der Welt umhergehen,</v>{/if}
{#if urlLang === 'de'}<v lang="de">um die Seelen zu verderben,</v>{/if}
{#if urlLang === 'en'}<v lang="en">and all the evil spirits</v>{/if}
{#if urlLang === 'en'}<v lang="en">who prowl about the world seeking the ruin of souls.</v>{/if}
{#if showLatin}<v lang="la">divína virtúte, in inférnum detrúde. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">durch die Kraft Gottes in die Hölle. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,37 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Pater noster, qui es in cælis</v>{/if}
{#if urlLang === 'de'}<v lang="de">Vater unser, der Du bist im Himmel,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Our Father, Who art in heaven,</v>{/if}
{#if showLatin}<v lang="la">Sanctificétur nomen tuum</v>{/if}
{#if urlLang === 'de'}<v lang="de">geheiligt werde Dein Name;</v>{/if}
{#if urlLang === 'en'}<v lang="en">hallowed be Thy name;</v>{/if}
{#if showLatin}<v lang="la">Advéniat regnum tuum</v>{/if}
{#if urlLang === 'de'}<v lang="de">zu uns komme Dein Reich;</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thy kingdom come;</v>{/if}
{#if showLatin}<v lang="la">Fiat volúntas tua, sicut in cælo, et in terra.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Dein Wille geschehe, wie im Himmel, also auch auf Erden!</v>{/if}
{#if urlLang === 'en'}<v lang="en">Thy will be done on earth as it is in heaven.</v>{/if}
{#if showLatin}<v lang="la">Panem nostrum quotidiánum da nobis hódie.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Unser tägliches Brot gib uns heute;</v>{/if}
{#if urlLang === 'en'}<v lang="en">Give us this day our daily bread;</v>{/if}
{#if showLatin}<v lang="la">Et dimítte nobis debíta nostra,</v>{/if}
{#if urlLang === 'de'}<v lang="de">und vergib uns unsere Schulden,</v>{/if}
{#if urlLang === 'en'}<v lang="en">and forgive us our trespasses,</v>{/if}
{#if showLatin}<v lang="la">sicut et nos dimíttimus debitóribus nostris.</v>{/if}
{#if urlLang === 'de'}<v lang="de">wie auch wir vergeben unsern Schuldigern;</v>{/if}
{#if urlLang === 'en'}<v lang="en">as we forgive those who trespass against us;</v>{/if}
{#if showLatin}<v lang="la">Et ne nos indúcas in tentatiónem.</v>{/if}
{#if urlLang === 'de'}<v lang="de">und führe uns nicht in Versuchung.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and lead us not into temptation,</v>{/if}
{#if showLatin}<v lang="la">Sed líbera nos a malo. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Sondern erlöse uns von dem Übel. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">but deliver us from evil. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,41 @@
<script>
import Prayer from './Prayer.svelte';
import Paternoster from './Paternoster.svelte';
import AveMaria from './AveMaria.svelte';
import GloriaPatri from './GloriaPatri.svelte';
import AnimaChristi from './AnimaChristi.svelte';
import PrayerBeforeACrucifix from './PrayerBeforeACrucifix.svelte';
let {onlyIntro = false } = $props();
</script>
<Prayer hasLatin={false}>
{#snippet children(showLatin, urlLang)}
<p class="intro">
{#if urlLang === 'en'}A plenary indulgence is granted to the faithful who devoutly recite these prayers after Holy Communion. The usual conditions apply: sacramental confession, Eucharistic communion, prayer for the intentions of the Holy Father, and detachment from all sin, even venial.{/if}
{#if urlLang === 'de'}Den Gläubigen, die diese Gebete nach der heiligen Kommunion andächtig verrichten, wird ein vollkommener Ablass gewährt. Die üblichen Bedingungen gelten: sakramentale Beichte, eucharistische Kommunion, Gebet in den Anliegen des Heiligen Vaters und Loslösung von jeder Sünde, auch von lässlichen.{/if}
</p>
{/snippet}
</Prayer>
{#if !onlyIntro}
<Prayer>
{#snippet children(showLatin, urlLang)}
<h3> Ánima Christi </h3>
<AnimaChristi />
<h3>
{#if urlLang=='en'}Plenary Indulgence{:else}Vollkommener Ablass{/if}
</h3>
<h3>
{#if urlLang=='en'}Prayer Before a Crucifix{:else}Gebet vor einem Kruzifix{/if}
</h3>
<h4> Paternoster </h4>
<Paternoster />
<h4> Ave Maria </h4>
<AveMaria />
<h4> Gloria Patri </h4>
<GloriaPatri />
<PrayerBeforeACrucifix />
{/snippet}
</Prayer>
{/if}

View File

@@ -0,0 +1,161 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { getLanguageContext } from '$lib/contexts/languageContext.js';
let { latinPrimary = true, hasLatin = true, children } = $props<{ latinPrimary?: boolean, hasLatin?: boolean, children?: Snippet<[boolean, string]> }>();
// Get context if available (graceful fallback for standalone usage)
let showLatinStore;
let langStore;
try {
const context = getLanguageContext();
showLatinStore = context.showLatin;
langStore = context.lang;
} catch {
showLatinStore = null;
langStore = null;
}
let showLatin = $derived(showLatinStore ? $showLatinStore : true);
let urlLang = $derived(langStore ? $langStore : 'de');
</script>
<style>
/* === LAYOUT === */
.prayer-wrapper :global(p) {
display: flex;
flex-direction: column;
}
.prayer-wrapper.vernacular-primary :global(p) {
flex-direction: column-reverse;
}
.prayer-wrapper :global(v) {
display: block;
margin: 0;
}
/* === LANGUAGE VISIBILITY === */
.prayer-wrapper.lang-de :global(v:lang(en)),
.prayer-wrapper.lang-en :global(v:lang(de)),
.prayer-wrapper.monolingual :global(v:lang(la)) {
display: none;
}
/* === BASE COLORS (dark mode) === */
.prayer-wrapper :global(v:lang(la)) { color: var(--nord6); }
.prayer-wrapper :global(v:lang(de)),
.prayer-wrapper :global(v:lang(en)) { color: grey; }
/* No-Latin prayers: vernacular gets primary color */
.prayer-wrapper.no-latin :global(v:lang(de)),
.prayer-wrapper.no-latin :global(v:lang(en)) {
color: var(--nord6);
}
/* Vernacular primary overrides */
.prayer-wrapper.vernacular-primary :global(v:lang(de)),
.prayer-wrapper.vernacular-primary :global(v:lang(en)) {
color: var(--nord6);
}
.prayer-wrapper.vernacular-primary :global(v:lang(la)) {
color: grey;
}
/* Monolingual spacing */
.prayer-wrapper.monolingual :global(v:not(:lang(la))) {
color: var(--nord6);
margin-bottom: 0.5em;
}
/* === LIGHT MODE === */
@media (prefers-color-scheme: light) {
.prayer-wrapper :global(v:lang(la)),
.prayer-wrapper.vernacular-primary :global(v:lang(de)),
.prayer-wrapper.vernacular-primary :global(v:lang(en)),
.prayer-wrapper.monolingual :global(v:not(:lang(la))),
.prayer-wrapper.no-latin :global(v:lang(de)),
.prayer-wrapper.no-latin :global(v:lang(en)) {
color: black;
}
}
/* === INLINE / RUBRIC TEXT === */
/* Base: all vernacular inline text is grey */
.prayer-wrapper :global(v[lang=de] > i),
.prayer-wrapper :global(v[lang=en] > i) {
color: grey;
}
/* Monolingual override */
.prayer-wrapper.monolingual :global(v[lang=de] > i),
.prayer-wrapper.monolingual :global(v[lang=en] > i) {
color: var(--red);
}
/* Latin (always emphasized) */
.prayer-wrapper :global(v[lang=la] > i) {
color: var(--nord11);
font-weight: 900;
}
/* === MYSTERY TEXT (shared base) === */
.prayer-wrapper :global(v.mystery-text) {
font-weight: 700;
}
/* Latin mystery — always primary */
.prayer-wrapper :global(v.mystery-text:lang(la)),
.prayer-wrapper :global(v.mystery-text:lang(la) > i) {
color: var(--nord11) !important;
font-size: 1.1em;
}
/* Vernacular mystery — bilingual only */
.prayer-wrapper:not(.monolingual)
:global(v.mystery-text:lang(de)),
.prayer-wrapper:not(.monolingual)
:global(v.mystery-text:lang(en)),
.prayer-wrapper:not(.monolingual)
:global(v.mystery-text:lang(de) > i),
.prayer-wrapper:not(.monolingual)
:global(v.mystery-text:lang(en) > i) {
color: var(--nord12) !important;
font-size: 0.95em;
}
/* Vernacular-primary emphasis */
.prayer-wrapper.monolingual
:global(v.mystery-text:lang(de)),
.prayer-wrapper.monolingual
:global(v.mystery-text:lang(en)),
.prayer-wrapper.monolingual
:global(v.mystery-text:lang(de) > i),
.prayer-wrapper.monolingual
:global(v.mystery-text:lang(en) > i) {
color: var(--nord11) !important;
font-size: 1.1em;
}
.prayer-wrapper.vernacular-primary
:global(v.mystery-text:lang(la)) {
color: var(--nord12) !important;
font-size: 0.95em;
}
/* Monolingual: hide Latin mystery */
.prayer-wrapper.monolingual
:global(v.mystery-text:lang(la)) {
display: none;
}
</style>
<div
class="prayer-wrapper"
class:vernacular-primary={!latinPrimary}
class:monolingual={!showLatin}
class:no-latin={!hasLatin}
class:lang-de={urlLang === 'de'}
class:lang-en={urlLang === 'en'}
>
{@render children?.(showLatin, urlLang)}
</div>

View File

@@ -0,0 +1,38 @@
<script>
import Prayer from './Prayer.svelte';
import Paternoster from './Paternoster.svelte';
import AveMaria from './AveMaria.svelte';
import GloriaPatri from './GloriaPatri.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang=la>En ego, o bone et dulcíssime Jesu,</v>{/if}
{#if urlLang=='de'}<v lang=de>Siehe, o gütiger und milder Jesus,</v>{/if}
{#if urlLang=='en'}<v lang=en>Behold, O good and sweetest Jesus,</v>{/if}
{#if showLatin}<v lang=la>ante conspéctum tuum génibus me provólvo </v>{/if}
{#if urlLang=='de'}<v lang=de>ich werfe mich vor Deinen Augen auf die Knie. </v>{/if}
{#if urlLang=='en'}<v lang=en>I cast myself upon my knees in Thy sight,</v>{/if}
{#if showLatin}<v lang=la>ac máximo ánimi ardóre te oro atque obtéstor, </v>{/if}
{#if urlLang=='de'}<v lang=de>Inbrünstig bitte und beschwöre ich Dich:</v>{/if}
{#if urlLang=='en'}<v lang=en>and with the most fervent desire of my soul I pray and beseech Thee</v>{/if}
{#if showLatin}<v lang=la> ut meum in in cor vívidos fídei, spei et caritátis sensus, atque veram peccatórum meórum pœniténtiam,</v>{/if}
{#if urlLang=='de'}<v lang=de>Präge meinem Herzen lebendige Gefühle des Glaubens, der Hoffung und der Liebe ein</v>{/if}
{#if urlLang=='en'}<v lang=en>to impress upon my heart lively sentiments of faith, hope and charity,</v>{/if}
{#if showLatin} <v lang=la>éaque emendándi firmíssimam voluntátem velis</v>{/if}
{#if urlLang=='de'}<v lang=de>sowie wahre Reue über meine Sünden und den ganz festen Willen, mich zu bessern.</v>{/if}
{#if urlLang=='en'}<v lang=en>with true repentance for my sins and a most firm desire of amendment:</v>{/if}
{#if showLatin} <v lang=la> imprímere; dum magno ánimi afféctu et dolóre tua quinque vúlnera mecum ipse consídero ac mente contémplor,</v>{/if}
{#if urlLang=='de'}<v lang=de>Voll Liebe und Schmerz schaue ich Deine fünf Wunden und betrachte sie in meinem Geiste.</v>{/if}
{#if urlLang=='en'}<v lang=en>whilst with deep affection and grief of soul I consider within myself and mentally contemplate Thy five most precious Wounds,</v>{/if}
{#if showLatin}<v lang=la>illud præ óculis habens, quod jam in ore ponébat tuo David Prophéta de te, o bone Jesu:</v>{/if}
{#if urlLang=='de'}<v lang=de>Dabei halte ich mir vor Augen, was im Hinblick auf Dich, o guter Jesus, schon der Prophet David Dir in den Mund legte:</v>{/if}
{#if urlLang=='en'}<v lang=en>having before mine eyes that which David, the prophet, long ago spoke in Thine Own person concerning Thee, my Jesus:</v>{/if}
{#if showLatin}<v lang=la> «Fodérunt manus meas et pedes meos; dinumeravérunt ómnio ossa mea.» (Ps. 21, 17-18)</v>{/if}
{#if urlLang=='de'}<v lang=de>«Sie haben Meine Hände und Meine Füsse durchbohrt; alle meine Gebeine haben sie gezählt.» (Ps. 21, 17-18)</v>{/if}
{#if urlLang=='en'}<v lang=en>"They have pierced My hands and My feet, they have numbered all My bones." (Ps. 21:17-18</v>{/if}
<v lang=und>Amen.</v>
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,46 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Regína Cæli, lætáre, allelúia.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Freu dich, du Himmelskönigin, alleluja.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Queen of Heaven, rejoice, alleluia.</v>{/if}
{#if showLatin}<v lang="la">Quia quem meruísti portáre, allelúia.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Den du zu tragen würdig warst, alleluja.</v>{/if}
{#if urlLang === 'en'}<v lang="en">For He whom thou didst merit to bear, alleluia.</v>{/if}
{#if showLatin}<v lang="la">Resurréxit, sicut dixit, allelúia.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Er ist auferstanden, wie er gesagt hat, alleluja.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Has risen, as He said, alleluia.</v>{/if}
{#if showLatin}<v lang="la">Ora pro nobis Deum, allelúia.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Bitt Gott für uns, alleluja.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Pray for us to God, alleluia.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la"><i>℣.</i> Gaude et lætáre, Virgo María, allelúia.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℣.</i> Freu dich und frohlocke, Jungfrau Maria, alleluja.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℣.</i> Rejoice and be glad, O Virgin Mary, alleluia.</v>{/if}
{#if showLatin}<v lang="la"><i>℟.</i> Quia surréxit Dóminus vere, allelúia.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℟.</i> Denn der Herr ist wahrhaft auferstanden, alleluja.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℟.</i> For the Lord has truly risen, alleluia.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la"><i>℣.</i> Orémus.</v>{/if}
{#if urlLang === 'de'}<v lang="de"><i>℣.</i> Lasset uns beten.</v>{/if}
{#if urlLang === 'en'}<v lang="en"><i>℣.</i> Let us pray:</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Deus, qui per resurrectiónem Fílii tui Dómini nostri Iesu Christi mundum lætificáre dignátus es,</v>{/if}
{#if urlLang === 'de'}<v lang="de">O Gott, der du durch die Auferstehung deines Sohnes, unseres Herrn Jesus Christus, die Welt zu erfreuen dich gewürdigt hast,</v>{/if}
{#if urlLang === 'en'}<v lang="en">O God, who through the resurrection of Thy Son our Lord Jesus Christ didst vouchsafe to give joy to the world,</v>{/if}
{#if showLatin}<v lang="la">præsta, quǽsumus, ut per eius Genetrícem Vírginem Maríam perpétuæ capiámus gáudia vitæ.</v>{/if}
{#if urlLang === 'de'}<v lang="de">verleihe uns, wir bitten dich, dass wir durch seine Mutter, die Jungfrau Maria, die Freuden des ewigen Lebens erlangen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">grant, we beseech Thee, that through His Mother the Virgin Mary we may obtain the joys of everlasting life.</v>{/if}
{#if showLatin}<v lang="la">Per eúmdem Christum Dóminum nostrum. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Durch Christus, unseren Herrn. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Through the same Christ our Lord. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,42 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Orémus:</v>{/if}
{#if urlLang === 'de'}<v lang="de">Lasset uns beten:</v>{/if}
{#if urlLang === 'en'}<v lang="en">Let us pray:</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Déus, cújus Unigénitus,</v>{/if}
{#if urlLang === 'de'}<v lang="de">O Gott, dessen eingeborner Sohn</v>{/if}
{#if urlLang === 'en'}<v lang="en">O God, whose only begotten Son,</v>{/if}
{#if showLatin}<v lang="la">pér vítam, mórtem ét resurrectiónem súam</v>{/if}
{#if urlLang === 'de'}<v lang="de">durch sein Leben, seinen Tod und seine Auferstehung</v>{/if}
{#if urlLang === 'en'}<v lang="en">by His life, death, and resurrection,</v>{/if}
{#if showLatin}<v lang="la">nóbis salútis ætérnæ præmia comparávit:</v>{/if}
{#if urlLang === 'de'}<v lang="de">uns die Belohnung des ewigen Lebens verdient hat,</v>{/if}
{#if urlLang === 'en'}<v lang="en">has purchased for us the rewards of eternal life:</v>{/if}
{#if showLatin}<v lang="la">concéde, quæsumus;</v>{/if}
{#if urlLang === 'de'}<v lang="de">verleihe uns, wir bitten dich,</v>{/if}
{#if urlLang === 'en'}<v lang="en">grant, we beseech Thee,</v>{/if}
{#if showLatin}<v lang="la">út, hæc mystéria sanctíssimo beátæ Maríæ Vírginis Rosário recoléntes;</v>{/if}
{#if urlLang === 'de'}<v lang="de">dass wir, indem wir die Geheimisse des heiligen Rosenkranzes der allerseligsten Jungfrau ehren,</v>{/if}
{#if urlLang === 'en'}<v lang="en">that by meditating on these mysteries of the most holy Rosary of the Blessed Virgin Mary,</v>{/if}
{#if showLatin}<v lang="la">ét imitémur quód cóntinent,</v>{/if}
{#if urlLang === 'de'}<v lang="de">was sie enthalten nachahmen</v>{/if}
{#if urlLang === 'en'}<v lang="en">we may imitate what they contain</v>{/if}
{#if showLatin}<v lang="la">ét quód promíttunt, assequámur.</v>{/if}
{#if urlLang === 'de'}<v lang="de">und dadurch erlangen, was uns in denselben verheissen ist.</v>{/if}
{#if urlLang === 'en'}<v lang="en">and obtain what they promise.</v>{/if}
{#if showLatin}<v lang="la">Pér eúmdem Chrístum Dóminum nóstrum.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Durch unsern Herrn <i><sup></sup></i>Jesus Christus.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Through the same Christ our Lord.</v>{/if}
{#if showLatin}<v lang="la">Ámen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,46 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Salve, Regína,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Sei gegrüsst, o Königin,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Hail, Holy Queen,</v>{/if}
{#if showLatin}<v lang="la">máter misericórdiae;</v>{/if}
{#if urlLang === 'de'}<v lang="de">Mutter der Barmherzigkeit,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Mother of Mercy,</v>{/if}
{#if showLatin}<v lang="la">Víta, dulcédo et spes nóstra, sálve.</v>{/if}
{#if urlLang === 'de'}<v lang="de">unser Leben, unsre Wonne</v>{/if}
{#if urlLang === 'de'}<v lang="de">und unsere Hoffnung, sei gegrüsst!</v>{/if}
{#if urlLang === 'en'}<v lang="en">our life, our sweetness and our hope.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Ad te clamámus, éxsules fílii Hévae.</v>{/if}
{#if urlLang === 'de'}<v lang="de">Zu dir rufen wir verbannte Kinder Evas;</v>{/if}
{#if urlLang === 'en'}<v lang="en">To thee do we cry, poor banished children of Eve.</v>{/if}
{#if showLatin}<v lang="la">Ad te suspirámus,</v>{/if}
{#if urlLang === 'de'}<v lang="de">zu dir seufzen wir</v>{/if}
{#if urlLang === 'en'}<v lang="en">To thee do we send up our sighs,</v>{/if}
{#if showLatin}<v lang="la">geméntes et fléntes in hac lacrimárum válle.</v>{/if}
{#if urlLang === 'de'}<v lang="de">trauernd und weinend in diesem Tal der Tränen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">mourning and weeping in this valley of tears.</v>{/if}
{#if showLatin}<v lang="la">Eia ergo, Advocáta nóstra,</v>{/if}
{#if urlLang === 'de'}<v lang="de">Wohlan denn, unsre Fürsprecherin,</v>{/if}
{#if urlLang === 'en'}<v lang="en">Turn then, most gracious advocate,</v>{/if}
{#if showLatin}<v lang="la">íllos túos misericórdes óculos ad nos convérte.</v>{/if}
{#if urlLang === 'de'}<v lang="de">deine barmherzigen Augen wende zu uns</v>{/if}
{#if urlLang === 'en'}<v lang="en">thine eyes of mercy toward us.</v>{/if}
{#if showLatin}<v lang="la">Et <i><sup></sup></i>Jésum, benedíctum frúctum véntris túi,</v>{/if}
{#if urlLang === 'de'}<v lang="de">und nach diesem Elend zeige uns <i><sup></sup></i>Jesus,</v>{/if}
{#if urlLang === 'en'}<v lang="en">And after this our exile show unto us</v>{/if}
{#if showLatin}<v lang="la">nóbis post hoc exsílíum osténde.</v>{/if}
{#if urlLang === 'de'}<v lang="de">die gebenedeite Frucht deines Leibes.</v>{/if}
{#if urlLang === 'en'}<v lang="en">the blessed fruit of thy womb, <i><sup></sup></i>Jesus.</v>{/if}
{#if showLatin}<v lang="la">O clémens, o pía, o dúlcis Vírgo <i><sup></sup></i>María. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">O gütige, o milde, o süsse Jungfrau <i><sup></sup></i>Maria. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">O clement, O loving, O sweet Virgin <i><sup></sup></i>Mary. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -0,0 +1,48 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
{#snippet children(showLatin, urlLang)}
<p>
{#if showLatin}<v lang="la">Tantum ergo Sacraméntum</v>{/if}
{#if urlLang === 'de'}<v lang="de">Darum lasst uns tief verehren</v>{/if}
{#if urlLang === 'en'}<v lang="en">Therefore so great a Sacrament</v>{/if}
{#if showLatin}<v lang="la">venerémur cérnui:</v>{/if}
{#if urlLang === 'de'}<v lang="de">ein so grosses Sakrament;</v>{/if}
{#if urlLang === 'en'}<v lang="en">let us venerate with bowed heads;</v>{/if}
{#if showLatin}<v lang="la">et antíquum documéntum</v>{/if}
{#if urlLang === 'de'}<v lang="de">dieser Bund soll ewig währen</v>{/if}
{#if urlLang === 'en'}<v lang="en">and the old rite</v>{/if}
{#if showLatin}<v lang="la">novo cedat rítui:</v>{/if}
{#if urlLang === 'de'}<v lang="de">und den neuen Bund ersetzt.</v>{/if}
{#if urlLang === 'en'}<v lang="en">give way to the new:</v>{/if}
{#if showLatin}<v lang="la">præstet fides suppleméntum</v>{/if}
{#if urlLang === 'de'}<v lang="de">Unser Glaube soll uns lehren,</v>{/if}
{#if urlLang === 'en'}<v lang="en">let faith provide a supplement</v>{/if}
{#if showLatin}<v lang="la">sénsuum deféctui.</v>{/if}
{#if urlLang === 'de'}<v lang="de">was das Auge nicht erkennt.</v>{/if}
{#if urlLang === 'en'}<v lang="en">for the failure of the senses.</v>{/if}
</p>
<p>
{#if showLatin}<v lang="la">Genitóri, Genitóque</v>{/if}
{#if urlLang === 'de'}<v lang="de">Gott dem Vater und dem Sohne</v>{/if}
{#if urlLang === 'en'}<v lang="en">To the Begetter and the Begotten</v>{/if}
{#if showLatin}<v lang="la">laus et iubilátio,</v>{/if}
{#if urlLang === 'de'}<v lang="de">sei Lob und Preis und Ehre,</v>{/if}
{#if urlLang === 'en'}<v lang="en">be praise and jubilation,</v>{/if}
{#if showLatin}<v lang="la">salus, honor, virtus quoque</v>{/if}
{#if urlLang === 'de'}<v lang="de">Heil und Ruhm und Macht und Wonne</v>{/if}
{#if urlLang === 'en'}<v lang="en">salvation, honour, virtue also</v>{/if}
{#if showLatin}<v lang="la">sit et benedíctio:</v>{/if}
{#if urlLang === 'de'}<v lang="de">und Segen immerdar,</v>{/if}
{#if urlLang === 'en'}<v lang="en">and blessing too:</v>{/if}
{#if showLatin}<v lang="la">procedénti ab utróque</v>{/if}
{#if urlLang === 'de'}<v lang="de">und dem der von beiden ausgeht,</v>{/if}
{#if urlLang === 'en'}<v lang="en">to Him proceeding from both</v>{/if}
{#if showLatin}<v lang="la">compar sit laudátio. Amen.</v>{/if}
{#if urlLang === 'de'}<v lang="de">sei gleiche Ehre. Amen.</v>{/if}
{#if urlLang === 'en'}<v lang="en">let there be equal praise. Amen.</v>{/if}
</p>
{/snippet}
</Prayer>

View File

@@ -1,88 +0,0 @@
<script>
import Prayer from './Prayer.svelte';
import AveMaria from './AveMaria.svelte';
</script>
<Prayer>
<!-- First Versicle and Response -->
<p>
<v lang="la"><i>℣.</i> Angelus Domini nuntiavit Mariae.</v>
<v lang="de"><i>℣.</i> Der Engel des Herrn brachte Maria die Botschaft</v>
<v lang="en"><i>℣.</i> The Angel of the Lord declared unto Mary.</v>
<v lang="la"><i>℟.</i> Et concepit de Spiritu Sancto.</v>
<v lang="de"><i>℟.</i> und sie empfing vom Heiligen Geist.</v>
<v lang="en"><i>℟.</i> And she conceived of the Holy Spirit.</v>
</p>
</Prayer>
<!-- First Hail Mary -->
<AveMaria />
<Prayer>
<!-- Second Versicle and Response -->
<p>
<v lang="la"><i>℣.</i> Ecce ancilla Domini,</v>
<v lang="de"><i>℣.</i> Maria sprach: Siehe, ich bin die Magd des Herrn</v>
<v lang="en"><i>℣.</i> Behold the handmaid of the Lord.</v>
<v lang="la"><i>℟.</i> Fiat mihi secundum verbum tuum.</v>
<v lang="de"><i>℟.</i> mir geschehe nach Deinem Wort.</v>
<v lang="en"><i>℟.</i> Be it done unto me according to thy word.</v>
</p>
</Prayer>
<!-- Second Hail Mary -->
<AveMaria />
<Prayer>
<!-- Third Versicle and Response -->
<p>
<v lang="la"><i>℣.</i> Et Verbum caro factum est,</v>
<v lang="de"><i>℣.</i> Und das Wort ist Fleisch geworden</v>
<v lang="en"><i>℣.</i> And the Word was made flesh.</v>
<v lang="la"><i>℟.</i> Et habitavit in nobis.</v>
<v lang="de"><i>℟.</i> und hat unter uns gewohnt.</v>
<v lang="en"><i>℟.</i> And dwelt among us.</v>
</p>
</Prayer>
<!-- Third Hail Mary -->
<AveMaria />
<Prayer>
<!-- Fourth Versicle and Response -->
<p>
<v lang="la"><i>℣.</i> Ora pro nobis, sancta Dei Genetrix,</v>
<v lang="de"><i>℣.</i> Bitte für uns Heilige Gottesmutter</v>
<v lang="en"><i>℣.</i> Pray for us, O holy Mother of God.</v>
<v lang="la"><i>℟.</i> Ut digni efficiamur promissionibus Christi.</v>
<v lang="de"><i>℟.</i> auf dass wir würdig werden der Verheißungen Christi.</v>
<v lang="en"><i>℟.</i> That we may be made worthy of the promises of Christ.</v>
</p>
<!-- Closing Prayer -->
<p>
<v lang="la"><i>℣.</i> Oremus.</v>
<v lang="de"><i>℣.</i> Lasset uns beten.</v>
<v lang="en"><i>℣.</i> Let us pray:</v>
</p>
<p>
<v lang="la">
Gratiam tuam, quaesumus, Domine, mentibus nostris infunde; ut qui, Angelo nuntiante,
Christi Filii tui incarnationem cognovimus, per passionem eius et crucem ad
resurrectionis gloriam perducamur. Per eumdem Christum Dominum nostrum. Amen.
</v>
<v lang="de">
Allmächtiger Gott, gieße deine Gnade in unsere Herzen ein. Durch die Botschaft des
Engels haben wir die Menschwerdung Christi, deines Sohnes, erkannt. Lass uns durch
sein Leiden und Kreuz zur Herrlichkeit der Auferstehung gelangen. Darum bitten wir
durch Christus, unseren Herrn. Amen.
</v>
<v lang="en">
Pour forth, we beseech Thee, O Lord, Thy grace into our hearts, that we to whom the
Incarnation of Christ Thy Son was made known by the message of an angel, may by His
Passion and Cross be brought to the glory of His Resurrection. Through the same Christ
Our Lord. Amen.
</v>
</p>
</Prayer>

View File

@@ -1,28 +0,0 @@
<script lang="ts">
import Prayer from './Prayer.svelte';
let { mystery = "", mysteryLatin = "" } = $props<{ mystery?: string, mysteryLatin?: string }>();
</script>
<Prayer>
<p>
<v lang="la">Ave María, grátia plena. Dóminus tecum,</v>
<v lang="de">Gegrüsset seist du Maria, voll der Gnade; der Herr ist mit dir;</v>
<v lang="la">benedícta tu in muliéribus,</v>
<v lang="de">du bist gebenedeit unter den Weibern,</v>
<v lang="la">et benedíctus fructus ventris tui, {#if !mysteryLatin}Jesus.{/if}</v>
<v lang="de">und gebenedeit ist die Frucht deines Leibes, {#if !mystery}Jesus.{/if}</v>
{#if mysteryLatin}
<v lang="la" class="mystery-text">{mysteryLatin}</v>
{/if}
{#if mystery}
<v lang="de" class="mystery-text">{mystery}</v>
{/if}
</p>
<p>
<v lang="la">Sancta María, mater Dei, ora pro nobis peccatóribus,</v>
<v lang="de">Heilige Maria, Mutter Gottes, bitte für uns Sünder</v>
<v lang="la">nunc, et in hora mortis nostræ. Amen.</v>
<v lang="de">jetzt und in der Stunde unseres Todes! Amen.</v>
</p>
</Prayer>

View File

@@ -1,15 +0,0 @@
<p>
<v lang="de">Mein Herr und mein Gott,</v>
<v lang="de">nimm alles von mir,</v>
<v lang="de">was mich hindert zu Dir.</v>
</p>
<p>
<v lang="de">Mein Herr und mein Gott,</v>
<v lang="de">gib alles mir,</v>
<v lang="de">was mich führet zu Dir.</v>
</p>
<p>
<v lang="de">Mein Herr und mein Gott,</v>
<v lang="de">nimm mich mir</v>
<v lang="de">und gib mich ganz zu eigen Dir.</v>
</p>

View File

@@ -1,91 +0,0 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
<p>
<v lang="la">Credo in unum <i><sup></sup></i> Deum, Patrem omnipoténtem,</v>
<v lang="de">Ich glaub an den einen <i><sup></sup></i> Gott. Den allmächtigen Vater,</v>
<v lang="la">factórem cæli et terræ,</v>
<v lang="de">Schöpfer des Himmels und der Erde,</v>
<v lang="la">visibílium ómnium et invisibílium.</v>
<v lang="de">aller sichtbaren und unsichtbaren Dinge.</v>
<v lang="la">Et in unum Dóminum <i><sup></sup></i> Jesum Christum,</v>
<v lang="de">Und an den einen Herrn <i><sup></sup></i> Jesus Christus,</v>
<v lang="la">Fílium Dei unigénitum.</v>
<v lang="de">Gottes eingeborenen Sohn.</v>
<v lang="la">Et ex Patre natum ante ómnia sǽcula.</v>
<v lang="de">Er ist aus dem Vater geboren vor aller Zeit.</v>
<v lang="la">Deum de Deo,</v>
<v lang="de">Gott von Gott,</v>
<v lang="la">lumen de lúmine,</v>
<v lang="de">Licht vom Lichte,</v>
<v lang="la">Deum verum de Deo vero.</v>
<v lang="de">wahrer Gott vom wahren Gott;</v>
<v lang="la">Génitum, non factum,</v>
<v lang="de">Gezeugt, nicht geschaffen,</v>
<v lang="la">consubstantiálem Patri:</v>
<v lang="de">eines Wesens mit dem Vater;</v>
<v lang="la">per quem ómnia facta sunt.</v>
<v lang="de">durch Ihn ist alles geschaffen.</v>
<v lang="la">Qui propter nos hómines</v>
<v lang="de">Für uns Menschen</v>
<v lang="la">et propter nostram salútem</v>
<v lang="de">und um unsres Heiles willen</v>
<v lang="la">descéndit de cælis.</v>
<v lang="de">ist Er vom Himmel herabgestiegen.</v>
</p>
<p>
<v lang="la">Et incarnátus est de Spíritu Sancto</v>
<v lang="de">Er hat Fleisch angenommen durch den Hl. Geist</v>
<v lang="la">ex <i><sup></sup></i> María Vírgine:</v>
<v lang="de">aus <i><sup></sup></i> Maria, der Jungfrau</v>
<v lang="la">Et homo factus est.</v>
<v lang="de">und ist Mensch geworden.</v>
<v lang="la">Crucifíxus étiam pro nobis:</v>
<v lang="de">Gekreuzigt wurde Er sogar für uns;</v>
<v lang="la">sub Póntio Piláto passus, et sepúltus est.</v>
<v lang="de">unter Pontius Pilatus hat Er den Tod erlitten</v>
<v lang="de">und ist begraben worden</v>
</p>
<p>
<v lang="la">Et resurréxit tértia die,</v>
<v lang="de">Er ist auferstanden am dritten Tage,</v>
<v lang="la">secúndum Scriptúras.</v>
<v lang="de">gemäss der Schrift;</v>
<v lang="la">Et ascéndit in cáelum:</v>
<v lang="de">Er ist aufgefahren in den Himmel</v>
<v lang="la">sedet ad déxteram Patris.</v>
<v lang="de">und sitzet zur Rechten des Vaters.</v>
</p>
<p>
<v lang="la">Et íterum ventúrus est cum glória</v>
<v lang="de">Er wird wiederkommen in Herrlichkeit,</v>
<v lang="la">judicáre vivos et mórtuos:</v>
<v lang="de">Gericht zu halten über Lebende und Tote:</v>
<v lang="la">cujus regni non erit finis.</v>
<v lang="de">und Seines Reiches wird kein Endes sein.</v>
</p>
<p>
<v lang="la">Et in Spíritum Sanctum,</v>
<v lang="de">Ich glaube an den Heiligen Geist,</v>
<v lang="la">Dóminum et vivificántem:</v>
<v lang="de">den Herrn und Lebensspender,</v>
<v lang="la">qui ex Patre Filióque procédit.</v>
<v lang="de">der vom Vater und vom Sohne ausgeht.</v>
<v lang="la">Qui cum Patre et Fílio simul <i><sup></sup></i> adorátur et conglorificátur:</v>
<v lang="de">zugleich <i><sup></sup></i> angebetet und verherrlicht;</v>
<v lang="la">qui locútus est per Prophétas.</v>
<v lang="de">Er hat gesprochen durch die Propheten.</v>
<v lang="la">Et unam sanctam cathólicam et apostólicam Ecclésiam.</v>
<v lang="de">Ich glaube an die eine, heilige, katholische und apostolische Kirche.</v>
<v lang="la">Confíteor unum baptísma</v>
<v lang="de">Ich bekenne die eine Taufe</v>
<v lang="la">in remissiónem peccatórum.</v>
<v lang="de">zur Vergebung der Sünden.</v>
<v lang="la">Et exspécto resurrectiónem mortuórum.</v>
<v lang="de">Ich erwarte die Auferstehung der Toten.</v>
<v lang="la"><i></i> Et vitam ventúri sǽculi. Amen.</v>
<v lang="de"><i></i> Und das Leben der zukünftigen Welt. Amen.</v>
</p>
</Prayer>

View File

@@ -1,18 +0,0 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
<p>
<v lang="la">Ó mí Jésú, dímitte nóbís débita nostra,</v>
<v lang="de">O mein Jesus, verzeih' uns unsere Sünden,</v>
<v lang="la">líberá nós ab igne ínferní,</v>
<v lang="de">bewahre uns vor den Feuern der Hölle</v>
<v lang="la">condúc in cælum omnés animás, </v>
<v lang="de">und führe alle Seelen in den Himmel,</v>
<v lang="la">præsertim illás,</v>
<v lang="de">besonders jene,</v>
<v lang="la">quæ maximé indigent misericordiá tuá. Amen.</v>
<v lang="de">die Deiner Barmherzigkeit am meisten bedürfen. Amen.</v>
</p>
</Prayer>

View File

@@ -1,58 +0,0 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
<p>
<v lang="la">Glória in excélsis <i><sup></sup></i> Deo.</v>
<v lang="de">Ehre sei <i><sup></sup></i> Gott in der Höhe.</v>
<v lang="la">Et in terra pax homínibus</v>
<v lang="de">Und auf Erden Friede den Mesnchen,</v>
<v lang="la">bonæ voluntátis.</v>
<v lang="de">die guten Willens sind.</v>
<v lang="la">Laudámus te.</v>
<v lang="de">Wir loben Dich.</v>
<v lang="la">Benedícimus te.</v>
<v lang="de">Wir preisen Dich.</v>
<v lang="la"><i><sup></sup></i> Adorámus te.</v>
<v lang="de"><i><sup></sup></i> Wir beten Dich an.</v>
<v lang="la">Glorificámus te.</v>
<v lang="de">Wir verherrlichen Dich.</v>
<v lang="la"><i><sup></sup></i> Grátias ágimus tibi</v>
<v lang="de"><i><sup></sup></i> Wir sagen Dir Dank</v>
<v lang="la">propter magnam glóriam tuam.</v>
<v lang="de">ob Deiner grossen Herrlichkeit.</v>
<v lang="la">Dómine Deus, Rex cæléstis,</v>
<v lang="de">Herr und Gott, König des Himmels,</v>
<v lang="la">Deus Pater omnípotens.</v>
<v lang="de">Gott allmächtiger Vater!</v>
<v lang="la">Dómine Fili unigénite, <i><sup></sup></i> Jesu Christe.</v>
<v lang="de">Herr <i><sup></sup></i> Jesus Christus, eingeborener Sohn!</v>
<v lang="la">Dómine Deus, Agnus Dei,</v>
<v lang="de">Herr und Gott, Lamm Gottes,</v>
<v lang="la">Fílius Patris.</v>
<v lang="de">Sohn des Vaters!</v>
<v lang="la">Qui tollis peccáta mundi,</v>
<v lang="de">Du nimmst hinweg die Sünden der Welt:</v>
<v lang="la">miserére nobis.</v>
<v lang="de">erbarme Dich unser.</v>
<v lang="la">Qui tollis peccáta mundi,</v>
<v lang="de">Du nimmst hinwerg die Sünden der Welt.</v>
<v lang="la"><i><sup></sup></i> súscipe depreciatiónem nostram.</v>
<v lang="de"><i><sup></sup></i> nimm unser Flehen gnädig auf.</v>
<v lang="la">Qui sedes ad déxteram Patris,</v>
<v lang="de">Du sitzt zur Rechten des Vaters:</v>
<v lang="la">miserére nobis.</v>
<v lang="de">erbarme Dich unser.</v>
<v lang="la">Quóniam tu solus Sanctus.</v>
<v lang="de">Denn Du allein bist der Heilige.</v>
<v lang="la">Tu solus Altíssimus,</v>
<v lang="de">Du allein der Höchste,</v>
<v lang="la"><i><sup></sup></i> Jesu Christe.</v>
<v lang="de"><i><sup></sup></i> Jesus Christus,</v>
<v lang="la">Cum Sancto Spíritu</v>
<v lang="de">Mit dem Hl. Geiste,</v>
<v lang="la"><i></i> in glória Dei Patris. Amen.</v>
<v lang="de"><i></i> in der Herrlichkeit Gottes des Vaters. Amen.</v>
</p>
</Prayer>

View File

@@ -1,14 +0,0 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
<p>
<v lang="la">Glória Patri, et Fílio, et Spirítui Sancto.</v>
<v lang="de">Ehre sei dem Vater und dem Sohne und dem Hl. Geiste.</v>
<v lang="la">Sicut erat in princípio, et nunc, et semper:</v>
<v lang="de">Wie es war am Anfang, so auch jetzt und allezeit</v>
<v lang="la">et in sǽcula sæculórum. Amen.</v>
<v lang="de">und in Ewigkeit. Amen.</v>
</p>
</Prayer>

View File

@@ -1,8 +0,0 @@
<p>
<v>Jungfräulicher Vater Jesu,</v>
<v>Reinster Bräutigam Mariä,</v>
<v>Sankt Joseph, bitte Tag für Tag bei Jesus, dem Sohn Gottes.</v>
<v>Seine Kraft und Gnade soll uns stärken,</v>
<v>dass wir siegreich streiten im Leben</v>
<v>und die Krone von Ihm erhalten im Sterben.</v>
</p>

View File

@@ -1,10 +0,0 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
<p>
<v lang="la">In nómine <i></i> Patris, et Fílii, et Spíritus Sancti. Amen.</v>
<v lang="de">Im Namen des <i></i> Vaters und des Sohnes und des Heiligen Geistes. Amen.</v>
</p>
</Prayer>

View File

@@ -1,22 +0,0 @@
<p>
<v lang="la">Sáncte Míchael Archángele,</v>
<v lang="de">Heiliger Erzengel Michael,</v>
<v lang="la">defénde nos in proélio,</v>
<v lang="de">verteidige uns im Kampfe!</v>
<v lang="la">cóntra nequítam et insídias</v>
<v lang="de">Gegen die Bosheit und Nachstellungen</v>
<v lang="la">diáboli ésto præsídium.</v>
<v lang="de">des Teufels sei unser Schutz. </v>
<v lang="la">Ímperet ílli Déus, súpplices deprecámur:</v>
<v lang="de">»Gott gebiete ihm!«, so bitten wir flehentlich.</v>
<v lang="la">tuque, Prínceps milítæ cæléstis,</v>
<v lang="de">Du aber, Fürst der himmlischen Heerscharen,</v>
<v lang="la">Sátanam aliósque spíritus malígnos,</v>
<v lang="de">stosse den Satan und die anderen bösen Geister,</v>
<v lang="la">qui ad perditiónem animárum</v>
<v lang="la">pervagántur in múndo,</v>
<v lang="de">die in der Welt umhergehen,</v>
<v lang="de">um die Seelen zu verderben,</v>
<v lang="la">divína virtúte, in inférnum detrúde. Amen.</v>
<v lang="de">durch die Kraft Gottes in die Hölle. Amen.</v>
</p>

View File

@@ -1,26 +0,0 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
<p>
<v lang="la">Pater noster, qui es in cælis</v>
<v lang="de">Vater unser, der Du bist im Himmel,</v>
<v lang="la">Sanctificétur nomen tuum</v>
<v lang="de">geheiligt werde Dein Name;</v>
<v lang="la">Advéniat regnum tuum</v>
<v lang="de">zu uns komme Dein Reich;</v>
<v lang="la">Fiat volúntas tua, sicut in cælo, et in terra.</v>
<v lang="de">Dein Wille geschehe, wie im Himmel, also auch auf Erden!</v>
<v lang="la">Panem nostrum quotidiánum da nobis hódie.</v>
<v lang="de">Unser tägliches Brot gib uns heute;</v>
<v lang="la">Et dimítte nobis debíta nostra,</v>
<v lang="de">und vergib uns unsere Schulden,</v>
<v lang="la">sicut et nos dimíttimus debitóribus nostris.</v>
<v lang="de">wie auch wir vergeben unsern Schuldigern;</v>
<v lang="la">Et ne nos indúcas in tentatiónem.</v>
<v lang="de">und führe uns nicht in Versuchung.</v>
<v lang="la">Sed líbera nos a malo. Amen.</v>
<v lang="de">Sondern erlöse uns von dem Übel. Amen.</v>
</p>
</Prayer>

View File

@@ -1,128 +0,0 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { getLanguageContext } from '$lib/contexts/languageContext.js';
let { latinPrimary = true, children } = $props<{ latinPrimary?: boolean, children?: Snippet }>();
// Get context if available (graceful fallback for standalone usage)
let showLatinStore;
try {
const context = getLanguageContext();
showLatinStore = context.showLatin;
} catch {
showLatinStore = null;
}
let showLatin = $derived(showLatinStore ? $showLatinStore : true);
</script>
<style>
.prayer-wrapper :global(p) {
display: flex;
flex-direction: column;
}
/* Reverse order when German is primary */
.prayer-wrapper.german-primary :global(p) {
flex-direction: column-reverse;
}
.prayer-wrapper :global(v) {
margin: 0;
display: block;
}
/* Latin primary (default) */
.prayer-wrapper :global(v:lang(la)) {
color: var(--nord6);
}
.prayer-wrapper :global(v:lang(de)) {
color: grey;
}
.prayer-wrapper :global(i) {
font-style: normal;
color: var(--nord11);
font-weight: 900;
}
@media(prefers-color-scheme: light) {
.prayer-wrapper :global(v:lang(la)) {
color: black;
}
}
/* German primary mode */
.prayer-wrapper.german-primary :global(v:lang(de)) {
color: var(--nord6);
}
.prayer-wrapper.german-primary :global(v:lang(la)) {
color: grey;
}
@media(prefers-color-scheme: light) {
.prayer-wrapper.german-primary :global(v:lang(de)) {
color: black;
}
}
/* Mystery text styling */
.prayer-wrapper :global(v.mystery-text:lang(la)) {
color: var(--nord11) !important;
font-weight: 700;
font-size: 1.1em;
}
.prayer-wrapper :global(v.mystery-text:lang(de)) {
color: var(--nord12) !important;
font-weight: 700;
font-size: 0.95em;
}
.prayer-wrapper.german-primary :global(v.mystery-text:lang(de)) {
color: var(--nord11) !important;
font-weight: 700;
font-size: 1.1em;
}
.prayer-wrapper.german-primary :global(v.mystery-text:lang(la)) {
color: var(--nord12) !important;
font-weight: 700;
font-size: 0.95em;
}
/* Hide Latin in monolingual mode */
.prayer-wrapper.monolingual :global(v:lang(la)) {
display: none;
}
/* German gets primary styling in monolingual mode */
.prayer-wrapper.monolingual :global(v:lang(de)) {
color: var(--nord6);
margin-bottom: 0.5em;
}
@media(prefers-color-scheme: light) {
.prayer-wrapper.monolingual :global(v:lang(de)) {
color: black;
}
}
/* Hide Latin mystery text in monolingual mode */
.prayer-wrapper.monolingual :global(v.mystery-text:lang(la)) {
display: none;
}
/* German mystery text gets prominent styling in monolingual mode */
.prayer-wrapper.monolingual :global(v.mystery-text:lang(de)) {
color: var(--nord11) !important;
font-weight: 700;
font-size: 1.1em;
}
</style>
<div class="prayer-wrapper" class:german-primary={!latinPrimary} class:monolingual={!showLatin}>
{@render children?.()}
</div>

View File

@@ -1,30 +0,0 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
<p>
<v lang="la">Orémus:</v>
<v lang="de">Lasset uns beten:</v>
</p>
<p>
<v lang="la">Déus, cújus Unigénitus,</v>
<v lang="de">O Gott, dessen eingeborner Sohn</v>
<v lang="la">pér vítam, mórtem ét resurrectiónem súam</v>
<v lang="de">durch sein Leben, seinen Tod und seine Auferstehung</v>
<v lang="la">nóbis salútis ætérnæ præmia comparávit:</v>
<v lang="de">uns die Belohnung des ewigen Lebens verdient hat,</v>
<v lang="la">concéde, quæsumus;</v>
<v lang="de">verleihe uns, wir bitten dich,</v>
<v lang="la">út, hæc mystéria sanctíssimo beátæ Maríæ Vírginis Rosário recoléntes;</v>
<v lang="de">dass wir, indem wir die Geheimisse des heiligen Rosenkranzes der allerseligsten Jungfrau ehren,</v>
<v lang="la">ét imitémur quód cóntinent,</v>
<v lang="de">was sie enthalten nachahmen</v>
<v lang="la">ét quód promíttunt, assequámur.</v>
<v lang="de">und dadurch erlangen, was uns in denselben verheissen ist.</v>
<v lang="la">Pér eúmdem Chrístum Dóminum nóstrum.</v>
<v lang="de">Durch unsern Herrn Jesus Christus.</v>
<v lang="la">Ámen.</v>
<v lang="de">Amen.</v>
</p>
</Prayer>

View File

@@ -1,33 +0,0 @@
<script>
import Prayer from './Prayer.svelte';
</script>
<Prayer>
<p>
<v lang="la">Salve, Regína,</v>
<v lang="de">Sei gegrüsst, o Königin,</v>
<v lang="la">máter misericórdiae;</v>
<v lang="de">Mutter der Barmherzigkeit,</v>
<v lang="la">Víta, dulcédo et spes nóstra, sálve.</v>
<v lang="de">unser Leben, unsre Wonne</v>
<v lang="de">und unsere Hoffnung, sei gegrüsst!</v>
</p>
<p>
<v lang="la">Ad te clamámus, éxsules fílii Hévae.</v>
<v lang="de">Zu dir rufen wir verbannte Kinder Evas;</v>
<v lang="la">Ad te suspirámus,</v>
<v lang="de">zu dir seufzen wir</v>
<v lang="la">geméntes et fléntes in hac lacrimárum válle.</v>
<v lang="de">trauernd und weinend in diesem Tal der Tränen.</v>
<v lang="la">Eia ergo, Advocáta nóstra,</v>
<v lang="de">Wohlan denn, unsre Fürsprecherin,</v>
<v lang="la">íllos túos misericórdes óculos ad nos convérte.</v>
<v lang="de">deine barmherzigen Augen wende zu uns</v>
<v lang="la">Et Jésum, benedíctum frúctum véntris túi,</v>
<v lang="de">und nach diesem Elend zeige uns Jesus,</v>
<v lang="la">nóbis post hoc exsílíum osténde.</v>
<v lang="de">die gebenedeite Frucht deines Leibes.</v>
<v lang="la">O clémens, o pía, o dúlcis Vírgo María.</v>
<v lang="de">O gütige, o milde, o süsse Jungfrau Maria.</v>
</p>
</Prayer>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { do_on_key } from '$lib/components/do_on_key.js'
import { do_on_key } from '$lib/components/recipes/do_on_key.js'
import Check from '$lib/assets/icons/Check.svelte'
let {
@@ -23,7 +23,8 @@ let options = $state({
includeIngredients: false,
includeInstructions: false,
showLabel: true,
labelOverride: ''
labelOverride: '',
baseMultiplier: 1
});
// Reset options whenever type or modal state changes
@@ -46,6 +47,7 @@ function handleInsert() {
selectedRecipe = null;
options.labelOverride = '';
options.showLabel = true;
options.baseMultiplier = 1;
closeModal();
}
}
@@ -106,7 +108,6 @@ dialog[open]::backdrop {
dialog h2 {
font-size: 3rem;
font-family: sans-serif;
color: white;
text-align: center;
margin-top: 30vh;
@@ -121,7 +122,7 @@ dialog h2 {
margin-top: 2rem;
max-width: 600px;
padding: 2rem;
border-radius: 20px;
border-radius: var(--radius-card);
background-color: var(--blue);
color: white;
box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3);
@@ -134,22 +135,25 @@ dialog h2 {
}
.selector-content select,
.selector-content input[type="text"] {
.selector-content input[type="text"],
.selector-content input[type="number"] {
width: 100%;
padding: 0.5em 1em;
margin-top: 0.5em;
border-radius: 1000px;
border-radius: var(--radius-pill);
border: 2px solid var(--nord4);
background-color: white;
color: var(--nord0);
font-size: 1rem;
transition: 100ms;
transition: var(--transition-fast);
}
.selector-content select:hover,
.selector-content select:focus,
.selector-content input[type="text"]:hover,
.selector-content input[type="text"]:focus {
.selector-content input[type="text"]:focus,
.selector-content input[type="number"]:hover,
.selector-content input[type="number"]:focus {
border-color: var(--nord9);
transform: scale(1.02, 1.02);
}
@@ -171,10 +175,10 @@ dialog h2 {
.button-group button {
padding: 0.75em 2em;
font-size: 1.1rem;
border-radius: 1000px;
border-radius: var(--radius-pill);
border: none;
cursor: pointer;
transition: 200ms;
transition: var(--transition-normal);
font-weight: bold;
}
@@ -245,6 +249,18 @@ dialog h2 {
</label>
{/if}
<label>
Mengenfaktor (Multiplikator):
<input
type="number"
bind:value={options.baseMultiplier}
min="0"
step="any"
placeholder="1"
onkeydown={(event) => do_on_key(event, 'Enter', false, handleInsert)}
/>
</label>
<div class="button-group">
<button class="button-insert" onclick={handleInsert} disabled={!selectedRecipe}>
Einfügen

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import "$lib/css/nordtheme.css";
import "$lib/css/shake.css";
import "$lib/css/icon.css";
import { onMount } from "svelte";
@@ -37,15 +36,10 @@ const img_name = $derived(
const img_alt = $derived(
recipe.images?.[0]?.alt || recipe.name
);
const img_color = $derived(recipe.images?.[0]?.color || '');
</script>
<style>
.card_anchor{
border-radius: 20px;
cursor: pointer;
display: inline-block;
text-decoration: none;
color: inherit;
}
.card-main-link {
position: absolute;
inset: 0;
@@ -67,77 +61,60 @@ const img_alt = $derived(
--card-width: 300px;
position: relative;
flex-shrink: 0;
transition: 200ms;
transition: var(--transition-normal);
text-decoration: none;
box-sizing: border-box;
font-family: sans-serif;
cursor: pointer;
height: 525px;
width: 300px;
border-radius: 20px;
border-radius: var(--radius-card);
background-size: contain;
display: flex;
display: inline-flex;
flex-direction: column;
justify-content: end;
background-color: var(--blue);
box-shadow: 0em 0em 2em 0.1em rgba(0, 0, 0, 0.3);
background-color: var(--blue);
box-shadow: var(--shadow-lg);
color: inherit;
}
/* Position/size overrides for global g-icon-badge */
.icon{
font-family: "Noto Color Emoji", emoji, sans-serif;
border: none;
background: none;
cursor: pointer;
position: absolute;
top: -25px;
right: -25px;
width: 50px;
height: 50px;
border-radius: 50%;
font-size: 1.5em;
background-color: var(--nord0);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
box-shadow: 0em 0em 1em 0.1em rgba(0, 0, 0, 0.6);
transition: 100ms;
z-index: 5;
text-decoration: none;
}
.image{
width: 300px;
height: 255px;
object-fit: cover;
transition: 200ms;
transition: var(--transition-normal);
border-top-left-radius: inherit;
border-top-right-radius: inherit;
opacity: 0;
}
.blur{
filter: blur(10px);
.image.loaded{
opacity: 1;
}
.backdrop_blur{
backdrop-filter: blur(10px);
}
.div_image,
.div_div_image{
.card-image{
width: 300px;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
height: 255px;
position: absolute;
top: 0;
overflow: hidden;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
.div_div_image{
height: 255px;
position: absolute;
width: 300px;
top: 0;
}
.card:hover,
.card:focus-within{
transform: scale(1.02,1.02);
background-color: var(--red);
box-shadow: 0.2em 0.2em 2em 1em rgba(0, 0, 0, 0.3);
box-shadow: var(--shadow-hover);
}
.card:focus{
scale: 0.95 0.95;
@@ -153,7 +130,7 @@ const img_alt = $derived(
display: flex;
flex-direction: column;
justify-content: space-between;
transition: 100ms;
transition: var(--transition-fast);
}
.name{
font-size: 2em;
@@ -167,7 +144,7 @@ const img_alt = $derived(
}
.tags{
display: flex;
flex-wrap: wrap-reverse;
flex-wrap: wrap;
overflow: hidden;
column-gap: 0.25em;
padding-inline: 0.5em;
@@ -175,59 +152,28 @@ const img_alt = $derived(
margin-bottom:0.5em;
flex-grow: 0;
}
/* Overrides for Card tags - uses g-tag base, with Card-specific adjustments */
.tag{
cursor: pointer;
text-decoration: none;
background-color: var(--nord4);
color: var(--nord0);
border-radius: 100px;
padding-inline: 1em;
line-height: 1.5em;
margin-bottom: 0.5em;
transition: 100ms;
box-shadow: 0em 0em 0.2em 0.05em rgba(0, 0, 0, 0.3);
border: none;
position: relative;
color: black;
z-index: 2;
display: inline-block;
}
.tag:hover,
.tag:focus-visible
{
transform: scale(1.04, 1.04);
background-color: var(--nord8);
box-shadow: 0.2em 0.2em 0.2em 0.1em rgba(0, 0, 0, 0.3);
}
.tag:focus{
transition: 100ms;
scale: 0.9;
}
/* Position overrides for Card category */
.card_title .category{
position: absolute;
box-shadow: 0em 0em 1em 0.1em rgba(0, 0, 0, 0.6);
text-decoration: none;
color: var(--nord6);
font-size: 1.5rem;
top: -0.8em;
left: -0.5em;
background-color: var(--nord0);
padding-inline: 1em;
border-radius: 1000px;
transition: 100ms;
border: none;
cursor: pointer;
z-index: 2;
display: inline-block;
}
.card_title .category:hover,
.card_title .category:focus-within
{
box-shadow: -0.2em 0.2em 1em 0.1em rgba(0, 0, 0, 0.6);
background-color: var(--nord3);
transform: scale(1.05, 1.05)
}
.category:focus{
scale: 0.9 0.9;
.card_title .category:focus-within {
transform: scale(1.05);
}
.favorite-indicator{
@@ -263,16 +209,10 @@ const img_alt = $derived(
background-color: var(--nord12);
}
/* Override hover color for Card icon */
.icon:hover,
.icon:focus-visible
{
transform: scale(1.1, 1.1);
.icon:focus-visible {
background-color: var(--nord3);
box-shadow: 0.2em 0.2em 1em 0.1em rgba(0, 0, 0, 0.8);
}
.icon:focus {
transform: scale(0.9, 0.9);
}
.card:hover .icon,
@@ -285,18 +225,15 @@ const img_alt = $derived(
}
</style>
<div class=card_anchor class:search_me={search} data-tags="[{recipe.tags}]">
<div class="card" class:margin_right={do_margin_right}>
<div class="card" class:search_me={search} class:margin_right={do_margin_right} data-tags="[{recipe.tags}]">
<a href="{routePrefix}/{recipe.short_name}" class="card-main-link" aria-label="View recipe: {recipe.name}">
<span class="visually-hidden">View recipe: {recipe.name}</span>
</a>
<div class=div_div_image >
<div class=div_image style="background-image:url(https://bocken.org/static/rezepte/placeholder/{img_name})">
<noscript>
<img class="image backdrop_blur" src="https://bocken.org/static/rezepte/thumb/{img_name}" loading={loading_strat} alt="{img_alt}"/>
</noscript>
<img class="image backdrop_blur" class:blur={!isloaded} src={'https://bocken.org/static/rezepte/thumb/' + img_name} loading={loading_strat} alt="{img_alt}" onload={() => isloaded=true}/>
</div>
<div class="card-image" style:background-color={img_color}>
<noscript>
<img class="image loaded" src="https://bocken.org/static/rezepte/thumb/{img_name}" loading={loading_strat} alt="{img_alt}"/>
</noscript>
<img class="image" class:loaded={isloaded} src={'https://bocken.org/static/rezepte/thumb/' + img_name} loading={loading_strat} alt="{img_alt}" onload={() => isloaded=true}/>
</div>
{#if showFavoriteIndicator && isFavorite}
<div class="favorite-indicator">❤️</div>
@@ -313,19 +250,18 @@ const img_alt = $derived(
</div>
{/if}
{#if icon_override || recipe.season.includes(current_month)}
<a href="{routePrefix}/icon/{recipe.icon}" class=icon>{recipe.icon}</a>
<a href="{routePrefix}/icon/{recipe.icon}" class="icon g-icon-badge">{recipe.icon}</a>
{/if}
<div class="card_title">
<a href="{routePrefix}/category/{recipe.category}" class=category>{recipe.category}</a>
<a href="{routePrefix}/category/{recipe.category}" class="category g-pill g-btn-dark">{recipe.category}</a>
<div>
<div class=name>{@html recipe.name}</div>
<div class=description>{@html recipe.description}</div>
</div>
<div class=tags>
{#each recipe.tags as tag}
<a href="{routePrefix}/tag/{tag}" class=tag>{tag}</a>
<a href="{routePrefix}/tag/{tag}" class="tag g-pill g-interactive">{tag}</a>
{/each}
</div>
</div>
</div>
</div>

View File

@@ -5,71 +5,120 @@ import "$lib/css/shake.css"
import "$lib/css/icon.css"
import { onMount } from 'svelte'
let { card_data = $bindable(), image_preview_url = $bindable() } = $props<{ card_data: any, image_preview_url: string }>();
let {
card_data = $bindable(),
image_preview_url = $bindable(''),
selected_image_file = $bindable<File | null>(null),
short_name = ''
}: {
card_data: any,
image_preview_url: string,
selected_image_file: File | null,
short_name: string
} = $props();
onMount( () => {
fetch(image_preview_url, { method: 'HEAD' })
.then(response => {
if(response.redirected){
image_preview_url = ""
}
})
})
// Constants for validation
const ALLOWED_MIME_TYPES = ['image/webp', 'image/jpeg', 'image/jpg', 'image/png'];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
import { img } from '$lib/js/img_store';
// Handle file selection via onchange event
function handleFileSelect(event: Event) {
const input = event.currentTarget as HTMLInputElement;
const file = input.files?.[0];
if(!card_data.tags){
if (!file) {
return;
}
// Validate MIME type
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
alert('Invalid file type. Please upload a JPEG, PNG, or WebP image.');
input.value = '';
return;
}
// Validate file size
if (file.size > MAX_FILE_SIZE) {
alert(`File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`);
input.value = '';
return;
}
// Clean up old preview URL if exists
if (image_preview_url && image_preview_url.startsWith('blob:')) {
URL.revokeObjectURL(image_preview_url);
}
// Create preview and store file
image_preview_url = URL.createObjectURL(file);
selected_image_file = file;
}
// Check if initial image_preview_url redirects to placeholder
onMount(() => {
if (image_preview_url && !image_preview_url.startsWith('blob:')) {
const img = new Image();
img.onload = () => {
// Check if this is the placeholder image (150x150)
if (img.naturalWidth === 150 && img.naturalHeight === 150) {
image_preview_url = ""
}
};
img.onerror = () => {
image_preview_url = ""
};
img.src = image_preview_url;
}
});
// Initialize tags if needed
if (!card_data.tags) {
card_data.tags = []
}
//locals
// Tag management
let new_tag = $state("");
// Reference to file input for clearing
let fileInput: HTMLInputElement;
export function show_local_image(){
var file = this.files[0]
// allowed MIME types
var mime_types = [ 'image/webp' ];
// validate MIME
if(mime_types.indexOf(file.type) == -1) {
alert('Error : Incorrect file type');
return;
}
image_preview_url = URL.createObjectURL(file);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = e => {
img.update(() => e.target.result.split(',')[1]);
};
}
export function remove_selected_images(){
image_preview_url = ""
}
export function add_to_tags(){
if(new_tag){
if(! card_data.tags.includes(new_tag)){
card_data.tags.push(new_tag)
card_data.tags = card_data.tags;
}
function remove_selected_images() {
if (image_preview_url && image_preview_url.startsWith('blob:')) {
URL.revokeObjectURL(image_preview_url);
}
new_tag = ""
}
export function remove_from_tags(tag){
card_data.tags = card_data.tags.filter(item => item !== tag)
}
export function add_on_enter(event){
if(event.key === 'Enter'){
add_to_tags()
image_preview_url = "";
selected_image_file = null;
// Reset the file input
if (fileInput) {
fileInput.value = '';
}
}
export function remove_on_enter(event, tag){
if(event.key === 'Enter'){
card_data.tags = card_data.tags.filter(item => item !== tag)
function add_to_tags() {
if (new_tag && !card_data.tags.includes(new_tag)) {
card_data.tags = [...card_data.tags, new_tag];
}
new_tag = "";
}
function remove_from_tags(tag: string) {
card_data.tags = card_data.tags.filter((item: string) => item !== tag);
}
function add_on_enter(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault();
add_to_tags();
}
}
function remove_on_enter(event: KeyboardEvent, tag: string) {
if (event.key === 'Enter') {
card_data.tags = card_data.tags.filter((item: string) => item !== tag);
}
}
</script>
@@ -81,15 +130,14 @@ export function remove_on_enter(event, tag){
text-decoration: none;
position: relative;
box-sizing: border-box;
font-family: sans-serif;
width: var(--card-width);
aspect-ratio: 4/7;
border-radius: 20px;
border-radius: var(--radius-card);
background-size: contain;
display: flex;
flex-direction: column;
justify-content: end;
transition: 200ms;
transition: var(--transition-normal);
background-color: var(--blue);
box-shadow: 0em 0em 2em 0.1em rgba(0, 0, 0, 0.3);
z-index: 0;
@@ -106,7 +154,7 @@ export function remove_on_enter(event, tag){
justify-content: center;
align-items: center;
border-radius: 20px 20px 0 0 ;
transition: 200ms;
transition: var(--transition-normal);
}
.img_label_wrapper:hover{
background-color: var(--red);
@@ -120,7 +168,7 @@ export function remove_on_enter(event, tag){
top:0;
left: 0;
border-radius: 20px 20px 0 0;
transition: 200ms;
transition: var(--transition-normal);
}
.img_label_wrapper:hover .delete{
opacity: 100%;
@@ -129,7 +177,7 @@ export function remove_on_enter(event, tag){
width: 100px;
height: 100px;
fill: white;
transition: 200ms;
transition: var(--transition-normal);
}
.delete{
cursor: pointer;
@@ -139,7 +187,7 @@ export function remove_on_enter(event, tag){
left: 2rem;
opacity: 0%;
z-index: 4;
transition:200ms;
transition: var(--transition-normal);
}
.delete:hover{
transform: scale(1.2, 1.2);
@@ -171,14 +219,14 @@ input::placeholder{
text-align:center;
width: 2.6rem;
aspect-ratio: 1/1;
transition: 100ms;
transition: var(--transition-fast);
position: absolute;
font-size: 1.5rem;
top:-0.5em;
right:-0.5em;
padding: 0.25em;
background-color: var(--nord6);
border-radius:1000px;
border-radius: var(--radius-pill);
box-shadow: 0em 0em 2em 0.1em rgba(0, 0, 0, 0.6);
}
.card .icon:hover,
@@ -210,7 +258,7 @@ input::placeholder{
display: flex;
flex-direction: column;
justify-content: space-between;
transition: 100ms;
transition: var(--transition-fast);
}
.card .name{
all: unset;
@@ -257,7 +305,7 @@ input::placeholder{
padding-inline: 1em;
line-height: 1.5em;
margin-bottom: 0.5em;
transition: 100ms;
transition: var(--transition-fast);
box-shadow: 0.2em 0.2em 0.2em 0.05em rgba(0, 0, 0, 0.3);
}
.card .tag:hover,
@@ -281,8 +329,8 @@ input::placeholder{
width: 10rem;
background-color: var(--nord0);
padding-inline: 1em;
border-radius: 1000px;
transition: 100ms;
border-radius: var(--radius-pill);
transition: var(--transition-fast);
}
.card .title .category:hover,
@@ -363,10 +411,10 @@ input::placeholder{
<label class=img_label for=img_picker>
<svg class="upload over_img" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path d="M288 109.3V352c0 17.7-14.3 32-32 32s-32-14.3-32-32V109.3l-73.4 73.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l128-128c12.5-12.5 32.8-12.5 45.3 0l128 128c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L288 109.3zM64 352H192c0 35.3 28.7 64 64 64s64-28.7 64-64H448c35.3 0 64 28.7 64 64v32c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V416c0-35.3 28.7-64 64-64zM432 456a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg>
<svg class="upload over_img" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path d="M288 109.3V352c0 17.7-14.3 32-32 32s-32-14.3-32-32V109.3l-73.4 73.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l128-128c12.5-12.5 32.8-12.5 45.3 0l128 128c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L288 109.3zM64 352H192c0 35.3 28.7 64 64 64s64-28.7 64-64H448c35.3 0 64 28.7 64 64v32c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V416c0-35.3 28.7-64 64-64zM432 456a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg>
</label>
</div>
<input type="file" id=img_picker accept="image/webp image/jpeg" onchange={show_local_image}>
<input type="file" id="img_picker" accept="image/webp,image/jpeg,image/jpg,image/png" onchange={handleFileSelect} bind:this={fileInput}>
<div class=title>
<input class=category placeholder=Kategorie... bind:value={card_data.category}/>
<div>
@@ -374,7 +422,7 @@ input::placeholder{
<p contenteditable class=description placeholder=Kurzbeschreibung... bind:innerText={card_data.description}></p>
</div>
<div class=tags>
{#each card_data.tags as tag}
{#each card_data.tags as tag (tag)}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div class="tag" role="button" tabindex="0" onkeydown={(event) => remove_on_enter(event, tag)} onclick={() => remove_from_tags(tag)} aria-label="Tag {tag} entfernen">{tag}</div>
{/each}

View File

@@ -1,18 +1,25 @@
<script>
import "$lib/css/nordtheme.css";
import TagChip from './TagChip.svelte';
import TagChip from '$lib/components/recipes/TagChip.svelte';
let {
categories = [],
selected = null,
onChange = () => {},
lang = 'de'
lang = 'de',
useAndLogic = true
} = $props();
const isEnglish = $derived(lang === 'en');
const label = $derived(isEnglish ? 'Category' : 'Kategorie');
const selectLabel = $derived(isEnglish ? 'Select category...' : 'Kategorie auswählen...');
// Convert selected to array for OR mode, keep as single value for AND mode
const selectedArray = $derived(
useAndLogic
? (selected ? [selected] : [])
: (Array.isArray(selected) ? selected : (selected ? [selected] : []))
);
let inputValue = $state('');
let dropdownOpen = $state(false);
@@ -37,9 +44,22 @@
}
function handleCategorySelect(category) {
onChange(category);
inputValue = '';
dropdownOpen = false;
if (useAndLogic) {
// AND mode: single select
onChange(category);
inputValue = '';
dropdownOpen = false;
} else {
// OR mode: multi-select toggle
const currentSelected = Array.isArray(selected) ? selected : (selected ? [selected] : []);
if (currentSelected.includes(category)) {
const newSelected = currentSelected.filter(c => c !== category);
onChange(newSelected.length > 0 ? newSelected : null);
} else {
onChange([...currentSelected, category]);
}
inputValue = '';
}
}
function handleKeyDown(event) {
@@ -49,8 +69,7 @@
const matchedCat = categories.find(c => c.toLowerCase() === value.toLowerCase())
|| filteredCategories[0];
if (matchedCat) {
onChange(matchedCat);
inputValue = '';
handleCategorySelect(matchedCat);
}
} else if (event.key === 'Escape') {
dropdownOpen = false;
@@ -58,8 +77,14 @@
}
}
function handleRemove() {
onChange(null);
function handleRemove(category) {
if (useAndLogic) {
onChange(null);
} else {
const currentSelected = Array.isArray(selected) ? selected : (selected ? [selected] : []);
const newSelected = currentSelected.filter(c => c !== category);
onChange(newSelected.length > 0 ? newSelected : null);
}
}
</script>
@@ -109,7 +134,6 @@
input {
all: unset;
box-sizing: border-box;
font-family: sans-serif;
background: var(--nord0);
color: var(--nord6);
padding: 0.5rem 0.7rem;
@@ -188,7 +212,7 @@
{#each filteredCategories as category}
<TagChip
tag={category}
selected={false}
selected={selectedArray.includes(category)}
removable={false}
onToggle={() => handleCategorySelect(category)}
/>
@@ -197,15 +221,17 @@
{/if}
</div>
<!-- Selected category display below -->
{#if selected}
<!-- Selected categories display below -->
{#if selectedArray.length > 0}
<div class="selected-category">
<TagChip
tag={selected}
selected={true}
removable={true}
onToggle={handleRemove}
/>
{#each selectedArray as category}
<TagChip
tag={category}
selected={true}
removable={true}
onToggle={() => handleRemove(category)}
/>
{/each}
</div>
{/if}
</div>

View File

@@ -0,0 +1,194 @@
<script lang="ts">
import "$lib/css/shake.css";
let {
recipe,
current_month = 0,
icon_override = false,
isFavorite = false,
showFavoriteIndicator = false,
loading_strat = "lazy",
routePrefix = '/rezepte'
} = $props();
const img_name = $derived(
recipe.images?.[0]?.mediapath ||
`${recipe.germanShortName || recipe.short_name}.webp`
);
const img_alt = $derived(
recipe.images?.[0]?.alt || recipe.name
);
const img_color = $derived(recipe.images?.[0]?.color || '');
const isInSeason = $derived(icon_override || recipe.season?.includes(current_month));
function activateTransitions(event) {
const img = event.currentTarget.querySelector('.img-wrap img');
if (img) img.style.viewTransitionName = `recipe-${recipe.short_name}-img`;
}
</script>
<style>
.compact-card {
position: relative;
display: flex;
flex-direction: column;
border-radius: var(--radius-card);
overflow: hidden;
background: var(--color-surface);
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
cursor: pointer;
}
.compact-card:hover,
.compact-card:focus-within {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.compact-card:hover .img-wrap img {
transform: scale(1.05);
}
.compact-card:hover .icon,
.compact-card:focus-within .icon {
animation: shake 0.6s;
}
.card-link {
position: absolute;
inset: 0;
z-index: 1;
}
.img-wrap {
aspect-ratio: 3 / 2;
overflow: hidden;
}
.img-wrap img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s ease;
border-radius: var(--radius-card) var(--radius-card) 0 0;
}
.info {
position: relative;
padding: 0.5em 0.6em 0.5em;
flex: 1;
}
.name {
font-size: 0.85rem;
font-weight: 600;
line-height: 1.3;
margin: 0;
}
@media (min-width: 600px) {
.info {
padding: 0.8em 0.9em 0.7em;
}
.name {
font-size: 1.1rem;
}
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.3em;
margin-top: 0.5em;
position: relative;
z-index: 2;
}
.tag {
font-size: 0.7rem;
padding: 0.1rem 0.4rem;
border-radius: var(--radius-pill);
background-color: var(--nord5);
color: var(--nord3);
text-decoration: none;
cursor: pointer;
transition: transform var(--transition-fast), background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast);
box-shadow: var(--shadow-sm);
border: none;
display: inline-block;
}
.tag:hover,
.tag:focus-visible {
transform: scale(1.05);
background-color: var(--nord8);
box-shadow: var(--shadow-hover);
color: var(--nord0);
}
@media (min-width: 600px) {
.tag {
font-size: 0.9rem;
padding: 0.15rem 0.55rem;
}
}
@media (prefers-color-scheme: dark) {
.tag,
.tag:visited,
.tag:link {
background-color: var(--nord0);
color: var(--nord4);
}
.tag:hover,
.tag:focus-visible {
background-color: var(--nord8);
color: var(--nord0);
}
}
.icon {
position: absolute;
top: -1.2em;
right: 0.6em;
width: 2em;
height: 2em;
font-size: 1rem;
background-color: var(--nord0);
color: white;
z-index: 3;
}
@media (min-width: 600px) {
.icon {
font-size: 1.2rem;
}
}
.favorite {
position: absolute;
top: 0.5em;
left: 0.5em;
font-size: 1.1rem;
filter: drop-shadow(0 0 3px rgba(0,0,0,0.8));
z-index: 2;
pointer-events: none;
}
</style>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="compact-card" onclick={activateTransitions}>
<a href="{routePrefix}/{recipe.short_name}" class="card-link" aria-label={recipe.name}></a>
{#if showFavoriteIndicator && isFavorite}
<span class="favorite">❤️</span>
{/if}
<div class="img-wrap" style:background-color={img_color}>
<img
src="https://bocken.org/static/rezepte/thumb/{img_name}"
alt={img_alt}
loading={loading_strat}
data-recipe={recipe.short_name}
/>
</div>
<div class="info">
{#if isInSeason}
<a href="{routePrefix}/icon/{recipe.icon}" class="icon g-icon-badge">{recipe.icon}</a>
{/if}
<p class="name">{@html recipe.name}</p>
{#if recipe.tags?.length}
<div class="tags">
{#each recipe.tags as tag (tag)}
<a href="{routePrefix}/tag/{tag}" class="tag">{tag}</a>
{/each}
</div>
{/if}
</div>
</div>

View File

@@ -8,9 +8,9 @@ import Check from '$lib/assets/icons/Check.svelte'
import "$lib/css/action_button.css"
import { do_on_key } from '$lib/components/do_on_key.js'
import { do_on_key } from '$lib/components/recipes/do_on_key.js'
import { portions } from '$lib/js/portions_store.js'
import BaseRecipeSelector from '$lib/components/BaseRecipeSelector.svelte'
import BaseRecipeSelector from '$lib/components/recipes/BaseRecipeSelector.svelte'
let portions_local = $state()
portions.subscribe((p) => {
@@ -128,6 +128,7 @@ function handleSelect(recipe: any, options: any) {
includeIngredients: options.includeIngredients,
showLabel: options.showLabel,
labelOverride: options.labelOverride || '',
baseMultiplier: options.baseMultiplier || 1,
itemsBefore: [],
itemsAfter: []
};
@@ -397,11 +398,11 @@ input.heading{
padding-inline: 2rem;
font-size: 1.5rem;
width: 100%;
border-radius: 1000px;
border-radius: var(--radius-pill);
color: white;
justify-content: center;
align-items: center;
transition: 200ms;
transition: var(--transition-normal);
}
input.heading:hover{
background-color: var(--nord1);
@@ -411,7 +412,7 @@ input.heading:hover{
position: relative;
width: 300px;
margin-inline: auto;
transition: 200ms;
transition: var(--transition-normal);
}
.heading_wrapper:hover
{
@@ -429,8 +430,8 @@ input.heading:hover{
position: relative;
margin-block: 3rem;
width: 90%;
border-radius: 20px;
transition: 200ms;
border-radius: var(--radius-card);
transition: var(--transition-normal);
}
.shadow{
box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3);
@@ -449,14 +450,13 @@ input.heading:hover{
--font_size: 1.5rem;
top: -1em;
left: -1em;
font-family: sans-serif;
font-size: 1.5rem;
background-color: var(--nord0);
color: var(--nord4);
border-radius: 1000000px;
width: 23ch;
padding: 0.5em 1em;
transition: 100ms;
transition: var(--transition-fast);
box-shadow: 0.5em 0.5em 1em 0.4em rgba(0,0,0,0.3);
}
.category:hover{
@@ -470,7 +470,6 @@ input.heading:hover{
}
.add_ingredient{
font-family: sans-serif;
width: 100%;
display: flex;
flex-direction: row;
@@ -480,18 +479,18 @@ input.heading:hover{
font-size: 1.2rem;
padding: 2rem;
padding-top: 2.5rem;
border-radius: 20px;
border-radius: var(--radius-card);
background-color: var(--blue);
color: #bbb;
transition: 200ms;
transition: var(--transition-normal);
gap: 0.5rem;
}
.add_ingredient input{
border: 2px solid var(--nord4);
color: var(--nord4);
border-radius: 1000px;
border-radius: var(--radius-pill);
padding: 0.5em 1em;
transition: 100ms;
transition: var(--transition-fast);
}
.add_ingredient input:hover,
.add_ingredient input:focus-visible
@@ -536,7 +535,6 @@ dialog .adder{
}
dialog h2{
font-size: 3rem;
font-family: sans-serif;
color: white;
text-align: center;
margin-top: 30vh;
@@ -568,7 +566,7 @@ dialog h2{
border: none;
padding: 0;
margin: 0;
transition: 200ms;
transition: var(--transition-normal);
}
.move_buttons_container button:hover{
scale: 1.4;
@@ -610,10 +608,10 @@ h3{
}
.list_wrapper p[contenteditable]{
border: 2px solid grey;
border-radius: 1000px;
border-radius: var(--radius-pill);
padding: 0.25em 1em;
background-color: white;
transition: 200ms;
transition: var(--transition-normal);
}
.list_wrapper p[contenteditable]:hover,
.list_wrapper p[contenteditable]:focus-within{
@@ -702,12 +700,12 @@ h3{
margin-block: 1rem;
padding: 1em 2em;
font-size: 1.1rem;
border-radius: 1000px;
border-radius: var(--radius-pill);
background-color: var(--nord9);
color: white;
border: none;
cursor: pointer;
transition: 200ms;
transition: var(--transition-normal);
box-shadow: 0 0 0.5em 0.1em rgba(0,0,0,0.2);
}
@@ -737,18 +735,30 @@ h3{
<div class="reference-container">
<div class="reference-header">
<div class="move_buttons_container">
<button onclick={() => update_list_position(list_index, 1)} aria-label={t[lang].moveReferenceUpAria}>
<button type="button" onclick={() => update_list_position(list_index, 1)} aria-label={t[lang].moveReferenceUpAria}>
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button onclick={() => update_list_position(list_index, -1)} aria-label={t[lang].moveReferenceDownAria}>
<button type="button" onclick={() => update_list_position(list_index, -1)} aria-label={t[lang].moveReferenceDownAria}>
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</button>
</div>
<div class="reference-badge">
📋 {t[lang].baseRecipe}: {list.name || t[lang].unnamed}
<div style="margin-top: 0.5em;">
<label style="font-size: 0.9em; display: flex; align-items: center; gap: 0.5em;">
{t[lang].baseMultiplier || 'Mengenfaktor'}:
<input
type="number"
bind:value={list.baseMultiplier}
min="0.1"
step="0.1"
style="width: 5em; padding: 0.25em 0.5em; border-radius: 5px; border: 1px solid var(--nord4);"
/>
</label>
</div>
</div>
<div class="mod_icons">
<button class="action_button button_subtle" onclick={() => removeReference(list_index)} aria-label={t[lang].removeReferenceAria}>
<button type="button" class="action_button button_subtle" onclick={() => removeReference(list_index)} aria-label={t[lang].removeReferenceAria}>
<Cross fill="var(--nord11)"></Cross>
</button>
</div>
@@ -762,24 +772,24 @@ h3{
<div class=move_buttons_container>
<!-- Empty for consistency -->
</div>
<button onclick={() => editItemFromReference(list_index, 'before', item_index)} class="ingredient-amount-button">
<button type="button" onclick={() => editItemFromReference(list_index, 'before', item_index)} class="ingredient-amount-button">
{item.amount} {item.unit}
</button>
<button class="force_wrap ingredient-name-button" onclick={() => editItemFromReference(list_index, 'before', item_index)}>
<button type="button" class="force_wrap ingredient-name-button" onclick={() => editItemFromReference(list_index, 'before', item_index)}>
{@html item.name}
</button>
<div class="mod_icons">
<button class="action_button button_subtle" onclick={() => editItemFromReference(list_index, 'before', item_index)} aria-label={t[lang].editIngredientAria}>
<button type="button" class="action_button button_subtle" onclick={() => editItemFromReference(list_index, 'before', item_index)} aria-label={t[lang].editIngredientAria}>
<Pen fill="var(--nord6)" height="1em" width="1em"></Pen>
</button>
<button class="action_button button_subtle" onclick={() => removeItemFromReference(list_index, 'before', item_index)} aria-label={t[lang].removeIngredientAria}>
<button type="button" class="action_button button_subtle" onclick={() => removeItemFromReference(list_index, 'before', item_index)} aria-label={t[lang].removeIngredientAria}>
<Cross fill="var(--nord6)" height="1em" width="1em"></Cross>
</button>
</div>
{/each}
</div>
{/if}
<button class="action_button button_subtle add-to-reference-button" onclick={() => openAddToReferenceModal(list_index, 'before')}>
<button type="button" class="action_button button_subtle add-to-reference-button" onclick={() => openAddToReferenceModal(list_index, 'before')}>
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> {t[lang].addIngredientBefore}
</button>
@@ -789,7 +799,7 @@ h3{
</div>
<!-- Items after base recipe -->
<button class="action_button button_subtle add-to-reference-button" onclick={() => openAddToReferenceModal(list_index, 'after')}>
<button type="button" class="action_button button_subtle add-to-reference-button" onclick={() => openAddToReferenceModal(list_index, 'after')}>
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> {t[lang].addIngredientAfter}
</button>
{#if list.itemsAfter && list.itemsAfter.length > 0}
@@ -799,17 +809,17 @@ h3{
<div class=move_buttons_container>
<!-- Empty for consistency -->
</div>
<button onclick={() => editItemFromReference(list_index, 'after', item_index)} class="ingredient-amount-button">
<button type="button" onclick={() => editItemFromReference(list_index, 'after', item_index)} class="ingredient-amount-button">
{item.amount} {item.unit}
</button>
<button class="force_wrap ingredient-name-button" onclick={() => editItemFromReference(list_index, 'after', item_index)}>
<button type="button" class="force_wrap ingredient-name-button" onclick={() => editItemFromReference(list_index, 'after', item_index)}>
{@html item.name}
</button>
<div class="mod_icons">
<button class="action_button button_subtle" onclick={() => editItemFromReference(list_index, 'after', item_index)} aria-label={t[lang].editIngredientAria}>
<button type="button" class="action_button button_subtle" onclick={() => editItemFromReference(list_index, 'after', item_index)} aria-label={t[lang].editIngredientAria}>
<Pen fill="var(--nord6)" height="1em" width="1em"></Pen>
</button>
<button class="action_button button_subtle" onclick={() => removeItemFromReference(list_index, 'after', item_index)} aria-label={t[lang].removeIngredientAria}>
<button type="button" class="action_button button_subtle" onclick={() => removeItemFromReference(list_index, 'after', item_index)} aria-label={t[lang].removeIngredientAria}>
<Cross fill="var(--nord6)" height="1em" width="1em"></Cross>
</button>
</div>
@@ -821,15 +831,15 @@ h3{
<!-- svelte-ignore a11y_click_events_have_key_events -->
<h3>
<div class=move_buttons_container>
<button onclick="{() => update_list_position(list_index, 1)}" aria-label="Liste nach oben verschieben">
<button type="button" onclick="{() => update_list_position(list_index, 1)}" aria-label="Liste nach oben verschieben">
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button onclick="{() => update_list_position(list_index, -1)}" aria-label="Liste nach unten verschieben">
<button type="button" onclick="{() => update_list_position(list_index, -1)}" aria-label="Liste nach unten verschieben">
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</button>
</div>
<button onclick="{() => show_modal_edit_subheading_ingredient(list_index)}" class="subheading-button">
<button type="button" onclick="{() => show_modal_edit_subheading_ingredient(list_index)}" class="subheading-button">
{#if list.name }
{list.name}
{:else}
@@ -837,38 +847,38 @@ h3{
{/if}
</button>
<div class=mod_icons>
<button class="action_button button_subtle" onclick="{() => show_modal_edit_subheading_ingredient(list_index)}" aria-label={t[lang].editHeading}>
<button type="button" class="action_button button_subtle" onclick="{() => show_modal_edit_subheading_ingredient(list_index)}" aria-label={t[lang].editHeading}>
<Pen fill=var(--nord1)></Pen> </button>
<button class="action_button button_subtle" onclick="{() => remove_list(list_index)}" aria-label={t[lang].removeList}>
<button type="button" class="action_button button_subtle" onclick="{() => remove_list(list_index)}" aria-label={t[lang].removeList}>
<Cross fill=var(--nord1)></Cross></button>
</div>
</h3>
<div class=ingredients_grid>
{#each list.list as ingredient, ingredient_index (ingredient_index)}
<div class=move_buttons_container>
<button onclick="{() => update_ingredient_position(list_index, ingredient_index, 1)}" aria-label={t[lang].moveUpAria}>
<button type="button" onclick="{() => update_ingredient_position(list_index, ingredient_index, 1)}" aria-label={t[lang].moveUpAria}>
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button onclick="{() => update_ingredient_position(list_index, ingredient_index, -1)}" aria-label={t[lang].moveDownAria}>
<button type="button" onclick="{() => update_ingredient_position(list_index, ingredient_index, -1)}" aria-label={t[lang].moveDownAria}>
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</button>
</div>
<button onclick={() => show_modal_edit_ingredient(list_index, ingredient_index)} class="ingredient-amount-button">
<button type="button" onclick={() => show_modal_edit_ingredient(list_index, ingredient_index)} class="ingredient-amount-button">
{ingredient.amount} {ingredient.unit}
</button>
<button class="force_wrap ingredient-name-button" onclick={() => show_modal_edit_ingredient(list_index, ingredient_index)}>
<button type="button" class="force_wrap ingredient-name-button" onclick={() => show_modal_edit_ingredient(list_index, ingredient_index)}>
{@html ingredient.name}
</button>
<div class=mod_icons><button class="action_button button_subtle" onclick={() => show_modal_edit_ingredient(list_index, ingredient_index)} aria-label={t[lang].editIngredientAria}>
<div class=mod_icons><button type="button" class="action_button button_subtle" onclick={() => show_modal_edit_ingredient(list_index, ingredient_index)} aria-label={t[lang].editIngredientAria}>
<Pen fill=var(--nord1) height=1em width=1em></Pen></button>
<button class="action_button button_subtle" onclick="{() => remove_ingredient(list_index, ingredient_index)}" aria-label={t[lang].removeIngredientAria}><Cross fill=var(--nord1) height=1em width=1em></Cross></button></div>
<button type="button" class="action_button button_subtle" onclick="{() => remove_ingredient(list_index, ingredient_index)}" aria-label={t[lang].removeIngredientAria}><Cross fill=var(--nord1) height=1em width=1em></Cross></button></div>
{/each}
</div>
{/if}
{/each}
<!-- Button to insert base recipe -->
<button class="insert-base-recipe-button" onclick={() => openSelector(ingredients.length)}>
<button type="button" class="insert-base-recipe-button" onclick={() => openSelector(ingredients.length)}>
<Plus fill="white" style="display: inline; width: 1.5em; height: 1.5em; vertical-align: middle;"></Plus>
{t[lang].insertBaseRecipe}
</button>
@@ -880,7 +890,7 @@ h3{
<input type="text" placeholder="250..." bind:value={new_ingredient.amount} onkeydown={(event) => do_on_key(event, 'Enter', false, add_new_ingredient)}>
<input type="text" placeholder="mL..." bind:value={new_ingredient.unit} onkeydown={(event) => do_on_key(event, 'Enter', false, add_new_ingredient)}>
<input type="text" placeholder="Milch..." bind:value={new_ingredient.name} onkeydown={(event) => do_on_key(event, 'Enter', false, add_new_ingredient)}>
<button onclick={() => add_new_ingredient()} class=action_button>
<button type="button" onclick={() => add_new_ingredient()} class=action_button>
<Plus fill=white style="width: 2rem; height: 2rem;"></Plus>
</button>
</div>
@@ -893,7 +903,7 @@ h3{
<input type="text" placeholder="250..." bind:value={edit_ingredient.amount} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<input type="text" placeholder="mL..." bind:value={edit_ingredient.unit} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<input type="text" placeholder="Milch..." bind:value={edit_ingredient.name} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<button class=action_button onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)} onclick={edit_ingredient_and_close_modal}>
<button type="button" class=action_button onkeydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)} onclick={edit_ingredient_and_close_modal}>
<Check fill=white style="width: 2rem; height: 2rem;"></Check>
</button>
</div>
@@ -904,7 +914,7 @@ h3{
<h2>{t[lang].renameCategory}</h2>
<div class=heading_wrapper>
<input class=heading type="text" bind:value={edit_heading.name} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} >
<button class=action_button onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} onclick={edit_subheading_and_close_modal}>
<button type="button" class=action_button onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} onclick={edit_subheading_and_close_modal}>
<Check fill=white style="width:2rem; height:2rem;"></Check>
</button>
</div>

View File

@@ -5,11 +5,10 @@ import Cross from '$lib/assets/icons/Cross.svelte'
import Plus from '$lib/assets/icons/Plus.svelte'
import Check from '$lib/assets/icons/Check.svelte'
import '$lib/css/nordtheme.css'
import "$lib/css/action_button.css"
import { do_on_key } from '$lib/components/do_on_key.js'
import BaseRecipeSelector from '$lib/components/BaseRecipeSelector.svelte'
import { do_on_key } from '$lib/components/recipes/do_on_key.js'
import BaseRecipeSelector from '$lib/components/recipes/BaseRecipeSelector.svelte'
let { lang = 'de' as 'de' | 'en', instructions = $bindable(), add_info = $bindable() } = $props<{ lang?: 'de' | 'en', instructions: any, add_info: any }>();
@@ -125,6 +124,7 @@ function handleSelect(recipe: any, options: any) {
includeInstructions: options.includeInstructions,
showLabel: options.showLabel,
labelOverride: options.labelOverride || '',
baseMultiplier: options.baseMultiplier || 1,
stepsBefore: [],
stepsAfter: []
};
@@ -246,7 +246,8 @@ export function add_new_step(){
instructions[list_index].steps.push(new_step.step)
}
const el = document.querySelector("#step")
el.innerHTML = step_placeholder
el.innerHTML = ""
new_step.step = ""
instructions = instructions //tells svelte to update dom
}
@@ -400,7 +401,7 @@ export function update_step_position(list_index, step_index, direction){
border: none;
padding: 0;
margin: 0;
transition: 200ms;
transition: var(--transition-normal);
}
.move_buttons_container button:hover{
scale: 1.4;
@@ -439,11 +440,11 @@ input.heading{
padding-inline: 2rem;
font-size: 1.5rem;
width: 100%;
border-radius: 1000px;
border-radius: var(--radius-pill);
color: white;
justify-content: center;
align-items: center;
transition: 200ms;
transition: var(--transition-normal);
}
input.heading:hover,
input.heading:focus-visible
@@ -455,7 +456,7 @@ input.heading:focus-visible
position: relative;
width: min(300px, 95dvw);
margin-inline: auto;
transition: 200ms;
transition: var(--transition-normal);
}
.heading_wrapper:hover,
.heading_wrapper:focus-visible
@@ -473,8 +474,8 @@ input.heading:focus-visible
position: relative;
margin-block: 3rem;
width: 90%;
border-radius: 20px;
transition: 200ms;
border-radius: var(--radius-card);
transition: var(--transition-normal);
background-color: var(--blue);
padding: 1.5rem 2rem;
}
@@ -495,14 +496,13 @@ dialog .adder{
--font_size: 1.5rem;
top: -1em;
left: -1em;
font-family: sans-serif;
font-size: 1.5rem;
background-color: var(--nord0);
color: var(--nord4);
border-radius: 1000000px;
width: 23ch;
padding: 0.5em 1em;
transition: 100ms;
transition: var(--transition-fast);
box-shadow: 0.5em 0.5em 1em 0.4em rgba(0,0,0,0.3);
}
.category:hover,
@@ -518,15 +518,14 @@ dialog .adder{
}
.add_step p{
font-family: sans-serif;
width: 100%;
font-size: 1.2rem;
border-radius: 20px;
border-radius: var(--radius-card);
border: 2px solid var(--nord4);
border-radius: 30px;
padding: 0.5em 1em;
color: var(--nord4);
transition: 100ms;
transition: var(--transition-fast);
}
.add_step p:hover,
.add_step p:focus-visible
@@ -542,14 +541,13 @@ dialog{
background-color: rgba(255,255,255, 0.001);
border: unset;
margin: 0;
transition: 200ms;
transition: var(--transition-normal);
}
dialog .adder{
margin-top: 5rem;
}
dialog h2{
font-size: 3rem;
font-family: sans-serif;
color: white;
text-align: center;
margin-top: 30vh;
@@ -646,10 +644,10 @@ h3{
display: inline;
padding: 0.25em 1em;
border: 2px solid grey;
border-radius: 1000px;
border-radius: var(--radius-pill);
}
.additional_info div:has(p[contenteditable]){
transition: 200ms;
transition: var(--transition-normal);
display: inline;
}
.additional_info div:has(p[contenteditable]):hover,
@@ -729,12 +727,12 @@ h3{
margin-block: 1rem;
padding: 1em 2em;
font-size: 1.1rem;
border-radius: 1000px;
border-radius: var(--radius-pill);
background-color: var(--nord9);
color: white;
border: none;
cursor: pointer;
transition: 200ms;
transition: var(--transition-normal);
box-shadow: 0 0 0.5em 0.1em rgba(0,0,0,0.2);
}
@@ -788,18 +786,30 @@ h3{
<div class="reference-container">
<div class="reference-header">
<div class="move_buttons_container">
<button onclick={() => update_list_position(list_index, 1)} aria-label={t[lang].moveReferenceUpAria}>
<button type="button" onclick={() => update_list_position(list_index, 1)} aria-label={t[lang].moveReferenceUpAria}>
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button onclick={() => update_list_position(list_index, -1)} aria-label={t[lang].moveReferenceDownAria}>
<button type="button" onclick={() => update_list_position(list_index, -1)} aria-label={t[lang].moveReferenceDownAria}>
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</button>
</div>
<div class="reference-badge">
📋 {t[lang].baseRecipe}: {list.name || t[lang].unnamed}
<div style="margin-top: 0.5em;">
<label style="font-size: 0.9em; display: flex; align-items: center; gap: 0.5em;">
{t[lang].baseMultiplier || 'Mengenfaktor'}:
<input
type="number"
bind:value={list.baseMultiplier}
min="0.1"
step="0.1"
style="width: 5em; padding: 0.25em 0.5em; border-radius: 5px; border: 1px solid var(--nord4);"
/>
</label>
</div>
</div>
<div class="mod_icons">
<button class="action_button button_subtle" onclick={() => removeReference(list_index)} aria-label={t[lang].removeReferenceAria}>
<button type="button" class="action_button button_subtle" onclick={() => removeReference(list_index)} aria-label={t[lang].removeReferenceAria}>
<Cross fill="var(--nord11)"></Cross>
</button>
</div>
@@ -815,14 +825,14 @@ h3{
<div class="move_buttons_container step_move_buttons">
<!-- Empty for consistency -->
</div>
<button onclick={() => editStepFromReference(list_index, 'before', step_index)} class="step-button" style="flex-grow: 1;">
<button type="button" onclick={() => editStepFromReference(list_index, 'before', step_index)} class="step-button" style="flex-grow: 1;">
{@html step}
</button>
<div>
<button class="action_button button_subtle" onclick={() => editStepFromReference(list_index, 'before', step_index)} aria-label={t[lang].editStepAria}>
<button type="button" class="action_button button_subtle" onclick={() => editStepFromReference(list_index, 'before', step_index)} aria-label={t[lang].editStepAria}>
<Pen fill="var(--nord6)" height="1em" width="1em"></Pen>
</button>
<button class="action_button button_subtle" onclick={() => removeStepFromReference(list_index, 'before', step_index)} aria-label={t[lang].removeStepAria}>
<button type="button" class="action_button button_subtle" onclick={() => removeStepFromReference(list_index, 'before', step_index)} aria-label={t[lang].removeStepAria}>
<Cross fill="var(--nord6)" height="1em" width="1em"></Cross>
</button>
</div>
@@ -831,7 +841,7 @@ h3{
{/each}
</ol>
{/if}
<button class="action_button button_subtle add-to-reference-button" onclick={() => openAddToReferenceModal(list_index, 'before')}>
<button type="button" class="action_button button_subtle add-to-reference-button" onclick={() => openAddToReferenceModal(list_index, 'before')}>
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> {t[lang].addStepBefore}
</button>
@@ -841,7 +851,7 @@ h3{
</div>
<!-- Steps after base recipe -->
<button class="action_button button_subtle add-to-reference-button" onclick={() => openAddToReferenceModal(list_index, 'after')}>
<button type="button" class="action_button button_subtle add-to-reference-button" onclick={() => openAddToReferenceModal(list_index, 'after')}>
<Plus fill="var(--nord9)" height="1em" width="1em"></Plus> {t[lang].addStepAfter}
</button>
{#if list.stepsAfter && list.stepsAfter.length > 0}
@@ -853,14 +863,14 @@ h3{
<div class="move_buttons_container step_move_buttons">
<!-- Empty for consistency -->
</div>
<button onclick={() => editStepFromReference(list_index, 'after', step_index)} class="step-button" style="flex-grow: 1;">
<button type="button" onclick={() => editStepFromReference(list_index, 'after', step_index)} class="step-button" style="flex-grow: 1;">
{@html step}
</button>
<div>
<button class="action_button button_subtle" onclick={() => editStepFromReference(list_index, 'after', step_index)} aria-label={t[lang].editStepAria}>
<button type="button" class="action_button button_subtle" onclick={() => editStepFromReference(list_index, 'after', step_index)} aria-label={t[lang].editStepAria}>
<Pen fill="var(--nord6)" height="1em" width="1em"></Pen>
</button>
<button class="action_button button_subtle" onclick={() => removeStepFromReference(list_index, 'after', step_index)} aria-label={t[lang].removeStepAria}>
<button type="button" class="action_button button_subtle" onclick={() => removeStepFromReference(list_index, 'after', step_index)} aria-label={t[lang].removeStepAria}>
<Cross fill="var(--nord6)" height="1em" width="1em"></Cross>
</button>
</div>
@@ -874,23 +884,23 @@ h3{
<!-- svelte-ignore a11y_click_events_have_key_events -->
<h3>
<div class=move_buttons_container>
<button onclick="{() => update_list_position(list_index, 1)}" aria-label={t[lang].moveListUpAria}>
<button type="button" onclick="{() => update_list_position(list_index, 1)}" aria-label={t[lang].moveListUpAria}>
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button onclick="{() => update_list_position(list_index, -1)}" aria-label={t[lang].moveListDownAria}>
<button type="button" onclick="{() => update_list_position(list_index, -1)}" aria-label={t[lang].moveListDownAria}>
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</button>
</div>
<button onclick={() => show_modal_edit_subheading_step(list_index)} class="subheading-button">
<button type="button" onclick={() => show_modal_edit_subheading_step(list_index)} class="subheading-button">
{#if list.name}
{list.name}
{:else}
{t[lang].empty}
{/if}
</button>
<button class="action_button button_subtle" onclick="{() => show_modal_edit_subheading_step(list_index)}" aria-label={t[lang].editHeading}>
<button type="button" class="action_button button_subtle" onclick="{() => show_modal_edit_subheading_step(list_index)}" aria-label={t[lang].editHeading}>
<Pen fill=var(--nord1)></Pen> </button>
<button class="action_button button_subtle" onclick="{() => remove_list(list_index)}" aria-label={t[lang].removeList}>
<button type="button" class="action_button button_subtle" onclick="{() => remove_list(list_index)}" aria-label={t[lang].removeList}>
<Cross fill=var(--nord1)></Cross>
</button>
</h3>
@@ -899,21 +909,21 @@ h3{
<!-- svelte-ignore a11y_click_events_have_key_events -->
<li>
<div class="move_buttons_container step_move_buttons">
<button onclick="{() => update_step_position(list_index, step_index, 1)}" aria-label={t[lang].moveUpAria}>
<button type="button" onclick="{() => update_step_position(list_index, step_index, 1)}" aria-label={t[lang].moveUpAria}>
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button onclick="{() => update_step_position(list_index, step_index, -1)}" aria-label={t[lang].moveDownAria}>
<button type="button" onclick="{() => update_step_position(list_index, step_index, -1)}" aria-label={t[lang].moveDownAria}>
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</button>
</div>
<div>
<button onclick={() => show_modal_edit_step(list_index, step_index)} class="step-button">
<button type="button" onclick={() => show_modal_edit_step(list_index, step_index)} class="step-button">
{@html step}
</button>
<div><button class="action_button button_subtle" onclick={() => show_modal_edit_step(list_index, step_index)} aria-label={t[lang].editStepAria}>
<div><button type="button" class="action_button button_subtle" onclick={() => show_modal_edit_step(list_index, step_index)} aria-label={t[lang].editStepAria}>
<Pen fill=var(--nord1)></Pen>
</button>
<button class="action_button button_subtle" onclick="{() => remove_step(list_index, step_index)}" aria-label={t[lang].removeStepAria}>
<button type="button" class="action_button button_subtle" onclick="{() => remove_step(list_index, step_index)}" aria-label={t[lang].removeStepAria}>
<Cross fill=var(--nord1)></Cross>
</button>
</div></div>
@@ -924,7 +934,7 @@ h3{
{/each}
<!-- Button to insert base recipe -->
<button class="insert-base-recipe-button" onclick={() => openSelector(instructions.length)}>
<button type="button" class="insert-base-recipe-button" onclick={() => openSelector(instructions.length)}>
<Plus fill="white" style="display: inline; width: 1.5em; height: 1.5em; vertical-align: middle;"></Plus>
{t[lang].insertBaseRecipe}
</button>
@@ -934,7 +944,7 @@ h3{
<input class=category type="text" bind:value={new_step.name} placeholder={t[lang].categoryOptional} onkeydown={(event) => do_on_key(event, 'Enter', false , add_new_step)} >
<div class=add_step>
<p id=step contenteditable onfocus='{clear_step}' onblur={add_placeholder} bind:innerText={new_step.step} onkeydown={(event) => do_on_key(event, 'Enter', true , add_new_step)}></p>
<button onclick={() => add_new_step()} class=action_button>
<button type="button" onclick={() => add_new_step()} class=action_button>
<Plus fill=white style="height: 2rem; width: 2rem"></Plus>
</button>
@@ -946,7 +956,7 @@ h3{
<input class=category type="text" bind:value={edit_step.name} placeholder={t[lang].subcategoryOptional} onkeydown={(event) => do_on_key(event, 'Enter', false , edit_step_and_close_modal)}>
<div class=add_step>
<p id=step contenteditable bind:innerText={edit_step.step} onkeydown={(event) => do_on_key(event, 'Enter', true , edit_step_and_close_modal)}></p>
<button class=action_button onclick="{() => edit_step_and_close_modal()}" >
<button type="button" class=action_button onclick="{() => edit_step_and_close_modal()}" >
<Check fill=white style="height: 2rem; width: 2rem"></Check>
</button>
</div>
@@ -957,7 +967,7 @@ h3{
<h2>{t[lang].renameCategory}</h2>
<div class=heading_wrapper>
<input class="heading" type="text" bind:value={edit_heading.name} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_steps_and_close_modal)}>
<button onclick={edit_subheading_steps_and_close_modal} class=action_button>
<button type="button" onclick={edit_subheading_steps_and_close_modal} class=action_button>
<Check fill=white style="height: 2rem; width: 2rem"></Check>
</button>
</div>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
let { note = $bindable("") } = $props<{ note?: string }>();
</script>
<style>
div{
background-color: var(--red);
color: white;
padding: 1em;
font-size: 1.1rem;
max-width: 400px;
margin-inline: auto;
box-shadow: 0.2em 0.2em 0.5em 0.1em rgba(0, 0, 0, 0.3);
margin-bottom: 1em;
}
h3{
margin-block: 0;
}
textarea {
width: 100%;
min-height: 80px;
padding: 0.5em;
border-radius: 5px;
border: none;
color: white;
font-size: 1rem;
resize: vertical;
margin-top: 0.5em;
background-color: transparent;
}
textarea::placeholder {
color: rgba(255, 255, 255, 0.6);
}
</style>
<div>
<h3>Notiz:</h3>
<textarea bind:value={note} placeholder="Füge eine Notiz für dieses Rezept hinzu..."></textarea>
</div>

View File

@@ -1,6 +1,5 @@
<script>
import "$lib/css/nordtheme.css";
import Toggle from './Toggle.svelte';
import Toggle from '$lib/components/Toggle.svelte';
let {
enabled = false,
@@ -14,20 +13,20 @@
let checked = $state(enabled);
// Watch for changes to checked and call onToggle
$effect(() => {
checked = enabled;
// Track checked as dependency
const currentChecked = checked;
// Call onToggle whenever checked changes
onToggle(currentChecked);
});
function handleChange() {
onToggle(checked);
}
</script>
<style>
.filter-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 0.3rem;
max-width: 100%;
align-items: center;
}
@@ -69,6 +68,5 @@
<Toggle
bind:checked={checked}
label=""
onchange={handleChange}
/>
</div>

View File

@@ -1,10 +1,10 @@
<script>
import "$lib/css/nordtheme.css";
import CategoryFilter from './CategoryFilter.svelte';
import TagFilter from './TagFilter.svelte';
import IconFilter from './IconFilter.svelte';
import SeasonFilter from './SeasonFilter.svelte';
import FavoritesFilter from './FavoritesFilter.svelte';
import LogicModeToggle from './LogicModeToggle.svelte';
let {
availableCategories = [],
@@ -15,13 +15,16 @@
selectedIcon = null,
selectedSeasons = [],
selectedFavoritesOnly = false,
useAndLogic = true,
lang = 'de',
isLoggedIn = false,
hideFavoritesFilter = false,
onCategoryChange = () => {},
onTagToggle = () => {},
onIconChange = () => {},
onSeasonChange = () => {},
onFavoritesToggle = () => {}
onFavoritesToggle = () => {},
onLogicModeToggle = () => {}
} = $props();
const isEnglish = $derived(lang === 'en');
@@ -40,7 +43,7 @@
<style>
.filter-wrapper {
width: 900px;
max-width: 95vw;
max-width: 80vw;
margin: 1rem auto 2rem;
}
@@ -84,11 +87,11 @@
}
.filter-panel.with-favorites {
grid-template-columns: 120px 120px 1fr 160px 90px;
grid-template-columns: 110px 120px 120px 1fr 160px 90px;
}
.filter-panel.without-favorites {
grid-template-columns: 120px 120px 1fr 160px;
grid-template-columns: 110px 120px 120px 1fr 160px;
}
@media (max-width: 968px) {
@@ -133,12 +136,19 @@
<span class="arrow" class:open={filtersOpen}>▼</span>
</button>
<div class="filter-panel" class:open={filtersOpen} class:with-favorites={isLoggedIn} class:without-favorites={!isLoggedIn}>
<div class="filter-panel" class:open={filtersOpen} class:with-favorites={isLoggedIn && !hideFavoritesFilter} class:without-favorites={!isLoggedIn || hideFavoritesFilter}>
<LogicModeToggle
{useAndLogic}
onToggle={onLogicModeToggle}
{lang}
/>
<CategoryFilter
categories={availableCategories}
selected={selectedCategory}
onChange={onCategoryChange}
{lang}
{useAndLogic}
/>
<IconFilter
@@ -146,6 +156,7 @@
selected={selectedIcon}
onChange={onIconChange}
{lang}
{useAndLogic}
/>
<TagFilter
@@ -162,7 +173,7 @@
{months}
/>
{#if isLoggedIn}
{#if isLoggedIn && !hideFavoritesFilter}
<FavoritesFilter
enabled={selectedFavoritesOnly}
onToggle={onFavoritesToggle}

Some files were not shown because too many files have changed in this diff Show More