feat(web): Design Mosaik Runde 3 - Reiter in der Seitenleiste, lebendigere Kacheln

- Dashboard-Reiter stehen in der aufgeklappten Seitenleiste als Liste
  unter "Dashboard" (Ziehen sortiert senkrecht, Umbenennen/Loeschen/Neu
  im Bearbeitungsmodus); eingeklappt und auf dem Handy wie bisher in der
  Kopfzeile. Die Kopfzeile zeigt sonst den Namen des Dashboards.
- Begruessung einzeilig auf Hoehe der Befehlsleiste.
- Kachel-Groesse: kollidiert die gezogene Groesse mit einem Nachbarn, wird
  nur die freie Achse uebernommen - vorher verwarf react-grid-layout mit
  preventCollision die ganze Aenderung, sobald die Maus eine Zeile nach
  unten wackelte (Favoriten liessen sich deshalb oft nicht schmaler ziehen).
- Ansicht: belegte Spalten mittig, Begruessung/Befehlsleiste ruecken mit.
- Kalender: einzeilige Terminliste mit "Heute"/"Morgen" in Akzentfarbe,
  Ort im Tooltip, Standardhoehe 16 statt 12.
- Hintergrund "Bluete" im Dunkelmodus weggelassen (Farbstufen), Ersatz Nebel.
- Kacheln: Symbol-Chip in der Akzentfarbe, leichtes Anheben beim Zeigen,
  gestaffeltes Einblenden beim Laden (ohne bei reduzierter Bewegung).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 15:58:01 +02:00
parent b06a2d1066
commit 8532b63097
20 changed files with 552 additions and 73 deletions
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
BACKGROUND_PRESETS,
DARK_FALLBACK_ID,
loadDashboardBackground,
presetBackground,
saveDashboardBackground,
@@ -44,11 +45,13 @@ describe('dashboard-background (Design „Mosaik“, Prototyp)', () => {
expect(loadDashboardBackground('u1')).toEqual({ kind: 'none' });
});
it('jeder eingebaute Hintergrund hat eine helle und eine dunkle Fassung', () => {
it('jeder eingebaute Hintergrund liefert hell und dunkel ein Bild (dunkel notfalls den Ersatz)', () => {
for (const preset of BACKGROUND_PRESETS) {
expect(presetBackground(preset.id, false)).toBeTruthy();
expect(presetBackground(preset.id, true)).toBeTruthy();
expect(presetBackground(preset.id, false)).not.toBe(presetBackground(preset.id, true));
}
// Runde 3: „Bluete“ hat keine dunkle Fassung, dunkel greift der Ersatz.
expect(presetBackground('bloom', true)).toBe(presetBackground(DARK_FALLBACK_ID, true));
});
});
+16 -3
View File
@@ -75,9 +75,17 @@ export interface BackgroundPreset {
/** Uebersetzungsschluessel unter `dashboard.background.presets`. */
labelKey: string;
light: string;
dark: string;
/**
* `null` = im Dunkelmodus nicht angeboten (Runde 3). Wer den Hintergrund
* im hellen Modus gewaehlt hat, sieht im dunklen den Ersatz
* {@link DARK_FALLBACK_ID}.
*/
dark: string | null;
}
/** Ersatz im Dunkelmodus fuer Hintergruende ohne dunkle Fassung. */
export const DARK_FALLBACK_ID: BackgroundPresetId = 'mist';
// Runde 2: nur Gelb-, Grau- und Graphittoene — keine bunten Modulfarben.
export const BACKGROUND_PRESETS: BackgroundPreset[] = [
{ id: 'mist', labelKey: 'mist', light: '#e4e9f1', dark: '#1d2330' },
@@ -86,7 +94,10 @@ export const BACKGROUND_PRESETS: BackgroundPreset[] = [
id: 'bloom',
labelKey: 'bloom',
light: bloom('#eceef1', '#f0e9c6', '#d3d7de', '#dddbd5'),
dark: bloom('#1b1d22', '#37342a', '#2a2e35', '#2f2e2a'),
// Runde 3: im Dunkelmodus weggelassen — der weichgezeichnete Verlauf
// zwischen sehr dunklen Toenen zeigt sichtbare Farbstufen (8 Bit reichen
// dort nicht), auch mit Korn. Ersatz: DARK_FALLBACK_ID.
dark: null,
},
{
id: 'dunes',
@@ -105,7 +116,9 @@ export const BACKGROUND_PRESETS: BackgroundPreset[] = [
export function presetBackground(id: BackgroundPresetId, dark: boolean): string | null {
const preset = BACKGROUND_PRESETS.find((p) => p.id === id);
if (!preset) return null;
return dark ? preset.dark : preset.light;
if (!dark) return preset.light;
if (preset.dark !== null) return preset.dark;
return BACKGROUND_PRESETS.find((p) => p.id === DARK_FALLBACK_ID)?.dark ?? null;
}
function storageKey(userId: string): string {