feat(ui): UI-001 Designsystem-Grundlage - Dark Mode + Readiness-Farbtokens
CI / backend-tests (push) Successful in 2m1s
CI / frontend-build (push) Successful in 18s

Epic 20 (UI-Redesign) angelegt, Stufe 1: CSS-Variablen für Dark Mode
(System-Präferenz + manueller Schalter via ThemeToggle, localStorage-
persistiert) und eigene Farbtokens für Einsatzbereitschaft (ready/
limited/not-ready/unknown), Basis für ReadinessBadge in UI-003.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-06 09:57:49 +02:00
co-authored by Claude Sonnet 5
parent 01263753bf
commit bccb8d1933
5 changed files with 351 additions and 3 deletions
+4 -3
View File
@@ -272,10 +272,11 @@ eigenes Modul (MAINT-\*, MABEA hat nur Prüfung, keine Wartung)**.
| `17_zustaendigkeit.md` | ZUST-001 … ZUST-002 (vollständig, nachträglich ergänzt) | | `17_zustaendigkeit.md` | ZUST-001 … ZUST-002 (vollständig, nachträglich ergänzt) |
| `18_notifications.md` | NOTIF-001 … NOTIF-002 (vollständig, nachträglich ergänzt) | | `18_notifications.md` | NOTIF-001 … NOTIF-002 (vollständig, nachträglich ergänzt) |
| `19_satelliten_server.md` | SAT-001 (Platzhalter, nachträglich ergänzt, bewusst zurückgestellt) | | `19_satelliten_server.md` | SAT-001 (Platzhalter, nachträglich ergänzt, bewusst zurückgestellt) |
| `20_ui_redesign.md` | UI-001 … UI-008 (vollständig, nachträglich ergänzt, in Umsetzung) |
**Alle 19 Epics durchgegangen** (16 ursprüngliche + 17 Zuständigkeit/18 Notifications/19 **Alle 20 Epics durchgegangen** (16 ursprüngliche + 17 Zuständigkeit/18 Notifications/19
Satelliten-Server, nachträglich aus dem Abgleich mit den alten Arbeitskarten ergänzt). Satelliten-Server/20 UI-Redesign, nachträglich ergänzt). Detaillierungsphase
Detaillierungsphase abgeschlossen (2026-09-05). abgeschlossen (2026-09-05), UI-Redesign-Epic ergänzt (2026-09-06).
## Nachtrag: Externe Roadmap-Bewertung Shelf/Resgrid/Emergency Mgmt (2026-09-05) ## Nachtrag: Externe Roadmap-Bewertung Shelf/Resgrid/Emergency Mgmt (2026-09-05)
+173
View File
@@ -0,0 +1,173 @@
# Epic 20 — UI-Redesign (moderne BOS-Plattform-Optik)
Ausgangspunkt: externer Redesign-Prompt vom 2026-09-06 (Nutzer-Vorgabe, siehe
Referenzen). **Keine Neuentwicklung, keine Entfernung bestehender Funktionen**
bestehende API/Fachlogik/PWA/Offline-Sync bleiben unverändert, nur die Darstellung
wird modernisiert. React/Vite/TypeScript bleibt, kein Framework-Wechsel.
Aktueller Stand vor Redesign: 39 Seiten-/Komponenten-Dateien, ~7.000 Zeilen, einfaches
`global.css` (560 Zeilen) ohne Designsystem, keine UI-Bibliothek.
---
## UI-001 — Designsystem-Grundlage
- **Ziel:** Ein zentrales Set an Design-Tokens (Farben, Typografie, Abstände,
Radien, Schatten) statt verstreuter Inline-Styles.
- **Beschreibung:** CSS-Variablen (Light + Dark), zentrale Status-Farben
(einsatzbereit/eingeschränkt/nicht einsatzbereit/unbekannt), Basis-Komponenten
(Button, Card, Input, Badge, Table) vereinheitlicht.
- **Benutzerwert:** Konsistentes, hochwertiges Erscheinungsbild in der ganzen App.
- **Abhängigkeiten:** keine
- **Backend:** keins
- **Frontend:** `styles/tokens.css`, überarbeitetes `global.css`, Dark-Mode via
`prefers-color-scheme` + manuellem Umschalter
- **Mobile:** Basis für alle Folge-Kacheln
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** Dark Mode umschaltbar, alle Basis-Elemente nutzen Tokens
statt Hardcoded-Werten.
- **Tests:** keine (reines Styling), visuelle Kontrolle.
- **DoD:** offen — noch nicht begonnen.
## UI-002 — App-Shell & Navigation
- **Ziel:** Moderne Sidebar (Desktop) / kompakte Navigation (Mobile) statt
bisheriger einfacher Navigation in `AppShell.tsx`.
- **Beschreibung:** Bereiche Dashboard/Objekte/Fahrzeuge/Material/Lager/
Kontrollen/Prüfungen/Mängel/Personal/Administration. Globale Suche `Ctrl/⌘+K`.
Quick Actions (QR scannen, Kontrolle starten, Mangel melden, Material buchen).
- **Benutzerwert:** Schnelle Orientierung, weniger Klicks zu Kernaktionen.
- **Abhängigkeiten:** UI-001
- **Backend:** keins (Suche client-seitig über bereits geladene Objekt-/Material-Listen,
kein neuer Such-Endpunkt in Phase 1)
- **Frontend:** `AppShell.tsx` umgebaut, neue `CommandPalette`-Komponente
- **Mobile:** Bottom-Navigation oder Drawer statt Sidebar
- **Rechte:** Navigationspunkte nach Rolle gefiltert (bereits vorhandene Rollen-Logik
wiederverwenden)
- **Akzeptanzkriterien:** alle bestehenden Routen weiter erreichbar, Suche findet
Objekte per Name/Code.
- **Tests:** keine neuen (reines Routing/UI).
- **DoD:** offen.
## UI-003 — Zentrale Statuskomponenten
- **Ziel:** Wiederverwendbare Status-Anzeigen statt an mehreren Stellen dupliziertem
Badge-Markup.
- **Beschreibung:** `ReadinessBadge` (einsatzbereit/eingeschränkt/nicht
einsatzbereit/unbekannt — „unbekannt" NIE als einsatzbereit darstellen),
`SyncStatus` (online/offline/synchronisiert), `InspectionStatus`,
`InventoryStatus`. Jede Komponente kapselt Farbe+Icon+Text an einer Stelle.
- **Benutzerwert:** Konsistente, sofort verständliche Statusdarstellung überall.
- **Abhängigkeiten:** UI-001
- **Backend:** keins
- **Frontend:** `components/status/ReadinessBadge.tsx` etc., bestehende Ad-hoc-Badges
(`DashboardPage.tsx`, `AktePage.tsx`, `ObjektListPage.tsx`) darauf umgestellt
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** kein Status mehr als Inline-`<span className="badge...">`
dupliziert.
- **Tests:** ggf. kleiner Komponententest je Status-Mapping.
- **DoD:** offen.
## UI-004 — Dashboard neu
- **Ziel:** „Was ist einsatzbereit und was muss ich jetzt tun?" in 5 Sekunden
beantwortbar.
- **Beschreibung:** Einsatzbereitschaft-Übersicht (einsatzbereit/eingeschränkt/
nicht einsatzbereit/unbekannt), offene Mängel, fällige Kontrollen/Prüfungen,
fehlendes Material, aktuelle Aufgaben — alles aus bereits vorhandenen
Dashboard-/Fehlbestand-/Mangel-Endpunkten, keine Fake-Daten.
- **Benutzerwert:** Kernversprechen der Kachel — sofortiger Überblick.
- **Abhängigkeiten:** UI-001, UI-003
- **Backend:** keins zusätzlich (bestehende Dashboard-Kennzahlen wiederverwenden)
- **Frontend:** `DashboardPage.tsx` neu strukturiert, Card-Grid statt Liste
- **Mobile:** gestapeltes Layout
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** alle geforderten Kennzahlen sichtbar, keine Platzhalter-/
Fake-Werte.
- **Tests:** bestehende Dashboard-Tests bleiben grün.
- **DoD:** offen.
## UI-005 — Objektakte modernisiert
- **Ziel:** `AktePage.tsx` (bereits FILE-006/Akte-Aggregation) optisch/strukturell
aufwerten, inkl. begründeter Einsatzbereitschafts-Anzeige.
- **Beschreibung:** Status mit Begründung (z. B. „🔴 Nicht einsatzbereit — Funkgerät
defekt, 2 Beladungspositionen fehlen, Prüfung überfällig"), Tabs oder klar
getrennte Bereiche für Beladung/Kontrollen/Mängel/Prüfungen/Wartungen/
Dokumente/Historie.
- **Benutzerwert:** Sofort verständlich, WARUM ein Objekt nicht einsatzbereit ist.
- **Abhängigkeiten:** UI-001, UI-003
- **Backend:** ggf. kleine Ergänzung am `/akte/objekt/{id}`-Endpunkt, falls die
Begründungs-Texte nicht schon aus den Einzelfeldern ableitbar sind (prüfen vor
Umsetzung)
- **Frontend:** `AktePage.tsx` Umbau, bisherige 6 gleichartigen Card-Blöcke
(bereits als Simplification-Fund dokumentiert) auf gemeinsame Komponente
vereinheitlichen
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** „unbekannt/nie kontrolliert" wird nie als einsatzbereit
dargestellt (harte Regel, Akzeptanzkriterium testbar).
- **Tests:** Begründungslogik testen (welche Bedingungen führen zu welchem Status).
- **DoD:** offen.
## UI-006 — Kontrolle & Beladung modernisiert
- **Ziel:** Bestehenden Kontrollworkflow (QR/Objekt → Kontrolle starten → Position
prüfen → OK/Abweichung → speichern → Abschluss) beibehalten, Fortschritt
(`38 / 40 geprüft`) und fehlerhafte Positionen visuell hervorheben.
- **Benutzerwert:** Schnellere, fehlerfreiere Kontrolle vor Ort.
- **Abhängigkeiten:** UI-001, UI-003
- **Backend:** keins
- **Frontend:** `KontrollPage.tsx` + `pages/kontrolle/*` Fortschrittsanzeige,
hervorgehobene Abweichungen
- **Mobile:** Priorität — Haupteinsatzort dieses Screens
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** Fortschritt live sichtbar, Abweichungen nicht zu übersehen.
- **Tests:** bestehende Kontroll-Tests bleiben grün.
- **DoD:** offen.
## UI-007 — QR-Scan-Aktionsleiste
- **Ziel:** Nach Scan direkt Aktionen anbieten (Kontrolle starten/Mangel melden/
Informationen), nicht nur Direkt-Navigation.
- **Beschreibung:** Baut auf MOBILE-002-Fix (Scan → Akte) auf, ergänzt auf der
Akte-Seite eine hervorgehobene Aktionsleiste statt nur einem Button.
- **Benutzerwert:** Weniger Klicks nach dem Scan.
- **Abhängigkeiten:** UI-005, MOBILE-002 (bereits erledigt)
- **Backend:** keins
- **Frontend:** Aktionsleiste in `AktePage.tsx`
- **Rechte:** je Aktion bestehende Rollenprüfung wiederverwenden
- **Akzeptanzkriterien:** alle drei Aktionen von der Akte aus erreichbar.
- **Tests:** keine neuen.
- **DoD:** offen.
## UI-008 — Restliche Module (Material, Lager, Personal, Administration)
- **Ziel:** Verbleibende Seiten im neuen Designsystem angleichen.
- **Beschreibung:** Admin-Tabs (`LagerSection.tsx` u.a.), `MangelListePage.tsx`,
`FehlbestandListePage.tsx` auf Tokens/Statuskomponenten umstellen.
- **Benutzerwert:** Durchgängiges Erscheinungsbild, keine Stilbrüche.
- **Abhängigkeiten:** UI-001..003
- **Backend:** keins
- **Frontend:** verbleibende Seiten
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** keine Seite mehr im alten Stil.
- **Tests:** bestehende Tests bleiben grün.
- **DoD:** offen.
---
**Reihenfolge (zwingend, laut Vorgabe):** UI-001 → UI-002 → UI-003 → UI-004 → UI-005
→ UI-006 → UI-007 → UI-008. Jede Stufe wird einzeln abgeschlossen und bestätigt,
bevor die nächste beginnt (Arbeitskacheln-Methodik: kein Direktcoden ohne Freigabe
je Schritt).
**Nicht-Ziele:** kein Framework-Wechsel, keine Breaking Changes an API/Fachlogik,
keine Fake-Daten, Offline/PWA/IndexedDB-Sync bleibt unangetastet, bestehende Tests
bleiben grün und werden erweitert wo sinnvoll.
**WCAG 2.2 AA:** als Leitplanke bei UI-001 (Kontraste/Fokus-Stile) und UI-002
(Tastaturbedienung Command Palette) mitgedacht, kein separater Prüf-Task.
## Referenzen
Externer Redesign-Prompt, vom Nutzer am 2026-09-06 eingebracht (kompakte Version
für Coding-KI, Zielrepo MABEA).
+2
View File
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
import { NavLink, useNavigate } from "react-router-dom"; import { NavLink, useNavigate } from "react-router-dom";
import { useAuth } from "../auth/AuthContext"; import { useAuth } from "../auth/AuthContext";
import { ThemeToggle } from "./ThemeToggle";
/** Gemeinsamer Rahmen (Kopfzeile + Navigation) für alle eingeloggten Screens. */ /** Gemeinsamer Rahmen (Kopfzeile + Navigation) für alle eingeloggten Screens. */
export function AppShell({ children }: { children: ReactNode }) { export function AppShell({ children }: { children: ReactNode }) {
@@ -41,6 +42,7 @@ export function AppShell({ children }: { children: ReactNode }) {
{istAdmin ? "Administration" : "Verwaltung"} {istAdmin ? "Administration" : "Verwaltung"}
</NavLink> </NavLink>
)} )}
<ThemeToggle />
<button className="btn btn-secondary" onClick={handleLogout}> <button className="btn btn-secondary" onClick={handleLogout}>
Abmelden Abmelden
</button> </button>
+58
View File
@@ -0,0 +1,58 @@
import { useEffect, useState } from "react";
type Theme = "system" | "light" | "dark";
const SPEICHER_KEY = "mabea-theme";
function angewandtesTheme(theme: Theme): void {
if (theme === "system") {
document.documentElement.removeAttribute("data-theme");
} else {
document.documentElement.setAttribute("data-theme", theme);
}
}
function gespeichertesTheme(): Theme {
try {
const wert = localStorage.getItem(SPEICHER_KEY);
if (wert === "light" || wert === "dark") return wert;
} catch {
// localStorage evtl. blockiert (privater Modus) - Systemeinstellung als Fallback
}
return "system";
}
const REIHENFOLGE: Theme[] = ["system", "light", "dark"];
const LABEL: Record<Theme, string> = { system: "System", light: "Hell", dark: "Dunkel" };
/** UI-001 (Epic 20): manueller Dark-Mode-Schalter, Präferenz überlebt Reload. */
export function ThemeToggle() {
const [theme, setTheme] = useState<Theme>(() => gespeichertesTheme());
useEffect(() => {
angewandtesTheme(theme);
try {
if (theme === "system") localStorage.removeItem(SPEICHER_KEY);
else localStorage.setItem(SPEICHER_KEY, theme);
} catch {
// Speichern optional - beim nächsten Laden greift dann Systemeinstellung
}
}, [theme]);
function weiterschalten() {
const naechster = REIHENFOLGE[(REIHENFOLGE.indexOf(theme) + 1) % REIHENFOLGE.length];
setTheme(naechster);
}
return (
<button
type="button"
className="theme-toggle"
onClick={weiterschalten}
title="Farbschema wechseln (System/Hell/Dunkel)"
aria-label={`Farbschema: ${LABEL[theme]}, klicken zum Wechseln`}
>
{theme === "dark" ? "🌙" : theme === "light" ? "☀️" : "🖥️"} {LABEL[theme]}
</button>
);
}
+114
View File
@@ -21,6 +21,14 @@
--color-success: #1e7a34; --color-success: #1e7a34;
--color-success-bg: #e7f5ea; --color-success-bg: #e7f5ea;
--color-muted-bg: #eef0f3; --color-muted-bg: #eef0f3;
--color-ready: #1e7a34;
--color-ready-bg: #e7f5ea;
--color-limited: #9a6700;
--color-limited-bg: #fff3d6;
--color-not-ready: #b3261e;
--color-not-ready-bg: #fbeaea;
--color-unknown: #55606b;
--color-unknown-bg: #e9ebee;
--radius: 8px; --radius: 8px;
--radius-sm: 6px; --radius-sm: 6px;
--shadow-sm: none; --shadow-sm: none;
@@ -29,6 +37,73 @@
--font-sans: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif; --font-sans: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
} }
/* UI-001 (Epic 20): Dark Mode - Systemeinstellung per prefers-color-scheme,
manueller Override per [data-theme] auf <html> (ThemeToggle-Komponente,
Präferenz in localStorage). Reihenfolge wichtig: System-Media-Query zuerst,
[data-theme="dark"] danach, damit der manuelle Schalter in beide Richtungen
gewinnt. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg: #14171a;
--color-surface: #1c2024;
--color-surface-sunken: #22262b;
--color-border: #333941;
--color-border-strong: #454c55;
--color-text: #e8eaed;
--color-text-muted: #9aa4ae;
--color-primary: #5b9bd8;
--color-primary-hover: #7bb0e2;
--color-primary-soft: #1e2c3a;
--color-danger: #e37c76;
--color-danger-bg: #3a2224;
--color-warning: #e0b34d;
--color-warning-bg: #3a3120;
--color-success: #6cc386;
--color-success-bg: #1f3324;
--color-muted-bg: #262b31;
--color-ready: #6cc386;
--color-ready-bg: #1f3324;
--color-limited: #e0b34d;
--color-limited-bg: #3a3120;
--color-not-ready: #e37c76;
--color-not-ready-bg: #3a2224;
--color-unknown: #9aa4ae;
--color-unknown-bg: #262b31;
}
}
:root[data-theme="dark"] {
--color-bg: #14171a;
--color-surface: #1c2024;
--color-surface-sunken: #22262b;
--color-border: #333941;
--color-border-strong: #454c55;
--color-text: #e8eaed;
--color-text-muted: #9aa4ae;
--color-primary: #5b9bd8;
--color-primary-hover: #7bb0e2;
--color-primary-soft: #1e2c3a;
--color-danger: #e37c76;
--color-danger-bg: #3a2224;
--color-warning: #e0b34d;
--color-warning-bg: #3a3120;
--color-success: #6cc386;
--color-success-bg: #1f3324;
--color-muted-bg: #262b31;
--color-ready: #6cc386;
--color-ready-bg: #1f3324;
--color-limited: #e0b34d;
--color-limited-bg: #3a3120;
--color-not-ready: #e37c76;
--color-not-ready-bg: #3a2224;
--color-unknown: #9aa4ae;
--color-unknown-bg: #262b31;
}
body {
transition: background-color 0.15s ease, color 0.15s ease;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
@@ -348,6 +423,45 @@ select.input:focus {
color: var(--color-danger); color: var(--color-danger);
} }
/* UI-003 (Epic 20): Einsatzbereitschafts-Status - eigene Klassen statt
Wiederverwendung von badge-success/warning/danger, weil "unbekannt" fachlich
NIE wie "einsatzbereit" aussehen darf (siehe ReadinessBadge-Komponente). */
.badge-ready {
background: var(--color-ready-bg);
color: var(--color-ready);
}
.badge-limited {
background: var(--color-limited-bg);
color: var(--color-limited);
}
.badge-not-ready {
background: var(--color-not-ready-bg);
color: var(--color-not-ready);
}
.badge-unknown {
background: var(--color-unknown-bg);
color: var(--color-unknown);
}
.theme-toggle {
background: transparent;
border: 1px solid var(--color-border-strong);
color: var(--color-text-muted);
border-radius: var(--radius-sm);
padding: 0.4rem 0.6rem;
cursor: pointer;
font-size: 0.85rem;
line-height: 1;
}
.theme-toggle:hover {
background: var(--color-surface-sunken);
color: var(--color-text);
}
.alert { .alert {
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
padding: 0.75rem 1rem; padding: 0.75rem 1rem;