Files
2026-05-10 12:19:58 +02:00

264 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```mermaid
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`:
```typescript
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`):
```typescript
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:
```javascript
// 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
```mermaid
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
```typescript
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)
```css
: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)
```css
.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