Feature: Dark Mode — System/Hell/Dunkel-Toggle in Einstellungen

CSS-Variablen für Dark Mode in design-system.css (system + manuell via
data-theme), Flash-freies Inline-Script in index.html, Toggle in
settings.js (by_theme in localStorage). SW by-v210, APP_VER 179.
This commit is contained in:
rene 2026-04-18 18:40:34 +02:00
parent eb72d6f675
commit eb3f7b94b2
4 changed files with 112 additions and 14 deletions

View file

@ -115,21 +115,70 @@
--safe-right: env(safe-area-inset-right, 0px); --safe-right: env(safe-area-inset-right, 0px);
} }
/* Dark Mode — vorbereitet, nicht aktiv */ /* Dark Mode — System-Präferenz (nur wenn kein manuelles Theme gesetzt) */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root:not([data-theme="light"]):not([data-theme="dark"]) {
--c-bg: #1A1410; --c-bg: #1A1410;
--c-surface: #241C14; --c-surface: #241C14;
--c-surface-2: #2E2418; --c-surface-2: #2E2418;
--c-surface-3: #3A2E20; --c-surface-3: #3A2E20;
--c-border: #4A3C2C; --c-border: #4A3C2C;
--c-border-light: #3A2E20; --c-border-light: #3A2E20;
--c-primary-subtle: #2A1C0A;
--c-primary-soft: #2E1E08;
--c-nature-subtle: #1A2214;
--c-sky-subtle: #141C22;
--c-danger-subtle: #2A100A;
--c-success-subtle: #122010;
--c-warning-subtle: #261A08;
--c-info-subtle: #10182A;
--c-text: #F0EAE0; --c-text: #F0EAE0;
--c-text-secondary: #C0B0A0; --c-text-secondary: #C0B0A0;
--c-text-muted: #806A58; --c-text-muted: #806A58;
--c-text-inverse: #2A1F14;
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.30);
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.35), 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.40), 0 2px 4px rgba(0, 0, 0, 0.25);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45), 0 4px 8px rgba(0, 0, 0, 0.30);
--shadow-xl: 0 16px 40px rgba(0, 0, 0, 0.50), 0 8px 16px rgba(0, 0, 0, 0.35);
} }
} }
/* Manuelles Dark-Theme via data-theme="dark" (überschreibt auch prefers-color-scheme: light) */
:root[data-theme="dark"] {
--c-bg: #1A1410;
--c-surface: #241C14;
--c-surface-2: #2E2418;
--c-surface-3: #3A2E20;
--c-border: #4A3C2C;
--c-border-light: #3A2E20;
--c-primary-subtle: #2A1C0A;
--c-primary-soft: #2E1E08;
--c-nature-subtle: #1A2214;
--c-sky-subtle: #141C22;
--c-danger-subtle: #2A100A;
--c-success-subtle: #122010;
--c-warning-subtle: #261A08;
--c-info-subtle: #10182A;
--c-text: #F0EAE0;
--c-text-secondary: #C0B0A0;
--c-text-muted: #806A58;
--c-text-inverse: #2A1F14;
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.30);
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.35), 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.40), 0 2px 4px rgba(0, 0, 0, 0.25);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45), 0 4px 8px rgba(0, 0, 0, 0.30);
--shadow-xl: 0 16px 40px rgba(0, 0, 0, 0.50), 0 8px 16px rgba(0, 0, 0, 0.35);
}
/* ------------------------------------------------------------ /* ------------------------------------------------------------
2. RESET & BASE 2. RESET & BASE
------------------------------------------------------------ */ ------------------------------------------------------------ */

View file

@ -170,6 +170,10 @@
<div class="page-body page-container"></div> <div class="page-body page-container"></div>
</section> </section>
<section class="page" id="page-onboarding">
<div class="page-body page-container"></div>
</section>
<section class="page active" id="page-diary"> <section class="page active" id="page-diary">
<div class="page-body page-container"> <div class="page-body page-container">
<!-- wird von diary.js befüllt --> <!-- wird von diary.js befüllt -->

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. Router, State-Management, Navigation, Initialisierung.
============================================================ */ ============================================================ */
const APP_VER = '175'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VER = '179'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => { const App = (() => {
@ -33,6 +33,7 @@ const App = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
const pages = { const pages = {
welcome: { title: 'Willkommen', module: null }, welcome: { title: 'Willkommen', module: null },
onboarding: { title: 'Einrichtung', module: null, requiresAuth: true },
diary: { title: 'Tagebuch', module: null, requiresAuth: true }, diary: { title: 'Tagebuch', module: null, requiresAuth: true },
health: { title: 'Gesundheit', module: null, requiresAuth: true }, health: { title: 'Gesundheit', module: null, requiresAuth: true },
'dog-profile': { title: 'Mein Hund', module: null, requiresAuth: true }, 'dog-profile': { title: 'Mein Hund', module: null, requiresAuth: true },
@ -413,9 +414,9 @@ const App = (() => {
} }
await _loadDogs(); await _loadDogs();
// Eingeloggter User ohne Hund (z.B. nach Reload) → direkt zur Hund-Anlage // Eingeloggter User ohne Hund → Onboarding-Wizard (einmalig)
if (state.dogs.length === 0) { if (state.dogs.length === 0 && !localStorage.getItem('by_onboarding_done')) {
navigate('dog-profile'); navigate('onboarding');
} }
_updateNotifBadge(); _updateNotifBadge();

View file

@ -182,6 +182,32 @@ window.Page_settings = (() => {
App-Einstellungen App-Einstellungen
</div> </div>
<div class="card-body" style="padding:0"> <div class="card-body" style="padding:0">
<!-- Dark-Mode-Auswahl -->
<div style="display:flex;align-items:center;gap:var(--space-3);
padding:var(--space-4);border-bottom:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#moon"></use></svg>
<div style="flex:1">
<div style="font-weight:500">Dark Mode</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
Erscheinungsbild der App
</div>
</div>
<select id="select-theme"
style="padding:var(--space-1) var(--space-2);
border:1.5px solid var(--c-border);
border-radius:var(--radius-md);
background:var(--c-surface);
color:var(--c-text);
font-size:var(--text-sm);
font-family:inherit;
cursor:pointer">
<option value="system" ${(localStorage.getItem('by_theme')||'system') === 'system' ? 'selected' : ''}>System</option>
<option value="light" ${localStorage.getItem('by_theme') === 'light' ? 'selected' : ''}>Hell</option>
<option value="dark" ${localStorage.getItem('by_theme') === 'dark' ? 'selected' : ''}>Dunkel</option>
</select>
</div>
<div style="display:flex;align-items:center;gap:var(--space-3); <div style="display:flex;align-items:center;gap:var(--space-3);
padding:var(--space-4);border-bottom:1px solid var(--c-border)"> padding:var(--space-4);border-bottom:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#eye-slash"></use></svg> <svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#eye-slash"></use></svg>
@ -420,6 +446,24 @@ window.Page_settings = (() => {
} }
}); });
document.getElementById('select-theme')?.addEventListener('change', e => {
const val = e.target.value;
localStorage.setItem('by_theme', val);
const html = document.documentElement;
if (val === 'dark') {
html.setAttribute('data-theme', 'dark');
} else if (val === 'light') {
html.setAttribute('data-theme', 'light');
} else {
html.removeAttribute('data-theme');
}
UI.toast.info(
val === 'dark' ? 'Dark Mode aktiviert.' :
val === 'light' ? 'Hell-Modus aktiviert.' :
'Theme folgt der Systemeinstellung.'
);
});
document.getElementById('toggle-pocket-mode')?.addEventListener('change', e => { document.getElementById('toggle-pocket-mode')?.addEventListener('change', e => {
localStorage.setItem('by_pocket_mode', String(e.target.checked)); localStorage.setItem('by_pocket_mode', String(e.target.checked));
UI.toast.info(e.target.checked UI.toast.info(e.target.checked