feat(calendar): grid color polish, month header layout, direct event links
- month label left + larger, prev/next arrows grouped right - weekday headers colored (himmel/stein by weekday/weekend) - day cells colored by weekday/weekend + event state - event anchor IDs, onDestroy SSR fix, ImageModal component - copy link button, action buttons always show text Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,10 +11,7 @@
|
||||
mapsUrl,
|
||||
type CalendarItemICS,
|
||||
} from "$lib/calendar-ics";
|
||||
import {
|
||||
getCmsImageUrl,
|
||||
extractCmsImageField,
|
||||
} from "$lib/rusty-image";
|
||||
import { getCmsImageUrl, extractCmsImageField } from "$lib/rusty-image";
|
||||
import "$lib/iconify-offline";
|
||||
import Icon from "@iconify/svelte";
|
||||
import ImageModal from "$lib/components/ImageModal.svelte";
|
||||
@@ -432,9 +429,7 @@
|
||||
const webcalUrl = $derived(
|
||||
feedHost ? `webcal://${feedHost}/api/kalender.ics` : "#",
|
||||
);
|
||||
const icsUrl = $derived(
|
||||
feedHost ? `/api/kalender.ics` : "#",
|
||||
);
|
||||
const icsUrl = $derived(feedHost ? `/api/kalender.ics` : "#");
|
||||
const isApplePlatform = $derived(
|
||||
feedHost
|
||||
? /iPhone|iPad|iPod|Macintosh/.test(navigator.userAgent)
|
||||
@@ -566,7 +561,10 @@
|
||||
class="event-item {isNext ? 'event-item-next' : ''}"
|
||||
data-urgency={urgency}
|
||||
>
|
||||
<details class="group/event" open={openByDefault || linkedSlug === eventId(item)}>
|
||||
<details
|
||||
class="group/event"
|
||||
open={openByDefault || linkedSlug === eventId(item)}
|
||||
>
|
||||
<summary
|
||||
class="cursor-pointer list-none p-3 flex items-start gap-3 hover:bg-himmel-50/60 focus-visible:outline focus-visible:outline-2 focus-visible:outline-himmel-500"
|
||||
>
|
||||
@@ -716,12 +714,28 @@
|
||||
{#if item.image}
|
||||
{@const imageField = extractCmsImageField(item.image)}
|
||||
{#if imageField}
|
||||
{@const thumbSrc = getCmsImageUrl(imageField.url, { width: 160, height: 160, fit: "cover" })}
|
||||
{@const fullSrc = getCmsImageUrl(imageField.url, { width: 1400, fit: "contain" })}
|
||||
{@const imageAlt = imageField.alt ?? (typeof item.image === "object" && "description" in item.image ? (item.image.description ?? "") : "")}
|
||||
{@const thumbSrc = getCmsImageUrl(imageField.url, {
|
||||
width: 160,
|
||||
height: 160,
|
||||
fit: "cover",
|
||||
})}
|
||||
{@const fullSrc = getCmsImageUrl(imageField.url, {
|
||||
width: 1400,
|
||||
fit: "contain",
|
||||
})}
|
||||
{@const imageAlt =
|
||||
imageField.alt ??
|
||||
(typeof item.image === "object" &&
|
||||
"description" in item.image
|
||||
? (item.image.description ?? "")
|
||||
: "")}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (modalImage = { src: fullSrc, alt: imageAlt })}
|
||||
onclick={() =>
|
||||
(modalImage = {
|
||||
src: fullSrc,
|
||||
alt: imageAlt,
|
||||
})}
|
||||
class="group relative w-24 h-24 rounded overflow-hidden border border-stein-200 shrink-0 hover:opacity-90 transition-opacity"
|
||||
aria-label="Bild vergrößern"
|
||||
>
|
||||
@@ -731,8 +745,14 @@
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span class="absolute inset-0 flex items-center justify-center bg-black/0 group-hover:bg-black/20 transition-colors">
|
||||
<Icon icon="mdi:magnify-plus" class="size-5 text-white opacity-0 group-hover:opacity-100 transition-opacity drop-shadow" aria-hidden="true" />
|
||||
<span
|
||||
class="absolute inset-0 flex items-center justify-center bg-black/0 group-hover:bg-black/20 transition-colors"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:magnify-plus"
|
||||
class="size-5 text-white opacity-0 group-hover:opacity-100 transition-opacity drop-shadow"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -767,7 +787,9 @@
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap">{t(T.calendar_open_maps)}</span>
|
||||
<span class="whitespace-nowrap"
|
||||
>{t(T.calendar_open_maps)}</span
|
||||
>
|
||||
</a>
|
||||
{/if}
|
||||
{#if item.attachment?.src}
|
||||
@@ -775,10 +797,18 @@
|
||||
<a
|
||||
href={dlSrc}
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={item.attachment.label || "Anhang herunterladen"}
|
||||
aria-label={item.attachment.label ||
|
||||
"Anhang herunterladen"}
|
||||
>
|
||||
<Icon icon="mdi:file-download" class="size-3.5 shrink-0" aria-hidden="true" />
|
||||
<span class="whitespace-nowrap">{item.attachment.label || "Anhang herunterladen"}</span>
|
||||
<Icon
|
||||
icon="mdi:file-download"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap"
|
||||
>{item.attachment.label ||
|
||||
"Anhang herunterladen"}</span
|
||||
>
|
||||
</a>
|
||||
{/if}
|
||||
<button
|
||||
@@ -793,7 +823,9 @@
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap">{t(T.calendar_download_ics)}</span>
|
||||
<span class="whitespace-nowrap"
|
||||
>{t(T.calendar_download_ics)}</span
|
||||
>
|
||||
</button>
|
||||
<a
|
||||
href={googleCalUrl(toICS(item))}
|
||||
@@ -807,7 +839,9 @@
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap">{t(T.calendar_add_to_google)}</span>
|
||||
<span class="whitespace-nowrap"
|
||||
>{t(T.calendar_add_to_google)}</span
|
||||
>
|
||||
</a>
|
||||
{#if feedHost}
|
||||
{@const shareKey =
|
||||
@@ -820,17 +854,22 @@
|
||||
aria-label={t(T.post_action_copy)}
|
||||
>
|
||||
<Icon
|
||||
icon={copied ? "mdi:check" : "mdi:link-variant"}
|
||||
icon={copied
|
||||
? "mdi:check"
|
||||
: "mdi:link-variant"}
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap">{copied ? t(T.post_action_copied) : t(T.post_action_copy)}</span>
|
||||
<span class="whitespace-nowrap"
|
||||
>{copied
|
||||
? t(T.post_action_copied)
|
||||
: t(T.post_action_copy)}</span
|
||||
>
|
||||
</button>
|
||||
{@const shared = shareToastKey === shareKey}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() =>
|
||||
shareEvent(item, title, locText)}
|
||||
onclick={() => shareEvent(item, title, locText)}
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={t(T.calendar_share_aria)}
|
||||
>
|
||||
@@ -841,7 +880,11 @@
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap">{shared ? t(T.calendar_share_copied) : t(T.calendar_share)}</span>
|
||||
<span class="whitespace-nowrap"
|
||||
>{shared
|
||||
? t(T.calendar_share_copied)
|
||||
: t(T.calendar_share)}</span
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
@@ -855,7 +898,9 @@
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap">{t(T.calendar_jump_to_month)}</span>
|
||||
<span class="whitespace-nowrap"
|
||||
>{t(T.calendar_jump_to_month)}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -908,52 +953,52 @@
|
||||
Apple: webcal:// Link → öffnet nativ Kalender.app / iOS Kalender.
|
||||
Alle anderen: direkter ICS-Download als Fallback. -->
|
||||
{#if feedHost}
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 px-4 py-2.5 border-b border-stein-200 bg-himmel-100"
|
||||
>
|
||||
{#if isApplePlatform}
|
||||
<a
|
||||
href={webcalUrl}
|
||||
aria-label={t(T.calendar_subscribe_aria)}
|
||||
class="btn-primary btn-sm inline-flex items-center gap-1.5 min-h-[36px]"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:calendar-plus"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(T.calendar_subscribe)}
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={icsUrl}
|
||||
download="windwiderstand-termine.ics"
|
||||
aria-label={t(T.calendar_subscribe_aria)}
|
||||
class="btn-primary btn-sm inline-flex items-center gap-1.5 min-h-[36px]"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:calendar-plus"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(T.calendar_subscribe)}
|
||||
</a>
|
||||
{/if}
|
||||
{#if items.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
onclick={exportAll}
|
||||
class="btn-outline btn-sm inline-flex items-center gap-1.5 min-h-[36px]"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:download"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(T.calendar_export_all)}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 px-4 py-2.5 border-b border-stein-200 bg-himmel-100"
|
||||
>
|
||||
{#if isApplePlatform}
|
||||
<a
|
||||
href={webcalUrl}
|
||||
aria-label={t(T.calendar_subscribe_aria)}
|
||||
class="btn-primary btn-sm inline-flex items-center gap-1.5 min-h-[36px]"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:calendar-plus"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(T.calendar_subscribe)}
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={icsUrl}
|
||||
download="windwiderstand-termine.ics"
|
||||
aria-label={t(T.calendar_subscribe_aria)}
|
||||
class="btn-primary btn-sm inline-flex items-center gap-1.5 min-h-[36px]"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:calendar-plus"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(T.calendar_subscribe)}
|
||||
</a>
|
||||
{/if}
|
||||
{#if items.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
onclick={exportAll}
|
||||
class="btn-outline btn-sm inline-flex items-center gap-1.5 min-h-[36px]"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:download"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(T.calendar_export_all)}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Kalender-Widget: oben, kompakt auf Mobile-Breite gecappt (sonst
|
||||
@@ -964,41 +1009,45 @@
|
||||
class="calendar-widget bg-himmel-100 text-himmel-900 p-4"
|
||||
>
|
||||
<div class="w-full">
|
||||
<div class="flex items-center justify-between gap-2 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-xs text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
|
||||
aria-label={t(T.calendar_prev_month)}
|
||||
onclick={prevMonth}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:chevron-left"
|
||||
class="size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<div class="flex items-center justify-between gap-2 mb-2">
|
||||
<span
|
||||
class="text-sm font-medium text-himmel-900 capitalize whitespace-nowrap"
|
||||
class="text-xl font-semibold text-himmel-900 capitalize whitespace-nowrap"
|
||||
>{monthLabel}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-xs text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
|
||||
aria-label={t(T.calendar_next_month)}
|
||||
onclick={nextMonth}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:chevron-right"
|
||||
class="size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-xs text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
|
||||
aria-label={t(T.calendar_prev_month)}
|
||||
onclick={prevMonth}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:chevron-left"
|
||||
class="size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-xs text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
|
||||
aria-label={t(T.calendar_next_month)}
|
||||
onclick={nextMonth}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:chevron-right"
|
||||
class="size-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 gap-1.5 text-center text-sm">
|
||||
{#each weekdays as w}
|
||||
{#each weekdays as w, i}
|
||||
<div
|
||||
class="py-1.5 text-xs font-semibold text-himmel-800"
|
||||
class="py-1.5 text-xs font-bold rounded-xs {i >= 5
|
||||
? 'bg-stein-100/80 text-stein-600'
|
||||
: 'bg-himmel-200/50 text-himmel-800'}"
|
||||
>
|
||||
{w}
|
||||
</div>
|
||||
@@ -1014,14 +1063,22 @@
|
||||
{@const isSelected = selectedDay === key}
|
||||
{@const isToday = key === todayKey}
|
||||
{@const isFuture = key >= todayKey}
|
||||
{@const isWeekend =
|
||||
d.getDay() === 0 || d.getDay() === 6}
|
||||
{#if hasEvents}
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-10 rounded-xs flex flex-col items-center justify-center min-w-0 transition-colors cursor-pointer {isCurrentMonth
|
||||
? 'text-himmel-900 hover:bg-himmel-200'
|
||||
: 'text-himmel-700 opacity-60 hover:opacity-80'} bg-himmel-200/70 font-semibold hover:bg-himmel-300 {isSelected
|
||||
? 'ring-2 ring-himmel-700 bg-himmel-300'
|
||||
: ''} {isToday ? 'today-cell' : ''}"
|
||||
class="relative h-10 rounded-xs flex flex-col items-center justify-center min-w-0 transition-colors cursor-pointer font-semibold
|
||||
{isCurrentMonth
|
||||
? 'text-himmel-900'
|
||||
: 'text-himmel-700 opacity-60 hover:opacity-80'}
|
||||
{isWeekend
|
||||
? 'bg-erde-100 hover:bg-erde-200'
|
||||
: 'bg-himmel-200/80 hover:bg-himmel-300'}
|
||||
{isSelected
|
||||
? 'ring-2 ring-himmel-700 brightness-95'
|
||||
: ''}
|
||||
{isToday ? 'today-cell' : ''}"
|
||||
onclick={() => selectDay(key)}
|
||||
onmouseenter={(e) => showTooltip(e, key)}
|
||||
onmouseleave={hideTooltip}
|
||||
@@ -1049,11 +1106,14 @@
|
||||
</button>
|
||||
{:else}
|
||||
<div
|
||||
class="relative h-10 rounded-xs flex flex-col items-center justify-center min-w-0 cursor-default bg-himmel-50/40 {isCurrentMonth
|
||||
class="relative h-10 rounded-xs flex flex-col items-center justify-center min-w-0 cursor-default
|
||||
{isWeekend
|
||||
? 'bg-stein-100/60'
|
||||
: 'bg-himmel-50/50'}
|
||||
{isCurrentMonth
|
||||
? 'text-himmel-800'
|
||||
: 'text-himmel-600 opacity-50'} {isToday
|
||||
? 'today-cell'
|
||||
: ''}"
|
||||
: 'text-himmel-600 opacity-50'}
|
||||
{isToday ? 'today-cell' : ''}"
|
||||
aria-current={isToday ? "date" : undefined}
|
||||
>
|
||||
<span class="text-xs">{d.getDate()}</span>
|
||||
@@ -1080,19 +1140,23 @@
|
||||
>
|
||||
{#each tooltipEvents.slice(0, 3) as ev}
|
||||
<div class="cal-tooltip-row">
|
||||
<span class="cal-tooltip-title">{ev.title || t(T.calendar_untitled)}</span>
|
||||
<span class="cal-tooltip-title"
|
||||
>{ev.title || t(T.calendar_untitled)}</span
|
||||
>
|
||||
{#if ev.timeStr}
|
||||
<span class="cal-tooltip-time">{ev.timeStr}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if tooltipEvents.length > 3}
|
||||
<div class="cal-tooltip-more">+{tooltipEvents.length - 3} weitere</div>
|
||||
<div class="cal-tooltip-more">
|
||||
+{tooltipEvents.length - 3} weitere
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Events: voll Breite, unten. Akkordeon-Liste, in natürlichem
|
||||
<!-- Events: voll Breite, unten. Akkordeon-Liste, in natürlichem
|
||||
Page-Flow (kein internes Scrollen). Kalender-Klick filtert auf
|
||||
einen Tag; ohne Filter Liste ab heute, gruppiert nach Tag. -->
|
||||
<div class="calendar-events-panel bg-stein-0 border-t border-stein-200">
|
||||
@@ -1225,7 +1289,11 @@
|
||||
</div>
|
||||
|
||||
{#if modalImage}
|
||||
<ImageModal src={modalImage.src} alt={modalImage.alt} onclose={() => (modalImage = null)} />
|
||||
<ImageModal
|
||||
src={modalImage.src}
|
||||
alt={modalImage.alt}
|
||||
onclose={() => (modalImage = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@@ -1235,7 +1303,11 @@
|
||||
.calendar-block .today-cell {
|
||||
outline: 2px solid var(--color-wald-400, #84e0a4);
|
||||
outline-offset: -2px;
|
||||
background-color: color-mix(in srgb, var(--color-wald-200, #bbf0cf) 40%, transparent);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-wald-200, #bbf0cf) 40%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
/* Event-Rows visuell trennen: Card-Optik mit Urgency-Akzent links,
|
||||
Zebra-Stripe ab dem zweiten Eintrag, deutlicher Separator
|
||||
|
||||
Reference in New Issue
Block a user