Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions packages/raystack/components/theme-provider/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react';

Expand Down Expand Up @@ -47,7 +48,8 @@ const Theme = ({
nonce,
style = 'modern',
accentColor = 'indigo',
grayColor = 'gray'
grayColor = 'gray',
onThemeChange
}: ThemeProviderProps) => {
const [theme, setThemeState] = useState(() =>
getTheme(storageKey, defaultTheme)
Expand Down Expand Up @@ -124,7 +126,7 @@ const Theme = ({
// Unsupported
}
},
[forcedTheme]
[storageKey]
);

const handleMediaQuery = useCallback(
Expand All @@ -136,7 +138,7 @@ const Theme = ({
applyTheme('system');
}
},
[theme, forcedTheme]
[theme, forcedTheme, enableSystem, applyTheme]
);

// Always listen to System preference
Expand Down Expand Up @@ -171,6 +173,25 @@ const Theme = ({
applyTheme(forcedTheme ?? theme);
}, [forcedTheme, theme]);

// Fire onThemeChange on actual changes, skipping the initial mount.
// Ref keeps the latest callback without re-firing when consumers pass an inline function.
const onThemeChangeRef = useRef(onThemeChange);
useEffect(() => {
onThemeChangeRef.current = onThemeChange;
});
const themeChangeMounted = useRef(false);
useEffect(() => {
if (!themeChangeMounted.current) {
themeChangeMounted.current = true;
return;
}
if (theme) {
const resolved =
theme === 'system' && resolvedTheme ? resolvedTheme : theme;
onThemeChangeRef.current?.(theme, resolved);
}
}, [theme, resolvedTheme]);

const providerValue = useMemo(
() => ({
theme,
Expand Down Expand Up @@ -277,7 +298,7 @@ const ThemeScript = memo(
setColorScheme = true
) => {
const resolvedName = value ? value[name] : name;
const val = literal ? name + `|| ''` : `'${resolvedName}'`;
const val = literal ? name : `'${resolvedName}'`;
let text = '';

// MUCH faster to set colorScheme alongside HTML attribute/class
Expand All @@ -293,13 +314,17 @@ const ThemeScript = memo(
}

if (attribute === 'class') {
if (literal || resolvedName) {
if (literal) {
text += `if(${val})c.add(${val})`;
} else if (resolvedName) {
text += `c.add(${val})`;
} else {
text += `null`;
}
} else {
if (resolvedName) {
if (literal) {
text += `if(${val})d[s](n,${val})`;
} else if (resolvedName) {
text += `d[s](n,${val})`;
}
}
Expand Down
6 changes: 4 additions & 2 deletions packages/raystack/components/theme-provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface UseThemeProps {
/** If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */
resolvedTheme?: string;
/** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */
systemTheme?: "dark" | "light";
systemTheme?: 'dark' | 'light';
}

export interface ThemeProviderProps {
Expand All @@ -33,7 +33,7 @@ export interface ThemeProviderProps {
/** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */
defaultTheme?: string;
/** HTML attribute modified based on the active theme. Accepts `class` and `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.) */
attribute?: string | "class";
attribute?: string | 'class';
/** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
value?: ValueObject;
/** Nonce string to pass to the inline script for CSP headers */
Expand All @@ -46,4 +46,6 @@ export interface ThemeProviderProps {
accentColor?: 'indigo' | 'orange' | 'mint';
/** Gray color variant for the theme, options are 'gray', 'mauve', or 'slate' */
grayColor?: 'gray' | 'mauve' | 'slate';
/** Called when the active theme changes. `resolvedTheme` is the actual applied theme (`'light'`/`'dark'` when `theme` is `'system'`). Not fired on initial mount. */
onThemeChange?: (theme: string, resolvedTheme: string) => void;
}
Loading