feat(web): Dashboard mit Begruessung, Befehlsleiste und ruhigeren Kacheln
Begruessung nach Tageszeit mit Datum, Befehlsleiste oben rechts statt schwebendem Knopf, einheitliche Kachel-Kopfzeilen mit Symbol, Kalender mit Pfeilknoepfen, runden Tagen und Terminpunkten, Favoriten als 36-px-Zeilen bzw. App-Starter-Kacheln, Notiz-Kaestchen im Akzent, leerer Zustand mit drei Vorschlaegen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -243,21 +243,23 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
const hoverEvents = hover ? (eventsByDate.get(hover.key) ?? []) : [];
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 overflow-hidden p-3">
|
||||
<div className="flex h-full flex-col gap-2 overflow-hidden px-3 pb-3 pt-1">
|
||||
{showMonth && (
|
||||
<div data-testid="calendar-month" className="flex shrink-0 flex-col gap-1">
|
||||
{/* Nav-Zeile */}
|
||||
<div className="grid grid-cols-[1fr_1.4fr_1fr] gap-1">
|
||||
{/* Nav-Zeile: Pfeilknoepfe links/rechts, Monatsname mittig */}
|
||||
<div className="grid grid-cols-[auto_1fr_auto] items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="widgetNoDrag rounded-md px-1 py-[clamp(3px,0.9cqh,6px)] text-[clamp(10px,2.6cqw,13px)] leading-none text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
aria-label={t('calendar.monthPrev')}
|
||||
title={t('calendar.monthPrev')}
|
||||
className="widgetNoDrag flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={showPrev}
|
||||
>
|
||||
{t('calendar.monthPrev')}
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6" /></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="widgetNoDrag truncate rounded-md px-1 py-[clamp(3px,0.9cqh,6px)] text-[clamp(11px,2.8cqw,14px)] font-semibold leading-none text-foreground transition-colors hover:bg-muted"
|
||||
className="widgetNoDrag mx-auto truncate rounded-md px-2 py-1 text-sm font-semibold leading-5 text-foreground transition-colors hover:bg-muted"
|
||||
title={t('calendar.monthToday')}
|
||||
onClick={showToday}
|
||||
>
|
||||
@@ -265,31 +267,35 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="widgetNoDrag rounded-md px-1 py-[clamp(3px,0.9cqh,6px)] text-[clamp(10px,2.6cqw,13px)] leading-none text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
aria-label={t('calendar.monthNext')}
|
||||
title={t('calendar.monthNext')}
|
||||
className="widgetNoDrag flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
onClick={showNext}
|
||||
>
|
||||
{t('calendar.monthNext')}
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m9 18 6-6-6-6" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Wochentagskopf */}
|
||||
<div className="grid grid-cols-7 gap-px text-center text-[clamp(9px,2.2cqw,12px)] font-medium text-muted-foreground">
|
||||
<div className="grid grid-cols-7 text-center text-[11px] font-medium leading-4 text-muted-foreground">
|
||||
{WEEKDAY_LABELS.map((label) => (
|
||||
<div key={label}>{label}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Raster */}
|
||||
<div className="grid grid-cols-7 gap-px">
|
||||
<div className="grid grid-cols-7 gap-y-0.5">
|
||||
{days.map((day) => {
|
||||
const hasEvents = day.events.length > 0;
|
||||
const badges = buildDayBadges(day.events);
|
||||
const single = badges.length === 1;
|
||||
// Tag als Kreis (Design „Mosaik“): heute gelb gefuellt, Termine
|
||||
// als kleine Punkte unter der Zahl — je Kalenderquelle einer,
|
||||
// in deren Farbe; ohne Farbe im Akzentton, Rest grau.
|
||||
const cellClass = [
|
||||
'widgetNoDrag relative flex min-h-[clamp(16px,5.5cqh,40px)] items-start rounded-md px-1 py-0.5 text-[clamp(9px,2.4cqw,13px)] leading-none',
|
||||
'widgetNoDrag relative mx-auto flex h-[clamp(20px,7cqw,28px)] w-[clamp(20px,7cqw,28px)] items-center justify-center rounded-full text-[clamp(11px,2.9cqw,13px)] leading-none tabular-nums',
|
||||
day.inCurrentMonth ? 'text-foreground' : 'text-muted-foreground/70',
|
||||
day.isToday ? 'bg-primary font-semibold text-primary-foreground' : '',
|
||||
hasEvents ? 'cursor-default hover:bg-muted' : '',
|
||||
hasEvents && !day.isToday ? 'cursor-default font-medium hover:bg-muted' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
@@ -298,23 +304,20 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
<>
|
||||
<span>{day.date.getDate()}</span>
|
||||
{hasEvents && (
|
||||
// Wrapper traegt die Positionierung, die einzelnen Kreise nicht
|
||||
// mehr (mehrere Kreise stehen sonst uebereinander).
|
||||
<span
|
||||
data-testid="calendar-day-badges"
|
||||
className="absolute bottom-px right-px flex items-end gap-px"
|
||||
className="absolute bottom-[3px] left-1/2 flex -translate-x-1/2 items-center gap-[2px]"
|
||||
>
|
||||
{badges.map((badge) => {
|
||||
const badgeClass = [
|
||||
'flex items-center justify-center rounded-full px-0.5 font-semibold leading-none',
|
||||
single
|
||||
? 'h-[clamp(10px,3cqw,16px)] min-w-[clamp(10px,3cqw,16px)] text-[clamp(7px,1.8cqw,10px)]'
|
||||
: 'h-[clamp(8px,2.4cqw,12px)] min-w-[clamp(8px,2.4cqw,12px)] text-[clamp(6px,1.5cqw,8px)]',
|
||||
'block h-1 w-1 rounded-full',
|
||||
badge.rest
|
||||
? 'bg-muted-foreground text-background'
|
||||
? 'bg-muted-foreground'
|
||||
: badge.color
|
||||
? 'text-white'
|
||||
: 'bg-primary text-primary-foreground',
|
||||
? ''
|
||||
: day.isToday
|
||||
? 'bg-primary-foreground'
|
||||
: 'bg-primary-strong',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
@@ -322,10 +325,11 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
<span
|
||||
key={badge.key}
|
||||
data-testid={badge.rest ? 'calendar-day-count-rest' : 'calendar-day-count'}
|
||||
data-count={badge.count}
|
||||
className={badgeClass}
|
||||
style={badge.color && !badge.rest ? { backgroundColor: badge.color } : undefined}
|
||||
>
|
||||
{badge.count}
|
||||
<span className="sr-only">{badge.count}</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
@@ -360,7 +364,7 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
data-date={day.key}
|
||||
data-today={day.isToday || undefined}
|
||||
data-outside={!day.inCurrentMonth || undefined}
|
||||
className={`${cellClass} w-full text-left`}
|
||||
className={cellClass}
|
||||
aria-label={ariaLabel}
|
||||
onMouseEnter={(e) =>
|
||||
setHover({ key: day.key, rect: e.currentTarget.getBoundingClientRect() })
|
||||
@@ -395,7 +399,7 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
|
||||
{maxEvents > 0 && (
|
||||
<section className="flex min-h-0 flex-1 flex-col gap-1">
|
||||
<h3 className="shrink-0 text-[clamp(10px,2.6cqw,13px)] font-semibold text-foreground">
|
||||
<h3 className="shrink-0 text-xs font-semibold text-muted-foreground">
|
||||
{t('calendar.upcomingTitle')}
|
||||
</h3>
|
||||
{upcoming.length === 0 ? (
|
||||
@@ -403,24 +407,24 @@ export function CalendarWidget({ config }: WidgetProps) {
|
||||
{t('calendar.emptyNoEvents')}
|
||||
</p>
|
||||
) : (
|
||||
<ul data-testid="calendar-upcoming" className="min-h-0 flex-1 space-y-1 overflow-y-auto">
|
||||
<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-start gap-2 rounded-md px-2 py-1 hover:bg-muted">
|
||||
<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="mt-1 h-2 w-2 shrink-0 rounded-full"
|
||||
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-[clamp(9px,2.2cqw,12px)] text-muted-foreground">
|
||||
<p className="truncate text-[11px] leading-4 text-muted-foreground">
|
||||
{formatEventDate(event)}
|
||||
</p>
|
||||
<p className="truncate text-[clamp(10px,2.5cqw,14px)] font-medium text-foreground">
|
||||
<p className="truncate text-[13px] font-medium leading-[18px] text-foreground">
|
||||
{event.title}
|
||||
</p>
|
||||
{event.location && (
|
||||
<p className="truncate text-[clamp(9px,2.1cqw,12px)] text-muted-foreground">
|
||||
<p className="truncate text-[11px] leading-4 text-muted-foreground">
|
||||
{event.location}
|
||||
</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user