big upgrade
This commit is contained in:
263
.kiro/specs/system-theme-detection/design.md
Normal file
263
.kiro/specs/system-theme-detection/design.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user