From 219038b3ee847c17f75e641e2c8abc885ecf2e2d Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 25 Sep 2026 14:49:01 +0200 Subject: [PATCH] feat(web): Dashboard-Kacheln im Fluent-Stil und waehlbarer Hintergrund Einheitliche Kachel-Kopfzeile, ruhigere Notiz-/Kalender-/Favoriten- Darstellung, 12 px Raster-Abstand, gestrichelte Kontur und Griff im Bearbeitungsmodus. Neuer Knopf Hintergrund: Keiner, fuenf eingebaute (Nebel, Salbei, Bluete, Duenen, Mosaik) oder eigenes Bild aus den Bilderrahmen-Bildern; Prototyp speichert im localStorage je Benutzer. Mit Hintergrund werden Kacheln Mica-artig durchscheinend (Kontrast >= 4,5:1 auch im schlechtesten Fall). Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/web/src/app/(portal)/page.tsx | 12 +- .../dashboard/dashboard-background.tsx | 276 ++++++++++++++++++ .../dashboard/dashboard-grid.test.tsx | 4 +- .../components/dashboard/dashboard-grid.tsx | 2 +- .../components/dashboard/dashboard-tabs.tsx | 2 +- .../components/dashboard/edit-mode-toggle.tsx | 6 +- .../dashboard/widgets/calculator-widget.tsx | 2 +- .../dashboard/widgets/calendar-widget.tsx | 20 +- .../dashboard/widgets/favorites-widget.tsx | 16 +- .../dashboard/widgets/note-widget.tsx | 6 +- .../dashboard/widgets/proxmox-widget.tsx | 4 +- .../dashboard/widgets/search-widget.tsx | 2 +- .../dashboard/widgets/widget-wrapper.tsx | 8 +- .../dashboard/widgets/xframe-widget.test.tsx | 4 +- .../dashboard/widgets/xframe-widget.tsx | 4 +- apps/web/src/lib/dashboard-background.test.ts | 54 ++++ apps/web/src/lib/dashboard-background.ts | 142 +++++++++ 17 files changed, 521 insertions(+), 43 deletions(-) create mode 100644 apps/web/src/components/dashboard/dashboard-background.tsx create mode 100644 apps/web/src/lib/dashboard-background.test.ts create mode 100644 apps/web/src/lib/dashboard-background.ts diff --git a/apps/web/src/app/(portal)/page.tsx b/apps/web/src/app/(portal)/page.tsx index ad9523c..a4d6279 100644 --- a/apps/web/src/app/(portal)/page.tsx +++ b/apps/web/src/app/(portal)/page.tsx @@ -5,6 +5,8 @@ import { useTranslations } from 'next-intl'; import { DashboardGrid } from '@/components/dashboard/dashboard-grid'; import { DashboardTabs } from '@/components/dashboard/dashboard-tabs'; import { EditModeToggle } from '@/components/dashboard/edit-mode-toggle'; +import { BackgroundPicker, DashboardBackdrop, useDashboardBackground } from '@/components/dashboard/dashboard-background'; +import { useAuthStore } from '@/lib/stores/auth-store'; import { WidgetCatalogModal } from '@/components/dashboard/widget-catalog-modal'; import { registerWidget } from '@/components/dashboard/widget-registry'; import { ClockWidget } from '@/components/dashboard/widgets/clock-widget'; @@ -45,6 +47,10 @@ interface ActiveModule { export default function DashboardPage() { const t = useTranslations('widgets'); const [catalogOpen, setCatalogOpen] = useState(false); + // Hintergrund je Benutzer (Design „Mosaik“, Prototyp mit localStorage). + const userId = useAuthStore((s) => s.user?.id); + const { background, choose: chooseBackground } = useDashboardBackground(userId); + const hasBackground = background.kind !== 'none'; // quick-260922-m1h: Slugs der Module, die dieser Benutzer nutzen darf — // der Katalog blendet Kacheln gesperrter Module damit aus. `null` heisst // "noch unbekannt oder Abruf fehlgeschlagen" und ist fail-closed. @@ -115,7 +121,8 @@ export default function DashboardPage() { } return ( -
+
+ {/* Reiterleiste (quick-260923-ad9) — seit quick-260924-h7x per Portal in der Kopfzeile, hier nimmt sie keinen Platz mehr ein. Sie bleibt waehrend eines Reiterwechsels stehen, nur das Raster zeigt eine @@ -151,11 +158,12 @@ export default function DashboardPage() { Grid direkt unter der Kopfzeile beginnt (12 + 8 + 8 = 28 px statt 60 px). Im Bearbeitungsmodus steht "Widget hinzufuegen" links neben dem Haekchen. */}
+ {isEditMode && } {isEditMode && ( + + ); + }; + + return ( +
+ + + {open && ( +
+

{t('title')}

+

{t('hint')}

+ +

{t('builtIn')}

+
    + {option('none', t('none'), NO_BACKGROUND, { backgroundColor: 'var(--background)' }, + , + )} + {BACKGROUND_PRESETS.map((preset) => { + const css = dark ? preset.dark : preset.light; + return option( + preset.id, + t(`presets.${preset.labelKey}`), + { kind: 'preset', id: preset.id }, + css.startsWith('url(') + ? { backgroundImage: css, backgroundSize: 'cover', backgroundPosition: 'center' } + : { backgroundColor: css }, + ); + })} +
+ +

{t('ownImages')}

+
    + {(images ?? []).map((image) => + option( + image.id, + image.originalName, + { kind: 'image', imageId: image.id }, + { + backgroundImage: `url("${dashboardImageSrc(image.id)}")`, + backgroundSize: 'cover', + backgroundPosition: 'center', + }, + ), + )} +
  • + +
  • +
+ handleUpload(e.target.files?.[0])} + /> + {images !== null && images.length === 0 && ( +

{t('noImages')}

+ )} + {error && ( +

+ {error} +

+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/dashboard/dashboard-grid.test.tsx b/apps/web/src/components/dashboard/dashboard-grid.test.tsx index 2220bff..43fc340 100644 --- a/apps/web/src/components/dashboard/dashboard-grid.test.tsx +++ b/apps/web/src/components/dashboard/dashboard-grid.test.tsx @@ -165,7 +165,7 @@ describe('DashboardGrid', () => { expect(document.querySelector('.widget-drag-handle')).toBeInTheDocument(); }); - it('quick-260916-bwo Test 4: Grid-Props — 24/20/12/8/2 Spalten, rowHeight 20, margin 8, Breakpoints unveraendert, containerPadding folgt dem margin', async () => { + it('quick-260916-bwo Test 4: Grid-Props — 24/20/12/8/2 Spalten, rowHeight 20, margin 12, Breakpoints unveraendert, containerPadding folgt dem margin', async () => { captured.props = null; const { DashboardGrid } = await import('./dashboard-grid'); render( @@ -181,7 +181,7 @@ describe('DashboardGrid', () => { expect(captured.props).not.toBeNull(); expect(captured.props?.cols).toEqual({ lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 }); expect(captured.props?.rowHeight).toBe(20); - expect(captured.props?.margin).toEqual([8, 8]); + expect(captured.props?.margin).toEqual([12, 12]); expect(captured.props?.breakpoints).toEqual({ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }); expect(captured.props?.containerPadding).toBeUndefined(); }); diff --git a/apps/web/src/components/dashboard/dashboard-grid.tsx b/apps/web/src/components/dashboard/dashboard-grid.tsx index 854921e..2d15867 100644 --- a/apps/web/src/components/dashboard/dashboard-grid.tsx +++ b/apps/web/src/components/dashboard/dashboard-grid.tsx @@ -229,7 +229,7 @@ export function DashboardGrid({ cols={COLS} layouts={effectiveLayouts as ResponsiveLayouts} rowHeight={20} - margin={[8, 8] as [number, number]} + margin={[12, 12] as [number, number]} // containerPadding bewusst nicht gesetzt: react-grid-layout 2.2.3 nimmt // dann das margin (gemessen: effectiveContainerPadding = containerPadding ?? margin). dragConfig={{ diff --git a/apps/web/src/components/dashboard/dashboard-tabs.tsx b/apps/web/src/components/dashboard/dashboard-tabs.tsx index a66068e..8a09a6d 100644 --- a/apps/web/src/components/dashboard/dashboard-tabs.tsx +++ b/apps/web/src/components/dashboard/dashboard-tabs.tsx @@ -507,7 +507,7 @@ export function DashboardTabs({ role="alertdialog" aria-modal="true" aria-label={t('tabs.deleteDialogTitle')} - className="relative z-[60] mx-4 max-w-md rounded-lg border border-border bg-card p-6 shadow-xl" + className="relative z-[60] mx-4 max-w-md rounded-lg bg-card dark:border dark:border-border p-6 shadow-xl" >

{t('tabs.deleteDialogTitle')} diff --git a/apps/web/src/components/dashboard/edit-mode-toggle.tsx b/apps/web/src/components/dashboard/edit-mode-toggle.tsx index 54ab702..b730592 100644 --- a/apps/web/src/components/dashboard/edit-mode-toggle.tsx +++ b/apps/web/src/components/dashboard/edit-mode-toggle.tsx @@ -21,11 +21,7 @@ export function EditModeToggle({ isEditMode, onToggle }: EditModeToggleProps) {