@@ -6,6 +6,9 @@ import { type LanguageCode, languages, defaultLanguage, getLanguageDirection } f
66// Storage key
77const PREFERENCES_KEY = 'bfmpay_preferences'
88
9+ type ThemePreference = 'light' | 'dark' | 'system'
10+ type ResolvedTheme = Exclude < ThemePreference , 'system' >
11+
912// Currency types
1013export type CurrencyCode = 'USD' | 'CNY' | 'EUR' | 'JPY' | 'KRW'
1114
@@ -21,7 +24,7 @@ export const currencies: Record<CurrencyCode, { symbol: string; name: string }>
2124export interface PreferencesState {
2225 language : LanguageCode
2326 currency : CurrencyCode
24- theme : 'light' | 'dark' | 'system'
27+ theme : ThemePreference
2528}
2629
2730// Default state
@@ -31,6 +34,68 @@ const defaultState: PreferencesState = {
3134 theme : 'system' ,
3235}
3336
37+ let systemThemeMediaQuery : MediaQueryList | null = null
38+ let systemThemeListener : ( ( event : MediaQueryListEvent ) => void ) | null = null
39+
40+ function getSystemThemeMediaQuery ( ) : MediaQueryList | null {
41+ if ( typeof window === 'undefined' ) return null
42+ if ( typeof window . matchMedia !== 'function' ) return null
43+ return window . matchMedia ( '(prefers-color-scheme: dark)' )
44+ }
45+
46+ function resolveTheme ( theme : ThemePreference ) : ResolvedTheme {
47+ if ( theme === 'dark' || theme === 'light' ) return theme
48+ const mediaQuery = getSystemThemeMediaQuery ( )
49+ return mediaQuery ?. matches ? 'dark' : 'light'
50+ }
51+
52+ function applyThemeToDocument ( theme : ThemePreference ) : void {
53+ if ( typeof document === 'undefined' ) return
54+ const resolved = resolveTheme ( theme )
55+ document . documentElement . classList . toggle ( 'dark' , resolved === 'dark' )
56+ }
57+
58+ function stopSystemThemeSync ( ) : void {
59+ if ( ! systemThemeMediaQuery || ! systemThemeListener ) return
60+
61+ if ( typeof systemThemeMediaQuery . removeEventListener === 'function' ) {
62+ systemThemeMediaQuery . removeEventListener ( 'change' , systemThemeListener )
63+ } else if ( typeof systemThemeMediaQuery . removeListener === 'function' ) {
64+ systemThemeMediaQuery . removeListener ( systemThemeListener )
65+ }
66+
67+ systemThemeMediaQuery = null
68+ systemThemeListener = null
69+ }
70+
71+ function startSystemThemeSync ( ) : void {
72+ const mediaQuery = getSystemThemeMediaQuery ( )
73+ if ( ! mediaQuery ) return
74+
75+ // Ensure idempotency: don't attach multiple listeners.
76+ if ( systemThemeMediaQuery === mediaQuery && systemThemeListener ) return
77+
78+ stopSystemThemeSync ( )
79+ systemThemeMediaQuery = mediaQuery
80+ systemThemeListener = ( event ) => {
81+ if ( preferencesStore . state . theme !== 'system' ) return
82+ if ( typeof document === 'undefined' ) return
83+ document . documentElement . classList . toggle ( 'dark' , event . matches )
84+ }
85+
86+ if ( typeof mediaQuery . addEventListener === 'function' ) {
87+ mediaQuery . addEventListener ( 'change' , systemThemeListener )
88+ } else if ( typeof mediaQuery . addListener === 'function' ) {
89+ mediaQuery . addListener ( systemThemeListener )
90+ }
91+ }
92+
93+ function syncTheme ( theme : ThemePreference ) : void {
94+ applyThemeToDocument ( theme )
95+ if ( theme === 'system' ) startSystemThemeSync ( )
96+ else stopSystemThemeSync ( )
97+ }
98+
3499// Load from localStorage
35100function loadFromStorage ( ) : PreferencesState {
36101 try {
@@ -86,6 +151,7 @@ export const preferencesActions = {
86151 saveToStorage ( newState )
87152 return newState
88153 } )
154+ syncTheme ( theme )
89155 } ,
90156
91157 /** εε§ε - εζ₯ i18n θ―θ¨εζεζΉε */
@@ -96,6 +162,7 @@ export const preferencesActions = {
96162 }
97163 // Set document direction for RTL support
98164 document . documentElement . dir = getLanguageDirection ( state . language )
165+ syncTheme ( state . theme )
99166 } ,
100167}
101168
0 commit comments