返回 presentation-ai
PresentationThemeProvider.tsx
根目录 / src / components / presentation / providers / PresentationThemeProvider.tsx
1 "use client";
2
3 import { useTheme as useGlobalTheme } from "next-themes";
4 import {
5 createContext,
6 useCallback,
7 useContext,
8 useEffect,
9 useState,
10 } from "react";
11
12 type Theme = "light" | "dark";
13
14 interface PresentationThemeContextValue {
15 theme: Theme;
16 setTheme: (theme: Theme) => void;
17 resolvedTheme: Theme;
18 }
19
20 const PresentationThemeContext = createContext<
21 PresentationThemeContextValue | undefined
22 >(undefined);
23
24 /**
25 * Custom hook to access the presentation theme context.
26 * This is a drop-in replacement for next-themes' useTheme within the presentation route.
27 *
28 * If used outside of a PresentationThemeProvider (e.g., globally under RootLayout),
29 * it seamlessly falls back to using the global next-themes system, avoiding runtime errors.
30 */
31 export function usePresentationTheme() {
32 const context = useContext(PresentationThemeContext);
33 const globalTheme = useGlobalTheme();
34
35 if (context === undefined) {
36 const theme = (globalTheme.theme === "dark" ? "dark" : "light") as Theme;
37 const resolvedTheme = (
38 globalTheme.resolvedTheme === "dark" ? "dark" : "light"
39 ) as Theme;
40
41 return {
42 theme,
43 setTheme: (newTheme: Theme) => {
44 globalTheme.setTheme(newTheme);
45 },
46 resolvedTheme,
47 };
48 }
49 return context;
50 }
51
52 /**
53 * A custom theme provider for the presentation section.
54 * This creates a completely isolated theme context that doesn't affect other browser tabs.
55 *
56 * Unlike the previous version, this provider:
57 * - Does not use localStorage or listen to storage events, eliminating cross-tab flickering.
58 * - Manages theme state completely in-memory per tab.
59 * - Dynamically updates the global <html> element's dark class for the current tab only,
60 * ensuring portals (dialogs, dropdowns, etc.) receive the correct theme.
61 * - Safely restores the original html element classes and color scheme when unmounted.
62 */
63 export function PresentationThemeProvider({
64 children,
65 defaultTheme = "dark",
66 storageKey: _storageKey, // kept for backward compatibility (ignored/unused now)
67 syncWithDefaultTheme = false,
68 }: {
69 children: React.ReactNode;
70 defaultTheme?: Theme;
71 storageKey?: string | null;
72 syncWithDefaultTheme?: boolean;
73 }) {
74 const [theme, setThemeState] = useState<Theme>(defaultTheme);
75
76 // Sync state if syncWithDefaultTheme is true and defaultTheme changes
77 useEffect(() => {
78 if (syncWithDefaultTheme) {
79 setThemeState(defaultTheme);
80 }
81 }, [defaultTheme, syncWithDefaultTheme]);
82
83 const setTheme = useCallback(
84 (newTheme: Theme) => {
85 setThemeState(newTheme);
86 },
87 [],
88 );
89
90 // Capture original document classes and color scheme, and restore on unmount
91 useEffect(() => {
92 if (typeof window === "undefined") return;
93 const root = window.document.documentElement;
94 const originalClass = root.className;
95 const originalColorScheme = root.style.colorScheme;
96
97 return () => {
98 root.className = originalClass;
99 root.style.colorScheme = originalColorScheme;
100 };
101 }, []);
102
103 // Update HTML class and color-scheme for the current tab
104 useEffect(() => {
105 if (typeof window === "undefined") return;
106 const root = window.document.documentElement;
107 if (theme === "dark") {
108 root.classList.add("dark");
109 root.style.colorScheme = "dark";
110 } else {
111 root.classList.remove("dark");
112 root.style.colorScheme = "light";
113 }
114 }, [theme]);
115
116 const value: PresentationThemeContextValue = {
117 theme,
118 setTheme,
119 resolvedTheme: theme,
120 };
121
122 return (
123 <PresentationThemeContext.Provider value={value}>
124 <PresentationThemeWrapper>{children}</PresentationThemeWrapper>
125 </PresentationThemeContext.Provider>
126 );
127 }
128
129 /**
130 * Inner wrapper that applies the theme class to a div element.
131 * This ensures Tailwind's dark: variants work correctly within the presentation.
132 */
133 function PresentationThemeWrapper({ children }: { children: React.ReactNode }) {
134 const { theme } = usePresentationTheme();
135 return (
136 <div
137 className={
138 theme === "dark"
139 ? "dark bg-background text-foreground h-full w-full"
140 : "bg-background text-foreground h-full w-full"
141 }
142 >
143 {children}
144 </div>
145 );
146 }
147
147 lines Plain Text