Color-System Implementation — Konzept A¶
Live-Stand des Color-Systems in operator-ui + tenant-ui. Source-of- Truth-Tokens:
concept-a.md. Implementation-Branch:platform/color-system-implementation-concept-a, Merge7261426a4d297b5f2876adf04516ee1d8b7bc275.
Was ist live¶
| Schicht | Inhalt |
|---|---|
| Backend | Neue Tabelle user_preferences (Composite-PK (realm, username)). Lazy-Create. API: GET/PATCH /api/v1/users/me/preferences. Migration 0008_user_preferences.py |
| Frontend Tokens | 27 semantische Tokens × 3 Modi (light, dark, auto via prefers-color-scheme) × 2 Surfaces (operator, tenant). Identisches Schema in beiden Frontends |
| Theme-Provider | useTheme()-Composable in beiden Frontends. Lädt aus Backend (Cross-Device-Sync), Fallback LocalStorage. System-Theme-Listener für Auto-Mode-Live-Update |
| Toggle-Component | <ThemeToggle> in beiden Topbars (rechts neben Logout). 3-State-Cycle Sun → Moon → Auto |
| Walkthrough-Drift-Fix | 11 Hardcoded-Color-Stellen migriert von --kora-primary (Indigo-800 als Text) auf --kora-link (kontrastsicher in beiden Modi). Sidebar-Aktiv im Dark-Mode war 1.7:1 → jetzt 8.2:1 |
| 8-Drift-Fix | Operator-UI hatte 24 Tokens, Tenant-UI 16. Beide Frontends teilen jetzt dasselbe 27-Token-Schema |
Token-Schema (Auszug, Light-Mode-Operator-Surface)¶
| Kategorie | Token | Wert | Verwendung |
|---|---|---|---|
| Brand | --kora-primary |
#3730a3 |
Buttons-BG, aktive Tab-Indikator |
| Brand | --kora-primary-on-dark |
#a5b4fc Dark / #818cf8 Light |
Text-Use auf Dark-Surface |
| Brand | --kora-accent |
#f59e0b (Amber) |
Legacy-Alias |
| Brand | --kora-badge-bg / -fg |
#f59e0b / #18181b |
Operator-Badge — eigene Token-Klasse, surface-unabhängig |
| Surface | --kora-bg / -secondary / -tertiary / -elevated |
Slate-100 bis Zinc-900-Range | Page / Card / Sidebar / Modal |
| Text | --kora-text / -secondary / -muted |
Slate-900 / 600 / 400 | Body / Captions / Disabled |
| Border | --kora-border / -strong / -subtle |
Slate-200 / 300 / 100 | Default / Hover-Active / Divider |
| State | --kora-success / -warning / -danger / -info |
Emerald-700 / Amber-700 / Red-700 / Blue-700 | Status-Badges, Banner |
| Interactive | --kora-link |
Blue-300 (Dark) / Indigo-700 (Light) | Inline-Links + Sidebar-Aktiv-Text (löst Walkthrough-Drift) |
Vollständige Werte für alle vier Modus-Surface-Quadranten:
concept-a.md.
Theme-Toggle-UX¶
- Default für neue User:
auto(System-Preference viaprefers-color-scheme) - Persistenz: Backend (Cross-Device-Sync) + LocalStorage-Fallback
- Toggle-Position: rechts neben Logout-Button in der Topbar (beide Frontends)
- 3-State-Cycle: Klick auf Sun → Moon, auf Moon → Auto, auf Auto → Sun
- Tooltip: „Helles Theme — klicken für Dunkel" / „Dunkles Theme — klicken für Auto" / „Auto (System-Theme) — klicken für Hell"
- A11y:
aria-labelmit aktuellem State, Tab-fokussierbar, Enter/Space cyclen
Operator-Badge — Token-Klasse¶
Vor der Implementation war der Operator-Badge an --kora-accent gekoppelt;
in tenant-ui war --kora-accent = #013859 (Navy), was den Badge unlesbar
gemacht hätte. Lösung:
- Eigene Tokens
--kora-badge-bg/--kora-badge-fgin beiden Frontends - Tenant-UI rendert die Component nicht (
<ThemeToggle>ja, Operator- Badge nein), aber die Tokens existieren trotzdem für künftige Operator- Surface-Vorschauen (Block 17 / Tenant-Impersonation) - Werte: Light
#f59e0b/#18181b(5.0:1 ✅ AA); Dark#fbbf24/#18181b(9.5:1 ✅ AAA)
Walkthrough-Drift — vorher/nachher¶
| Stelle | Vor (Dark) | Nach (Dark) | Verbesserung |
|---|---|---|---|
Sidebar-Aktiv (BaseLayout.vue:167) |
#3730a3 Text auf #1e1e1e (1.7:1 ❌) |
--kora-link Blue-300 auf #0f1115 (8.2:1 ✅ AAA) |
+6.5:1 |
Hinweis-Card-Headline (ConnectorsPage.vue:101) |
1.6:1 ❌ | 8.2:1 ✅ AAA | +6.6:1 |
| Inline-Link in Hinweis-Card | 1.6:1 ❌ | 8.2:1 ✅ AAA | +6.6:1 |
Tab-Aktiv (TenantsDetailPage.vue:297) |
1.7:1 ❌ | 8.2:1 ✅ AAA | +6.5:1 |
Audit-Filter Badge (AuditLogPage.vue:345) |
1.7:1 ❌ | 8.2:1 ✅ AAA | +6.5:1 |
7 weitere color: var(--kora-primary)-Stellen |
je 1.6–1.7:1 ❌ | je 8.2:1 ✅ AAA | gleich |
11 von 11 Drift-Stellen behoben.
API: User-Preferences¶
| Methode | Pfad | Auth | Body | Response |
|---|---|---|---|---|
GET |
/api/v1/users/me/preferences |
Bearer (beide Realms) | — | {"preferred_theme":"light"\|"dark"\|"auto"} |
PATCH |
/api/v1/users/me/preferences |
Bearer (beide Realms) | {"preferred_theme":...} |
wie GET |
Lazy-Create: Erste GET/PATCH-Anfrage legt automatisch einen Default-
Eintrag (auto) an. Kein Pre-Provisioning bei Login nötig.
Validierung: ungültiger Wert → 422 (Pydantic-Literal + DB-CHECK).
Audit-Skip: User-Preference-Updates schreiben kein Audit-Log (siehe Prompt Phase 1d).
Migration-Pfad für künftige Color-Updates¶
- Token-Set erweitern in
concept-a.md(Source-of-Truth) - Beide tokens.css-Dateien synchron updaten (operator-ui + tenant-ui). 4 Quadranten: light×operator, dark×operator, light×tenant, dark×tenant
- WCAG-Kontrast verifizieren für jedes neue Pair, in
concept-a.mddokumentieren - Bundle-grep nach hardcoded Hex-Werten — sollten 0 sein außer in den tokens.css-Dateien selbst
- Vitest updaten falls Token-Werte in Tests asserted werden
- Browser-Test: beide Frontends in beiden Modi, beide Surfaces
Testing¶
| Layer | Coverage |
|---|---|
| Backend Unit | 0 (Service-Logik wird im Integration-Test mit echter DB geprüft) |
| Backend Integration | 8/8 grün — Lazy-Create, Cross-Realm, Validation, Audit-Skip, Username-Edge-Case |
| Backend Smoke | scripts/smoke-color-system.sh 6/6 |
| Frontend Vitest (operator-ui) | 8 neue useTheme-Tests, total 108/108 grün |
| Frontend Vitest (tenant-ui) | 8 neue useTheme-Tests, total 16/16 grün |
| Frontend Playwright (operator-ui) | 2 neue theme-toggle-Tests, total 12/12 grün |
| Frontend Playwright (tenant-ui) | 2/2 grün (existing template-update.spec ist test.skip) |
Cross-Sektions-Verweise¶
- Bake-Off-Discovery + Pro/Contra:
PORTING-color-system.md - Konzept-A-Tokens (vollständig, siegreich aus Bake-Off):
concept-a.md - Konzept-B und das Vergleichs-Bake-Off-HTML wurden im Reorg 2026-05-09 entfernt (verworfene Variante). Bake-Off-Bilanz und Pro/Contra liegen konsolidiert in der Concept-A-Page oben.