66 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
280 changed files with 43623 additions and 5854 deletions

1
.npmrc
View File

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

View File

@@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"prebuild": "npx vite-node scripts/generate-mystery-verses.ts", "prebuild": "bash scripts/subset-emoji-font.sh && npx vite-node scripts/generate-mystery-verses.ts",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",

View File

@@ -5,48 +5,82 @@
import { writeFileSync } from 'fs'; import { writeFileSync } from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
import { lookupReference } from '../src/lib/server/bible'; import { lookupReference } from '../src/lib/server/bible';
import { mysteryReferences } from '../src/lib/data/mysteryDescriptions'; import { mysteryReferences, mysteryReferencesEnglish, theologicalVirtueReference, theologicalVirtueReferenceEnglish } from '../src/lib/data/mysteryDescriptions';
import type { MysteryDescription, VerseData } from '../src/lib/data/mysteryDescriptions'; import type { MysteryDescription, VerseData } from '../src/lib/data/mysteryDescriptions';
const tsvPath = resolve('static/allioli.tsv'); function generateVerseData(
references: Record<string, readonly { title: string; reference: string }[]>,
tsvPath: string
): Record<string, MysteryDescription[]> {
const result: Record<string, MysteryDescription[]> = {};
const mysteryDescriptions: Record<string, MysteryDescription[]> = {}; for (const [mysteryType, refs] of Object.entries(references)) {
const descriptions: MysteryDescription[] = [];
for (const [mysteryType, references] of Object.entries(mysteryReferences)) { for (const ref of refs) {
const descriptions: MysteryDescription[] = []; const lookup = lookupReference(ref.reference, tsvPath);
for (const ref of references) { let text = '';
const result = lookupReference(ref.reference, tsvPath); let verseData: VerseData | null = null;
let text = ''; if (lookup && lookup.verses.length > 0) {
let verseData: VerseData | null = null; 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}`);
}
if (result && result.verses.length > 0) { descriptions.push({
text = `«${result.verses.map((v) => v.text).join(' ')}»`; title: ref.title,
verseData = { reference: ref.reference,
book: result.book, text,
chapter: result.chapter, verseData
verses: result.verses });
};
} else {
console.warn(`No verses found for: ${ref.reference}`);
} }
descriptions.push({ result[mysteryType] = descriptions;
title: ref.title,
reference: ref.reference,
text,
verseData
});
} }
mysteryDescriptions[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 const output = `// Auto-generated by scripts/generate-mystery-verses.ts — do not edit manually
import type { MysteryDescription } from './mysteryDescriptions'; import type { MysteryDescription } from './mysteryDescriptions';
export const mysteryVerseData: Record<string, MysteryDescription[]> = ${JSON.stringify(mysteryDescriptions, null, '\t')}; 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'); const outPath = resolve('src/lib/data/mysteryVerseData.ts');

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'); 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 COLOR SYSTEM
Based on Nord Theme with semantic naming Based on Nord Theme with semantic naming
@@ -109,6 +118,35 @@
--color-warning: var(--nord13); --color-warning: var(--nord13);
--color-error: var(--nord11); --color-error: var(--nord11);
--color-info: var(--nord10); --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); color: var(--color-link-hover);
} }
/* ============================================ /* ============================================
FORM STYLES GLOBAL UTILITY CLASSES
============================================ */ ============================================ */
form { /* Pill-shaped element base */
background-color: var(--color-bg-secondary); .g-pill {
display: flex; border-radius: var(--radius-pill);
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);
border: none; border: none;
padding: 0.5em 1em;
font-size: 1.3em;
border-radius: 1000px;
margin-top: 1em;
transition: 100ms;
cursor: pointer; cursor: pointer;
display: inline-block;
text-decoration: none;
transition: var(--transition-fast);
} }
form:not(.search) button:hover, /* Interactive hover/focus effects */
form:not(.search) button:focus-visible { .g-interactive {
background-color: var(--color-accent-hover); transition: var(--transition-fast);
scale: 1.1; }
.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 { /* Light background button (with dark mode) */
background-color: var(--color-accent-active); .g-btn-light {
background-color: var(--nord5);
color: var(--nord0);
box-shadow: var(--shadow-sm);
} }
@media (prefers-color-scheme: dark) {
form p { .g-btn-light {
max-width: 400px; background-color: var(--nord0);
margin-top: 0; color: white;
} }
}
form h4 {
margin-bottom: 0; /* Dark background button */
} .g-btn-dark,
.g-btn-dark:visited,
@media screen and (max-width: 600px) { .g-btn-dark:link {
form { background-color: var(--nord0);
margin-top: 0; 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

@@ -63,9 +63,11 @@ async function authorization({ event, resolve }) {
} }
// Bible verse functionality for error pages // 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 { try {
const response = await fetch('/api/glaube/bibel/zufallszitat'); const response = await fetch(endpoint);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@@ -80,11 +82,14 @@ export const handleError: HandleServerError = async ({ error, event, status, mes
console.error('Error occurred:', { error, status, message, url: event.url.pathname }); console.error('Error occurred:', { error, status, message, url: event.url.pathname });
// Add Bible verse to error context // 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 { return {
message: message, message: message,
bibleQuote bibleQuote,
lang: isEnglish ? 'en' : 'de'
}; };
}; };

View File

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

View File

@@ -1,83 +0,0 @@
<script>
let { x = 0, y = 0, size = 40 } = $props();
</script>
<style>
.bg{
fill: white;
}
.fg{
stroke: var(--nord2);
fill: var(--nord2);
stroke-linejoin: round;
stroke-linecap: round;
}
@media (prefers-color-scheme: light) {
.bg{
fill: var(--nord3);
}
.fg{
stroke: white;
fill: white;
}
}
</style>
<svg {x} {y} width={size} height={size} viewBox="0 0 334 326" xmlns="http://www.w3.org/2000/svg">
<path class="bg" 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 class="bg" 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 class="bg" 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 class="bg" 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 class="bg" 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 class="bg" 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 class="bg" 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 class="fg" transform="matrix(.99979 .020664 -.020664 .99979 2.2515 -4.8909)" stroke="var(--nord2)">
<path 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 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 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>
<g class="fg">
<path 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 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 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 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 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 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 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>
<g class="fg">
<path 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 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 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 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 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 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 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>
<g class="fg">
<path 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 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 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 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 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 class="bg" height="33.325" width="33.325" y="146.77" x="151.78"/>
<g class="fg">
<path 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 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 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 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 class="fg">
<path 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"/>
<path 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" />
<path 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"/>
<path 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"/>
<path 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)"/>
<path 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)"/>
<path 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)"/>
<path 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)"/>
<path 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)"/>
<path 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"/>
</g>
</g>
</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,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> <style>
.favorite-button { .favorite-button {
all: unset; all: unset;
font-family: "Noto Color Emoji", "Noto Color Emoji Subset", emoji, sans-serif;
font-size: 1.5rem; font-size: 1.5rem;
cursor: pointer; cursor: pointer;
transition: 100ms; transition: var(--transition-fast);
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5)); filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
position: absolute; position: absolute;
bottom: 0.5em; bottom: 0.5em;

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import "$lib/css/nordtheme.css"
import { onMount } from "svelte"; import { onMount } from "svelte";
import { page } from '$app/stores'; import { page } from '$app/stores';
import Symbol from "./Symbol.svelte" import Symbol from "./Symbol.svelte"
@@ -24,10 +23,9 @@ let underlineWidth = $state(0);
let disableTransition = $state(false); let disableTransition = $state(false);
function toggle_sidebar(state){ function toggle_sidebar(state){
// state: force hidden state (optional) const checkbox = document.getElementById('nav-toggle')
const nav_el = document.querySelector("nav") if(state === undefined) checkbox.checked = !checkbox.checked
if(state === undefined) nav_el.hidden = !nav_el.hidden else checkbox.checked = !state
else nav_el.hidden = state
} }
function updateUnderline() { function updateUnderline() {
@@ -94,16 +92,17 @@ nav{
background-color: var(--nord0); background-color: var(--nord0);
top: 0; top: 0;
z-index: 10; z-index: 10;
display: flex !important; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between !important; justify-content: space-between !important;
align-items: center; align-items: center;
box-shadow: 0 1em 1rem 0rem rgba(0,0,0,0.4); box-shadow: 0 1em 1rem 0rem rgba(0,0,0,0.4);
height: 4rem; height: var(--header-h);
padding-left: 0.5rem; padding-left: 0.5rem;
view-transition-name: site-header;
} }
nav[hidden]{ .nav-toggle{
display:block; display: none;
} }
:global(.site_header li), :global(.site_header li),
@@ -117,22 +116,20 @@ nav[hidden]{
:global(.site_header li>a) :global(.site_header li>a)
{ {
text-decoration: none; text-decoration: none;
font-family: sans-serif; font-size: 1rem;
font-size: 1.2rem;
color: inherit; color: inherit;
border-radius: 1000px; border-radius: var(--radius-pill);
padding: 0.5rem 0.75rem; padding: 0.4rem 0.6rem;
} }
:global(a.entry), :global(a.entry),
:global(a.entry:link), :global(a.entry:link),
:global(a.entry:visited) :global(a.entry:visited)
{ {
text-decoration: none; text-decoration: none;
font-family: sans-serif; font-size: 1rem;
font-size: 1.2rem;
color: white !important; color: white !important;
border-radius: 1000px; border-radius: var(--radius-pill);
padding: 0.5rem 0.75rem; padding: 0.4rem 0.6rem;
} }
:global(.site_header li:hover), :global(.site_header li:hover),
@@ -179,6 +176,9 @@ nav[hidden]{
display: none; display: none;
padding-inline: 0.5rem; padding-inline: 0.5rem;
} }
.header-shadow{
display: none;
}
.right-buttons{ .right-buttons{
display: flex; display: flex;
align-items: center; align-items: center;
@@ -190,9 +190,10 @@ nav[hidden]{
gap: 0.5rem; gap: 0.5rem;
} }
:global(svg.symbol){ :global(svg.symbol){
height: 4rem; --symbol-size: calc(var(--header-h) - 1rem);
width: 4rem; width: var(--symbol-size);
border-radius: 10000px; border-radius: 10000px;
margin: 0.25rem;
} }
/*:global(a:has(svg.symbol)){ /*:global(a:has(svg.symbol)){
padding: 0 !important; padding: 0 !important;
@@ -201,6 +202,8 @@ nav[hidden]{
margin-left: 1rem; margin-left: 1rem;
}*/ }*/
.wrapper{ .wrapper{
--header-h: 3rem;
--symbol-size: calc(var(--header-h) - 1rem);
display:flex; display:flex;
flex-direction: column; flex-direction: column;
min-height: 100svh; min-height: 100svh;
@@ -209,95 +212,111 @@ footer{
padding-block: 1rem; padding-block: 1rem;
text-align: center; text-align: center;
margin-top: auto; margin-top: auto;
position: relative;
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
.button_wrapper{ .button_wrapper{
box-shadow: 0 1em 1rem 0rem rgba(0,0,0,0.4);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
position: sticky; position: sticky;
background-color: var(--nord0); background-color: var(--nord0);
width: 100%; width: 100%;
height: 4rem; height: var(--header-h);
top: 0; top: 0;
z-index: 9999; z-index: 9999;
} }
.nav_button{ .header-shadow{
border: unset;
background-color: unset;
display: block; 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; fill: white;
margin-inline: 0.5rem; margin-inline: 0.5rem;
width: 2rem; width: 1.25rem;
aspect-ratio: 1; height: 1.25rem;
cursor: pointer;
} }
.nav_button svg{ .nav_button svg{
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: 100ms; transition: var(--transition-fast);
} }
.nav_button:focus{ .nav_button:hover,
fill: var(--red); .nav_button:active,
.nav-toggle:focus-visible + .nav_button{
fill: var(--nord8);
scale: 0.9; scale: 0.9;
} }
.nav_site{ .nav_site:not(.no-links){
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
height: 100vh; /* dvh does not work, breaks because of transition and only being applied after scroll ends*/ height: 100vh; /* dvh does not work, breaks because of transition and only being applied after scroll ends*/
margin-bottom: 50vh; margin-bottom: 50vh;
width: min(95svw, 25em); width: min(95svw, 25em);
transition: transform 100ms; z-index: 9998;
z-index: 10;
flex-direction: column; flex-direction: column;
justify-content: flex-start !important;
align-items: left;
justify-content: space-between!important;
padding-inline: 0.5rem; 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% ; width: 100% ;
} }
.nav_site :first-child{ .nav_site:not(.no-links) :first-child{
display:none; display:none;
} }
.nav_site[hidden]{ .nav_site:not(.no-links){
transform: translateX(100%); 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; margin-bottom: 2rem;
} }
.nav_site .links-wrapper { .nav_site:not(.no-links) .links-wrapper {
align-self: flex-start;
width: 100%; width: 100%;
margin: 2rem; padding: 0 2rem;
} }
:global(.site_header){ :global(.site_header){
flex-direction: column; flex-direction: column;
padding-top: min(10rem, 10vh);
align-items: flex-start; align-items: flex-start;
} }
:global(.site_header li, .site_header a){ :global(.site_header li, .site_header a){
font-size: 4rem; font-size: 1.5rem;
} }
:global(.site_header li > a, .site_header a){ :global(.site_header li > a, .site_header a){
font-size: 2rem; font-size: 1.3rem;
} }
:global(.site_header li:hover), :global(.site_header li:hover),
:global(.site_header li:focus-within){ :global(.site_header li:focus-within){
transform: unset; transform: unset;
} }
.nav_site .header-right{ .nav_site:not(.no-links) .header-right{
flex-direction: column; flex-direction: column;
position: absolute; position: absolute;
bottom: 2rem; bottom: 2rem;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
.language-selector-desktop{ .nav_site:not(.no-links) .language-selector-desktop{
display: none; display: none;
} }
.active-underline { .active-underline {
@@ -310,17 +329,44 @@ footer{
text-underline-offset: 0.3rem; 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> </style>
<div class=wrapper lang=de> <div class=wrapper lang=de>
<div> <div>
{#if links}
<div class=button_wrapper> <div class=button_wrapper>
<a href="/" aria-label="Home"><Symbol></Symbol></a> <a href="/" aria-label="Home"><Symbol></Symbol></a>
<div class="right-buttons"> <div class="right-buttons">
{@render language_selector_mobile?.()} {@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>
</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> <a href="/" aria-label="Home"><Symbol></Symbol></a>
<div class="links-wrapper"> <div class="links-wrapper">
{@render links?.()} {@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,14 +5,19 @@
import { languageStore } from '$lib/stores/language'; import { languageStore } from '$lib/stores/language';
import { onMount } from 'svelte'; 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 currentPath = $state('');
let langButton: HTMLButtonElement; let langButton: HTMLButtonElement;
let langOptions: HTMLDivElement; let isOpen = $state(false);
// Faith subroute mappings // Faith subroute mappings
const faithSubroutes: Record<string, Record<string, string>> = { const faithSubroutes: Record<string, Record<string, string>> = {
en: { gebete: 'prayers', rosenkranz: 'rosary', angelus: 'angelus' }, en: { gebete: 'prayers', rosenkranz: 'rosary' },
de: { prayers: 'gebete', rosary: 'rosenkranz', angelus: 'angelus' } de: { prayers: 'gebete', rosary: 'rosenkranz' }
}; };
$effect(() => { $effect(() => {
@@ -34,30 +39,58 @@
}); });
function toggle_language_options(){ function toggle_language_options(){
if (langOptions) { isOpen = !isOpen;
langOptions.hidden = !langOptions.hidden;
}
} }
function convertFaithPath(path: string, targetLang: 'de' | 'en'): string { function convertFaithPath(path: string, targetLang: 'de' | 'en'): string {
// Extract the current base and subroute
const faithMatch = path.match(/^\/(glaube|faith)(\/(.+))?$/); const faithMatch = path.match(/^\/(glaube|faith)(\/(.+))?$/);
if (!faithMatch) return path; if (!faithMatch) return path;
const targetBase = targetLang === 'en' ? 'faith' : 'glaube'; const targetBase = targetLang === 'en' ? 'faith' : 'glaube';
const subroute = faithMatch[3]; // e.g., "gebete", "rosenkranz", "angelus" const rest = faithMatch[3]; // e.g., "gebete", "rosenkranz/sub", "angelus"
if (!subroute) { if (!rest) {
// Main faith page
return `/${targetBase}`; return `/${targetBase}`;
} }
// Convert subroute // Split on / to convert just the first segment (gebete→prayers, etc.)
const convertedSubroute = faithSubroutes[targetLang][subroute] || subroute; const parts = rest.split('/');
return `/${targetBase}/${convertedSubroute}`; 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') { async function switchLanguage(lang: 'de' | 'en') {
isOpen = false;
// Update the shared language store immediately // Update the shared language store immediately
languageStore.set(lang); languageStore.set(lang);
@@ -117,7 +150,7 @@
onMount(() => { onMount(() => {
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
if(langButton && !langButton.contains(e.target as Node)){ if(langButton && !langButton.contains(e.target as Node)){
if (langOptions) langOptions.hidden = true; isOpen = false;
} }
}; };
@@ -159,8 +192,27 @@
width: 10ch; width: 10ch;
padding: 0.5rem; padding: 0.5rem;
z-index: 1000; 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%; width: 100%;
background-color: transparent; background-color: transparent;
color: white; color: white;
@@ -171,32 +223,38 @@
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
text-align: left; text-align: left;
text-decoration: none;
transition: background-color 100ms; transition: background-color 100ms;
box-sizing: border-box;
} }
.language-options button:hover{ .language-options a:hover{
background-color: var(--nord2); background-color: var(--nord2);
} }
.language-options button.active{ .language-options a.active{
background-color: var(--nord14); background-color: var(--nord8);
color: var(--nord0);
font-weight: 700;
} }
</style> </style>
<div class="language-selector"> <div class="language-selector">
<button bind:this={langButton} onclick={toggle_language_options} class="language-button"> <button bind:this={langButton} onclick={toggle_language_options} class="language-button">
{$languageStore.toUpperCase()} {displayLang.toUpperCase()}
</button> </button>
<div bind:this={langOptions} class="language-options" hidden> <div class="language-options" class:open={isOpen}>
<button <a
class:active={$languageStore === 'de'} href={dePath}
onclick={() => switchLanguage('de')} class:active={displayLang === 'de'}
onclick={(e) => { e.preventDefault(); switchLanguage('de'); }}
> >
DE DE
</button> </a>
<button <a
class:active={$languageStore === 'en'} href={enPath}
onclick={() => switchLanguage('en')} class:active={displayLang === 'en'}
onclick={(e) => { e.preventDefault(); switchLanguage('en'); }}
> >
EN EN
</button> </a>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,12 @@
<style> <style>
:global(.links_grid a:nth-child(4n)), :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); background-color: var(--nord4);
fill: var(--nord11); fill: var(--nord11);
} }
:global(.links_grid a:nth-child(4n+1)), :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); background-color: var(--nord6);
fill: var(--nord10); fill: var(--nord10);
} }
@@ -20,7 +20,7 @@
:global(a){ :global(a){
text-decoration: unset; text-decoration: unset;
color: var(--nord0); color: var(--nord0);
transition: 200ms; transition: var(--transition-normal);
} }
:global(.links_grid a:hover){ :global(.links_grid a:hover){
box-shadow: 1em 1em 2em 1em rgba(0,0,0, 0.3); box-shadow: 1em 1em 2em 1em rgba(0,0,0, 0.3);
@@ -30,7 +30,7 @@
} }
.links_grid{ .links_grid{
display: 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; gap: 2rem;
max-width: 1000px; max-width: 1000px;
margin-inline: auto; margin-inline: auto;
@@ -43,7 +43,7 @@
justify-content: center; justify-content: center;
text-decoration: unset; text-decoration: unset;
color: var(--nord0); color: var(--nord0);
transition: 200ms; transition: var(--transition-normal);
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
position: relative; position: relative;
@@ -64,8 +64,50 @@
right: 0.5rem; right: 0.5rem;
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
fill: var(--nord0); fill: var(--nord3);
opacity: 0.6; 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){ @media (prefers-color-scheme: dark){
@@ -73,26 +115,25 @@
color: white; color: white;
} }
:global(.links_grid a .lock-icon){ :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)),
:global(.links_grid a:nth-child(4n) svg){ :global(.links_grid a:nth-child(4n) svg:not(.lock-icon)){
background-color: var(--nord6-dark); background-color: var(--nord6-dark);
fill: var(--nord11); fill: var(--nord11);
} }
:global(.links_grid a:nth-child(4n+1)), :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); background-color: var(--accent-dark);
fill: var(--nord9); fill: var(--nord9);
} }
:global(.links_grid a:nth-child(4n+2)), :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); background-color: var(--nord1);
fill: var(--nord8); fill: var(--nord8);
} }
:global(.links_grid a:nth-child(4n+3)), :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); background-color: var(--background-dark);
fill: var(--nord7); fill: var(--nord7);
} }

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

@@ -1,5 +1,4 @@
<script> <script>
import "$lib/css/nordtheme.css";
let { let {
value = $bindable(''), value = $bindable(''),
@@ -17,11 +16,10 @@
input { input {
all: unset; all: unset;
box-sizing: border-box; box-sizing: border-box;
font-family: sans-serif;
background: var(--nord0); background: var(--nord0);
color: #fff; color: #fff;
padding: 0.7rem 2rem; padding: 0.7rem 2rem;
border-radius: 1000px; border-radius: var(--radius-pill);
width: 100%; width: 100%;
} }
input::placeholder { input::placeholder {
@@ -36,7 +34,7 @@ input::placeholder {
font-size: 1.6rem; font-size: 1.6rem;
display: flex; display: flex;
align-items: center; align-items: center;
transition: 100ms; transition: var(--transition-fast);
filter: drop-shadow(0.4em 0.5em 0.4em rgba(0,0,0,0.4)); filter: drop-shadow(0.4em 0.5em 0.4em rgba(0,0,0,0.4));
} }

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <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> </script>
<style> <style>
@@ -7,17 +7,20 @@
display: inline-flex; display: inline-flex;
} }
.toggle-wrapper label { .toggle-wrapper label,
.toggle-wrapper a {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
cursor: pointer; cursor: pointer;
font-size: 0.95rem; font-size: 0.95rem;
color: var(--nord4); color: var(--nord4);
text-decoration: none;
} }
@media(prefers-color-scheme: light) { @media(prefers-color-scheme: light) {
.toggle-wrapper label { .toggle-wrapper label,
.toggle-wrapper a {
color: var(--nord2); color: var(--nord2);
} }
} }
@@ -26,10 +29,12 @@
user-select: none; user-select: none;
} }
/* iOS-style toggle switch */ /* iOS-style toggle switch — shared by checkbox and link variants */
.toggle-track,
.toggle-wrapper input[type="checkbox"] { .toggle-wrapper input[type="checkbox"] {
appearance: none; appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
margin: 0;
width: 44px; width: 44px;
height: 24px; height: 24px;
background: var(--nord2); background: var(--nord2);
@@ -40,18 +45,22 @@
outline: none; outline: none;
border: none; border: none;
flex-shrink: 0; flex-shrink: 0;
display: inline-block;
} }
@media(prefers-color-scheme: light) { @media(prefers-color-scheme: light) {
.toggle-track,
.toggle-wrapper input[type="checkbox"] { .toggle-wrapper input[type="checkbox"] {
background: var(--nord4); background: var(--nord4);
} }
} }
.toggle-track.checked,
.toggle-wrapper input[type="checkbox"]:checked { .toggle-wrapper input[type="checkbox"]:checked {
background: var(--accent-color); background: var(--accent-color);
} }
.toggle-track::before,
.toggle-wrapper input[type="checkbox"]::before { .toggle-wrapper input[type="checkbox"]::before {
content: ''; content: '';
position: absolute; position: absolute;
@@ -65,14 +74,22 @@
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
} }
.toggle-track.checked::before,
.toggle-wrapper input[type="checkbox"]:checked::before { .toggle-wrapper input[type="checkbox"]:checked::before {
transform: translateX(20px); transform: translateX(20px);
} }
</style> </style>
<div class="toggle-wrapper" style="--accent-color: {accentColor}"> <div class="toggle-wrapper" style="--accent-color: {accentColor}">
<label> {#if href}
<input type="checkbox" bind:checked /> <a {href} onclick={(e) => { e.preventDefault(); checked = !checked; }}>
<span>{label}</span> <span class="toggle-track" class:checked></span>
</label> <span>{label}</span>
</a>
{:else}
<label>
<input type="checkbox" bind:checked />
<span>{label}</span>
</label>
{/if}
</div> </div>

View File

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

View File

@@ -2,7 +2,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { Chart, registerables } from 'chart.js'; 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 canvas = $state();
let chart = $state(); let chart = $state();
@@ -42,6 +42,19 @@
return categoryColorMap[category] || nordColors[index % nordColors.length]; 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() { function createChart() {
if (!canvas || !data.datasets) return; if (!canvas || !data.datasets) return;
@@ -135,7 +148,6 @@
}, },
onClick: (event, legendItem, legend) => { onClick: (event, legendItem, legend) => {
const datasetIndex = legendItem.datasetIndex; const datasetIndex = legendItem.datasetIndex;
const clickedMeta = chart.getDatasetMeta(datasetIndex);
// Check if only this dataset is currently visible // Check if only this dataset is currently visible
const onlyThisVisible = chart.data.datasets.every((dataset, idx) => { const onlyThisVisible = chart.data.datasets.every((dataset, idx) => {
@@ -156,6 +168,7 @@
} }
chart.update(); chart.update();
emitFilter();
} }
}, },
title: { title: {
@@ -229,6 +242,7 @@
} }
chart.update(); chart.update();
emitFilter();
} }
} }
}, },

View File

@@ -3,7 +3,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import ProfilePicture from './ProfilePicture.svelte'; 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 { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters'; import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters';

View File

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

View File

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

View File

@@ -3,9 +3,10 @@
interface Props { interface Props {
holy?: boolean; holy?: boolean;
burst?: boolean; burst?: boolean;
fire ?: boolean;
} }
let { holy = false, burst = false }: Props = $props(); let { holy = false, burst = false, fire = false}: Props = $props();
const burstParticles = [ const burstParticles = [
{ x: 10, y: 0, size: 8, delay: 0, dur: 1.6 }, { x: 10, y: 0, size: 8, delay: 0, dur: 1.6 },
@@ -52,22 +53,22 @@
{:else} {:else}
<div class="fire" class:holy-fire={holy}> <div class="fire" class:holy-fire={holy}>
<div class="fire-left"> <div class="fire-left">
<div class="main-fire"></div> {#if fire}<div class="main-fire"></div>{/if}
<div class="particle-fire"></div> <div class="particle-fire"></div>
</div> </div>
<div class="fire-center"> <div class="fire-center">
<div class="main-fire"></div> {#if fire}<div class="main-fire"></div>{/if}
<div class="particle-fire"></div> <div class="particle-fire"></div>
</div> </div>
<div class="fire-right"> <div class="fire-right">
<div class="main-fire"></div> {#if fire}<div class="main-fire"></div>{/if}
<div class="particle-fire"></div> <div class="particle-fire"></div>
</div> </div>
<div class="fire-bottom"> <div class="fire-bottom">
<div class="main-fire"></div> {#if fire}<div class="main-fire"></div>{/if}
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -1,13 +1,17 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getLanguageContext } from '$lib/contexts/languageContext.js'; 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) // Get the language context (must be created by parent page)
const { showLatin, lang } = getLanguageContext(); const { showLatin, lang } = getLanguageContext();
// Local state for the checkbox // Local state for the checkbox
let showBilingual = true; let showBilingual = initialLatin !== undefined ? initialLatin : true;
// Flag to prevent saving before we've loaded from localStorage // Flag to prevent saving before we've loaded from localStorage
let hasLoadedFromStorage = false; let hasLoadedFromStorage = false;
@@ -26,10 +30,12 @@
: 'Lateinisch und Deutsch anzeigen'; : 'Lateinisch und Deutsch anzeigen';
onMount(() => { onMount(() => {
// Load from localStorage // Only load from localStorage if no URL param was set
const saved = localStorage.getItem('rosary_showBilingual'); if (!hasUrlLatin) {
if (saved !== null) { const saved = localStorage.getItem('rosary_showBilingual');
showBilingual = saved === 'true'; if (saved !== null) {
showBilingual = saved === 'true';
}
} }
// Now allow saving // Now allow saving
@@ -40,5 +46,6 @@
<Toggle <Toggle
bind:checked={showBilingual} bind:checked={showBilingual}
{label} {label}
{href}
accentColor="var(--nord14)" accentColor="var(--nord14)"
/> />

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

@@ -54,11 +54,11 @@
{#if phase >= 2} {#if phase >= 2}
<FireEffect holy={phase>=4} /> <FireEffect holy={phase>=4} fire={phase>=3}/>
{/if} {/if}
{#if showBurst} {#if showBurst}
<FireEffect holy={phase>=4} burst /> <FireEffect holy={phase>=4} burst fire={phase>=3}/>
{/if} {/if}
<span class="number">{value}</span> <span class="number">{value}</span>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { getRosaryStreak } from '$lib/stores/rosaryStreak.svelte'; import { getRosaryStreak } from '$lib/stores/rosaryStreak.svelte';
import StreakAura from '$lib/components/StreakAura.svelte'; import StreakAura from '$lib/components/faith/StreakAura.svelte';
import { tick, onMount } from 'svelte'; import { tick, onMount } from 'svelte';
let burst = $state(false); let burst = $state(false);
@@ -10,9 +10,10 @@ let streak = $state<ReturnType<typeof getRosaryStreak> | null>(null);
interface Props { interface Props {
streakData?: { length: number; lastPrayed: string | null } | null; streakData?: { length: number; lastPrayed: string | null } | null;
lang?: 'de' | 'en'; lang?: 'de' | 'en';
isLoggedIn?: boolean;
} }
let { streakData = null, lang = 'de' }: Props = $props(); let { streakData = null, lang = 'de', isLoggedIn = false }: Props = $props();
const isEnglish = $derived(lang === 'en'); const isEnglish = $derived(lang === 'en');
@@ -29,9 +30,12 @@ const labels = $derived({
}); });
// Initialize store on mount (client-side only) // 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(() => { onMount(() => {
streak = getRosaryStreak(); const s = getRosaryStreak();
streak.initWithServerData(streakData, streakData !== null); s.initWithServerData(streakData, isLoggedIn);
streak = s;
}); });
async function pray() { async function pray() {
@@ -42,23 +46,25 @@ async function pray() {
} }
</script> </script>
<div class="streak-container"> <div class="streak-container" class:no-js-hidden={!isLoggedIn}>
<div class="streak-display"> <div class="streak-display">
<StreakAura value={displayLength} {burst} /> <StreakAura value={displayLength} {burst} />
<span class="streak-label">{labels.days}</span> <span class="streak-label">{labels.days}</span>
</div> </div>
<button <form method="POST" action="?/pray" onsubmit={(e) => { e.preventDefault(); pray(); }}>
class="streak-button" <button
onclick={pray} class="streak-button"
disabled={prayedToday} type="submit"
aria-label={labels.ariaLabel} disabled={prayedToday}
> aria-label={labels.ariaLabel}
{#if prayedToday} >
{labels.prayedToday} {#if prayedToday}
{:else} {labels.prayedToday}
{labels.prayed} {:else}
{/if} {labels.prayed}
</button> {/if}
</button>
</form>
</div> </div>
<style> <style>
@@ -119,6 +125,15 @@ async function pray() {
opacity: 0.7; 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) { @media (prefers-color-scheme: light) {
.streak-button:disabled { .streak-button:disabled {
background: var(--nord4); background: var(--nord4);

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

@@ -1,7 +1,20 @@
<script> <script>
import Prayer from './Prayer.svelte'; import Prayer from './Prayer.svelte';
let { intro = false } = $props();
</script> </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> <Prayer>
{#snippet children(showLatin, urlLang)} {#snippet children(showLatin, urlLang)}
<p> <p>

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,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,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

@@ -38,9 +38,9 @@
{#if showLatin}<v lang="la">nóbis post hoc exsílíum osténde.</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 === '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 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.</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.</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.</v>{/if} {#if urlLang === 'en'}<v lang="en">O clement, O loving, O sweet Virgin <i><sup></sup></i>Mary. Amen.</v>{/if}
</p> </p>
{/snippet} {/snippet}
</Prayer> </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,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { browser } from '$app/environment'; 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' import Check from '$lib/assets/icons/Check.svelte'
let { let {
@@ -108,7 +108,6 @@ dialog[open]::backdrop {
dialog h2 { dialog h2 {
font-size: 3rem; font-size: 3rem;
font-family: sans-serif;
color: white; color: white;
text-align: center; text-align: center;
margin-top: 30vh; margin-top: 30vh;
@@ -123,7 +122,7 @@ dialog h2 {
margin-top: 2rem; margin-top: 2rem;
max-width: 600px; max-width: 600px;
padding: 2rem; padding: 2rem;
border-radius: 20px; border-radius: var(--radius-card);
background-color: var(--blue); background-color: var(--blue);
color: white; color: white;
box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3); box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3);
@@ -141,12 +140,12 @@ dialog h2 {
width: 100%; width: 100%;
padding: 0.5em 1em; padding: 0.5em 1em;
margin-top: 0.5em; margin-top: 0.5em;
border-radius: 1000px; border-radius: var(--radius-pill);
border: 2px solid var(--nord4); border: 2px solid var(--nord4);
background-color: white; background-color: white;
color: var(--nord0); color: var(--nord0);
font-size: 1rem; font-size: 1rem;
transition: 100ms; transition: var(--transition-fast);
} }
.selector-content select:hover, .selector-content select:hover,
@@ -176,10 +175,10 @@ dialog h2 {
.button-group button { .button-group button {
padding: 0.75em 2em; padding: 0.75em 2em;
font-size: 1.1rem; font-size: 1.1rem;
border-radius: 1000px; border-radius: var(--radius-pill);
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: 200ms; transition: var(--transition-normal);
font-weight: bold; font-weight: bold;
} }

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import "$lib/css/nordtheme.css";
import "$lib/css/shake.css"; import "$lib/css/shake.css";
import "$lib/css/icon.css"; import "$lib/css/icon.css";
import { onMount } from "svelte"; import { onMount } from "svelte";
@@ -37,6 +36,8 @@ const img_name = $derived(
const img_alt = $derived( const img_alt = $derived(
recipe.images?.[0]?.alt || recipe.name recipe.images?.[0]?.alt || recipe.name
); );
const img_color = $derived(recipe.images?.[0]?.color || '');
</script> </script>
<style> <style>
.card-main-link { .card-main-link {
@@ -63,7 +64,6 @@ const img_alt = $derived(
transition: var(--transition-normal); transition: var(--transition-normal);
text-decoration: none; text-decoration: none;
box-sizing: border-box; box-sizing: border-box;
font-family: sans-serif;
cursor: pointer; cursor: pointer;
height: 525px; height: 525px;
width: 300px; width: 300px;
@@ -95,21 +95,16 @@ const img_alt = $derived(
transition: var(--transition-normal); transition: var(--transition-normal);
border-top-left-radius: inherit; border-top-left-radius: inherit;
border-top-right-radius: inherit; border-top-right-radius: inherit;
opacity: 0;
} }
.blur{ .image.loaded{
filter: blur(10px); opacity: 1;
}
.backdrop_blur{
backdrop-filter: blur(10px);
} }
.card-image{ .card-image{
width: 300px; width: 300px;
height: 255px; height: 255px;
position: absolute; position: absolute;
top: 0; top: 0;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
overflow: hidden; overflow: hidden;
border-top-left-radius: inherit; border-top-left-radius: inherit;
border-top-right-radius: inherit; border-top-right-radius: inherit;
@@ -234,11 +229,11 @@ const img_alt = $derived(
<a href="{routePrefix}/{recipe.short_name}" class="card-main-link" aria-label="View recipe: {recipe.name}"> <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> <span class="visually-hidden">View recipe: {recipe.name}</span>
</a> </a>
<div class="card-image" style="background-image:url(https://bocken.org/static/rezepte/placeholder/{img_name})"> <div class="card-image" style:background-color={img_color}>
<noscript> <noscript>
<img class="image backdrop_blur" src="https://bocken.org/static/rezepte/thumb/{img_name}" loading={loading_strat} alt="{img_alt}"/> <img class="image loaded" src="https://bocken.org/static/rezepte/thumb/{img_name}" loading={loading_strat} alt="{img_alt}"/>
</noscript> </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}/> <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> </div>
{#if showFavoriteIndicator && isFavorite} {#if showFavoriteIndicator && isFavorite}
<div class="favorite-indicator">❤️</div> <div class="favorite-indicator">❤️</div>

View File

@@ -130,15 +130,14 @@ function remove_on_enter(event: KeyboardEvent, tag: string) {
text-decoration: none; text-decoration: none;
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
font-family: sans-serif;
width: var(--card-width); width: var(--card-width);
aspect-ratio: 4/7; aspect-ratio: 4/7;
border-radius: 20px; border-radius: var(--radius-card);
background-size: contain; background-size: contain;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: end; justify-content: end;
transition: 200ms; transition: var(--transition-normal);
background-color: var(--blue); background-color: var(--blue);
box-shadow: 0em 0em 2em 0.1em rgba(0, 0, 0, 0.3); box-shadow: 0em 0em 2em 0.1em rgba(0, 0, 0, 0.3);
z-index: 0; z-index: 0;
@@ -155,7 +154,7 @@ function remove_on_enter(event: KeyboardEvent, tag: string) {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 20px 20px 0 0 ; border-radius: 20px 20px 0 0 ;
transition: 200ms; transition: var(--transition-normal);
} }
.img_label_wrapper:hover{ .img_label_wrapper:hover{
background-color: var(--red); background-color: var(--red);
@@ -169,7 +168,7 @@ function remove_on_enter(event: KeyboardEvent, tag: string) {
top:0; top:0;
left: 0; left: 0;
border-radius: 20px 20px 0 0; border-radius: 20px 20px 0 0;
transition: 200ms; transition: var(--transition-normal);
} }
.img_label_wrapper:hover .delete{ .img_label_wrapper:hover .delete{
opacity: 100%; opacity: 100%;
@@ -178,7 +177,7 @@ function remove_on_enter(event: KeyboardEvent, tag: string) {
width: 100px; width: 100px;
height: 100px; height: 100px;
fill: white; fill: white;
transition: 200ms; transition: var(--transition-normal);
} }
.delete{ .delete{
cursor: pointer; cursor: pointer;
@@ -188,7 +187,7 @@ function remove_on_enter(event: KeyboardEvent, tag: string) {
left: 2rem; left: 2rem;
opacity: 0%; opacity: 0%;
z-index: 4; z-index: 4;
transition:200ms; transition: var(--transition-normal);
} }
.delete:hover{ .delete:hover{
transform: scale(1.2, 1.2); transform: scale(1.2, 1.2);
@@ -220,14 +219,14 @@ input::placeholder{
text-align:center; text-align:center;
width: 2.6rem; width: 2.6rem;
aspect-ratio: 1/1; aspect-ratio: 1/1;
transition: 100ms; transition: var(--transition-fast);
position: absolute; position: absolute;
font-size: 1.5rem; font-size: 1.5rem;
top:-0.5em; top:-0.5em;
right:-0.5em; right:-0.5em;
padding: 0.25em; padding: 0.25em;
background-color: var(--nord6); 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); box-shadow: 0em 0em 2em 0.1em rgba(0, 0, 0, 0.6);
} }
.card .icon:hover, .card .icon:hover,
@@ -259,7 +258,7 @@ input::placeholder{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
transition: 100ms; transition: var(--transition-fast);
} }
.card .name{ .card .name{
all: unset; all: unset;
@@ -306,7 +305,7 @@ input::placeholder{
padding-inline: 1em; padding-inline: 1em;
line-height: 1.5em; line-height: 1.5em;
margin-bottom: 0.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); box-shadow: 0.2em 0.2em 0.2em 0.05em rgba(0, 0, 0, 0.3);
} }
.card .tag:hover, .card .tag:hover,
@@ -330,8 +329,8 @@ input::placeholder{
width: 10rem; width: 10rem;
background-color: var(--nord0); background-color: var(--nord0);
padding-inline: 1em; padding-inline: 1em;
border-radius: 1000px; border-radius: var(--radius-pill);
transition: 100ms; transition: var(--transition-fast);
} }
.card .title .category:hover, .card .title .category:hover,

View File

@@ -1,6 +1,5 @@
<script> <script>
import "$lib/css/nordtheme.css"; import TagChip from '$lib/components/recipes/TagChip.svelte';
import TagChip from './TagChip.svelte';
let { let {
categories = [], categories = [],
@@ -135,7 +134,6 @@
input { input {
all: unset; all: unset;
box-sizing: border-box; box-sizing: border-box;
font-family: sans-serif;
background: var(--nord0); background: var(--nord0);
color: var(--nord6); color: var(--nord6);
padding: 0.5rem 0.7rem; padding: 0.5rem 0.7rem;

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 "$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 { 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() let portions_local = $state()
portions.subscribe((p) => { portions.subscribe((p) => {
@@ -398,11 +398,11 @@ input.heading{
padding-inline: 2rem; padding-inline: 2rem;
font-size: 1.5rem; font-size: 1.5rem;
width: 100%; width: 100%;
border-radius: 1000px; border-radius: var(--radius-pill);
color: white; color: white;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transition: 200ms; transition: var(--transition-normal);
} }
input.heading:hover{ input.heading:hover{
background-color: var(--nord1); background-color: var(--nord1);
@@ -412,7 +412,7 @@ input.heading:hover{
position: relative; position: relative;
width: 300px; width: 300px;
margin-inline: auto; margin-inline: auto;
transition: 200ms; transition: var(--transition-normal);
} }
.heading_wrapper:hover .heading_wrapper:hover
{ {
@@ -430,8 +430,8 @@ input.heading:hover{
position: relative; position: relative;
margin-block: 3rem; margin-block: 3rem;
width: 90%; width: 90%;
border-radius: 20px; border-radius: var(--radius-card);
transition: 200ms; transition: var(--transition-normal);
} }
.shadow{ .shadow{
box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3); box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3);
@@ -450,14 +450,13 @@ input.heading:hover{
--font_size: 1.5rem; --font_size: 1.5rem;
top: -1em; top: -1em;
left: -1em; left: -1em;
font-family: sans-serif;
font-size: 1.5rem; font-size: 1.5rem;
background-color: var(--nord0); background-color: var(--nord0);
color: var(--nord4); color: var(--nord4);
border-radius: 1000000px; border-radius: 1000000px;
width: 23ch; width: 23ch;
padding: 0.5em 1em; 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); box-shadow: 0.5em 0.5em 1em 0.4em rgba(0,0,0,0.3);
} }
.category:hover{ .category:hover{
@@ -471,7 +470,6 @@ input.heading:hover{
} }
.add_ingredient{ .add_ingredient{
font-family: sans-serif;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -481,18 +479,18 @@ input.heading:hover{
font-size: 1.2rem; font-size: 1.2rem;
padding: 2rem; padding: 2rem;
padding-top: 2.5rem; padding-top: 2.5rem;
border-radius: 20px; border-radius: var(--radius-card);
background-color: var(--blue); background-color: var(--blue);
color: #bbb; color: #bbb;
transition: 200ms; transition: var(--transition-normal);
gap: 0.5rem; gap: 0.5rem;
} }
.add_ingredient input{ .add_ingredient input{
border: 2px solid var(--nord4); border: 2px solid var(--nord4);
color: var(--nord4); color: var(--nord4);
border-radius: 1000px; border-radius: var(--radius-pill);
padding: 0.5em 1em; padding: 0.5em 1em;
transition: 100ms; transition: var(--transition-fast);
} }
.add_ingredient input:hover, .add_ingredient input:hover,
.add_ingredient input:focus-visible .add_ingredient input:focus-visible
@@ -537,7 +535,6 @@ dialog .adder{
} }
dialog h2{ dialog h2{
font-size: 3rem; font-size: 3rem;
font-family: sans-serif;
color: white; color: white;
text-align: center; text-align: center;
margin-top: 30vh; margin-top: 30vh;
@@ -569,7 +566,7 @@ dialog h2{
border: none; border: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
transition: 200ms; transition: var(--transition-normal);
} }
.move_buttons_container button:hover{ .move_buttons_container button:hover{
scale: 1.4; scale: 1.4;
@@ -611,10 +608,10 @@ h3{
} }
.list_wrapper p[contenteditable]{ .list_wrapper p[contenteditable]{
border: 2px solid grey; border: 2px solid grey;
border-radius: 1000px; border-radius: var(--radius-pill);
padding: 0.25em 1em; padding: 0.25em 1em;
background-color: white; background-color: white;
transition: 200ms; transition: var(--transition-normal);
} }
.list_wrapper p[contenteditable]:hover, .list_wrapper p[contenteditable]:hover,
.list_wrapper p[contenteditable]:focus-within{ .list_wrapper p[contenteditable]:focus-within{
@@ -703,12 +700,12 @@ h3{
margin-block: 1rem; margin-block: 1rem;
padding: 1em 2em; padding: 1em 2em;
font-size: 1.1rem; font-size: 1.1rem;
border-radius: 1000px; border-radius: var(--radius-pill);
background-color: var(--nord9); background-color: var(--nord9);
color: white; color: white;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: 200ms; transition: var(--transition-normal);
box-shadow: 0 0 0.5em 0.1em rgba(0,0,0,0.2); box-shadow: 0 0 0.5em 0.1em rgba(0,0,0,0.2);
} }

View File

@@ -5,11 +5,10 @@ import Cross from '$lib/assets/icons/Cross.svelte'
import Plus from '$lib/assets/icons/Plus.svelte' import Plus from '$lib/assets/icons/Plus.svelte'
import Check from '$lib/assets/icons/Check.svelte' import Check from '$lib/assets/icons/Check.svelte'
import '$lib/css/nordtheme.css'
import "$lib/css/action_button.css" 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 BaseRecipeSelector from '$lib/components/BaseRecipeSelector.svelte' 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 }>(); let { lang = 'de' as 'de' | 'en', instructions = $bindable(), add_info = $bindable() } = $props<{ lang?: 'de' | 'en', instructions: any, add_info: any }>();
@@ -402,7 +401,7 @@ export function update_step_position(list_index, step_index, direction){
border: none; border: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
transition: 200ms; transition: var(--transition-normal);
} }
.move_buttons_container button:hover{ .move_buttons_container button:hover{
scale: 1.4; scale: 1.4;
@@ -441,11 +440,11 @@ input.heading{
padding-inline: 2rem; padding-inline: 2rem;
font-size: 1.5rem; font-size: 1.5rem;
width: 100%; width: 100%;
border-radius: 1000px; border-radius: var(--radius-pill);
color: white; color: white;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transition: 200ms; transition: var(--transition-normal);
} }
input.heading:hover, input.heading:hover,
input.heading:focus-visible input.heading:focus-visible
@@ -457,7 +456,7 @@ input.heading:focus-visible
position: relative; position: relative;
width: min(300px, 95dvw); width: min(300px, 95dvw);
margin-inline: auto; margin-inline: auto;
transition: 200ms; transition: var(--transition-normal);
} }
.heading_wrapper:hover, .heading_wrapper:hover,
.heading_wrapper:focus-visible .heading_wrapper:focus-visible
@@ -475,8 +474,8 @@ input.heading:focus-visible
position: relative; position: relative;
margin-block: 3rem; margin-block: 3rem;
width: 90%; width: 90%;
border-radius: 20px; border-radius: var(--radius-card);
transition: 200ms; transition: var(--transition-normal);
background-color: var(--blue); background-color: var(--blue);
padding: 1.5rem 2rem; padding: 1.5rem 2rem;
} }
@@ -497,14 +496,13 @@ dialog .adder{
--font_size: 1.5rem; --font_size: 1.5rem;
top: -1em; top: -1em;
left: -1em; left: -1em;
font-family: sans-serif;
font-size: 1.5rem; font-size: 1.5rem;
background-color: var(--nord0); background-color: var(--nord0);
color: var(--nord4); color: var(--nord4);
border-radius: 1000000px; border-radius: 1000000px;
width: 23ch; width: 23ch;
padding: 0.5em 1em; 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); box-shadow: 0.5em 0.5em 1em 0.4em rgba(0,0,0,0.3);
} }
.category:hover, .category:hover,
@@ -520,15 +518,14 @@ dialog .adder{
} }
.add_step p{ .add_step p{
font-family: sans-serif;
width: 100%; width: 100%;
font-size: 1.2rem; font-size: 1.2rem;
border-radius: 20px; border-radius: var(--radius-card);
border: 2px solid var(--nord4); border: 2px solid var(--nord4);
border-radius: 30px; border-radius: 30px;
padding: 0.5em 1em; padding: 0.5em 1em;
color: var(--nord4); color: var(--nord4);
transition: 100ms; transition: var(--transition-fast);
} }
.add_step p:hover, .add_step p:hover,
.add_step p:focus-visible .add_step p:focus-visible
@@ -544,14 +541,13 @@ dialog{
background-color: rgba(255,255,255, 0.001); background-color: rgba(255,255,255, 0.001);
border: unset; border: unset;
margin: 0; margin: 0;
transition: 200ms; transition: var(--transition-normal);
} }
dialog .adder{ dialog .adder{
margin-top: 5rem; margin-top: 5rem;
} }
dialog h2{ dialog h2{
font-size: 3rem; font-size: 3rem;
font-family: sans-serif;
color: white; color: white;
text-align: center; text-align: center;
margin-top: 30vh; margin-top: 30vh;
@@ -648,10 +644,10 @@ h3{
display: inline; display: inline;
padding: 0.25em 1em; padding: 0.25em 1em;
border: 2px solid grey; border: 2px solid grey;
border-radius: 1000px; border-radius: var(--radius-pill);
} }
.additional_info div:has(p[contenteditable]){ .additional_info div:has(p[contenteditable]){
transition: 200ms; transition: var(--transition-normal);
display: inline; display: inline;
} }
.additional_info div:has(p[contenteditable]):hover, .additional_info div:has(p[contenteditable]):hover,
@@ -731,12 +727,12 @@ h3{
margin-block: 1rem; margin-block: 1rem;
padding: 1em 2em; padding: 1em 2em;
font-size: 1.1rem; font-size: 1.1rem;
border-radius: 1000px; border-radius: var(--radius-pill);
background-color: var(--nord9); background-color: var(--nord9);
color: white; color: white;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: 200ms; transition: var(--transition-normal);
box-shadow: 0 0 0.5em 0.1em rgba(0,0,0,0.2); box-shadow: 0 0 0.5em 0.1em rgba(0,0,0,0.2);
} }

View File

@@ -25,7 +25,6 @@ textarea {
font-size: 1rem; font-size: 1rem;
resize: vertical; resize: vertical;
margin-top: 0.5em; margin-top: 0.5em;
font-family: sans-serif;
background-color: transparent; background-color: transparent;
} }
textarea::placeholder { textarea::placeholder {

View File

@@ -1,6 +1,5 @@
<script> <script>
import "$lib/css/nordtheme.css"; import Toggle from '$lib/components/Toggle.svelte';
import Toggle from './Toggle.svelte';
let { let {
enabled = false, enabled = false,

View File

@@ -1,5 +1,4 @@
<script> <script>
import "$lib/css/nordtheme.css";
import CategoryFilter from './CategoryFilter.svelte'; import CategoryFilter from './CategoryFilter.svelte';
import TagFilter from './TagFilter.svelte'; import TagFilter from './TagFilter.svelte';
import IconFilter from './IconFilter.svelte'; import IconFilter from './IconFilter.svelte';
@@ -44,7 +43,7 @@
<style> <style>
.filter-wrapper { .filter-wrapper {
width: 900px; width: 900px;
max-width: 95vw; max-width: 80vw;
margin: 1rem auto 2rem; margin: 1rem auto 2rem;
} }

View File

@@ -1,16 +1,15 @@
<script lang="ts"> <script lang="ts">
import '$lib/css/nordtheme.css';
import "$lib/css/shake.css" import "$lib/css/shake.css"
let { icon, ...restProps } = $props<{ icon: string, [key: string]: any }>(); let { icon, ...restProps } = $props<{ icon: string, [key: string]: any }>();
</script> </script>
<style> <style>
a{ a{
font-family: "Noto Color Emoji", emoji; font-family: "Noto Color Emoji", "Noto Color Emoji Subset", emoji;
font-size: 2rem; font-size: 2rem;
text-decoration: none; text-decoration: none;
padding: 0.5em; padding: 0.5em;
background-color: var(--nord4); background-color: var(--nord4);
border-radius: 1000px; border-radius: var(--radius-pill);
box-shadow: 0em 0em 0.5em 0.2em rgba(0, 0, 0, 0.2); box-shadow: 0em 0em 0.5em 0.2em rgba(0, 0, 0, 0.2);
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {

View File

@@ -1,6 +1,5 @@
<script> <script>
import "$lib/css/nordtheme.css"; import TagChip from '$lib/components/recipes/TagChip.svelte';
import TagChip from './TagChip.svelte';
let { let {
availableIcons = [], availableIcons = [],
@@ -127,7 +126,7 @@
input { input {
all: unset; all: unset;
box-sizing: border-box; box-sizing: border-box;
font-family: "Noto Color Emoji", emoji, sans-serif; font-family: "Noto Color Emoji", "Noto Color Emoji Subset", emoji, sans-serif;
background: var(--nord0); background: var(--nord0);
color: var(--nord6); color: var(--nord6);
padding: 0.5rem 0.7rem; padding: 0.5rem 0.7rem;
@@ -147,7 +146,6 @@
input::placeholder { input::placeholder {
color: var(--nord4); color: var(--nord4);
font-family: sans-serif;
} }
input:hover { input:hover {

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import '$lib/css/nordtheme.css'; import Recipes from '$lib/components/recipes/Recipes.svelte';
import Recipes from '$lib/components/Recipes.svelte';
import Search from './Search.svelte'; import Search from './Search.svelte';
let { let {
@@ -27,12 +26,12 @@
<style> <style>
a{ a{
font-family: "Noto Color Emoji", emoji, sans-serif; font-family: "Noto Color Emoji", "Noto Color Emoji Subset", emoji, sans-serif;
font-size: 2rem; font-size: 2rem;
text-decoration: none; text-decoration: none;
padding: 0.5em; padding: 0.5em;
background-color: var(--nord4); background-color: var(--nord4);
border-radius: 1000px; border-radius: var(--radius-pill);
box-shadow: 0em 0em 0.5em 0.2em rgba(0, 0, 0, 0.2); box-shadow: 0em 0em 0.5em 0.2em rgba(0, 0, 0, 0.2);
} }
a:hover, a:hover,

View File

@@ -4,8 +4,6 @@ import { onNavigate } from "$app/navigation";
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { page } from '$app/stores'; import { page } from '$app/stores';
import HefeSwapper from './HefeSwapper.svelte'; import HefeSwapper from './HefeSwapper.svelte';
import '$lib/css/recipe-links.css';
let { data } = $props(); let { data } = $props();
// Helper function to multiply numbers in ingredient amounts // Helper function to multiply numbers in ingredient amounts
@@ -310,9 +308,6 @@ function adjust_amount(string, multiplier){
// No need for complex yeast toggle handling - everything is calculated server-side now // No need for complex yeast toggle handling - everything is calculated server-side now
</script> </script>
<style> <style>
*{
font-family: sans-serif;
}
.ingredients{ .ingredients{
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;

View File

@@ -1,5 +1,4 @@
<script> <script>
import '$lib/css/recipe-links.css';
let { data } = $props(); let { data } = $props();
let multiplier = $state(data.multiplier || 1); let multiplier = $state(data.multiplier || 1);
@@ -101,9 +100,6 @@ const labels = $derived({
}); });
</script> </script>
<style> <style>
*{
font-family: sans-serif;
}
ol li::marker{ ol li::marker{
font-weight: bold; font-weight: bold;
color: var(--blue); color: var(--blue);

View File

@@ -1,5 +1,4 @@
<script> <script>
import "$lib/css/nordtheme.css";
let { let {
useAndLogic = true, useAndLogic = true,
@@ -42,7 +41,7 @@
.filter-label { .filter-label {
font-size: 0.9rem; font-size: 0.9rem;
color: var(--nord2); color: var(--nord1);
font-weight: 600; font-weight: 600;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
text-align: center; text-align: center;
@@ -66,7 +65,7 @@
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
color: var(--nord4); color: var(--nord3);
font-weight: 600; font-weight: 600;
} }
@@ -87,7 +86,7 @@
} }
.toggle-switch.or-mode { .toggle-switch.or-mode {
background: var(--nord13); background: var(--nord12);
} }
.toggle-knob { .toggle-knob {
@@ -122,7 +121,7 @@
} }
.toggle-switch.or-mode + .mode-label.or { .toggle-switch.or-mode + .mode-label.or {
color: var(--nord13); color: var(--nord12);
} }
</style> </style>

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import "$lib/css/nordtheme.css"
let { title = '', children } = $props<{ title?: string, children?: Snippet }>(); let { title = '', children } = $props<{ title?: string, children?: Snippet }>();
</script> </script>
<style> <style>

View File

@@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import CardAdd from '$lib/components/CardAdd.svelte'; import CardAdd from '$lib/components/recipes/CardAdd.svelte';
import MediaScroller from '$lib/components/MediaScroller.svelte'; import MediaScroller from '$lib/components/recipes/MediaScroller.svelte';
import Card from '$lib/components/Card.svelte'; import Search from '$lib/components/recipes/Search.svelte';
import Search from '$lib/components/Search.svelte'; import SeasonSelect from '$lib/components/recipes/SeasonSelect.svelte';
import SeasonSelect from '$lib/components/SeasonSelect.svelte'; import CreateIngredientList from '$lib/components/recipes/CreateIngredientList.svelte';
import CreateIngredientList from '$lib/components/CreateIngredientList.svelte'; import CreateStepList from '$lib/components/recipes/CreateStepList.svelte';
import CreateStepList from '$lib/components/CreateStepList.svelte';
let { let {
card_data = $bindable({}), card_data = $bindable({}),
@@ -59,7 +58,7 @@ input.temp{
display: block; display: block;
margin: 1rem auto; margin: 1rem auto;
padding: 0.2em 1em; padding: 0.2em 1em;
border-radius: 1000px; border-radius: var(--radius-pill);
background-color: var(--nord4); background-color: var(--nord4);
} }

View File

@@ -1,7 +1,6 @@
<script> <script>
import {onMount} from "svelte"; import {onMount} from "svelte";
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import "$lib/css/nordtheme.css";
import FilterPanel from './FilterPanel.svelte'; import FilterPanel from './FilterPanel.svelte';
import { getCategories } from '$lib/js/categories'; import { getCategories } from '$lib/js/categories';
@@ -264,7 +263,7 @@
$effect(() => { $effect(() => {
const loadFilterData = async () => { const loadFilterData = async () => {
try { try {
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte'; const apiBase = `/api/${isEnglish ? 'recipes' : 'rezepte'}`;
const [tagsRes, iconsRes] = await Promise.all([ const [tagsRes, iconsRes] = await Promise.all([
fetch(`${apiBase}/items/tag`), fetch(`${apiBase}/items/tag`),
fetch('/api/rezepte/items/icon') fetch('/api/rezepte/items/icon')
@@ -308,11 +307,10 @@
input#search { input#search {
all: unset; all: unset;
box-sizing: border-box; box-sizing: border-box;
font-family: sans-serif;
background: var(--nord0); background: var(--nord0);
color: #fff; color: #fff;
padding: 0.7rem 2rem; padding: 0.7rem 2rem;
border-radius: 1000px; border-radius: var(--radius-pill);
width: 100%; width: 100%;
} }
input::placeholder{ input::placeholder{
@@ -320,15 +318,15 @@ input::placeholder{
} }
.search { .search {
width: 500px; width: 560px;
max-width: 85vw; max-width: 88vw;
position: relative; position: relative;
margin: 2.5rem auto 1.2rem; margin: 0 auto;
font-size: 1.6rem; font-size: 1.6rem;
display: flex; display: flex;
align-items: center; align-items: center;
transition: 100ms; transition: var(--transition-fast);
filter: drop-shadow(0.4em 0.5em 0.4em rgba(0,0,0,0.4)) filter: drop-shadow(0 4px 12px rgba(0,0,0,0.25));
} }
.search:hover, .search:hover,
@@ -362,6 +360,20 @@ scale: 0.8 0.8;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
/* Reserves space for FilterPanel before JS hydrates, preventing layout shift. */
.filter-placeholder {
display: block;
width: 900px;
max-width: 95vw;
margin: 1rem auto 2rem;
height: 3.85rem;
}
@media (max-width: 968px) {
.filter-placeholder {
width: auto;
height: calc(2.05rem + 2px);
}
}
</style> </style>
<form class="search" method="get" action={buildSearchUrl('')} onsubmit={(e) => { e.preventDefault(); handleSubmit(e); }}> <form class="search" method="get" action={buildSearchUrl('')} onsubmit={(e) => { e.preventDefault(); handleSubmit(e); }}>
{#if selectedCategory}<input type="hidden" name="category" value={selectedCategory} />{/if} {#if selectedCategory}<input type="hidden" name="category" value={selectedCategory} />{/if}
@@ -408,4 +420,6 @@ scale: 0.8 0.8;
onFavoritesToggle={handleFavoritesToggle} onFavoritesToggle={handleFavoritesToggle}
onLogicModeToggle={handleLogicModeToggle} onLogicModeToggle={handleLogicModeToggle}
/> />
{:else}
<div class="filter-placeholder"></div>
{/if} {/if}

View File

@@ -1,6 +1,5 @@
<script> <script>
import "$lib/css/nordtheme.css"; import TagChip from '$lib/components/recipes/TagChip.svelte';
import TagChip from './TagChip.svelte';
let { let {
selectedSeasons = [], selectedSeasons = [],
@@ -121,7 +120,6 @@
input { input {
all: unset; all: unset;
box-sizing: border-box; box-sizing: border-box;
font-family: sans-serif;
background: var(--nord0); background: var(--nord0);
color: var(--nord6); color: var(--nord6);
padding: 0.5rem 0.7rem; padding: 0.5rem 0.7rem;

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import '$lib/css/nordtheme.css'; import Recipes from '$lib/components/recipes/Recipes.svelte';
import Recipes from '$lib/components/Recipes.svelte';
import Search from './Search.svelte'; import Search from './Search.svelte';
let { let {
@@ -29,12 +28,11 @@
<style> <style>
a.month{ a.month{
text-decoration: unset; text-decoration: unset;
font-family: sans-serif; border-radius: var(--radius-pill);
border-radius: 1000px;
background-color: var(--blue); background-color: var(--blue);
color: var(--nord5); color: var(--nord5);
padding: 0.5em; padding: 0.5em;
transition: 100ms; transition: var(--transition-fast);
min-width: 4em; min-width: 4em;
text-align: center; text-align: center;
} }

View File

@@ -1,5 +1,4 @@
<script lang=ts> <script lang=ts>
import "$lib/css/nordtheme.css"
import { season } from '$lib/js/season_store.js' import { season } from '$lib/js/season_store.js'
import {onMount} from "svelte"; import {onMount} from "svelte";
import {do_on_key} from "./do_on_key"; import {do_on_key} from "./do_on_key";
@@ -46,15 +45,15 @@ label{
padding: 0.25em 1em; padding: 0.25em 1em;
margin-inline: 0.1em; margin-inline: 0.1em;
line-height: 2em; line-height: 2em;
border-radius: 1000px; border-radius: var(--radius-pill);
cursor: pointer; cursor: pointer;
position: relative; position: relative;
transition: 100ms; transition: var(--transition-fast);
user-select: none; user-select: none;
} }
.checkbox_container{ .checkbox_container{
transition: 100ms; transition: var(--transition-fast);
} }
.checkbox_container:hover, .checkbox_container:hover,
.checkbox_container:focus-within .checkbox_container:focus-within

View File

@@ -1,5 +1,4 @@
<script> <script>
import "$lib/css/nordtheme.css";
let { let {
tag = '', tag = '',
@@ -17,7 +16,7 @@
.tag-chip { .tag-chip {
all: unset; all: unset;
padding: 0.4rem 0.8rem; padding: 0.4rem 0.8rem;
border-radius: 1000px; border-radius: var(--radius-pill);
font-size: 0.9rem; font-size: 0.9rem;
cursor: pointer; cursor: pointer;
transition: all 100ms ease; transition: all 100ms ease;

View File

@@ -1,6 +1,5 @@
<script> <script>
import "$lib/css/nordtheme.css"; import TagChip from '$lib/components/recipes/TagChip.svelte';
import TagChip from './TagChip.svelte';
let { let {
availableTags = [], availableTags = [],
@@ -118,7 +117,6 @@
input { input {
all: unset; all: unset;
box-sizing: border-box; box-sizing: border-box;
font-family: sans-serif;
background: var(--nord0); background: var(--nord0);
color: var(--nord6); color: var(--nord6);
padding: 0.5rem 0.7rem; padding: 0.5rem 0.7rem;

View File

@@ -1,16 +1,11 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
let { src, placeholder_src, alt = "", children } = $props(); let { src, color = '', alt = "", transitionName = '', children } = $props();
let isloaded = $state(false);
let isredirected = $state(false); let isredirected = $state(false);
onMount(() => { onMount(() => {
const el = document.querySelector("img")
if(el?.complete){
isloaded = true
}
fetch(src, { method: 'HEAD' }) fetch(src, { method: 'HEAD' })
.then(response => { .then(response => {
isredirected = response.redirected isredirected = response.redirected
@@ -21,9 +16,7 @@
if(isredirected){ if(isredirected){
return return
} }
if(document.querySelector("img").complete){ document.querySelector("#img_carousel").showModal();
document.querySelector("#img_carousel").showModal();
}
} }
function close_dialog_img(){ function close_dialog_img(){
document.querySelector("#img_carousel").close(); document.querySelector("#img_carousel").close();
@@ -31,7 +24,7 @@
import Cross from "$lib/assets/icons/Cross.svelte"; import Cross from "$lib/assets/icons/Cross.svelte";
import "$lib/css/action_button.css"; import "$lib/css/action_button.css";
import "$lib/css/shake.css"; import "$lib/css/shake.css";
import { do_on_key } from "./do_on_key"; import { do_on_key } from "$lib/components/recipes/do_on_key";
</script> </script>
<style> <style>
:root { :root {
@@ -79,21 +72,25 @@
margin: 0; margin: 0;
} }
.image-wrap {
position: absolute;
top: 0;
left: 0;
right: 0;
margin-inline: auto;
width: min(1000px, 100dvw);
height: max(60dvh,600px);
overflow: hidden;
}
.image{ .image{
display: block; display: block;
position: absolute; position: absolute;
top: 0; top: 0;
width: min(1000px, 100dvw); width: min(1000px, 100dvw);
z-index: -1;
opacity: 0;
transition: 200ms;
height: max(60dvh,600px); height: max(60dvh,600px);
object-fit: cover; object-fit: cover;
object-position: 50% 20%; object-position: 50% 20%;
backdrop-filter: blur(20px);
filter: blur(20px);
z-index: -10;
} }
.image-container::after { .image-container::after {
@@ -106,34 +103,6 @@
:global(h1){ :global(h1){
width: 100%; width: 100%;
} }
.placeholder{
background-repeat: no-repeat;
background-size: cover;
background-position: 50% 20%;
position: absolute;
width: min(1000px, 100dvw);
height: max(60dvh,600px);
z-index: -2;
}
.placeholder_blur{
width: inherit;
height: inherit;
backdrop-filter: blur(20px);
}
div:has(.placeholder){
position: absolute;
top: 0;
left: 0;
right: 0;
margin-inline: auto;
width: min(1000px, 100dvw);
height: max(60dvh,600px);
overflow: hidden;
}
.unblur.image{
filter: blur(0px) !important;
opacity: 1;
}
/* DIALOG */ /* DIALOG */
dialog{ dialog{
@@ -174,15 +143,13 @@ dialog button{
<figure class="image-container"> <figure class="image-container">
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class:zoom-in={isloaded && !isredirected} onclick={show_dialog_img}> <div class:zoom-in={!isredirected} onclick={show_dialog_img}>
<div class=placeholder style="background-image:url({placeholder_src})" > <div class="image-wrap" style:background-color={color}>
<div class=placeholder_blur> <img class="image" {src} {alt} style:view-transition-name={transitionName || null}/>
<img class="image" class:unblur={isloaded} {src} onload={() => {isloaded=true}} {alt}/>
</div>
</div> </div>
<noscript> <noscript>
<div class=placeholder style="background-image:url({placeholder_src})" > <div class="image-wrap" style:background-color={color}>
<img class="image unblur" {src} onload={() => {isloaded=true}} {alt}/> <img class="image" {src} {alt}/>
</div> </div>
</noscript> </noscript>
</div> </div>
@@ -191,7 +158,7 @@ dialog button{
</section> </section>
<dialog id=img_carousel> <dialog id=img_carousel>
<img class:unblur={isloaded} {src} {alt}> <img {src} {alt}>
<button class=action_button onkeydown={(event) => do_on_key(event, 'Enter', false, close_dialog_img)} onclick={close_dialog_img}> <button class=action_button onkeydown={(event) => do_on_key(event, 'Enter', false, close_dialog_img)} onclick={close_dialog_img}>
<Cross fill=white width=2rem height=2rem></Cross> <Cross fill=white width=2rem height=2rem></Cross>
</button> </button>

View File

@@ -0,0 +1,162 @@
<script>
let { item, ondelete, onedit, isEnglish = false } = $props();
function getDomain(url) {
try {
return new URL(url).hostname.replace(/^www\./, '');
} catch {
return url;
}
}
</script>
<style>
.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);
}
.card:hover,
.card:focus-within {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.accent {
height: 6px;
background: linear-gradient(90deg, var(--nord10), var(--nord9));
}
.body {
padding: 0.8em 0.9em 0.6em;
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5em;
}
.name {
font-size: 1.1rem;
font-weight: 600;
line-height: 1.3;
margin: 0;
}
.links {
display: flex;
flex-wrap: wrap;
gap: 0.35em;
}
.link-pill {
font-size: 0.78rem;
padding: 0.15rem 0.55rem;
border-radius: var(--radius-pill);
background-color: var(--nord5);
color: var(--nord3);
text-decoration: none;
box-shadow: var(--shadow-sm);
transition: transform var(--transition-fast), background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast);
}
.link-pill:hover,
.link-pill:focus-visible {
transform: scale(1.05);
background-color: var(--nord8);
box-shadow: var(--shadow-hover);
color: var(--nord0);
}
@media (prefers-color-scheme: dark) {
.link-pill {
background-color: var(--nord0);
color: var(--nord4);
}
.link-pill:hover,
.link-pill:focus-visible {
background-color: var(--nord8);
color: var(--nord0);
}
}
.notes {
font-size: 0.85rem;
color: var(--nord3);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
@media (prefers-color-scheme: dark) {
.notes {
color: var(--nord4);
}
}
.footer {
font-size: 0.72rem;
color: var(--nord3);
margin-top: auto;
padding-top: 0.3em;
}
@media (prefers-color-scheme: dark) {
.footer {
color: var(--nord4);
}
}
.card-btn {
position: absolute;
top: 0.5em;
background: var(--nord11);
color: white;
border: none;
border-radius: var(--radius-pill);
width: 1.6em;
height: 1.6em;
font-size: 0.85rem;
cursor: pointer;
display: grid;
place-items: center;
opacity: 0;
transition: opacity var(--transition-fast);
z-index: 2;
}
.card:hover .card-btn,
.card:focus-within .card-btn {
opacity: 1;
}
.delete-btn {
right: 0.5em;
}
.delete-btn:hover {
background: var(--nord12);
}
.edit-btn {
right: 2.4em;
background: var(--nord10);
}
.edit-btn:hover {
background: var(--nord9);
}
</style>
<div class="card">
<div class="accent"></div>
<button class="card-btn edit-btn" onclick={() => onedit(item)} aria-label={isEnglish ? 'Edit' : 'Bearbeiten'}></button>
<button class="card-btn delete-btn" onclick={() => ondelete(item._id)} aria-label={isEnglish ? 'Delete' : 'Löschen'}></button>
<div class="body">
<p class="name">{item.name}</p>
{#if item.links?.length}
<div class="links">
{#each item.links as link (link.url)}
<a class="link-pill g-pill" href={link.url} target="_blank" rel="noopener noreferrer">
{link.label || getDomain(link.url)}
</a>
{/each}
</div>
{/if}
{#if item.notes}
<p class="notes">{item.notes}</p>
{/if}
<div class="footer">
{isEnglish ? 'Added by' : 'Hinzugefügt von'} {item.addedBy}
</div>
</div>
</div>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { TranslatedRecipeType } from '$types/types'; import type { TranslatedRecipeType } from '$types/types';
import TranslationFieldComparison from './TranslationFieldComparison.svelte'; import TranslationFieldComparison from './TranslationFieldComparison.svelte';
import CreateIngredientList from './CreateIngredientList.svelte'; import CreateIngredientList from '$lib/components/recipes/CreateIngredientList.svelte';
import CreateStepList from './CreateStepList.svelte'; import CreateStepList from '$lib/components/recipes/CreateStepList.svelte';
import GenerateAltTextButton from './GenerateAltTextButton.svelte'; import GenerateAltTextButton from './GenerateAltTextButton.svelte';
interface Props { interface Props {

View File

@@ -6,9 +6,10 @@ const LANGUAGE_CONTEXT_KEY = Symbol('language');
/** /**
* Creates or updates a language context for prayer components * Creates or updates a language context for prayer components
* @param {Object} options * @param {Object} options
* @param {'de' | 'en'} options.urlLang - The URL language (de for /glaube, en for /faith) * @param {'de' | 'en'} [options.urlLang] - The URL language (de for /glaube, en for /faith)
* @param {boolean} [options.initialLatin] - Initial state for Latin/bilingual display
*/ */
export function createLanguageContext({ urlLang = 'de' } = {}) { export function createLanguageContext({ urlLang = 'de', initialLatin = true } = {}) {
// Check if context already exists (e.g., during navigation) // Check if context already exists (e.g., during navigation)
if (hasContext(LANGUAGE_CONTEXT_KEY)) { if (hasContext(LANGUAGE_CONTEXT_KEY)) {
const existing = getContext(LANGUAGE_CONTEXT_KEY); const existing = getContext(LANGUAGE_CONTEXT_KEY);
@@ -17,7 +18,7 @@ export function createLanguageContext({ urlLang = 'de' } = {}) {
return existing; return existing;
} }
const showLatin = writable(true); // true = bilingual (Latin + vernacular), false = monolingual const showLatin = writable(initialLatin); // true = bilingual (Latin + vernacular), false = monolingual
const lang = writable(urlLang); // 'de' or 'en' based on URL const lang = writable(urlLang); // 'de' or 'en' based on URL
setContext(LANGUAGE_CONTEXT_KEY, { setContext(LANGUAGE_CONTEXT_KEY, {

View File

@@ -1,3 +1,5 @@
@import "./shake.css";
:root{ :root{
--angle: 15deg; --angle: 15deg;
} }
@@ -5,10 +7,10 @@
border: none; border: none;
cursor: pointer; cursor: pointer;
background-color: var(--red); background-color: var(--red);
transition: 200ms; transition: var(--transition-normal);
box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3); box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3);
padding: 1rem; padding: 1rem;
border-radius: 1000px; border-radius: var(--radius-pill);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -26,33 +28,3 @@
transition: 50ms; transition: 50ms;
scale: 0.8 0.8; scale: 0.8 0.8;
} }
@keyframes shake{
0%{
transform: rotate(0)
scale(1,1);
}
25%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(--angle)
scale(1.2,1.2)
;
}
50%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(calc(-1 * --angle))
scale(1.2,1.2);
}
74%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(--angle)
scale(1.2, 1.2);
}
100%{
transform: rotate(0)
scale(1.2, 1.2);
}
}

View File

@@ -1,51 +0,0 @@
form{
background-color: var(--nord5);
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;
}
@media (prefers-color-scheme: dark){
form{
background-color: var(--accent-dark);
}
}
form label{
font-size: 1.2em;
}
form input{
display: block;
font-size: 1.2rem;
}
form button{
background-color: var(--red);
color: white;
border: none;
padding: 0.5em 1em;
font-size: 1.3em;
border-radius: 1000px;
margin-top: 1em;
transition: 100ms;
}
form button:hover,
form button:focus-visible
{
scale: 1.1;
}
form p{
max-width: 400px;
margin-top: 0;
}
form h4{
margin-bottom:0;
}
@media screen and (max-width: 600px){
form{
margin-top: 0;
}
}

View File

@@ -1,178 +0,0 @@
:root{
--nord0: #2E3440;
--nord1: #3B4252;
--nord2: #434C5E;
--nord3: #4C566A;
--nord4: #D8DEE9;
--nord5: #E5E9F0;
--nord6: #ECEFF4;
--nord7: #8FBCBB;
--nord8: #88C0D0;
--nord9: #81A1C1;
--nord10: #5E81AC;
--nord11: #BF616A;
--nord12: #D08770;
--nord13: #EBCB8B;
--nord14: #A3BE8C;
--nord15: #B48EAD;
--lightblue: var(--nord9);
--blue: var(--nord10);
--red: var(--nord11);
--orange: var(--nord12);
--yellow: var(--nord13);
--green: var(--nord14);
--purple: var(--nord15);
--nord6-dark: #292c31;
--accent-dark: #1f1f21;
--background-dark: #21201b;
--font-default-dark: #ffffff;
/* 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;
}
a:not(:visited){
color: var(--blue);
}
a:visited{
color: var(--purple);
}
@media (prefers-color-scheme: dark) {
a:not(:visited){
color: var(--nord8);
}
}
*{
box-sizing: border-box;
font-family: Helvetica, Arial, "Noto Sans", sans-serif
}
body{
margin:0;
padding:0;
background-color: #fbf9f3;
overflow-x: hidden;
}
@media (prefers-color-scheme: dark) {
body{
color: white;
background-color: var(--background-dark);
}
}
/* ========================================
Global Utility Classes
Use these in components to avoid CSS duplication
======================================== */
/* Pill-shaped element base */
.g-pill {
border-radius: var(--radius-pill);
border: none;
cursor: pointer;
display: inline-block;
text-decoration: none;
transition: var(--transition-fast);
}
/* 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;
}
/* Light background button (with dark mode) */
.g-btn-light {
background-color: var(--nord5);
color: var(--nord0);
box-shadow: var(--shadow-sm);
}
@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", 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 {
font-size: 1.1rem;
padding: 0.25em 1em;
border-radius: var(--radius-pill);
background-color: var(--nord5);
color: var(--nord0);
text-decoration: none;
cursor: pointer;
transition: 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 {
color: var(--nord0);
}
}

View File

@@ -1,198 +0,0 @@
@font-face {
font-family: 'LibertineMinimal';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/LinLibertine_minimal.woff2) format('woff2'),
url(/fonts/LinLibertine_minimal.ttf) format('truetype');
}
.sbeads{
fill: var(--nord10);
}
.chain{
stroke:black;
stroke-width: 0.7;
stroke-miterlimit: 4;
stroke: gray;
fill: none;
}
.sbeads circle.hitbox{
r: 3.2px;
stroke-width:0;
}
#start1 circle{
cx:15.559271px;
cy: 20.881956px;
}
#start2 circle{
cx:21.633902px;
cy:20.367514px;
}
#start3 circle{
cx:27.96961px;
cy:21.178484px;
}
#lbead5 circle{
cx:118.50725px;
cy:59.477211px;
}
#lbead4 circle{
cx:126.81134px;
cy:15.751753px;
}
#lbead1 circle{
cx:7.6719489px;
cy:25.364584px;
}
#lbead2 circle{
cx:36.798512px;
cy:23.486462px;
}
#lbead3 circle{
cx:84.105789px;
cy:3.0456686px;
}
#lbead6 circle{
cx:72.185097px;
cy:64.006859px;
}
#start1:hover .msg,
#start2:hover .msg,
#start3:hover .msg,
#secret1:hover .msg,
#secret2:hover .msg,
#secret3:hover .msg,
#secret4:hover .msg,
#secret5:hover .msg,
#lbeads .beforedecades:hover .msg,
#lbeads .afterdecade:hover .msg,
#cross:hover .msg
{
display:block;
}
#start1:hover .sbeads circle:not(.hitbox),
#start2:hover .sbeads circle:not(.hitbox),
#start3:hover .sbeads circle:not(.hitbox),
#secret1:hover .sbeads circle,
#secret2:hover .sbeads circle,
#secret3:hover .sbeads circle,
#secret4:hover .sbeads circle,
#secret5:hover .sbeads circle
{
fill: var(--nord11);
r: 1.5px;
}
#lbead1:hover .lbead,
#lbead2:hover .lbead,
#lbead3:hover .lbead,
#lbead4:hover .lbead,
#lbead5:hover .lbead,
#lbead6:hover .lbead{
r: 2.8px;
fill: var(--nord11);
}
#cross:hover .symbol{
fill: var(--nord11);
stroke: var(--nord11);
stroke-width: 0.25;
}
#lbeads.msg{
display:block;
}
.sbeads circle{
r: 1.25px;
}
.msg .diff{
fill: var(--nord11);
}
.msg .b{
font-family: crosses;
font-weight: bold;
}
.msg .title{
fill: var(--nord10);
font-weight: bold;
font-size: 5px;
}
.msg{
font-size: 4px;
stroke: none;
fill: var(--nord4);
display:none;
}
text{
font-family: LibertineMinimal;
}
#lbeads circle.hitbox{
r:5px;
stroke:none;
stroke-width:0;
}
.lbead{
fill: var(--nord12);
r: 2.65px;
}
.hitbox{
opacity:0;
stroke-width: 2;
fill: red;
stroke: red;
}
#coin circle{
r: 2.7px;
fill:darkgray;
}
#coin text{
fill:var(--nord0);
font-size: 4.259px;
line-height:1.25;
font-family: crosses;
}
#cross .symbol{
font-family: crosses;
fill: var(--nord4);
font-size: 17.3637px;
line-height: 1.25;
stroke-width:0.434093
}
table{
width: 100%;
border-collapse: collapse;
}
td{
text-align:center;
border-left: 1px solid;
border-right: 1px solid;
border-color: var(--nord2);
padding-left: 5px;
padding-right: 5px;
}
tr :last-child{
border-right: none;
}
tr :first-child{
border-left: 0px solid;
}
thead td{
color: var(--nord4);
border-bottom-width: 3px;
border-bottom-color: var(--nord10);
border-bottom-style: dotted;
font-size: 110%;
font-weight: bold;
}
.table{
width:100%;
overflow-x: auto;
}

View File

@@ -105,3 +105,105 @@ export const mysteryReferences = {
} }
] ]
} as const; } as const;
// Reference for the three opening Ave Marias (Faith, Hope, Love)
export const theologicalVirtueReference = {
title: "Das Hohelied der Liebe",
reference: "1 Kor 13, 1-13"
} as const;
export const theologicalVirtueReferenceEnglish = {
title: "The Hymn to Love",
reference: "1 Cor 13:1-13"
} as const;
export const mysteryReferencesEnglish = {
lichtreichen: [
{
title: "The first Luminous Mystery: The Baptism in the Jordan.",
reference: "Mt 3:16-17"
},
{
title: "The second Luminous Mystery: The Wedding at Cana.",
reference: "Jn 2:1-5"
},
{
title: "The third Luminous Mystery: The Proclamation of the Kingdom of God.",
reference: "Mk 1:15"
},
{
title: "The fourth Luminous Mystery: The Transfiguration.",
reference: "Mt 17:1-2"
},
{
title: "The fifth Luminous Mystery: The Institution of the Holy Eucharist.",
reference: "Mt 26:26"
}
],
freudenreich: [
{
title: "The first Joyful Mystery: The Annunciation of the Archangel Gabriel to the Virgin Mary.",
reference: "Lk 1:26-27"
},
{
title: "The second Joyful Mystery: The Visitation of Mary to Elizabeth.",
reference: "Lk 1:39-42"
},
{
title: "The third Joyful Mystery: The Nativity of Jesus in the Stable at Bethlehem.",
reference: "Lk 2:1-7"
},
{
title: "The fourth Joyful Mystery: The Presentation of Jesus in the Temple.",
reference: "Lk 2:21-24"
},
{
title: "The fifth Joyful Mystery: The Finding of Jesus in the Temple.",
reference: "Lk 2:41-47"
}
],
schmerzhaften: [
{
title: "The first Sorrowful Mystery: The Agony of Jesus in the Garden.",
reference: "Mt 26:36-39"
},
{
title: "The second Sorrowful Mystery: The Scourging at the Pillar.",
reference: "Mt 27:26"
},
{
title: "The third Sorrowful Mystery: The Crowning with Thorns.",
reference: "Mt 27:27-29"
},
{
title: "The fourth Sorrowful Mystery: Jesus Carries the Heavy Cross.",
reference: "Mk 15:21-22"
},
{
title: "The fifth Sorrowful Mystery: The Crucifixion of Jesus.",
reference: "Lk 23:33-46"
}
],
glorreichen: [
{
title: "The first Glorious Mystery: The Resurrection of Jesus.",
reference: "Lk 24:1-6"
},
{
title: "The second Glorious Mystery: The Ascension of Jesus.",
reference: "Mk 16:19"
},
{
title: "The third Glorious Mystery: The Descent of the Holy Spirit.",
reference: "Acts 2:1-4"
},
{
title: "The fourth Glorious Mystery: The Assumption of Mary into Heaven.",
reference: "Lk 1:48-49"
},
{
title: "The fifth Glorious Mystery: The Coronation of Mary as Queen of Heaven and Earth.",
reference: "Apo 12:1"
}
]
} as const;

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