mirror of https://github.com/buster-so/buster.git
Add a stable theme provider
This commit is contained in:
parent
4dcf3ac677
commit
088b20bfdb
|
@ -1,9 +1,14 @@
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import { BusterNotificationsProvider } from '../BusterNotifications/BusterNotifications';
|
import { BusterNotificationsProvider } from '../BusterNotifications/BusterNotifications';
|
||||||
|
import { BusterThemeProvider } from './BusterThemeProvider';
|
||||||
|
|
||||||
// const ENABLE_DARK_MODE = false;
|
// const ENABLE_DARK_MODE = false;
|
||||||
|
|
||||||
export const BusterStyleProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
export const BusterStyleProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
return <BusterNotificationsProvider>{children}</BusterNotificationsProvider>;
|
return (
|
||||||
|
<BusterThemeProvider>
|
||||||
|
<BusterNotificationsProvider>{children}</BusterNotificationsProvider>
|
||||||
|
</BusterThemeProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
import type React from 'react';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { createContext, useContextSelector } from 'use-context-selector';
|
||||||
|
import { useLocalStorageState } from '../../hooks/useLocalStorageState';
|
||||||
|
|
||||||
|
// Define theme types
|
||||||
|
export type Theme = 'dark' | 'light' | 'system';
|
||||||
|
|
||||||
|
// Theme provider state type
|
||||||
|
export type ThemeProviderState = {
|
||||||
|
theme: Theme;
|
||||||
|
setTheme: (theme: Theme) => void;
|
||||||
|
isDark: boolean;
|
||||||
|
isLight: boolean;
|
||||||
|
systemTheme: 'dark' | 'light';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Theme provider props
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
defaultTheme?: Theme;
|
||||||
|
storageKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the context
|
||||||
|
const ThemeProviderContext = createContext<ThemeProviderState | null>(null);
|
||||||
|
|
||||||
|
// Theme provider component
|
||||||
|
export function BusterThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = 'system',
|
||||||
|
storageKey = 'buster-theme',
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setThemeState] = useLocalStorageState<Theme>(storageKey, {
|
||||||
|
defaultValue: defaultTheme,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track system preference
|
||||||
|
const [systemTheme, setSystemTheme] = useState<'dark' | 'light'>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
return 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate actual theme (resolving 'system' to actual value)
|
||||||
|
const resolvedTheme = theme === 'system' ? systemTheme : theme;
|
||||||
|
const isDark = resolvedTheme === 'dark';
|
||||||
|
const isLight = resolvedTheme === 'light';
|
||||||
|
|
||||||
|
// Apply theme to document root
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
|
||||||
|
// Remove both classes first
|
||||||
|
root.classList.remove('light', 'dark');
|
||||||
|
|
||||||
|
// Add the appropriate class
|
||||||
|
root.classList.add(resolvedTheme);
|
||||||
|
}, [resolvedTheme]);
|
||||||
|
|
||||||
|
// Listen for system theme changes
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
|
setSystemTheme(e.matches ? 'dark' : 'light');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listener
|
||||||
|
mediaQuery.addEventListener('change', handleChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener('change', handleChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Theme setter that persists to localStorage
|
||||||
|
const setTheme = useCallback(
|
||||||
|
(newTheme: Theme) => {
|
||||||
|
localStorage.setItem(storageKey, newTheme);
|
||||||
|
setThemeState(newTheme);
|
||||||
|
},
|
||||||
|
[storageKey, setThemeState]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Context value
|
||||||
|
const value: ThemeProviderState = useMemo(
|
||||||
|
() => ({
|
||||||
|
theme,
|
||||||
|
setTheme,
|
||||||
|
isDark,
|
||||||
|
isLight,
|
||||||
|
systemTheme,
|
||||||
|
}),
|
||||||
|
[theme, setTheme, isDark, isLight, systemTheme]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selector hook with type safety
|
||||||
|
export function useBusterTheme<T>(selector: (state: ThemeProviderState) => T): T {
|
||||||
|
return useContextSelector(ThemeProviderContext, (state) => {
|
||||||
|
if (!state) {
|
||||||
|
throw new Error('useBusterTheme must be used within a BusterThemeProvider');
|
||||||
|
}
|
||||||
|
return selector(state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectTheme = (state: ThemeProviderState) => state.theme;
|
||||||
|
const selectSetTheme = (state: ThemeProviderState) => state.setTheme;
|
||||||
|
const selectIsDark = (state: ThemeProviderState) => state.isDark;
|
||||||
|
const selectIsLight = (state: ThemeProviderState) => state.isLight;
|
||||||
|
const selectSystemTheme = (state: ThemeProviderState) => state.systemTheme;
|
||||||
|
const selectThemeState = (state: ThemeProviderState) => state;
|
||||||
|
|
||||||
|
export const useTheme = () => useBusterTheme(selectTheme);
|
||||||
|
export const useSetTheme = () => useBusterTheme(selectSetTheme);
|
||||||
|
export const useIsDarkTheme = () => useBusterTheme(selectIsDark);
|
||||||
|
export const useIsLightTheme = () => useBusterTheme(selectIsLight);
|
||||||
|
export const useSystemTheme = () => useBusterTheme(selectSystemTheme);
|
||||||
|
export const useThemeState = () => useBusterTheme(selectThemeState);
|
|
@ -1 +1,2 @@
|
||||||
export * from './BusterStyles';
|
export * from './BusterStyles';
|
||||||
|
export * from './BusterThemeProvider';
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { isServer } from '@tanstack/react-query';
|
import { isServer } from '@tanstack/react-query';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useMemoizedFn } from './useMemoizedFn';
|
import { useMemoizedFn } from './useMemoizedFn';
|
||||||
import { useMount } from './useMount';
|
|
||||||
|
|
||||||
type SetState<S> = S | ((prevState?: S) => S);
|
type SetState<S> = S | ((prevState?: S) => S);
|
||||||
|
|
||||||
|
@ -27,7 +26,7 @@ interface Options<T> {
|
||||||
export function useLocalStorageState<T>(
|
export function useLocalStorageState<T>(
|
||||||
key: string,
|
key: string,
|
||||||
options?: Options<T>
|
options?: Options<T>
|
||||||
): [T | undefined, (value?: SetState<T>) => void] {
|
): [T, (value?: SetState<T>) => void] {
|
||||||
const {
|
const {
|
||||||
defaultValue,
|
defaultValue,
|
||||||
serializer = JSON.stringify,
|
serializer = JSON.stringify,
|
||||||
|
@ -92,7 +91,7 @@ export function useLocalStorageState<T>(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [state, setState] = useState<T | undefined>(getInitialValue);
|
const [state, setState] = useState<T>(getInitialValue as T);
|
||||||
|
|
||||||
// Initialize state from localStorage on mount
|
// Initialize state from localStorage on mount
|
||||||
// useMount(() => {
|
// useMount(() => {
|
||||||
|
@ -126,7 +125,7 @@ export function useLocalStorageState<T>(
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(value);
|
setState(value as T);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
|
|
Loading…
Reference in New Issue