feat(calendar): hover tooltip, light theme grid, UX polish
- Hover tooltip on event cells (pointer:fine only, position:fixed bypasses overflow-hidden) — shows title + right-aligned time, max 3 events, "+N weitere" - Grid background changed to himmel-100 (light), all text tokens updated to dark - Today cell: wald-200 green background + wald-400 outline - Empty day cells: subtle bg-himmel-50/40 tint - Event badge larger on desktop (sm:min-w-4.5 sm:h-4.5 sm:text-[10px]) - Weekday headers: font-semibold text-himmel-800 - Calendar takes full width inside widget (removed max-w-2xl cap) - Web Share API per event (navigator.share / clipboard fallback, Kopiert\! toast) - Subscribe bar: webcal:// on Apple, direct ICS download on other platforms Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -405,6 +405,24 @@
|
||||
t(T.calendar_weekday_so),
|
||||
]);
|
||||
|
||||
let hasHover = $state(false);
|
||||
let tooltipDay = $state<string | null>(null);
|
||||
let tooltipPos = $state({ x: 0, y: 0 });
|
||||
|
||||
function showTooltip(e: MouseEvent, key: string) {
|
||||
if (!hasHover) return;
|
||||
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
tooltipPos = { x: r.left + r.width / 2, y: r.top };
|
||||
tooltipDay = key;
|
||||
}
|
||||
function hideTooltip() {
|
||||
tooltipDay = null;
|
||||
}
|
||||
|
||||
const tooltipEvents = $derived(
|
||||
tooltipDay ? (eventsByDay.get(tooltipDay) ?? []) : [],
|
||||
);
|
||||
|
||||
let feedHost = $state("");
|
||||
const webcalUrl = $derived(
|
||||
feedHost ? `webcal://${feedHost}/api/kalender.ics` : "#",
|
||||
@@ -474,6 +492,9 @@
|
||||
|
||||
onMount(() => {
|
||||
feedHost = window.location.host;
|
||||
hasHover = window.matchMedia(
|
||||
"(hover: hover) and (pointer: fine)",
|
||||
).matches;
|
||||
tickTimer = setInterval(() => {
|
||||
nowMs = Date.now();
|
||||
}, 60_000);
|
||||
@@ -852,13 +873,13 @@
|
||||
Tag-Grid. Klick auf Tag filtert Liste unten. -->
|
||||
<div
|
||||
bind:this={widgetEl}
|
||||
class="calendar-widget bg-himmel-800 text-himmel-100 p-4"
|
||||
class="calendar-widget bg-himmel-100 text-himmel-900 p-4"
|
||||
>
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<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-100 hover:bg-himmel-700 hover:text-himmel-50 transition-colors"
|
||||
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}
|
||||
>
|
||||
@@ -869,12 +890,12 @@
|
||||
/>
|
||||
</button>
|
||||
<span
|
||||
class="text-sm font-medium text-himmel-100 capitalize whitespace-nowrap"
|
||||
class="text-sm font-medium text-himmel-900 capitalize whitespace-nowrap"
|
||||
>{monthLabel}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded-xs text-himmel-100 hover:bg-himmel-700 hover:text-himmel-50 transition-colors"
|
||||
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}
|
||||
>
|
||||
@@ -889,7 +910,7 @@
|
||||
<div class="grid grid-cols-7 gap-1.5 text-center text-sm">
|
||||
{#each weekdays as w}
|
||||
<div
|
||||
class="py-1.5 text-xs font-medium text-himmel-100 opacity-80"
|
||||
class="py-1.5 text-xs font-semibold text-himmel-800"
|
||||
>
|
||||
{w}
|
||||
</div>
|
||||
@@ -909,11 +930,13 @@
|
||||
<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-100 hover:bg-himmel-700'
|
||||
: 'text-himmel-100 opacity-60 hover:opacity-80'} bg-himmel-700/50 font-semibold hover:bg-himmel-600 {isSelected
|
||||
? 'ring-2 ring-himmel-400 bg-himmel-600'
|
||||
? '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' : ''}"
|
||||
onclick={() => selectDay(key)}
|
||||
onmouseenter={(e) => showTooltip(e, key)}
|
||||
onmouseleave={hideTooltip}
|
||||
aria-pressed={isSelected}
|
||||
aria-current={isToday ? "date" : undefined}
|
||||
aria-label="{d.toLocaleDateString('de-DE', {
|
||||
@@ -928,7 +951,7 @@
|
||||
>
|
||||
<span class="text-xs">{d.getDate()}</span>
|
||||
<span
|
||||
class="absolute -bottom-1 -right-1 inline-flex items-center justify-center min-w-3.5 h-3.5 px-0.5 rounded-full text-stein-0 text-[9px] font-medium leading-none {isFuture
|
||||
class="absolute -bottom-1 -right-1 inline-flex items-center justify-center min-w-3.5 h-3.5 sm:min-w-4.5 sm:h-4.5 px-0.5 rounded-full text-stein-0 text-[9px] sm:text-[10px] font-medium leading-none {isFuture
|
||||
? 'bg-wald-400'
|
||||
: 'bg-error'}"
|
||||
aria-hidden="true"
|
||||
@@ -938,9 +961,9 @@
|
||||
</button>
|
||||
{:else}
|
||||
<div
|
||||
class="relative h-10 rounded-xs flex flex-col items-center justify-center min-w-0 cursor-default {isCurrentMonth
|
||||
? 'text-himmel-100'
|
||||
: 'text-himmel-100 opacity-50'} {isToday
|
||||
class="relative h-10 rounded-xs flex flex-col items-center justify-center min-w-0 cursor-default bg-himmel-50/40 {isCurrentMonth
|
||||
? 'text-himmel-800'
|
||||
: 'text-himmel-600 opacity-50'} {isToday
|
||||
? 'today-cell'
|
||||
: ''}"
|
||||
aria-current={isToday ? "date" : undefined}
|
||||
@@ -959,7 +982,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events: voll Breite, unten. Akkordeon-Liste, in natürlichem
|
||||
<!-- Hover-Tooltip: fixed-positioned → nicht durch overflow-hidden geclipt.
|
||||
Nur auf pointer:fine Geräten (hasHover). Zeigt max 3 Events des Tages. -->
|
||||
{#if hasHover && tooltipDay && tooltipEvents.length > 0}
|
||||
<div
|
||||
class="cal-tooltip"
|
||||
style="left:{tooltipPos.x}px; top:{tooltipPos.y}px;"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{#each tooltipEvents.slice(0, 3) as ev}
|
||||
<div class="cal-tooltip-row">
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- 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">
|
||||
@@ -1098,6 +1143,7 @@
|
||||
.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);
|
||||
}
|
||||
/* Event-Rows visuell trennen: Card-Optik mit Urgency-Akzent links,
|
||||
Zebra-Stripe ab dem zweiten Eintrag, deutlicher Separator
|
||||
@@ -1151,6 +1197,77 @@
|
||||
.calendar-block .day-group + .day-group {
|
||||
border-top: 4px solid var(--color-stein-100, #ede8df);
|
||||
}
|
||||
/* Hover-Tooltip: fixed über dem Grid, zeigt Event-Vorschau auf hover.
|
||||
pointer-events:none damit kein mouseenter/leave Loop entsteht. */
|
||||
:global(.cal-tooltip) {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
transform: translateX(-50%) translateY(calc(-100% - 8px));
|
||||
background: #fff;
|
||||
color: var(--color-stein-900, #1c1a17);
|
||||
border: 1px solid var(--color-stein-200, #e0dcd5);
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
pointer-events: none;
|
||||
min-width: 130px;
|
||||
max-width: 220px;
|
||||
}
|
||||
:global(.cal-tooltip::after) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--color-stein-200, #e0dcd5);
|
||||
}
|
||||
:global(.cal-tooltip::before) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: calc(100% - 1px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
:global(.cal-tooltip-row) {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
padding: 1px 0;
|
||||
}
|
||||
:global(.cal-tooltip-row + .cal-tooltip-row) {
|
||||
border-top: 1px solid var(--color-stein-100, #ede8df);
|
||||
margin-top: 2px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
:global(.cal-tooltip-title) {
|
||||
font-weight: 500;
|
||||
color: var(--color-stein-900, #1c1a17);
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
:global(.cal-tooltip-time) {
|
||||
font-size: 10px;
|
||||
color: var(--color-stein-500, #7a7469);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
:global(.cal-tooltip-more) {
|
||||
font-size: 10px;
|
||||
color: var(--color-stein-500, #7a7469);
|
||||
margin-top: 3px;
|
||||
border-top: 1px solid var(--color-stein-100, #ede8df);
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
/* Print: alle <details> geöffnet, Chevrons aus, Day-Groups nicht
|
||||
splitten. */
|
||||
@media print {
|
||||
|
||||
Reference in New Issue
Block a user