Skip to content

Commit 359ec18

Browse files
creilly11235claude
andcommitted
feat(apollo-vertex): add resolvedTheme and disableTransitionOnChange to ThemeProvider
Upstream improvements from vertical-medical-mrs shell-theme-provider: - resolvedTheme in context so consumers can read the actual light/dark value - disableTransitionOnChange prop to prevent CSS transition flash on theme switch - defaultTheme prop to allow overriding the default "system" fallback - Memoized context value to prevent unnecessary re-renders Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 32e864d commit 359ec18

1 file changed

Lines changed: 59 additions & 14 deletions

File tree

apps/apollo-vertex/registry/shell/shell-theme-provider.tsx

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import type { ReactNode } from "react";
2-
import { createContext, useContext, useEffect, useState } from "react";
2+
import {
3+
createContext,
4+
useCallback,
5+
useContext,
6+
useEffect,
7+
useMemo,
8+
useState,
9+
} from "react";
310
import { THEME_STORAGE_KEY } from "./shell-constants";
411

512
export type Theme = "light" | "dark" | "system";
@@ -82,17 +89,21 @@ export interface ThemeConfig {
8289

8390
interface ThemeProviderProps {
8491
children: ReactNode;
92+
defaultTheme?: Theme;
8593
themeConfig?: ThemeConfig;
8694
storageKey?: string;
95+
disableTransitionOnChange?: boolean;
8796
}
8897

8998
interface ThemeProviderState {
9099
theme: Theme;
100+
resolvedTheme: "light" | "dark";
91101
setTheme: (theme: Theme) => void;
92102
}
93103

94104
const ThemeProviderContext = createContext<ThemeProviderState>({
95105
theme: "system",
106+
resolvedTheme: "light",
96107
setTheme: () => null,
97108
});
98109

@@ -146,10 +157,26 @@ function getEffectiveTheme(theme: Theme): "light" | "dark" {
146157
return theme;
147158
}
148159

149-
function applyThemeClass(theme: Theme) {
160+
function applyThemeClass(theme: Theme, disableTransitions?: boolean) {
150161
const root = window.document.documentElement;
162+
const resolved = getEffectiveTheme(theme);
163+
164+
let styleEl: HTMLStyleElement | null = null;
165+
if (disableTransitions) {
166+
styleEl = document.createElement("style");
167+
styleEl.textContent =
168+
"*, *::before, *::after { transition: none !important }";
169+
document.head.append(styleEl);
170+
}
171+
151172
root.classList.remove("light", "dark");
152-
root.classList.add(getEffectiveTheme(theme));
173+
root.classList.add(resolved);
174+
175+
if (styleEl) {
176+
// Force reflow, then re-enable transitions
177+
const _ = root.offsetHeight;
178+
styleEl.remove();
179+
}
153180
}
154181

155182
function applyThemeConfig(config: ThemeConfig, theme: Theme) {
@@ -180,35 +207,48 @@ function clearThemeConfig() {
180207

181208
export function ThemeProvider({
182209
children,
210+
defaultTheme = "system",
183211
themeConfig,
184212
storageKey = THEME_STORAGE_KEY,
213+
disableTransitionOnChange,
185214
}: ThemeProviderProps) {
186215
const [theme, setThemeState] = useState<Theme>(() => {
187-
if (typeof window === "undefined") return "system";
216+
if (typeof window === "undefined") return defaultTheme;
188217
const stored = localStorage.getItem(storageKey);
189-
return isValidTheme(stored) ? stored : "system";
218+
return isValidTheme(stored) ? stored : defaultTheme;
190219
});
191220

192-
const setTheme = (newTheme: Theme) => {
193-
localStorage.setItem(storageKey, newTheme);
194-
setThemeState(newTheme);
195-
};
221+
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">(() =>
222+
typeof window === "undefined" ? "light" : getEffectiveTheme(theme),
223+
);
224+
225+
const setTheme = useCallback(
226+
(newTheme: Theme) => {
227+
localStorage.setItem(storageKey, newTheme);
228+
setThemeState(newTheme);
229+
},
230+
[storageKey],
231+
);
196232

197233
// Apply light/dark class to document root
198234
useEffect(() => {
199-
applyThemeClass(theme);
200-
}, [theme]);
235+
applyThemeClass(theme, disableTransitionOnChange);
236+
setResolvedTheme(getEffectiveTheme(theme));
237+
}, [theme, disableTransitionOnChange]);
201238

202239
// Listen for system theme changes when in system mode
203240
useEffect(() => {
204241
if (theme !== "system") return;
205242

206243
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
207-
const handleChange = () => applyThemeClass(theme);
244+
const handleChange = () => {
245+
applyThemeClass(theme, disableTransitionOnChange);
246+
setResolvedTheme(getEffectiveTheme(theme));
247+
};
208248

209249
mediaQuery.addEventListener("change", handleChange);
210250
return () => mediaQuery.removeEventListener("change", handleChange);
211-
}, [theme]);
251+
}, [theme, disableTransitionOnChange]);
212252

213253
// Cross-tab sync: update React state when theme changes in another tab
214254
useEffect(() => {
@@ -230,8 +270,13 @@ export function ThemeProvider({
230270
return () => clearThemeConfig();
231271
}, [themeConfig, theme]);
232272

273+
const value = useMemo(
274+
() => ({ theme, resolvedTheme, setTheme }),
275+
[theme, resolvedTheme, setTheme],
276+
);
277+
233278
return (
234-
<ThemeProviderContext.Provider value={{ theme, setTheme }}>
279+
<ThemeProviderContext.Provider value={value}>
235280
{children}
236281
</ThemeProviderContext.Provider>
237282
);

0 commit comments

Comments
 (0)