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 { PropsWithChildren } from 'react';
|
||||
import { BusterNotificationsProvider } from '../BusterNotifications/BusterNotifications';
|
||||
import { BusterThemeProvider } from './BusterThemeProvider';
|
||||
|
||||
// const ENABLE_DARK_MODE = false;
|
||||
|
||||
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 './BusterThemeProvider';
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import { isServer } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMemoizedFn } from './useMemoizedFn';
|
||||
import { useMount } from './useMount';
|
||||
|
||||
type SetState<S> = S | ((prevState?: S) => S);
|
||||
|
||||
|
@ -27,7 +26,7 @@ interface Options<T> {
|
|||
export function useLocalStorageState<T>(
|
||||
key: string,
|
||||
options?: Options<T>
|
||||
): [T | undefined, (value?: SetState<T>) => void] {
|
||||
): [T, (value?: SetState<T>) => void] {
|
||||
const {
|
||||
defaultValue,
|
||||
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
|
||||
// useMount(() => {
|
||||
|
@ -126,7 +125,7 @@ export function useLocalStorageState<T>(
|
|||
return newState;
|
||||
});
|
||||
} else {
|
||||
setState(value);
|
||||
setState(value as T);
|
||||
}
|
||||
} catch (error) {
|
||||
onError?.(error);
|
||||
|
|
Loading…
Reference in New Issue