Fix guest PWA dark mode contrast

This commit is contained in:
Codex Agent
2026-01-22 15:47:26 +01:00
parent b6e0005734
commit 91bb09248a
6 changed files with 95 additions and 83 deletions

View File

@@ -40,6 +40,10 @@ export const DEFAULT_EVENT_BRANDING: EventBranding = {
const DEFAULT_PRIMARY = DEFAULT_EVENT_BRANDING.primaryColor.toLowerCase();
const DEFAULT_SECONDARY = DEFAULT_EVENT_BRANDING.secondaryColor.toLowerCase();
const DEFAULT_BACKGROUND = DEFAULT_EVENT_BRANDING.backgroundColor.toLowerCase();
const LIGHT_LUMINANCE_THRESHOLD = 0.65;
const DARK_LUMINANCE_THRESHOLD = 0.35;
const DARK_FALLBACK_SURFACE = '#0f172a';
const LIGHT_FALLBACK_SURFACE = '#ffffff';
const FONT_SCALE_MAP: Record<'s' | 'm' | 'l', number> = {
s: 0.94,
m: 1,
@@ -110,22 +114,69 @@ function resolveBranding(input?: EventBranding | null): EventBranding {
};
}
function applyCssVariables(branding: EventBranding) {
type ThemeVariant = 'light' | 'dark';
function resolveThemeVariant(
mode: EventBranding['mode'],
backgroundColor: string,
appearanceOverride: 'light' | 'dark' | null,
): ThemeVariant {
const prefersDark = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
? window.matchMedia('(prefers-color-scheme: dark)').matches
: false;
const backgroundLuminance = relativeLuminance(backgroundColor || DEFAULT_EVENT_BRANDING.backgroundColor);
const backgroundPrefers = backgroundLuminance >= LIGHT_LUMINANCE_THRESHOLD
? 'light'
: backgroundLuminance <= DARK_LUMINANCE_THRESHOLD
? 'dark'
: null;
if (mode === 'dark') {
return 'dark';
}
if (mode === 'light') {
return 'light';
}
if (appearanceOverride) {
return appearanceOverride;
}
if (backgroundPrefers) {
return backgroundPrefers;
}
return prefersDark ? 'dark' : 'light';
}
function clampToTheme(color: string, theme: ThemeVariant): string {
const luminance = relativeLuminance(color);
if (theme === 'dark' && luminance >= LIGHT_LUMINANCE_THRESHOLD) {
return DARK_FALLBACK_SURFACE;
}
if (theme === 'light' && luminance <= DARK_LUMINANCE_THRESHOLD) {
return LIGHT_FALLBACK_SURFACE;
}
return color;
}
function applyCssVariables(branding: EventBranding, theme: ThemeVariant) {
if (typeof document === 'undefined') {
return;
}
const root = document.documentElement;
const background = branding.backgroundColor;
const surfaceCandidate = branding.palette?.surface ?? background;
const background = clampToTheme(branding.backgroundColor, theme);
const surfaceCandidate = clampToTheme(branding.palette?.surface ?? background, theme);
const backgroundLuminance = relativeLuminance(background);
const surfaceLuminance = relativeLuminance(surfaceCandidate);
const surface = Math.abs(surfaceLuminance - backgroundLuminance) < 0.06
? backgroundLuminance >= 0.6
? '#ffffff'
: '#0f172a'
? theme === 'light'
? LIGHT_FALLBACK_SURFACE
: DARK_FALLBACK_SURFACE
: surfaceCandidate;
const isLight = backgroundLuminance >= 0.6;
const isLight = theme === 'light';
const foreground = isLight ? '#1f2937' : '#f8fafc';
const mutedForeground = isLight ? '#6b7280' : '#cbd5e1';
const muted = isLight ? '#f6efec' : '#1f2937';
@@ -213,66 +264,22 @@ function applyThemeMode(
mode: EventBranding['mode'],
backgroundColor: string,
appearanceOverride: 'light' | 'dark' | null,
) {
): ThemeVariant {
if (typeof document === 'undefined') {
return;
return 'light';
}
const root = document.documentElement;
const prefersDark = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
? window.matchMedia('(prefers-color-scheme: dark)').matches
: false;
const backgroundLuminance = relativeLuminance(backgroundColor || DEFAULT_EVENT_BRANDING.backgroundColor);
const backgroundPrefers = backgroundLuminance >= 0.65
? 'light'
: backgroundLuminance <= 0.35
? 'dark'
: null;
const applyDark = () => root.classList.add('dark');
const applyLight = () => root.classList.remove('dark');
if (mode === 'dark') {
applyDark();
const theme = resolveThemeVariant(mode, backgroundColor, appearanceOverride);
if (theme === 'dark') {
root.classList.add('dark');
root.style.colorScheme = 'dark';
return;
return 'dark';
}
if (mode === 'light') {
applyLight();
root.style.colorScheme = 'light';
return;
}
if (appearanceOverride) {
if (appearanceOverride === 'dark') {
applyDark();
root.style.colorScheme = 'dark';
} else {
applyLight();
root.style.colorScheme = 'light';
}
return;
}
if (backgroundPrefers) {
if (backgroundPrefers === 'dark') {
applyDark();
root.style.colorScheme = 'dark';
} else {
applyLight();
root.style.colorScheme = 'light';
}
return;
}
if (prefersDark) {
applyDark();
root.style.colorScheme = 'dark';
return;
}
applyLight();
root.classList.remove('dark');
root.style.colorScheme = 'light';
return 'light';
}
export function EventBrandingProvider({
@@ -290,9 +297,9 @@ export function EventBrandingProvider({
if (typeof document !== 'undefined') {
document.documentElement.classList.add('guest-theme');
}
applyCssVariables(resolved);
const previousDark = typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : false;
applyThemeMode(resolved.mode ?? 'auto', resolved.backgroundColor, appearanceOverride);
const theme = applyThemeMode(resolved.mode ?? 'auto', resolved.backgroundColor, appearanceOverride);
applyCssVariables(resolved, theme);
return () => {
if (typeof document !== 'undefined') {
@@ -304,8 +311,12 @@ export function EventBrandingProvider({
document.documentElement.classList.remove('guest-theme');
}
resetCssVariables();
applyCssVariables(DEFAULT_EVENT_BRANDING);
applyThemeMode(DEFAULT_EVENT_BRANDING.mode ?? 'auto', DEFAULT_EVENT_BRANDING.backgroundColor, appearanceOverride);
const fallbackTheme = applyThemeMode(
DEFAULT_EVENT_BRANDING.mode ?? 'auto',
DEFAULT_EVENT_BRANDING.backgroundColor,
appearanceOverride,
);
applyCssVariables(DEFAULT_EVENT_BRANDING, fallbackTheme);
};
}, [appearanceOverride, resolved]);

View File

@@ -38,8 +38,9 @@ describe('EventBrandingProvider', () => {
expect(document.documentElement.classList.contains('guest-theme')).toBe(true);
expect(document.documentElement.classList.contains('dark')).toBe(true);
expect(document.documentElement.style.colorScheme).toBe('dark');
expect(document.documentElement.style.getPropertyValue('--guest-background')).toBe(sampleBranding.backgroundColor);
expect(document.documentElement.style.getPropertyValue('--guest-background')).toBe('#0f172a');
expect(document.documentElement.style.getPropertyValue('--guest-font-scale')).toBe('1.08');
expect(document.documentElement.style.getPropertyValue('--foreground')).toBe('#f8fafc');
});
unmount();