diff --git a/src/app.css b/src/app.css index c25ec29..4e9a5aa 100644 --- a/src/app.css +++ b/src/app.css @@ -279,12 +279,12 @@ h4 { } /* Content-Links: Himmel (Design System – Links) */ -main a:not(.no-underline) { +main a:not(.no-underline):not([class*="btn"]) { color: var(--color-link); text-decoration: underline; } -main a:not(.no-underline):hover { +main a:not(.no-underline):not([class*="btn"]):hover { color: var(--color-himmel-600); } @@ -317,9 +317,17 @@ main ol:not(.not-prose) li { list-style-type: decimal; } +/* Buttons – gemeinsame Basis */ +.btn-primary, +.btn-secondary, +.btn-tertiary, +.btn-outline { + @apply inline-flex items-center gap-1 no-underline whitespace-nowrap transition-colors duration-200 font-medium px-4 py-2 rounded-xs; +} + /* Buttons – Wald (Primary) */ .btn-primary { - @apply inline-block no-underline transition-colors duration-200 font-medium px-4 py-2 rounded-xs shadow-sm bg-btn-bg text-btn-txt; + @apply shadow-sm bg-btn-bg text-btn-txt; } .btn-primary:hover { @@ -328,13 +336,36 @@ main ol:not(.not-prose) li { /* Buttons – Stein outline (Secondary) */ .btn-secondary { - @apply inline-block no-underline transition-colors duration-200 font-medium px-4 py-2 rounded-xs border border-stein-300 bg-white text-stein-700 shadow-sm; + @apply border border-stein-300 bg-white text-stein-700 shadow-sm; } .btn-secondary:hover { @apply bg-stein-100 border-stein-400; } +/* Buttons – Himmel (Tertiary) */ +.btn-tertiary { + @apply shadow-sm bg-himmel-700 text-white; +} + +.btn-tertiary:hover { + @apply bg-himmel-800; +} + +/* Buttons – Stein-Border klein (Outline) */ +.btn-outline { + @apply border border-stein-300 text-stein-700; +} + +.btn-outline:hover { + @apply bg-stein-100; +} + +/* Größen-Modifier — nach den btn-* definiert damit override greift */ +.btn-sm { + @apply px-2 py-1 text-xs gap-1; +} + /* Dekorativer Hintergrund mit zwei verschwommenen Verlaufs-Blobs (Wald → Erde). Nutzung: Container mit relativer Positionierung; Blobs sitzen in ::before/::after. Inhalt sollte z-Index >= 0 haben (Standard); Blobs liegen unter Stacking-Kontext. */ @@ -571,30 +602,6 @@ footer a:hover, background: var(--color-container-breakout); } -/* Kalender: Container Query – links Kalender, rechts Eventblock ab ~500px Containerbreite */ -.calendar-block { - container-type: inline-size; - container-name: calendar; -} -@container calendar (min-width: 600px) { - .calendar-block .calendar-grid-wrapper { - grid-template-columns: auto 1fr; - } - .calendar-block .calendar-column { - min-width: 300px; - max-width: 450px; - } - .calendar-block .calendar-events-wrapper { - position: relative; - } - .calendar-block .calendar-events-panel { - position: absolute; - inset: 0; - border-top: none; - border-left: 1px solid var(--color-stein-200); - } -} - /* ── Print rules ──────────────────────────────────────────────────────────── Goal: print only the article. `print:hidden` on chrome (header/footer/ banner/breadcrumbs/comments) handles structural removal. The rules below diff --git a/src/lib/calendar-ics.ts b/src/lib/calendar-ics.ts index 37a07e6..029d8db 100644 --- a/src/lib/calendar-ics.ts +++ b/src/lib/calendar-ics.ts @@ -122,25 +122,7 @@ export function downloadICS(item: CalendarItemICS, filename: string): void { URL.revokeObjectURL(url); } -/** Google-Calendar's render-template URL. Times must be UTC, in - * YYYYMMDDTHHMMSSZ format, joined by `/`. */ -export function googleCalUrl(item: CalendarItemICS): string { - const start = item.start; - const end = - item.end ?? new Date(start.getTime() + 60 * 60 * 1000); - const dates = `${fmtICSDate(start)}/${fmtICSDate(end)}`; - const params = new URLSearchParams({ - action: "TEMPLATE", - text: item.title, - dates, - }); - if (item.description) params.set("details", item.description); - if (item.location) params.set("location", item.location); - return `https://calendar.google.com/calendar/render?${params.toString()}`; -} - -/** Generic maps URL — Google Maps search by query. Safe with addresses - * or place names. */ +/** OpenStreetMap search URL for an address or place name. */ export function mapsUrl(location: string): string { - return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(location)}`; + return `https://www.openstreetmap.org/search?query=${encodeURIComponent(location)}`; } diff --git a/src/lib/components/blocks/CalendarBlock.svelte b/src/lib/components/blocks/CalendarBlock.svelte index ec6a5d8..5c2b505 100644 --- a/src/lib/components/blocks/CalendarBlock.svelte +++ b/src/lib/components/blocks/CalendarBlock.svelte @@ -9,7 +9,6 @@ import type { Translations } from "$lib/translations"; import { downloadICS, - googleCalUrl, mapsUrl, type CalendarItemICS, } from "$lib/calendar-ics"; @@ -90,6 +89,17 @@ return ""; } + /** Location kann string ODER {text, lat, lng}-Objekt sein + * (RustyCMS-Resolve liefert eventLocation-Struktur). */ + function locationText(loc: unknown): string { + if (typeof loc === "string") return loc; + if (loc && typeof loc === "object") { + const t = (loc as { text?: unknown }).text; + if (typeof t === "string") return t; + } + return ""; + } + /** Normalised, sorted item list. Filters out malformed entries. */ const items = $derived.by((): EventCardItem[] => { const raw = block.items ?? []; @@ -312,10 +322,12 @@ } /** "Show me this in the calendar" — used from accordion bodies for - * events that may be in a different month than the current view. */ + * events that may be in a different month than the current view. + * Scrolls back up to the widget since it now sits above the list. */ function jumpToEventInGrid(ev: EventCardItem) { currentMonth = new Date(ev.start.getFullYear(), ev.start.getMonth(), 1); selectedDay = dayKey(ev.start); + widgetEl?.scrollIntoView({ block: "start", behavior: "smooth" }); } /** Visual urgency-bucket for the colored left-border on each event @@ -346,7 +358,7 @@ start: ev.start, end: ev.end ?? null, description: ev.description ?? undefined, - location: ev.location ?? undefined, + location: locationText(ev.location) || undefined, url: getEventLinkHref(ev.link) || undefined, }; } @@ -374,22 +386,12 @@ t(T.calendar_weekday_so), ]); - let panelEl: HTMLElement | null = $state(null); + let widgetEl: HTMLElement | null = $state(null); onMount(() => { tickTimer = setInterval(() => { nowMs = Date.now(); }, 60_000); - // After hydration, scroll the next-up event into view inside its - // panel (panel-local scroll, not page scroll — jumping the whole - // page when the block is below the fold would be hostile). - requestAnimationFrame(() => { - if (!panelEl) return; - const target = panelEl.querySelector(".event-item-next"); - if (target) { - target.scrollIntoView({ block: "start", behavior: "auto" }); - } - }); }); onDestroy(() => { if (tickTimer) clearInterval(tickTimer); @@ -406,10 +408,11 @@ {@const live = liveStatus(item)} {@const title = item.title || t(T.calendar_untitled)} {@const urgency = urgencyOf(item)} + {@const locText = locationText(item.location)} {@const hasBody = !!( item.description || eventHref || - item.location || + locText || item.isMultiDay )}
  • {:else} -
    -
    +
    +
    {item.start.toLocaleDateString("de-DE", { weekday: "short" })}
    -
    +
    {item.start.getDate()}
    -
    +
    {item.start.toLocaleDateString("de-DE", { month: "short" })}
    @@ -468,10 +471,10 @@ {item.timeStr} {/if} - {#if item.location} + {#if locText} {/if} {#if item.tags && item.tags.length > 0} @@ -496,18 +499,6 @@
    {relativeDays(item.start)}
    - {#if item.location} - - - {/if} {#if item.description}

    {item.description}

    {/if} @@ -526,33 +517,35 @@ href={eventHref} target="_blank" rel="noopener noreferrer" - class="inline-flex items-center gap-1 rounded-xs bg-himmel-700 text-himmel-50 px-2 py-1 font-medium hover:bg-himmel-800" + class="btn-tertiary btn-sm" > {t(T.calendar_more_info)}
    {/if} -
    - -
    + +
    +
    -
    +
    {#each weekdays as w}
    {w}
    {/each} {#each calendarDays as d} {#if d === null} - + {:else} {@const key = dayKey(d)} {@const isCurrentMonth = d.getMonth() === month} @@ -641,7 +639,7 @@ {#if hasEvents}
    - -
    -
    - {#if items.length === 0} -

    {t(T.calendar_no_events)}

    - {:else if selectedDay} -
    -
    - {new Date(selectedDay + "T12:00:00").toLocaleDateString("de-DE", { - weekday: "long", - day: "numeric", - month: "long", - })} -
    - + +
    + {#if items.length === 0} +

    {t(T.calendar_no_events)}

    + {:else if selectedDay} +
    +
    + {new Date(selectedDay + "T12:00:00").toLocaleDateString("de-DE", { + weekday: "long", + day: "numeric", + month: "long", + })}
    -
      - {#each selectedDayEvents as item} - {@render eventAccordion(item, true, false)} - {/each} -
    - {:else if futureEventsByDay.length > 0} -
    -

    - {t(T.calendar_all_upcoming)} -

    - - {t(T.calendar_event_count, { n: futureEvents.length })} - -
    -
      - {#each futureEventsByDay as group, gIdx} -
    • -
      - {fmtGroupHeader(group.date)} - · {relativeDays(group.date)} -
      -
        - {#each group.events as item, eIdx} - {@render eventAccordion(item, gIdx === 0, gIdx === 0 && eIdx === 0)} - {/each} -
      -
    • - {/each} -
    - {#if pastEvents.length > 0} -
    - - -
      - {#each pastEvents as item} - {@render eventAccordion(item, false, false)} + +
    +
      + {#each selectedDayEvents as item} + {@render eventAccordion(item, true, false)} + {/each} +
    + {:else if futureEventsByDay.length > 0} +
    +

    + {t(T.calendar_all_upcoming)} +

    + + {t(T.calendar_event_count, { n: futureEvents.length })} + +
    +
      + {#each futureEventsByDay as group, gIdx} +
    • +
      + {fmtGroupHeader(group.date)} + · {relativeDays(group.date)} +
      +
        + {#each group.events as item, eIdx} + {@render eventAccordion(item, gIdx === 0, gIdx === 0 && eIdx === 0)} {/each}
      - - {/if} - {:else if pastEvents.length > 0} - -
      - +
    • + {/each} +
    + {#if pastEvents.length > 0} +
    + -
      +
        {#each pastEvents as item} {@render eventAccordion(item, false, false)} {/each}
    - {:else} -

    {t(T.calendar_no_future_events)}

    {/if} -
    -
    + {:else if pastEvents.length > 0} + +
    + + +
      + {#each pastEvents as item} + {@render eventAccordion(item, false, false)} + {/each} +
    +
    + {:else} +

    {t(T.calendar_no_future_events)}

    + {/if}
    @@ -831,8 +824,8 @@ .calendar-block .day-group + .day-group { border-top: 4px solid var(--color-stein-100, #ede8df); } - /* Print: alle
    geöffnet, Chevrons aus, Akzent-Farben in - Graustufen damit der Druck lesbar bleibt. */ + /* Print: alle
    geöffnet, Chevrons aus, Day-Groups nicht + splitten. */ @media print { .calendar-block details > *:not(summary) { display: block !important; @@ -843,9 +836,5 @@ .calendar-block .day-group { break-inside: avoid; } - .calendar-block .calendar-events-panel { - overflow: visible !important; - max-height: none !important; - } } diff --git a/src/lib/iconify-mdi-subset.generated.json b/src/lib/iconify-mdi-subset.generated.json index 59363fa..ad34277 100644 --- a/src/lib/iconify-mdi-subset.generated.json +++ b/src/lib/iconify-mdi-subset.generated.json @@ -72,9 +72,6 @@ "download": { "body": "" }, - "google": { - "body": "" - }, "calendar-search": { "body": "" }, diff --git a/src/lib/translations.ts b/src/lib/translations.ts index a8baa2e..6ad8e54 100644 --- a/src/lib/translations.ts +++ b/src/lib/translations.ts @@ -121,7 +121,6 @@ const TRANSLATION_KEYS = [ "calendar_no_future_events", "calendar_clear_filter", "calendar_event_count", // {{n}} - "calendar_add_to_google", "calendar_download_ics", "calendar_open_maps", "calendar_show_past",