diff --git a/resources/js/guest/components/EmotionPicker.tsx b/resources/js/guest/components/EmotionPicker.tsx index bae6179..3512bbe 100644 --- a/resources/js/guest/components/EmotionPicker.tsx +++ b/resources/js/guest/components/EmotionPicker.tsx @@ -104,9 +104,9 @@ export default function EmotionPicker({

{headingTitle} - {headingSubtitle && {headingSubtitle}} + {headingSubtitle && {headingSubtitle}}

- {loading && Lade Emotionen…} + {loading && Lade Emotionen…}
)} @@ -146,12 +146,12 @@ export default function EmotionPicker({ {emotion.emoji}
-
{localizedName}
+
{localizedName}
{localizedDescription && ( -
{localizedDescription}
+
{localizedDescription}
)}
- + ); diff --git a/resources/js/guest/components/FiltersBar.tsx b/resources/js/guest/components/FiltersBar.tsx index 1ca92bc..6f85613 100644 --- a/resources/js/guest/components/FiltersBar.tsx +++ b/resources/js/guest/components/FiltersBar.tsx @@ -54,7 +54,7 @@ export default function FiltersBar({ 'inline-flex items-center gap-1 rounded-full px-3 py-1.5 transition', isActive ? 'bg-pink-500 text-white shadow' - : 'text-muted-foreground hover:bg-pink-50 hover:text-pink-600', + : 'text-muted-foreground hover:bg-pink-50 hover:text-pink-600 dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white', )} > {React.cloneElement(filter.icon as React.ReactElement, { className: 'h-3.5 w-3.5' })} diff --git a/resources/js/guest/components/GalleryPreview.tsx b/resources/js/guest/components/GalleryPreview.tsx index 926f437..4e6ed74 100644 --- a/resources/js/guest/components/GalleryPreview.tsx +++ b/resources/js/guest/components/GalleryPreview.tsx @@ -125,7 +125,7 @@ export default function GalleryPreview({ token }: Props) { 'inline-flex items-center rounded-full px-3 py-1.5 transition', isActive ? 'bg-pink-500 text-white shadow' - : 'text-muted-foreground hover:bg-pink-50 hover:text-pink-600', + : 'text-muted-foreground hover:bg-pink-50 hover:text-pink-600 dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white', )} > {filter.label} diff --git a/resources/js/guest/context/EventBrandingContext.tsx b/resources/js/guest/context/EventBrandingContext.tsx index ac9611a..2535be5 100644 --- a/resources/js/guest/context/EventBrandingContext.tsx +++ b/resources/js/guest/context/EventBrandingContext.tsx @@ -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]); diff --git a/resources/js/guest/context/__tests__/EventBrandingContext.test.tsx b/resources/js/guest/context/__tests__/EventBrandingContext.test.tsx index 0562be9..bc20522 100644 --- a/resources/js/guest/context/__tests__/EventBrandingContext.test.tsx +++ b/resources/js/guest/context/__tests__/EventBrandingContext.test.tsx @@ -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(); diff --git a/resources/js/guest/pages/HomePage.tsx b/resources/js/guest/pages/HomePage.tsx index 8beca27..ca4b1fd 100644 --- a/resources/js/guest/pages/HomePage.tsx +++ b/resources/js/guest/pages/HomePage.tsx @@ -620,7 +620,7 @@ export function MissionActionCard({ const isExpanded = card ? expandedTaskId === card.id : false; const isExpandable = Boolean(card && expandableTitles[card.id]); const titleClamp = isExpanded ? '' : 'line-clamp-2 sm:line-clamp-3'; - const titleClasses = `text-xl font-semibold leading-snug text-slate-900 sm:text-2xl break-words py-1 min-h-[3.75rem] sm:min-h-[4.5rem] ${titleClamp}`; + const titleClasses = `text-xl font-semibold leading-snug text-slate-900 dark:text-white sm:text-2xl break-words py-1 min-h-[3.75rem] sm:min-h-[4.5rem] ${titleClamp}`; const titleId = card ? `task-title-${card.id}` : undefined; const toggleExpanded = () => { if (!card) return; @@ -648,7 +648,7 @@ export function MissionActionCard({
-
+
@@ -664,15 +664,15 @@ export function MissionActionCard({ > {card?.emotion?.name ?? 'Fotoaufgabe'} -
+
Foto-Challenge
-
- +
+ ca. {durationMinutes} min
@@ -701,7 +701,7 @@ export function MissionActionCard({ {card.title}

Titel ein- oder ausklappen @@ -728,7 +728,7 @@ export function MissionActionCard({
) : ( -

Ziehe deine erste Mission oder wähle eine Stimmung.

+

Ziehe deine erste Mission oder wähle eine Stimmung.

)}
@@ -736,10 +736,10 @@ export function MissionActionCard({
-

+

{card.title}

-

{card.description}

+

{card.description}

@@ -770,7 +770,7 @@ export function MissionActionCard({