diff --git a/.ladle/components.tsx b/.ladle/components.tsx index c6e7beb..d87bd5a 100644 --- a/.ladle/components.tsx +++ b/.ladle/components.tsx @@ -24,23 +24,6 @@ export const StoryWrapper: React.FC<{ children: React.ReactNode }> = ({ children useEffect(() => { store.dispatch(setAppTheme(theme)); store.dispatch(setAppColorMode(colorMode)); - - switch (theme) { - case AppTheme.Dark: { - document?.documentElement?.classList.remove('theme-light'); - document?.documentElement?.classList.add('theme-dark'); - break; - } - case AppTheme.Light: { - document?.documentElement?.classList.remove('theme-dark'); - document?.documentElement?.classList.add('theme-light'); - break; - } - case AppTheme.Classic: { - document?.documentElement?.classList.remove('theme-dark', 'theme-light'); - break; - } - } }, [theme, colorMode]); useEffect(() => { diff --git a/public/System.png b/public/System.png new file mode 100644 index 0000000..24a68da Binary files /dev/null and b/public/System.png differ diff --git a/src/hooks/useAppThemeAndColorMode.tsx b/src/hooks/useAppThemeAndColorMode.tsx index 08572fa..1cbc3de 100644 --- a/src/hooks/useAppThemeAndColorMode.tsx +++ b/src/hooks/useAppThemeAndColorMode.tsx @@ -1,8 +1,9 @@ +import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { ThemeProvider } from 'styled-components'; -import { AppTheme, AppColorMode } from '@/state/configs'; -import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; +import { AppTheme, AppThemeSetting, AppColorMode, AppThemeSystemSetting } from '@/state/configs'; +import { getAppThemeSetting, getAppColorMode } from '@/state/configsSelectors'; import { Themes } from '@/styles/themes'; @@ -11,8 +12,37 @@ export const AppThemeAndColorModeProvider = ({ ...props }) => { }; export const useAppThemeAndColorModeContext = () => { - const theme: AppTheme = useSelector(getAppTheme); + const themeSetting: AppThemeSetting = useSelector(getAppThemeSetting); const colorMode: AppColorMode = useSelector(getAppColorMode); - return Themes[theme][colorMode]; + const darkModePref = globalThis.matchMedia('(prefers-color-scheme: dark)'); + + const [systemPreference, setSystemPreference] = useState( + darkModePref.matches ? AppTheme.Dark : AppTheme.Light + ); + + useEffect(() => { + const handler = (e) => { + if (e.matches) { + setSystemPreference(AppTheme.Dark); + } else { + setSystemPreference(AppTheme.Light); + } + }; + darkModePref.addEventListener('change', handler); + return () => darkModePref.removeEventListener('change', handler); + }, []); + + const getThemeFromSetting = (): AppTheme => { + switch (themeSetting) { + case AppThemeSystemSetting.System: + return systemPreference; + case AppTheme.Classic: + case AppTheme.Dark: + case AppTheme.Light: + return themeSetting; + } + }; + + return Themes[getThemeFromSetting()][colorMode]; }; diff --git a/src/state/configs.ts b/src/state/configs.ts index db1b3e7..7e14b16 100644 --- a/src/state/configs.ts +++ b/src/state/configs.ts @@ -12,13 +12,19 @@ export enum AppTheme { Light = 'Light', } +export enum AppThemeSystemSetting { + System = 'System', +} + +export type AppThemeSetting = AppTheme | AppThemeSystemSetting; + export enum AppColorMode { GreenUp = 'GreenUp', RedUp = 'RedUp', } export interface ConfigsState { - appTheme: AppTheme; + appTheme: AppThemeSetting; appColorMode: AppColorMode; feeTiers?: kollections.List; feeDiscounts?: FeeDiscount[]; @@ -26,22 +32,6 @@ export interface ConfigsState { hasSeenLaunchIncentives: boolean; } -const DOCUMENT_THEME_MAP = { - [AppTheme.Classic]: () => { - document?.documentElement?.classList.remove('theme-dark', 'theme-light'); - }, - [AppTheme.Dark]: () => { - document?.documentElement?.classList.remove('theme-light'); - document?.documentElement?.classList.add('theme-dark'); - }, - [AppTheme.Light]: () => { - document?.documentElement?.classList.remove('theme-dark'); - document?.documentElement?.classList.add('theme-light'); - }, -}; - -export const changeTheme = (theme: AppTheme) => DOCUMENT_THEME_MAP[theme](); - const initialState: ConfigsState = { appTheme: getLocalStorage({ key: LocalStorageKey.SelectedTheme, @@ -60,15 +50,12 @@ const initialState: ConfigsState = { }), }; -changeTheme(initialState.appTheme); - export const configsSlice = createSlice({ name: 'Inputs', initialState, reducers: { - setAppTheme: (state: ConfigsState, { payload }: PayloadAction) => { + setAppTheme: (state: ConfigsState, { payload }: PayloadAction) => { setLocalStorage({ key: LocalStorageKey.SelectedTheme, value: payload }); - changeTheme(payload); state.appTheme = payload; }, setAppColorMode: (state: ConfigsState, { payload }: PayloadAction) => { diff --git a/src/state/configsSelectors.ts b/src/state/configsSelectors.ts index 51dc198..706c088 100644 --- a/src/state/configsSelectors.ts +++ b/src/state/configsSelectors.ts @@ -1,6 +1,18 @@ import type { RootState } from './_store'; +import { AppTheme, AppThemeSystemSetting, AppThemeSetting } from './configs'; -export const getAppTheme = (state: RootState) => state.configs.appTheme; +export const getAppThemeSetting = (state: RootState): AppThemeSetting => state.configs.appTheme; + +export const getAppTheme = (state: RootState): AppTheme => { + switch (state.configs.appTheme) { + case AppThemeSystemSetting.System: + return globalThis.matchMedia('(prefers-color-scheme: dark)').matches + ? AppTheme.Dark + : AppTheme.Light; + default: + return state.configs.appTheme; + } +}; export const getAppColorMode = (state: RootState) => state.configs.appColorMode; diff --git a/src/views/dialogs/DisplaySettingsDialog.tsx b/src/views/dialogs/DisplaySettingsDialog.tsx index 7d4dffc..ceccc7b 100644 --- a/src/views/dialogs/DisplaySettingsDialog.tsx +++ b/src/views/dialogs/DisplaySettingsDialog.tsx @@ -5,8 +5,14 @@ import { Root, Item, Indicator } from '@radix-ui/react-radio-group'; import { useStringGetter } from '@/hooks'; -import { AppTheme, AppColorMode, setAppTheme, setAppColorMode } from '@/state/configs'; -import { getAppTheme, getAppColorMode } from '@/state/configsSelectors'; +import { + AppTheme, + AppThemeSystemSetting, + AppColorMode, + setAppTheme, + setAppColorMode, +} from '@/state/configs'; +import { getAppTheme, getAppThemeSetting, getAppColorMode } from '@/state/configsSelectors'; import { layoutMixins } from '@/styles/layoutMixins'; import { Themes } from '@/styles/themes'; @@ -27,7 +33,7 @@ export const DisplaySettingsDialog = ({ setIsOpen }: ElementProps) => { const dispatch = useDispatch(); const stringGetter = useStringGetter(); - const currentTheme: AppTheme = useSelector(getAppTheme); + const currentTheme: AppTheme = useSelector(getAppThemeSetting); const currentColorMode: AppColorMode = useSelector(getAppColorMode); const sectionHeader = (heading: string) => { @@ -45,36 +51,49 @@ export const DisplaySettingsDialog = ({ setIsOpen }: ElementProps) => { {[ { - theme: AppTheme.Classic, + themeSetting: AppTheme.Classic, label: STRING_KEYS.CLASSIC_DARK, }, { - theme: AppTheme.Dark, + themeSetting: AppThemeSystemSetting.System, + label: STRING_KEYS.SYSTEM, + }, + { + themeSetting: AppTheme.Dark, label: STRING_KEYS.DARK, }, { - theme: AppTheme.Light, + themeSetting: AppTheme.Light, label: STRING_KEYS.LIGHT, }, - ].map(({ theme, label }) => ( - { - dispatch(setAppTheme(theme)); - }} - > - - {stringGetter({ key: label })} - - - - - - - ))} + ].map(({ themeSetting, label }) => { + const theme = + themeSetting === AppThemeSystemSetting.System ? AppTheme.Dark : themeSetting; + + const backgroundColor = Themes[theme][currentColorMode].layer2; + const gridColor = Themes[theme][currentColorMode].borderDefault; + const textColor = Themes[theme][currentColorMode].textPrimary; + + return ( + { + dispatch(setAppTheme(themeSetting)); + }} + > + + {stringGetter({ key: label })} + + + + + + + ); + })} ); };