Add light/dark theme with system-preference detection
Neues client/src/lib/theme.ts verwaltet die Wahl (system/light/dark) in localStorage. Umschalter-Button im Header (zyklisch: System -> Hell -> Dunkel), Icon zeigt den aktuellen Modus. CSS-Variablen (--bg/--card/--text/--muted/ --bor/--cyan) bekommen einen Hell-Override über [data-theme="light"] bzw. @media(prefers-color-scheme:light) ohne explizites [data-theme="dark"] - so gewinnt eine manuelle Wahl in beide Richtungen gegen die Systemeinstellung. Header/Tabs/Login bleiben bewusst immer dunkel-navy (Markenidentität), nur der Content-Bereich wechselt. Inline-Script in index.html verhindert ein kurzes Aufblitzen des falschen Farbschemas vor dem ersten Paint. Temporärer test-build-only.yml Workflow zur Verifikation ohne Registry-Push. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3d070d4dd7
commit
c1bb51b480
@@ -0,0 +1,12 @@
|
|||||||
|
name: Test Build Only (temporary, no push)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Docker build (no push)
|
||||||
|
run: docker build -t materialschein-test:local .
|
||||||
@@ -7,6 +7,14 @@
|
|||||||
<link rel="icon" href="/icons/favicon-32.png" sizes="32x32" />
|
<link rel="icon" href="/icons/favicon-32.png" sizes="32x32" />
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
||||||
<title>BergVOLT Elektrotechnik – Materialschein</title>
|
<title>BergVOLT Elektrotechnik – Materialschein</title>
|
||||||
|
<script>
|
||||||
|
// Vor dem ersten Paint anwenden, damit bei explizit gewähltem Theme (abweichend
|
||||||
|
// von der Systemeinstellung) kein kurzes Aufblitzen des falschen Farbschemas passiert.
|
||||||
|
try {
|
||||||
|
var t = localStorage.getItem('bv_theme');
|
||||||
|
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
||||||
|
} catch (e) {}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useMsal } from '@azure/msal-react';
|
import { useMsal } from '@azure/msal-react';
|
||||||
|
import { getStoredTheme, setTheme, type ThemeMode } from '../lib/theme';
|
||||||
|
|
||||||
|
const THEME_ICON: Record<ThemeMode, string> = { system: '🖥️', light: '☀️', dark: '🌙' };
|
||||||
|
const THEME_LABEL: Record<ThemeMode, string> = { system: 'System', light: 'Hell', dark: 'Dunkel' };
|
||||||
|
const NEXT_THEME: Record<ThemeMode, ThemeMode> = { system: 'light', light: 'dark', dark: 'system' };
|
||||||
|
|
||||||
export default function Header({ localUser, onLocalLogout }: { localUser: string | null; onLocalLogout: () => void }) {
|
export default function Header({ localUser, onLocalLogout }: { localUser: string | null; onLocalLogout: () => void }) {
|
||||||
const { instance, accounts } = useMsal();
|
const { instance, accounts } = useMsal();
|
||||||
@@ -9,12 +15,19 @@ export default function Header({ localUser, onLocalLogout }: { localUser: string
|
|||||||
const displayName = localUser ?? account?.name ?? account?.username ?? '–';
|
const displayName = localUser ?? account?.name ?? account?.username ?? '–';
|
||||||
const roleLine = localUser ? 'Lokales Konto' : (account?.username ?? '');
|
const roleLine = localUser ? 'Lokales Konto' : (account?.username ?? '');
|
||||||
const initials = displayName.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
const initials = displayName.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
||||||
|
const [themeMode, setThemeMode] = useState<ThemeMode>(getStoredTheme());
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
if (account) instance.logoutPopup();
|
if (account) instance.logoutPopup();
|
||||||
onLocalLogout();
|
onLocalLogout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cycleTheme() {
|
||||||
|
const next = NEXT_THEME[themeMode];
|
||||||
|
setTheme(next);
|
||||||
|
setThemeMode(next);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
@@ -24,6 +37,13 @@ export default function Header({ localUser, onLocalLogout }: { localUser: string
|
|||||||
<div className="logo-sub">Elektrotechnik</div>
|
<div className="logo-sub">Elektrotechnik</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn-theme"
|
||||||
|
onClick={cycleTheme}
|
||||||
|
title={`Darstellung: ${THEME_LABEL[themeMode]} (klicken zum Wechseln)`}
|
||||||
|
>
|
||||||
|
{THEME_ICON[themeMode]}
|
||||||
|
</button>
|
||||||
<div className="hdr-user">
|
<div className="hdr-user">
|
||||||
<div className="hdr-av">{initials}</div>
|
<div className="hdr-av">{initials}</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export type ThemeMode = 'system' | 'light' | 'dark';
|
||||||
|
|
||||||
|
const KEY = 'bv_theme';
|
||||||
|
|
||||||
|
export function getStoredTheme(): ThemeMode {
|
||||||
|
const v = localStorage.getItem(KEY);
|
||||||
|
return v === 'light' || v === 'dark' ? v : 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTheme(mode: ThemeMode) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (mode === 'system') root.removeAttribute('data-theme');
|
||||||
|
else root.setAttribute('data-theme', mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTheme(mode: ThemeMode) {
|
||||||
|
localStorage.setItem(KEY, mode);
|
||||||
|
applyTheme(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initTheme() {
|
||||||
|
applyTheme(getStoredTheme());
|
||||||
|
}
|
||||||
@@ -2,9 +2,12 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { MsalProvider } from '@azure/msal-react';
|
import { MsalProvider } from '@azure/msal-react';
|
||||||
import { msalInstance } from './lib/authConfig';
|
import { msalInstance } from './lib/authConfig';
|
||||||
|
import { initTheme } from './lib/theme';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
|
initTheme();
|
||||||
|
|
||||||
msalInstance.initialize().then(() => {
|
msalInstance.initialize().then(() => {
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/* Dunkel ist die Grundeinstellung (bare :root). Header/Tabs/Login bleiben in
|
||||||
|
beiden Modi dunkel-navy (Markenidentität) – nur der Content-Bereich (Karten,
|
||||||
|
Tabellen, Formulare) wechselt. Siehe [data-theme="light"] weiter unten für
|
||||||
|
die Werte und wie System-Erkennung + expliziter Umschalter zusammenspielen. */
|
||||||
:root{
|
:root{
|
||||||
--bg:#0a1622;
|
--bg:#0a1622;
|
||||||
--bg2:#0f2033;
|
--bg2:#0f2033;
|
||||||
@@ -13,6 +17,39 @@
|
|||||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:var(--bg);color:var(--text);min-height:100vh;font-size:14px;line-height:1.4}
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:var(--bg);color:var(--text);min-height:100vh;font-size:14px;line-height:1.4}
|
||||||
button,input,select,textarea{font-family:inherit}
|
button,input,select,textarea{font-family:inherit}
|
||||||
|
|
||||||
|
/* ── Theme: Hell ──
|
||||||
|
Zwei Auslöser, die dieselben Werte setzen:
|
||||||
|
1) [data-theme="light"] – explizit vom Umschalter gewählt, unabhängig vom System.
|
||||||
|
2) @media(prefers-color-scheme:light) ohne explizites [data-theme="dark"] – folgt
|
||||||
|
der Systemeinstellung, solange niemand manuell "Dunkel" erzwungen hat. */
|
||||||
|
:root[data-theme="light"]{
|
||||||
|
--bg:#f3f6f8;--bg2:#ffffff;--card:#ffffff;--cyan:#0086a3;--text:#0f2130;--muted:#5c7686;--bor:#dbe4ea;
|
||||||
|
--sh:0 3px 16px rgba(15,35,50,.08);
|
||||||
|
}
|
||||||
|
:root[data-theme="light"] .btn-ghost{background:rgba(15,35,50,.05)}
|
||||||
|
:root[data-theme="light"] .status-aktiv{background:rgba(0,170,90,.12);color:#0a8a4c}
|
||||||
|
:root[data-theme="light"] .status-abgeschlossen{background:rgba(92,118,134,.12)}
|
||||||
|
:root[data-theme="light"] .badge-nym{background:rgba(0,110,200,.14);color:#0064b8}
|
||||||
|
:root[data-theme="light"] .badge-nyy{background:rgba(210,110,0,.14);color:#a35b00}
|
||||||
|
:root[data-theme="light"] .badge-h07{background:rgba(120,0,180,.14);color:#7a1fae}
|
||||||
|
:root[data-theme="light"] .badge-ysly{background:rgba(210,40,40,.14);color:#b32a2a}
|
||||||
|
:root[data-theme="light"] .badge-schlauch{background:rgba(20,150,90,.14);color:#0f7d4f}
|
||||||
|
|
||||||
|
@media(prefers-color-scheme:light){
|
||||||
|
:root:not([data-theme="dark"]){
|
||||||
|
--bg:#f3f6f8;--bg2:#ffffff;--card:#ffffff;--cyan:#0086a3;--text:#0f2130;--muted:#5c7686;--bor:#dbe4ea;
|
||||||
|
--sh:0 3px 16px rgba(15,35,50,.08);
|
||||||
|
}
|
||||||
|
:root:not([data-theme="dark"]) .btn-ghost{background:rgba(15,35,50,.05)}
|
||||||
|
:root:not([data-theme="dark"]) .status-aktiv{background:rgba(0,170,90,.12);color:#0a8a4c}
|
||||||
|
:root:not([data-theme="dark"]) .status-abgeschlossen{background:rgba(92,118,134,.12)}
|
||||||
|
:root:not([data-theme="dark"]) .badge-nym{background:rgba(0,110,200,.14);color:#0064b8}
|
||||||
|
:root:not([data-theme="dark"]) .badge-nyy{background:rgba(210,110,0,.14);color:#a35b00}
|
||||||
|
:root:not([data-theme="dark"]) .badge-h07{background:rgba(120,0,180,.14);color:#7a1fae}
|
||||||
|
:root:not([data-theme="dark"]) .badge-ysly{background:rgba(210,40,40,.14);color:#b32a2a}
|
||||||
|
:root:not([data-theme="dark"]) .badge-schlauch{background:rgba(20,150,90,.14);color:#0f7d4f}
|
||||||
|
}
|
||||||
|
|
||||||
/* Login */
|
/* Login */
|
||||||
.login-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;background:#070f18;position:relative;overflow:hidden;padding:20px}
|
.login-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;background:#070f18;position:relative;overflow:hidden;padding:20px}
|
||||||
.login-screen::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 70% 60% at 30% 30%,rgba(0,200,216,.08) 0%,transparent 70%)}
|
.login-screen::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 70% 60% at 30% 30%,rgba(0,200,216,.08) 0%,transparent 70%)}
|
||||||
@@ -39,6 +76,8 @@ header{background:linear-gradient(135deg,#081420 0%,#0c1c30 100%);border-bottom:
|
|||||||
.hdr-role{font-size:9.5px;color:var(--muted)}
|
.hdr-role{font-size:9.5px;color:var(--muted)}
|
||||||
.btn-logout{background:rgba(255,255,255,.06);border:1px solid var(--bor);color:var(--muted);border-radius:8px;padding:8px 12px;font-size:11px;font-weight:700;cursor:pointer;flex-shrink:0}
|
.btn-logout{background:rgba(255,255,255,.06);border:1px solid var(--bor);color:var(--muted);border-radius:8px;padding:8px 12px;font-size:11px;font-weight:700;cursor:pointer;flex-shrink:0}
|
||||||
.btn-logout:hover{color:#ff6b6b;border-color:rgba(255,85,85,.4)}
|
.btn-logout:hover{color:#ff6b6b;border-color:rgba(255,85,85,.4)}
|
||||||
|
.btn-theme{background:rgba(255,255,255,.06);border:1px solid var(--bor);border-radius:8px;width:36px;height:36px;font-size:15px;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;line-height:1}
|
||||||
|
.btn-theme:hover{background:rgba(255,255,255,.12)}
|
||||||
|
|
||||||
/* Tabs */
|
/* Tabs */
|
||||||
.tabs{background:#081420;display:flex;padding:0 20px;gap:2px;border-bottom:1px solid var(--bor);overflow-x:auto}
|
.tabs{background:#081420;display:flex;padding:0 20px;gap:2px;border-bottom:1px solid var(--bor);overflow-x:auto}
|
||||||
|
|||||||
Reference in New Issue
Block a user