feat(web): Design Mosaik Runde 3 - Reiter in der Seitenleiste, lebendigere Kacheln

- Dashboard-Reiter stehen in der aufgeklappten Seitenleiste als Liste
  unter "Dashboard" (Ziehen sortiert senkrecht, Umbenennen/Loeschen/Neu
  im Bearbeitungsmodus); eingeklappt und auf dem Handy wie bisher in der
  Kopfzeile. Die Kopfzeile zeigt sonst den Namen des Dashboards.
- Begruessung einzeilig auf Hoehe der Befehlsleiste.
- Kachel-Groesse: kollidiert die gezogene Groesse mit einem Nachbarn, wird
  nur die freie Achse uebernommen - vorher verwarf react-grid-layout mit
  preventCollision die ganze Aenderung, sobald die Maus eine Zeile nach
  unten wackelte (Favoriten liessen sich deshalb oft nicht schmaler ziehen).
- Ansicht: belegte Spalten mittig, Begruessung/Befehlsleiste ruecken mit.
- Kalender: einzeilige Terminliste mit "Heute"/"Morgen" in Akzentfarbe,
  Ort im Tooltip, Standardhoehe 16 statt 12.
- Hintergrund "Bluete" im Dunkelmodus weggelassen (Farbstufen), Ersatz Nebel.
- Kacheln: Symbol-Chip in der Akzentfarbe, leichtes Anheben beim Zeigen,
  gestaffeltes Einblenden beim Laden (ohne bei reduzierter Bewegung).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 15:58:01 +02:00
parent b06a2d1066
commit 8532b63097
20 changed files with 552 additions and 73 deletions
@@ -10,7 +10,9 @@ import {
buildCalendarDays,
buildDayBadges,
computeFetchWindow,
eventDayOffset,
formatEventDate,
formatEventDay,
formatEventTime,
formatMonthLabel,
groupEventsByDate,
@@ -407,30 +409,48 @@ export function CalendarWidget({ config }: WidgetProps) {
{t('calendar.emptyNoEvents')}
</p>
) : (
<ul data-testid="calendar-upcoming" className="min-h-0 flex-1 space-y-0.5 overflow-y-auto">
{upcoming.map((event) => (
<li key={event.id} className="flex items-stretch gap-2.5 rounded-md px-1.5 py-1 hover:bg-muted">
<span
data-testid="event-color-dot"
className="w-[3px] shrink-0 rounded-full"
style={{ backgroundColor: event.color || 'var(--muted-foreground)' }}
aria-hidden="true"
/>
<div className="min-w-0 flex-1">
<p className="truncate text-[11px] leading-4 text-muted-foreground">
{formatEventDate(event)}
</p>
<p className="truncate text-[13px] font-medium leading-[18px] text-foreground">
// Einzeilige Terminliste (Design „Mosaik“, Runde 3): Farbstrich,
// Tag, Titel, Uhrzeit rechts — rund 26 px je Termin statt 40, so
// passen auch in eine kleine Kachel mehrere Termine. „Heute“ und
// „Morgen“ stehen in der Akzentfarbe.
<ul data-testid="calendar-upcoming" className="min-h-0 flex-1 overflow-y-auto">
{upcoming.map((event) => {
const offset = eventDayOffset(event);
const dayLabel =
offset === 0
? t('calendar.today')
: offset === 1
? t('calendar.tomorrow')
: formatEventDay(event);
const soon = offset === 0 || offset === 1;
return (
<li
key={event.id}
title={[formatEventDate(event), event.title, event.location].filter(Boolean).join(' · ')}
className="flex h-[26px] items-center gap-2 rounded-md px-1.5 hover:bg-muted"
>
<span
data-testid="event-color-dot"
className="h-3.5 w-[3px] shrink-0 rounded-full"
style={{ backgroundColor: event.color || 'var(--muted-foreground)' }}
aria-hidden="true"
/>
<span
className={`w-[62px] shrink-0 truncate text-[11px] tabular-nums ${
soon ? 'font-semibold text-primary-strong' : 'text-muted-foreground'
}`}
>
{dayLabel}
</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-foreground">
{event.title}
</p>
{event.location && (
<p className="truncate text-[11px] leading-4 text-muted-foreground">
{event.location}
</p>
)}
</div>
</li>
))}
</span>
<span className="shrink-0 text-[11px] tabular-nums text-muted-foreground">
{event.allDay ? t('calendar.allDay') : formatEventTime(event.start)}
</span>
</li>
);
})}
</ul>
)}
</section>