Files
Webklar/.kiro/specs/system-theme-detection/design.md
2026-05-10 12:19:58 +02:00

11 KiB
Raw Blame History

Design-Dokument System-Theme-Erkennung (Light/Dark Mode)

Übersicht

Dieses Feature integriert die bereits installierte Bibliothek next-themes (v0.3.0) in die bestehende React + Vite + Tailwind + shadcn/ui-Webseite, um eine automatische Erkennung des Betriebssystem-Themes (hell/dunkel) sowie manuelles Umschalten zu ermöglichen.

Aktuell definiert :root in src/index.css das dunkle Farbschema. Es existiert zwar ein .dark-Block, dieser wird aber nicht aktiv genutzt. Die Tailwind-Konfiguration verwendet bereits darkMode: ["class"], was perfekt mit next-themes zusammenarbeitet.

Kernentscheidungen:

  • next-themes wird als ThemeProvider eingesetzt (bereits als Dependency vorhanden, attribute: "class" für Tailwind-Kompatibilität)
  • Das aktuelle :root-Farbschema wird zum Light Theme umstrukturiert, ein neues Dark Theme wird unter .dark definiert
  • Ein Theme-Toggle-Button wird im Header platziert (Desktop und Mobile)
  • Ein Inline-Script in index.html verhindert Theme-Flackern beim Laden (FOUC-Prevention)

Architektur

graph TD
    A[index.html  Inline-Script] -->|Setzt class auf html| B[html-Element]
    B --> C[ThemeProvider  next-themes]
    C --> D[App.tsx]
    D --> E[Header mit ThemeToggle]
    D --> F[Alle Seiten & Komponenten]
    
    G[localStorage  theme] <-->|Lesen/Schreiben| C
    H[prefers-color-scheme] -->|System-Erkennung| C
    
    I[index.css  :root Light] --> F
    J[index.css  .dark Dark] --> F

Datenfluss

  1. Beim Laden der Seite liest ein Inline-Script in index.html den localStorage-Wert (theme) aus. Falls keiner vorhanden ist, wird prefers-color-scheme geprüft. Die entsprechende CSS-Klasse (dark oder keine) wird auf <html> gesetzt noch bevor React rendert.
  2. ThemeProvider aus next-themes übernimmt die Verwaltung im React-Baum. Er synchronisiert den Zustand mit localStorage und reagiert auf Änderungen der System-Einstellung via matchMedia-Listener.
  3. Der ThemeToggle-Button im Header nutzt den useTheme()-Hook, um zwischen light, dark und system zu wechseln.
  4. Alle Komponenten nutzen weiterhin hsl(var(--...)) CSS-Variablen der Wechsel erfolgt rein über CSS-Klassen.

Komponenten und Schnittstellen

1. ThemeProvider-Wrapper (src/components/ThemeProvider.tsx)

Dünner Wrapper um ThemeProvider aus next-themes:

interface ThemeProviderProps {
  children: React.ReactNode;
  defaultTheme?: string;       // Standard: "system"
  storageKey?: string;          // Standard: "theme"
  enableSystem?: boolean;       // Standard: true
  disableTransitionOnChange?: boolean; // Standard: false
}

Konfiguration:

  • attribute="class" Tailwind-kompatibel
  • defaultTheme="system" Erstbesucher erhalten System-Theme
  • enableSystem={true} Automatische Erkennung aktiv
  • storageKey="theme" localStorage-Schlüssel

Wird in App.tsx als äußerster Wrapper um den gesamten Komponentenbaum eingebunden.

2. ThemeToggle-Komponente (src/components/ThemeToggle.tsx)

Button-Komponente mit Dropdown-Menü (shadcn/ui DropdownMenu):

interface ThemeToggleProps {
  className?: string;
}

Funktionalität:

  • Zeigt ein Icon basierend auf dem aktuellen Theme (Sun, Moon, Monitor aus lucide-react)
  • Dropdown mit drei Optionen: „Hell", „Dunkel", „System"
  • Nutzt useTheme() Hook für theme, setTheme, resolvedTheme
  • aria-label beschreibt den aktuellen Zustand
  • Tastatur-bedienbar (Tab, Enter/Space)
  • Mounted-Check verhindert Hydration-Mismatch (Icon wird erst nach Mount gerendert)

3. Anpassungen am Header (src/components/Header.tsx)

  • ThemeToggle wird im Desktop-NavBody neben dem „Kontakt"-Button eingefügt
  • ThemeToggle wird im Mobile-Nav-Header neben dem Hamburger-Menü eingefügt
  • Positionierung: rechts, vor dem Kontakt-Button (Desktop) bzw. links vom Toggle-Icon (Mobile)

4. Inline-Script in index.html

Ein <script>-Block im <head>, der vor dem React-Bundle ausgeführt wird:

// Liest localStorage("theme") aus
// Falls "system" oder nicht vorhanden: prüft prefers-color-scheme
// Setzt class="dark" auf <html> wenn Dark Mode aktiv

Dieses Script verhindert das sichtbare Flackern (FOUC) beim Seitenaufruf.

5. CSS-Variablen-Umstrukturierung (src/index.css)

  • :root → Light Theme (helle Hintergründe, dunkle Texte)
  • .dark → Dark Theme (das aktuelle :root-Farbschema wird hierhin verschoben)
  • Alle bestehenden Komponentenstile (.glass-nav, .card-minimal, .project-card, .btn, etc.) werden auf Theme-Kompatibilität geprüft und ggf. angepasst
  • Hardcodierte Farbwerte in Komponentenstilen werden durch CSS-Variablen ersetzt

Komponentendiagramm

graph LR
    subgraph App.tsx
        TP[ThemeProvider]
        TP --> Router
    end
    
    subgraph Header
        TT[ThemeToggle]
        TT -->|useTheme| TP
    end
    
    subgraph CSS
        LT[":root  Light Theme"]
        DT[".dark  Dark Theme"]
    end
    
    TP -->|class auf html| CSS

Datenmodelle

Theme-Zustand

type ThemeValue = "light" | "dark" | "system";

// Von next-themes bereitgestellt via useTheme()
interface ThemeState {
  theme: ThemeValue;           // Gewählte Präferenz ("light" | "dark" | "system")
  resolvedTheme: "light" | "dark"; // Tatsächlich angewendetes Theme
  setTheme: (theme: ThemeValue) => void;
  systemTheme: "light" | "dark";   // Aktuelle OS-Einstellung
}

localStorage-Schema

Schlüssel Wert Beschreibung
theme "light" | "dark" | "system" Gespeicherte Benutzerpräferenz

CSS-Custom-Properties (Light Theme neu zu erstellen)

:root {
  --background: 0 0% 100%;        /* Weiß */
  --foreground: 0 0% 9%;          /* Fast Schwarz */
  --card: 0 0% 98%;               /* Sehr helles Grau */
  --card-foreground: 0 0% 9%;
  --primary: 198 93% 42%;         /* Cyan-Blau (konsistent) */
  --primary-foreground: 0 0% 98%;
  --secondary: 0 0% 94%;          /* Helles Grau */
  --secondary-foreground: 0 0% 9%;
  --muted: 0 0% 94%;
  --muted-foreground: 0 0% 40%;
  --accent: 0 0% 94%;
  --accent-foreground: 0 0% 9%;
  --border: 0 0% 88%;
  --input: 0 0% 88%;
  --ring: 198 93% 42%;
  --destructive: 0 62% 50%;
  --destructive-foreground: 0 0% 98%;
  /* ... weitere Token analog */
}

CSS-Custom-Properties (Dark Theme bestehendes Schema)

.dark {
  --background: 0 0% 0%;          /* Schwarz (aktuelles :root) */
  --foreground: 0 0% 92%;
  --card: 0 0% 6%;
  --card-foreground: 0 0% 92%;
  --primary: 180 40% 50%;
  --primary-foreground: 0 0% 3%;
  /* ... restliche aktuelle :root-Werte */
}

Korrektheitseigenschaften (Correctness Properties)

Eine Korrektheitseigenschaft ist ein Merkmal oder Verhalten, das über alle gültigen Ausführungen eines Systems hinweg gelten soll im Wesentlichen eine formale Aussage darüber, was das System tun soll. Eigenschaften bilden die Brücke zwischen menschenlesbaren Spezifikationen und maschinell überprüfbaren Korrektheitsgarantien.

Property 1: System-Theme-Auflösung

Für jede beliebige System-Theme-Einstellung (hell oder dunkel) gilt: Wenn die Theme-Präferenz auf „system" steht (oder keine Präferenz gespeichert ist), dann soll das aufgelöste Theme (resolvedTheme) dem aktuellen System-Theme entsprechen.

Validates: Requirements 1.1, 1.2, 1.3, 2.5

Property 2: Explizite Theme-Wahl überschreibt System

Für jede beliebige System-Theme-Einstellung und für jede explizite Theme-Wahl ("light" oder "dark") gilt: Das aufgelöste Theme (resolvedTheme) soll immer der expliziten Wahl entsprechen, unabhängig vom System-Theme.

Validates: Requirements 2.3, 2.4

Property 3: Persistenz-Round-Trip

Für jeden gültigen Theme-Wert ("light", "dark", "system") gilt: Wenn der Wert über setTheme() gesetzt wird, dann soll der im localStorage gespeicherte Wert identisch sein, und beim erneuten Laden soll dieser gespeicherte Wert korrekt als aktive Präferenz wiederhergestellt werden.

Validates: Requirements 3.1, 3.2

Fehlerbehandlung

Szenario Verhalten
localStorage nicht verfügbar (blockiert, privater Modus) ThemeProvider fällt auf System-Theme-Erkennung zurück. Kein Fehler wird geworfen. Die Seite funktioniert normal, nur die Persistenz entfällt.
prefers-color-scheme nicht unterstützt (ältere Browser) next-themes fällt auf den defaultTheme zurück ("system" → wird als "light" aufgelöst).
Ungültiger Wert im localStorage next-themes ignoriert ungültige Werte und fällt auf defaultTheme zurück.
JavaScript deaktiviert Das Inline-Script in index.html wird nicht ausgeführt. Die Seite zeigt das Standard-CSS (Light Theme in :root). Kein Theme-Toggle verfügbar, aber die Seite bleibt nutzbar.
Hydration-Mismatch (SSR/SSG) Nicht relevant für Vite-SPA. Der Mounted-Check im ThemeToggle verhindert trotzdem Icon-Flackern.

Teststrategie

Property-Based Tests (fast-check)

Bibliothek: fast-check (für TypeScript/Vitest)

Konfiguration:

  • Minimum 100 Iterationen pro Property-Test
  • Jeder Test referenziert die zugehörige Design-Property
  • Tag-Format: Feature: system-theme-detection, Property {number}: {property_text}

Die drei Korrektheitseigenschaften werden als Property-Based Tests implementiert:

  1. Property 1 Theme-Auflösungslogik bei „system"-Präferenz: Generiere zufällige System-Theme-Werte, setze Präferenz auf „system", prüfe resolvedTheme === systemTheme.
  2. Property 2 Explizite Wahl überschreibt System: Generiere zufällige Kombinationen aus System-Theme und expliziter Wahl, prüfe resolvedTheme === expliziteWahl.
  3. Property 3 Persistenz-Round-Trip: Generiere zufällige gültige Theme-Werte, speichere via setTheme(), lese aus localStorage, prüfe Gleichheit. Simuliere Neustart, prüfe ob Wert wiederhergestellt wird.

Unit Tests (Vitest + React Testing Library)

Ergänzend zu den Property-Tests werden folgende Example-basierte Tests geschrieben:

  • ThemeToggle-Rendering: Button ist im Header vorhanden (Req. 2.1)
  • Dropdown-Optionen: Drei Optionen (Hell, Dunkel, System) sind vorhanden (Req. 2.2)
  • Icon-Zuordnung: Korrektes Icon für jeden Theme-Zustand (Req. 2.6)
  • CSS-Token-Vollständigkeit: Alle erforderlichen Custom Properties sind im Light Theme definiert (Req. 4.1)
  • Kontrastverhältnisse: Schlüssel-Farbpaare erfüllen WCAG-AA (Req. 4.2)
  • Tastatur-Bedienbarkeit: Tab-Navigation und Enter/Space funktionieren (Req. 6.1)
  • aria-label: Beschreibt den aktuellen Zustand korrekt (Req. 6.2)
  • aria-live: Zustandswechsel wird kommuniziert (Req. 6.3)

Edge-Case Tests

  • localStorage blockiert: Fallback auf System-Theme ohne Fehler (Req. 3.3)

Integrationstests

  • matchMedia-Listener: Simuliere OS-Theme-Wechsel, prüfe ob resolvedTheme sich aktualisiert (Req. 1.4)

Nicht automatisiert testbar

  • Visuell korrekte Darstellung der Komponentenstile im Light Theme (Req. 4.3) → Manuelle visuelle Prüfung
  • Kein sichtbares Flackern beim Laden (Req. 5.2) → Manuelle visuelle Prüfung