| 1 | import { useEffect, useRef, useState } from "react"; |
| 2 | |
| 3 | import { FontLoader } from "@/components/plate/utils/font-loader"; |
| 4 | import { usePresentationTheme } from "@/components/presentation/providers/PresentationThemeProvider"; |
| 5 | import { resolvePresentationThemeData } from "@/lib/presentation/theme-resolution"; |
| 6 | import { |
| 7 | setThemeVariables, |
| 8 | type ThemeProperties, |
| 9 | type themes, |
| 10 | } from "@/lib/presentation/themes"; |
| 11 | import { cn } from "@/lib/utils"; |
| 12 | import { usePresentationState } from "@/states/presentation-state"; |
| 13 | |
| 14 | interface ThemeBackgroundProps { |
| 15 | className?: string; |
| 16 | children: React.ReactNode; |
| 17 | themeOverride?: keyof typeof themes; |
| 18 | themeModeOverride?: "light" | "dark"; |
| 19 | themeDataOverride?: ThemeProperties; |
| 20 | suppressThemeUpdates?: boolean; |
| 21 | ignorePageBackgroundOverride?: boolean; |
| 22 | } |
| 23 | |
| 24 | export function ThemeBackground({ |
| 25 | className, |
| 26 | children, |
| 27 | themeOverride, |
| 28 | themeModeOverride, |
| 29 | themeDataOverride, |
| 30 | suppressThemeUpdates, |
| 31 | ignorePageBackgroundOverride = false, |
| 32 | }: ThemeBackgroundProps) { |
| 33 | const presentationTheme = usePresentationState((s) => s.theme); |
| 34 | const customThemeData = usePresentationState((s) => s.customThemeData); |
| 35 | // Use our custom presentation theme hook for isolated theme control |
| 36 | const { resolvedTheme, setTheme: setPresentationThemeMode } = |
| 37 | usePresentationTheme(); |
| 38 | const pageBackground = usePresentationState((s) => s.pageBackground); |
| 39 | const themeBackgroundRef = useRef<HTMLDivElement>(null); |
| 40 | |
| 41 | const theme = themeOverride ?? presentationTheme; |
| 42 | const themeData = |
| 43 | themeDataOverride ?? |
| 44 | resolvePresentationThemeData({ customThemeData, theme }); |
| 45 | const themeMode = themeModeOverride ?? themeData?.mode ?? resolvedTheme; |
| 46 | |
| 47 | const isDark = themeMode === "dark"; |
| 48 | const [mounted, setMounted] = useState(false); |
| 49 | |
| 50 | // Handle hydration mismatch by only rendering the gradient after mount |
| 51 | useEffect(() => { |
| 52 | setMounted(true); |
| 53 | }, []); |
| 54 | |
| 55 | // Apply theme variables whenever presentation theme or dark mode changes |
| 56 | useEffect(() => { |
| 57 | if (mounted && (theme || themeData)) { |
| 58 | // Check if we're using a custom theme or a predefined theme |
| 59 | if (themeData) { |
| 60 | setThemeVariables(themeData, themeBackgroundRef.current ?? undefined); |
| 61 | } |
| 62 | } |
| 63 | }, [theme, JSON.stringify(themeData), isDark, mounted]); |
| 64 | |
| 65 | // Sync the theme mode with the presentation theme |
| 66 | // When inside /presentation route, this sets the isolated presentation theme |
| 67 | // When elsewhere, this would set the global theme (existing behavior for other use cases) |
| 68 | useEffect(() => { |
| 69 | if (!mounted || !theme || suppressThemeUpdates) return; |
| 70 | |
| 71 | let currentThemeData: ThemeProperties | null = null; |
| 72 | |
| 73 | // Get the current theme data |
| 74 | currentThemeData = themeData ?? null; |
| 75 | |
| 76 | // Sync the theme mode to match presentation theme mode |
| 77 | if (currentThemeData) { |
| 78 | const presentationThemeMode = currentThemeData.mode; |
| 79 | if (resolvedTheme !== presentationThemeMode) { |
| 80 | setPresentationThemeMode(presentationThemeMode); |
| 81 | } |
| 82 | } |
| 83 | }, [ |
| 84 | theme, |
| 85 | themeData, |
| 86 | mounted, |
| 87 | resolvedTheme, |
| 88 | setPresentationThemeMode, |
| 89 | suppressThemeUpdates, |
| 90 | ]); |
| 91 | |
| 92 | // Get the current theme colors |
| 93 | const currentTheme: ThemeProperties | undefined = themeData ?? undefined; |
| 94 | |
| 95 | if (!currentTheme || !mounted) { |
| 96 | return ( |
| 97 | <div className={cn("h-max min-h-full w-full bg-background", className)}> |
| 98 | {children} |
| 99 | </div> |
| 100 | ); |
| 101 | } |
| 102 | |
| 103 | const colors = currentTheme.colors; |
| 104 | |
| 105 | // Get theme-level background configuration |
| 106 | const themeBackground = currentTheme.background; |
| 107 | |
| 108 | // Use theme background or fallback to neutral color |
| 109 | // Note: We don't use colors.background here because that's the slide color, |
| 110 | // and we want the theme background to be independent |
| 111 | const computedBackground = |
| 112 | (!ignorePageBackgroundOverride && pageBackground.backgroundOverride) || |
| 113 | themeBackground?.override || // Theme background |
| 114 | (isDark ? "#0a0a0a" : "#ffffff"); // Neutral fallback (not tied to slide color) |
| 115 | |
| 116 | const gradientStyle = { |
| 117 | background: computedBackground, |
| 118 | transition: currentTheme.transitions.default, |
| 119 | color: colors.text, |
| 120 | } as React.CSSProperties; |
| 121 | |
| 122 | return ( |
| 123 | <div |
| 124 | className={cn("theme-background h-max min-h-full w-full", className)} |
| 125 | style={gradientStyle} |
| 126 | ref={themeBackgroundRef} |
| 127 | > |
| 128 | <FontLoader |
| 129 | fontsToLoad={[currentTheme.fonts.heading, currentTheme.fonts.body]} |
| 130 | /> |
| 131 | {children} |
| 132 | </div> |
| 133 | ); |
| 134 | } |
| 135 |