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
@@ -2,6 +2,16 @@
import { useTranslations } from 'next-intl';
import { WIDGET_REGISTRY, type WidgetType } from '../widget-registry';
import { WidgetIcon } from './widget-icon';
/**
* Kacheltypen ohne eigene Kopfzeile, denen der Rahmen eine einheitliche
* Kopfzeile (Symbol + Name) gibt (Design „Mosaik“, Runde 2). Notiz,
* Favoriten, Proxmox und XFrame bringen ihre eigene (mit editierbarem Titel)
* mit; Suche und Bilderrahmen bleiben bewusst randlos — ein Suchfeld bzw.
* ein Foto braucht keine Ueberschrift.
*/
const FRAME_HEADER_TYPES = new Set(['calendar', 'clock', 'calculator', 'stopwatch']);
interface WidgetWrapperProps {
widget: {
@@ -47,7 +57,7 @@ export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperPro
Hoehenkette Karte -> Rumpf bleibt definit (cqh loest weiter auf). */}
{isEditMode && (
<div
className="absolute inset-x-0 top-0 z-10 flex h-5 items-center justify-center bg-muted/80"
className="absolute inset-x-0 top-0 z-10 flex h-5 items-center justify-center"
title={t('dragHint')}
data-testid="widget-drag-head"
>
@@ -105,7 +115,14 @@ export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperPro
im Bearbeitungsmodus zusaetzlich den Griff traegt.
quick-260916-dyv: Die Kopfleiste liegt als Overlay ueber dem Rumpf und
aendert die Hoehenkette nicht. */}
<div className="@container-size h-full">
<div className="flex h-full flex-col">
{WidgetComponent && definition && FRAME_HEADER_TYPES.has(widget.widgetType) && (
<div className="widget-head" data-testid="widget-frame-head">
<WidgetIcon type={widget.widgetType} />
<h2 className="truncate font-semibold text-foreground">{t(definition.nameKey)}</h2>
</div>
)}
<div className="@container-size min-h-0 flex-1">
{WidgetComponent ? (
<WidgetComponent
instanceId={widget.id}
@@ -124,6 +141,7 @@ export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperPro
</div>
)}
</div>
</div>
</article>
);
}