/** * Dashboard-Hintergrund (Prototyp, Design „Mosaik“). * * Wahl je Benutzer: kein Hintergrund, ein eingebauter (ruhige Flaechen und * drei abstrakte Motive in Gelb, Grau und Graphit — keine Regenbogen- * verlaeufe) oder ein eigenes Bild aus den Bilderrahmen-Bildern * (`/dashboard/images`, keine API-Aenderung). Gespeichert wird vorerst im * localStorage des Browsers, Schluessel je Benutzerkennung; ein Datenbankfeld * kann folgen, wenn die Funktion bleibt. */ export type BackgroundPresetId = 'mist' | 'pebble' | 'bloom' | 'dunes' | 'mosaic'; export type DashboardBackground = | { kind: 'none' } | { kind: 'preset'; id: BackgroundPresetId } | { kind: 'image'; imageId: string }; export const NO_BACKGROUND: DashboardBackground = { kind: 'none' }; function svgUrl(svg: string): string { return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`; } const VIEW = 'xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice"'; function bloom(base: string, a: string, b: string, c: string): string { return svgUrl( `` + `` + `` + `` + `` + // Feines Korn gegen Farbstufen im weichgezeichneten Verlauf. `` + ``, ); } function dunes(base: string, a: string, b: string, c: string): string { return svgUrl( `` + `` + `` + ``, ); } function mosaic(base: string, gray: string, alphas: number[], yellowAlpha: number): string { const tiles: [number, number][] = [ [1060, 120], [1260, 120], [1260, 320], [1060, 320], [1460, 320], [860, 520], [1260, 520], [1460, 720], [1060, 720], ]; const rects = tiles .map( ([x, y], i) => ``, ) .join(''); return svgUrl( `${rects}` + ``, ); } export interface BackgroundPreset { id: BackgroundPresetId; /** Uebersetzungsschluessel unter `dashboard.background.presets`. */ labelKey: string; light: string; dark: string; } // Runde 2: nur Gelb-, Grau- und Graphittoene — keine bunten Modulfarben. export const BACKGROUND_PRESETS: BackgroundPreset[] = [ { id: 'mist', labelKey: 'mist', light: '#e4e9f1', dark: '#1d2330' }, { id: 'pebble', labelKey: 'pebble', light: '#e9e7e3', dark: '#23221f' }, { id: 'bloom', labelKey: 'bloom', light: bloom('#eceef1', '#f0e9c6', '#d3d7de', '#dddbd5'), dark: bloom('#1b1d22', '#37342a', '#2a2e35', '#2f2e2a'), }, { id: 'dunes', labelKey: 'dunes', light: dunes('#eceef1', '#dcdfe4', '#caced5', '#b8bdc6'), dark: dunes('#1a1c20', '#23262b', '#2c3036', '#363a41'), }, { id: 'mosaic', labelKey: 'mosaic', light: mosaic('#eceef1', '#5b6270', [0.1, 0.16, 0.07, 0.13], 0.4), dark: mosaic('#1b1d22', '#c9ced8', [0.06, 0.1, 0.04, 0.08], 0.78), }, ]; 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; } function storageKey(userId: string): string { return `tessera.dashboardBackground.${userId}`; } function isValid(value: unknown): value is DashboardBackground { if (typeof value !== 'object' || value === null) return false; const v = value as Record; if (v.kind === 'none') return true; if (v.kind === 'preset') return BACKGROUND_PRESETS.some((p) => p.id === v.id); if (v.kind === 'image') return typeof v.imageId === 'string' && v.imageId !== ''; return false; } /** Liest die Wahl; jeder Fehler (privates Fenster, kaputter Wert) ergibt „kein Hintergrund“. */ export function loadDashboardBackground(userId: string): DashboardBackground { try { const raw = window.localStorage.getItem(storageKey(userId)); if (!raw) return NO_BACKGROUND; const parsed: unknown = JSON.parse(raw); return isValid(parsed) ? parsed : NO_BACKGROUND; } catch { return NO_BACKGROUND; } } export function saveDashboardBackground(userId: string, value: DashboardBackground): void { try { window.localStorage.setItem(storageKey(userId), JSON.stringify(value)); } catch { // Speicher gesperrt — die Wahl gilt dann nur fuer diese Sitzung. } }