feat(05-01): dashboard grid, clock widget, widget registry, store, and tests

- Install react-grid-layout@2.2.3 and react-resizable
- Create widget-registry.tsx with all 4 widget types, WIDGET_CONSTRAINTS, WidgetProps
- Create dashboard-api.ts with fetch/save layout and widget CRUD functions
- Create dashboard-store.ts (Zustand, NO persist — D-05) with edit mode and auto-save on exit
- Create DashboardGrid with react-grid-layout v2 Responsive, ResizeObserver width
- Create ClockWidget using Intl.DateTimeFormat (no manual UTC offsets)
- Create WidgetWrapper with drag handle and delete button in edit mode
- Create EditModeToggle (pencil/checkmark), WidgetCatalogModal (2x2 grid)
- Rewrite portal page.tsx as dashboard with grid, edit toggle, widget catalog
- Add ResizeObserver polyfill in test setup, CSS mock support in vitest config
- All 5 tests green (dashboard grid + clock widget)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 11:21:43 +02:00
parent 792b7661d8
commit d5e1c42e64
14 changed files with 1115 additions and 29 deletions
@@ -0,0 +1,61 @@
'use client';
import { useEffect, useState } from 'react';
import type { WidgetProps } from '../widget-registry';
/**
* Digital clock widget displaying time in a configurable timezone.
* Uses Intl.DateTimeFormat — never computes UTC offsets manually (RESEARCH).
*
* Config:
* - timezone: IANA timezone string (default 'Europe/Berlin')
* - showDate: boolean (default false) — show date below time
*/
export function ClockWidget({ config }: WidgetProps) {
const timezone = (config.timezone as string) || 'Europe/Berlin';
const showDate = config.showDate === true;
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const timer = setInterval(() => {
setNow(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
const timeFormatter = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: timezone,
});
const dateFormatter = new Intl.DateTimeFormat('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric',
timeZone: timezone,
});
return (
<div className="flex h-full flex-col items-center justify-center gap-1 p-2">
<time
role="time"
className="font-semibold tabular-nums text-foreground"
style={{ fontSize: 'clamp(28px, 4vw, 40px)' }}
dateTime={now.toISOString()}
>
{timeFormatter.format(now)}
</time>
{showDate && (
<span
data-testid="clock-date"
className="text-xs text-muted-foreground"
>
{dateFormatter.format(now)}
</span>
)}
</div>
);
}