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:
2026-09-26 13:09:05 +02:00
parent 228112014c
commit b06a2d1066
18 changed files with 484 additions and 135 deletions
@@ -6,9 +6,9 @@ import type { Compactor, ResponsiveLayouts } from 'react-grid-layout';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { useTranslations } from 'next-intl';
import { WIDGET_CONSTRAINTS, type WidgetType } from './widget-registry';
import { WIDGET_CONSTRAINTS, WIDGET_REGISTRY, type WidgetType } from './widget-registry';
import { WidgetIcon } from './widgets/widget-icon';
import { WidgetWrapper } from './widgets/widget-wrapper';
import { TesseraLogo } from '@/components/brand/tessera-logo';
// quick-260916-bwo: Raster doppelt so fein wie zuvor (24 statt 12 Spalten,
// 20 statt 40 px Zeilenhoehe, 8 statt 16 px Abstand). Gespeicherte Anordnungen
@@ -60,8 +60,15 @@ interface DashboardGridProps {
isEditMode: boolean;
onLayoutChange: (allLayouts: ResponsiveLayouts) => void;
onRemoveWidget: (id: string) => void;
/** Leerer Zustand: oeffnet den Widget-Katalog. */
onOpenCatalog?: () => void;
/** Leerer Zustand: fuegt eine vorgeschlagene Kachel direkt hinzu. */
onQuickAdd?: (type: WidgetType) => void;
}
/** Vorschlaege im leeren Zustand — Plattform-Kacheln, immer verfuegbar. */
const QUICK_ADD_TYPES: WidgetType[] = ['clock', 'calendar', 'note'];
/**
* quick-260916-dyv: Gespeicherte minW/minH aus den Konstanten ueberschreiben.
*
@@ -123,6 +130,8 @@ export function DashboardGrid({
isEditMode,
onLayoutChange,
onRemoveWidget,
onOpenCatalog,
onQuickAdd,
}: DashboardGridProps) {
const t = useTranslations('widgets');
const [width, setWidth] = useState(1200);
@@ -203,20 +212,46 @@ export function DashboardGrid({
// stabil bleibt.
const effectiveLayouts = useMemo(() => applyConstraintMinima(layouts, widgets), [layouts, widgets]);
// Empty state (D-02)
// Leerer Zustand (D-02, Design „Mosaik“ Runde 2): freundlich ohne
// Illustration — Titel, ein Satz, drei Vorschlaege zum direkten
// Hinzufuegen und der Hauptknopf zum Katalog.
if (widgets.length === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
{/* Bildmarke statt des frueheren handgezeichneten Vier-Quadrate-Symbols.
Ohne Rahmen und ohne Kontur: die Marke steht hier frei auf dem
Seitenhintergrund, nicht auf einer eigenen Flaeche. */}
<TesseraLogo variant="mark" size={64} plateOutline={false} />
<h2 className="text-lg font-semibold text-muted-foreground">
{t('emptyHeading')}
</h2>
<p className="text-sm text-muted-foreground">
{t('emptyBody')}
</p>
<div className="flex min-h-[55vh] flex-col items-center justify-center px-4 text-center">
<div className="dashboard-chrome flex max-w-md flex-col items-center rounded-xl px-6 py-8">
<span className="mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-muted text-muted-foreground">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="7" height="9" rx="1.5" />
<rect x="14" y="3" width="7" height="5" rx="1.5" />
<rect x="14" y="12" width="7" height="9" rx="1.5" />
<rect x="3" y="16" width="7" height="5" rx="1.5" />
</svg>
</span>
<h2 className="text-base font-semibold text-foreground">{t('emptyState.title')}</h2>
<p className="mt-1 text-sm text-muted-foreground">{t('emptyState.body')}</p>
{onQuickAdd && (
<ul className="mt-4 flex flex-wrap justify-center gap-2">
{QUICK_ADD_TYPES.map((type) => (
<li key={type}>
<button
type="button"
onClick={() => onQuickAdd(type)}
className="inline-flex h-8 items-center gap-2 rounded-full border border-border bg-card px-3 text-sm text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<WidgetIcon type={type} />
{t(WIDGET_REGISTRY[type].nameKey)}
</button>
</li>
))}
</ul>
)}
{onOpenCatalog && (
<button type="button" onClick={onOpenCatalog} className="btn btn-primary mt-5">
<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"><path d="M12 5v14M5 12h14" /></svg>
{t('addWidget')}
</button>
)}
</div>
</div>
);
}