Skip to content

Commit 1b19774

Browse files
committed
feat(theme): tokens canonicos webapp-shell + densidad compacta + reduce-motion
- RGB triplets con sufijo -rgb (sin romper legacy hex de graficas ni shadcn HSL) - tailwind: colores canonicos (canvas, elevated, brand, ok/warn/danger/info...) - ThemeProvider: densidad comoda/compacta (root 16/13.5px), reduce-motion, migracion de clave helios-theme -> helios-theme-mode, applyBootPreferences - main.tsx + index.html: boot anti-FOUC con las nuevas preferencias
1 parent cbbf11f commit 1b19774

5 files changed

Lines changed: 148 additions & 10 deletions

File tree

‎app/index.html‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
rel="stylesheet"
1919
/>
2020
<script>
21-
// Tema inicial antes del primer pintado (evita flash). Modos: auto | light | dark.
22-
// Auto: claro entre 06:50 y 21:40 (mock julio, España).
21+
// Preferencias antes del primer pintado (evita flash). Modos: auto | light | dark.
22+
// Auto: claro entre 06:50 y 21:40 (mock julio, España). Clave nueva helios-theme-mode
23+
// con fallback a la legacy helios-theme. Densidad compacta = root 13.5px.
2324
(function () {
2425
try {
25-
var mode = localStorage.getItem('helios-theme') || 'auto';
26+
var mode = localStorage.getItem('helios-theme-mode') || localStorage.getItem('helios-theme') || 'auto';
2627
var dark;
2728
if (mode === 'auto') {
2829
var now = new Date();
@@ -34,6 +35,8 @@
3435
if (dark) document.documentElement.classList.add('dark');
3536
var meta = document.getElementById('meta-theme-color');
3637
if (meta) meta.setAttribute('content', dark ? '#080D1A' : '#F4F6FA');
38+
if (localStorage.getItem('helios-density') === 'compact') document.documentElement.style.fontSize = '13.5px';
39+
if (localStorage.getItem('helios-reduce-motion') === '1') document.documentElement.classList.add('reduce-motion');
3740
} catch (e) {}
3841
})();
3942
(function () {

‎app/src/index.css‎

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,25 @@
44

55
@layer base {
66
:root {
7-
/* ── Helios · tema claro (día) ─────────────────────────── */
7+
/* ── Tokens canónicos webapp-shell (RGB triplets, claro) ──
8+
Sufijo -rgb: los nombres sin sufijo están ocupados por los
9+
legacy hex (gráficas SVG) y los HSL de shadcn. */
10+
--canvas-rgb: 244 246 250;
11+
--surface-rgb: 255 255 255;
12+
--elevated-rgb: 238 241 246;
13+
--hover-rgb: 226 232 240;
14+
--border-rgb: 227 232 240;
15+
--border-strong-rgb: 203 213 225;
16+
--text-primary-rgb: 12 20 37;
17+
--text-secondary-rgb: 91 107 132;
18+
--text-muted-rgb: 148 163 184;
19+
--accent-rgb: 245 158 11; /* ámbar marca Helios */
20+
--ok-rgb: 16 185 129;
21+
--warn-rgb: 217 119 6;
22+
--danger-rgb: 244 63 94;
23+
--info-rgb: 14 165 233;
24+
25+
/* ── Helios · tema claro (día) — legacy hex (SVG/gráficas) ── */
826
--bg: #F4F6FA;
927
--surface: #FFFFFF;
1028
--surface-2: #EEF1F6;
@@ -54,7 +72,23 @@
5472
}
5573

5674
.dark {
57-
/* ── Helios · tema oscuro (noche) ──────────────────────── */
75+
/* ── Tokens canónicos webapp-shell (RGB triplets, oscuro) ── */
76+
--canvas-rgb: 8 13 26;
77+
--surface-rgb: 16 24 40;
78+
--elevated-rgb: 24 35 56;
79+
--hover-rgb: 30 43 66;
80+
--border-rgb: 30 43 66;
81+
--border-strong-rgb: 42 58 82;
82+
--text-primary-rgb: 233 238 247;
83+
--text-secondary-rgb: 147 161 184;
84+
--text-muted-rgb: 92 107 133;
85+
--accent-rgb: 251 191 36;
86+
--ok-rgb: 52 211 153;
87+
--warn-rgb: 251 191 36;
88+
--danger-rgb: 251 113 133;
89+
--info-rgb: 56 189 248;
90+
91+
/* ── Helios · tema oscuro (noche) — legacy hex ── */
5892
--bg: #080D1A;
5993
--surface: #101828;
6094
--surface-2: #182338;
@@ -182,3 +216,19 @@
182216
background-image: linear-gradient(135deg, #F59E0B, #F97316);
183217
}
184218
}
219+
220+
/* Reducir animaciones: switch de Ajustes (clase en <html>) + SO */
221+
html.reduce-motion *,
222+
html.reduce-motion *::before,
223+
html.reduce-motion *::after {
224+
animation-duration: 0.01ms !important;
225+
transition-duration: 0.01ms !important;
226+
}
227+
@media (prefers-reduced-motion: reduce) {
228+
*,
229+
*::before,
230+
*::after {
231+
animation-duration: 0.01ms !important;
232+
transition-duration: 0.01ms !important;
233+
}
234+
}

‎app/src/main.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import { createRoot } from 'react-dom/client'
22
import { BrowserRouter } from 'react-router-dom'
33
import './index.css'
44
import './i18n'
5+
import { applyBootPreferences } from './theme/ThemeProvider'
56
import App from './App.tsx'
67

8+
// Preferencias (tema/densidad/reduce-motion) antes del primer render.
9+
applyBootPreferences()
10+
711
// Sin StrictMode: duplicaría los efectos de canvas/animación.
812
createRoot(document.getElementById('root')!).render(
913
<BrowserRouter>

‎app/src/theme/ThemeProvider.tsx‎

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,27 @@ import type { ReactNode } from 'react';
44

55
export type ThemeMode = 'auto' | 'light' | 'dark';
66
export 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. */
910
export const SUNRISE_MIN = 6 * 60 + 50;
1011
export 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

1419
interface 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

2130
const ThemeContext = createContext<ThemeContextValue | null>(null);
@@ -28,7 +37,7 @@ function computeAuto(): EffectiveTheme {
2837

2938
function 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+
5582
export 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>;

‎app/tailwind.config.js‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ module.exports = {
6262
solis: "var(--c-solis)",
6363
fox: "var(--c-fox)",
6464
marca: "#F59E0B",
65+
// Tokens canónicos webapp-shell (RGB triplets con sufijo -rgb).
66+
canvas: "rgb(var(--canvas-rgb) / <alpha-value>)",
67+
elevated: "rgb(var(--elevated-rgb) / <alpha-value>)",
68+
hover: "rgb(var(--hover-rgb) / <alpha-value>)",
69+
"border-strong": "rgb(var(--border-strong-rgb) / <alpha-value>)",
70+
"text-primary": "rgb(var(--text-primary-rgb) / <alpha-value>)",
71+
"text-secondary": "rgb(var(--text-secondary-rgb) / <alpha-value>)",
72+
"text-muted": "rgb(var(--text-muted-rgb) / <alpha-value>)",
73+
brand: {
74+
DEFAULT: "rgb(var(--accent-rgb) / <alpha-value>)",
75+
soft: "rgb(var(--accent-rgb) / 0.12)",
76+
},
77+
ok: "rgb(var(--ok-rgb) / <alpha-value>)",
78+
warn: "rgb(var(--warn-rgb) / <alpha-value>)",
79+
danger: "rgb(var(--danger-rgb) / <alpha-value>)",
80+
info: "rgb(var(--info-rgb) / <alpha-value>)",
6581
},
6682
borderRadius: {
6783
xl: "calc(var(--radius) + 4px)",

0 commit comments

Comments
 (0)