@@ -4,18 +4,27 @@ import type { ReactNode } from 'react';
44
55export type ThemeMode = 'auto' | 'light' | 'dark' ;
66export type EffectiveTheme = 'light' | 'dark' ;
7+ export type Density = 'comfortable' | 'compact' ;
78
89/** Amanecer 06:50 y atardecer 21:40 (mock julio, España). Fase 2: sun.sun vía HAOS. */
910export const SUNRISE_MIN = 6 * 60 + 50 ;
1011export const SUNSET_MIN = 21 * 60 + 40 ;
1112
12- const STORAGE_KEY = 'helios-theme' ;
13+ const SLUG = 'helios' ;
14+ const MODE_KEY = `${ SLUG } -theme-mode` ;
15+ const LEGACY_MODE_KEY = `${ SLUG } -theme` ; // clave antigua, se migra a MODE_KEY
16+ const DENSITY_KEY = `${ SLUG } -density` ;
17+ const REDUCE_MOTION_KEY = `${ SLUG } -reduce-motion` ;
1318
1419interface ThemeContextValue {
1520 mode : ThemeMode ;
1621 setMode : ( mode : ThemeMode ) => void ;
1722 effective : EffectiveTheme ;
1823 isDark : boolean ;
24+ density : Density ;
25+ setDensity : ( d : Density ) => void ;
26+ reduceMotion : boolean ;
27+ setReduceMotion : ( v : boolean ) => void ;
1928}
2029
2130const ThemeContext = createContext < ThemeContextValue | null > ( null ) ;
@@ -28,7 +37,7 @@ function computeAuto(): EffectiveTheme {
2837
2938function initialMode ( ) : ThemeMode {
3039 try {
31- const saved = localStorage . getItem ( STORAGE_KEY ) ;
40+ const saved = localStorage . getItem ( MODE_KEY ) ?? localStorage . getItem ( LEGACY_MODE_KEY ) ;
3241 if ( saved === 'auto' || saved === 'light' || saved === 'dark' ) return saved ;
3342 } catch {
3443 /* sin localStorage */
@@ -52,9 +61,41 @@ function applyTheme(effective: EffectiveTheme) {
5261 icon . href = effective === 'dark' ? '/favicon-dark.ico' : '/favicon-light.ico' ;
5362}
5463
64+ function applyDensity ( density : Density ) {
65+ document . documentElement . style . fontSize = density === 'compact' ? '13.5px' : '16px' ;
66+ }
67+
68+ /** Anti-FOUC: aplicar preferencias antes del primer render (main.tsx). */
69+ export function applyBootPreferences ( ) {
70+ try {
71+ applyTheme ( initialMode ( ) === 'auto' ? computeAuto ( ) : ( initialMode ( ) as EffectiveTheme ) ) ;
72+ const density = localStorage . getItem ( DENSITY_KEY ) ;
73+ if ( density === 'compact' || density === 'comfortable' ) applyDensity ( density ) ;
74+ if ( localStorage . getItem ( REDUCE_MOTION_KEY ) === '1' ) {
75+ document . documentElement . classList . add ( 'reduce-motion' ) ;
76+ }
77+ } catch {
78+ /* sin localStorage */
79+ }
80+ }
81+
5582export function ThemeProvider ( { children } : { children : ReactNode } ) {
5683 const [ mode , setModeState ] = useState < ThemeMode > ( initialMode ) ;
5784 const [ autoTheme , setAutoTheme ] = useState < EffectiveTheme > ( computeAuto ) ;
85+ const [ density , setDensityState ] = useState < Density > ( ( ) => {
86+ try {
87+ return localStorage . getItem ( DENSITY_KEY ) === 'compact' ? 'compact' : 'comfortable' ;
88+ } catch {
89+ return 'comfortable' ;
90+ }
91+ } ) ;
92+ const [ reduceMotion , setReduceMotionState ] = useState < boolean > ( ( ) => {
93+ try {
94+ return localStorage . getItem ( REDUCE_MOTION_KEY ) === '1' ;
95+ } catch {
96+ return false ;
97+ }
98+ } ) ;
5899
59100 const effective : EffectiveTheme = mode === 'auto' ? autoTheme : mode ;
60101
@@ -69,18 +110,42 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
69110 applyTheme ( effective ) ;
70111 } , [ effective ] ) ;
71112
113+ useEffect ( ( ) => {
114+ applyDensity ( density ) ;
115+ } , [ density ] ) ;
116+
72117 const setMode = useCallback ( ( next : ThemeMode ) => {
73118 setModeState ( next ) ;
74119 try {
75- localStorage . setItem ( STORAGE_KEY , next ) ;
120+ localStorage . setItem ( MODE_KEY , next ) ;
121+ localStorage . removeItem ( LEGACY_MODE_KEY ) ;
122+ } catch {
123+ /* sin localStorage */
124+ }
125+ } , [ ] ) ;
126+
127+ const setDensity = useCallback ( ( next : Density ) => {
128+ setDensityState ( next ) ;
129+ try {
130+ localStorage . setItem ( DENSITY_KEY , next ) ;
131+ } catch {
132+ /* sin localStorage */
133+ }
134+ } , [ ] ) ;
135+
136+ const setReduceMotion = useCallback ( ( next : boolean ) => {
137+ setReduceMotionState ( next ) ;
138+ try {
139+ localStorage . setItem ( REDUCE_MOTION_KEY , next ? '1' : '0' ) ;
140+ document . documentElement . classList . toggle ( 'reduce-motion' , next ) ;
76141 } catch {
77142 /* sin localStorage */
78143 }
79144 } , [ ] ) ;
80145
81146 const value = useMemo < ThemeContextValue > (
82- ( ) => ( { mode, setMode, effective, isDark : effective === 'dark' } ) ,
83- [ mode , setMode , effective ] ,
147+ ( ) => ( { mode, setMode, effective, isDark : effective === 'dark' , density , setDensity , reduceMotion , setReduceMotion } ) ,
148+ [ mode , setMode , effective , density , setDensity , reduceMotion , setReduceMotion ] ,
84149 ) ;
85150
86151 return < ThemeContext . Provider value = { value } > { children } </ ThemeContext . Provider > ;
0 commit comments