From 87973eaa4a378f61cdcb4cc487bef8f2aa0bf3be Mon Sep 17 00:00:00 2001 From: elpatron Date: Sun, 31 May 2026 12:38:01 +0200 Subject: [PATCH] fix: Light-Theme-Hintergrund auf PWA/Android reparieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der hardcodierte Inline-Style auf body überschrieb --app-body-bg und ließ hellen Modus mit dunklem Seitenhintergrund erscheinen. Theme-Bootstrap und dynamisches theme-color ergänzen alle Scheme/Theme-Kombinationen. Co-authored-by: Cursor --- client/index.html | 5 +- client/public/appearance-bootstrap.js | 44 ++++++++++++++++ client/src/services/appearance.test.ts | 70 ++++++++++++++++++++++++++ client/src/services/appearance.ts | 13 +++++ client/src/themes.css | 7 +++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 client/public/appearance-bootstrap.js create mode 100644 client/src/services/appearance.test.ts diff --git a/client/index.html b/client/index.html index b986e09..a94f889 100644 --- a/client/index.html +++ b/client/index.html @@ -17,7 +17,8 @@ - + + @@ -36,7 +37,7 @@ Kapteins Daagbok – Kostenloses digitales Yacht-Logbuch (werbefrei) - +
diff --git a/client/public/appearance-bootstrap.js b/client/public/appearance-bootstrap.js new file mode 100644 index 0000000..3eabb84 --- /dev/null +++ b/client/public/appearance-bootstrap.js @@ -0,0 +1,44 @@ +/** + * Applies saved appearance classes before CSS/JS bundle loads (prevents wrong flash on PWA). + * Logic mirrors client/src/services/appearance.ts + userPreferences.ts. + */ +(function () { + try { + var uid = localStorage.getItem('active_userid') + var theme = 'auto' + var scheme = 'auto' + + if (uid) { + theme = + localStorage.getItem('user_pref_theme_' + uid) || + localStorage.getItem('active_theme') || + 'auto' + scheme = + localStorage.getItem('user_pref_color_scheme_' + uid) || + localStorage.getItem('active_color_scheme') || + 'auto' + } else { + theme = localStorage.getItem('active_theme') || 'auto' + scheme = localStorage.getItem('active_color_scheme') || 'auto' + } + + var resolvedTheme = theme + if (resolvedTheme !== 'ocean' && resolvedTheme !== 'material' && resolvedTheme !== 'cupertino') { + var ua = navigator.userAgent || navigator.vendor || '' + if (/iPad|iPhone|iPod|Macintosh/.test(ua)) resolvedTheme = 'cupertino' + else if (/Android|Linux/.test(ua)) resolvedTheme = 'material' + else resolvedTheme = 'ocean' + } + + var resolvedScheme = scheme + if (resolvedScheme !== 'light' && resolvedScheme !== 'dark') { + resolvedScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + } + + var root = document.documentElement + root.classList.add('theme-' + resolvedTheme, 'scheme-' + resolvedScheme) + root.style.colorScheme = resolvedScheme + } catch (_) { + /* ignore storage / matchMedia errors */ + } +})() diff --git a/client/src/services/appearance.test.ts b/client/src/services/appearance.test.ts new file mode 100644 index 0000000..c551179 --- /dev/null +++ b/client/src/services/appearance.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + applyAppearanceToDocument, + resolveAppTheme, + resolveColorScheme, + type AppTheme, + type ResolvedColorScheme +} from './appearance.js' +import { setColorSchemePreference } from './userPreferences.js' + +const USER_ID = 'appearance-test-user' + +const COMBOS: Array<{ theme: AppTheme; scheme: ResolvedColorScheme }> = [ + { theme: 'ocean', scheme: 'dark' }, + { theme: 'ocean', scheme: 'light' }, + { theme: 'material', scheme: 'dark' }, + { theme: 'material', scheme: 'light' }, + { theme: 'cupertino', scheme: 'dark' }, + { theme: 'cupertino', scheme: 'light' } +] + +describe('appearance', () => { + beforeEach(() => { + localStorage.clear() + document.documentElement.className = '' + document.documentElement.style.colorScheme = '' + document.head.querySelector('meta[name="theme-color"]')?.remove() + }) + + it.each(COMBOS)('applies $theme · $scheme classes to document', ({ theme, scheme }) => { + applyAppearanceToDocument(theme, scheme) + + const root = document.documentElement + expect(root.classList.contains(`theme-${theme}`)).toBe(true) + expect(root.classList.contains(`scheme-${scheme}`)).toBe(true) + expect(root.style.colorScheme).toBe(scheme) + }) + + it('replaces previous theme classes when switching appearance', () => { + applyAppearanceToDocument('ocean', 'dark') + applyAppearanceToDocument('material', 'light') + + const root = document.documentElement + expect(root.classList.contains('theme-material')).toBe(true) + expect(root.classList.contains('theme-ocean')).toBe(false) + expect(root.classList.contains('scheme-light')).toBe(true) + expect(root.classList.contains('scheme-dark')).toBe(false) + }) + + it('resolves stored light scheme even when system prefers dark', () => { + vi.stubGlobal( + 'matchMedia', + vi.fn().mockReturnValue({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() }) + ) + localStorage.setItem('active_userid', USER_ID) + setColorSchemePreference(USER_ID, 'light') + + expect(resolveColorScheme()).toBe('light') + applyAppearanceToDocument('material', resolveColorScheme()) + expect(document.documentElement.classList.contains('scheme-light')).toBe(true) + }) + + it('auto theme picks material on Android user agent', () => { + vi.stubGlobal('navigator', { + ...navigator, + userAgent: 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36' + }) + expect(resolveAppTheme()).toBe('material') + }) +}) diff --git a/client/src/services/appearance.ts b/client/src/services/appearance.ts index 91bc8a1..be8d3ea 100644 --- a/client/src/services/appearance.ts +++ b/client/src/services/appearance.ts @@ -31,6 +31,18 @@ export function resolveAppTheme(): AppTheme { return 'ocean' } +function updateThemeColorMeta(root: HTMLElement): void { + const color = getComputedStyle(root).getPropertyValue('--app-theme-color').trim() + if (!color) return + let meta = document.querySelector('meta[name="theme-color"]') + if (!meta) { + meta = document.createElement('meta') + meta.setAttribute('name', 'theme-color') + document.head.appendChild(meta) + } + meta.setAttribute('content', color) +} + export function applyAppearanceToDocument( theme: AppTheme = resolveAppTheme(), scheme: ResolvedColorScheme = resolveColorScheme() @@ -39,6 +51,7 @@ export function applyAppearanceToDocument( root.classList.remove(...THEME_CLASSES, ...SCHEME_CLASSES) root.classList.add(`theme-${theme}`, `scheme-${scheme}`) root.style.colorScheme = scheme + updateThemeColorMeta(root) } export function subscribeToSystemColorScheme(onChange: () => void): () => void { diff --git a/client/src/themes.css b/client/src/themes.css index 2fbed8e..58325f2 100644 --- a/client/src/themes.css +++ b/client/src/themes.css @@ -6,6 +6,7 @@ /* Fallback before JS hydrates (ocean · dark) */ html { color-scheme: dark; + --app-theme-color: #0b0c10; --app-body-bg: radial-gradient(circle at center, #1b264f 0%, #0b0c10 100%); --app-text: #f1f5f9; --app-text-heading: #f8fafc; @@ -61,6 +62,7 @@ html { /* ===== OCEAN · DARK (default) ===== */ html.scheme-dark.theme-ocean { color-scheme: dark; + --app-theme-color: #0b0c10; --app-body-bg: radial-gradient(circle at center, #1b264f 0%, #0b0c10 100%); --app-text: #f1f5f9; --app-text-heading: #f8fafc; @@ -116,6 +118,7 @@ html.scheme-dark.theme-ocean { /* ===== OCEAN · LIGHT ===== */ html.scheme-light.theme-ocean { color-scheme: light; + --app-theme-color: #e2e8f0; --app-body-bg: linear-gradient(165deg, #dbeafe 0%, #f8fafc 42%, #e2e8f0 100%); --app-text: #1e293b; --app-text-heading: #0f172a; @@ -171,6 +174,7 @@ html.scheme-light.theme-ocean { /* ===== MATERIAL · DARK ===== */ html.scheme-dark.theme-material { color-scheme: dark; + --app-theme-color: #121212; --app-body-bg: #121212; --app-text: #f1f5f9; --app-text-heading: #f8fafc; @@ -226,6 +230,7 @@ html.scheme-dark.theme-material { /* ===== MATERIAL · LIGHT ===== */ html.scheme-light.theme-material { color-scheme: light; + --app-theme-color: #fafafa; --app-body-bg: #fafafa; --app-text: #212121; --app-text-heading: #111827; @@ -281,6 +286,7 @@ html.scheme-light.theme-material { /* ===== CUPERTINO · DARK ===== */ html.scheme-dark.theme-cupertino { color-scheme: dark; + --app-theme-color: #000000; --app-body-bg: #000000; --app-text: #ffffff; --app-text-heading: #ffffff; @@ -336,6 +342,7 @@ html.scheme-dark.theme-cupertino { /* ===== CUPERTINO · LIGHT ===== */ html.scheme-light.theme-cupertino { color-scheme: light; + --app-theme-color: #f2f2f7; --app-body-bg: #f2f2f7; --app-text: #1c1c1e; --app-text-heading: #000000;