feat(ui): UI-001 Designsystem-Grundlage - Dark Mode + Readiness-Farbtokens
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:
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
|
||||
import { NavLink, useNavigate } from "react-router-dom";
|
||||
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
/** Gemeinsamer Rahmen (Kopfzeile + Navigation) für alle eingeloggten Screens. */
|
||||
export function AppShell({ children }: { children: ReactNode }) {
|
||||
@@ -41,6 +42,7 @@ export function AppShell({ children }: { children: ReactNode }) {
|
||||
{istAdmin ? "Administration" : "Verwaltung"}
|
||||
</NavLink>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
<button className="btn btn-secondary" onClick={handleLogout}>
|
||||
Abmelden
|
||||
</button>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,14 @@
|
||||
--color-success: #1e7a34;
|
||||
--color-success-bg: #e7f5ea;
|
||||
--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-sm: 6px;
|
||||
--shadow-sm: none;
|
||||
@@ -29,6 +37,73 @@
|
||||
--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;
|
||||
}
|
||||
@@ -348,6 +423,45 @@ select.input:focus {
|
||||
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 {
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
Reference in New Issue
Block a user