fix: Light-Theme-Hintergrund auf PWA/Android reparieren
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 <cursoragent@cursor.com>
This commit is contained in:
+3
-2
@@ -17,7 +17,8 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Daagbok" />
|
||||
<meta name="theme-color" content="#1e293b" />
|
||||
<meta name="theme-color" content="#0b0c10" />
|
||||
<script src="/appearance-bootstrap.js"></script>
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Kapteins Daagbok" />
|
||||
@@ -36,7 +37,7 @@
|
||||
<script defer data-domain="kapteins-daagbok.eu" src="https://plausible.elpatron.me/js/script.tagged-events.js"></script>
|
||||
<title>Kapteins Daagbok – Kostenloses digitales Yacht-Logbuch (werbefrei)</title>
|
||||
</head>
|
||||
<body style="margin:0;background:#0b0c10;color:#e2e8f0">
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
})()
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user