6.1 KiB
Plan: Dark Mode para FarmaFinder Mobile
Objetivo
Añadir soporte de modo oscuro a la app móvil con:
- Detección automática del modo oscuro del sistema
- Toggle manual en la pantalla de perfil (Configuración)
- Persistencia de la preferencia del usuario
Arquitectura
1. Theme Store (Zustand + AsyncStorage)
Nuevo archivo: store/themeStore.ts
- Estado:
mode: 'light' | 'dark' | 'system' - Persistencia con
@react-native-async-storage/async-storage - Acciones:
setMode(),init()(carga del storage al arrancar) - Por defecto:
'system'(respeta el dispositivo)
2. Hook useThemeColor
Nuevo archivo: hooks/useThemeColor.ts
- Usa
useColorScheme()de React Native para detectar el color del sistema - Combina con el
modedel themeStore para determinar el tema efectivo - Retorna el objeto
colorsapropiado (light o dark) - Los componentes consumirán este hook en vez de importar
colorsdirectamente
3. Paleta de colores oscura
Modificar: constants/theme.ts
Añadir exportación darkColors con la paleta oscura:
| Token | Light (actual) | Dark (nuevo) |
|---|---|---|
background |
#fbfbfb |
#121212 |
card |
#ffffff |
#1e1e1e |
surfaceLow |
#f2f4f5 |
#2a2a2a |
surface |
#eceeef |
#333333 |
surfaceHigh |
#e6e8e9 |
#3a3a3a |
border |
#c0c9bb |
#3a3a3a |
separator |
#c0c9bb |
#3a3a3a |
text |
#111417 |
#f0f0f0 |
textSecondary |
#41493e |
#a0a0a0 |
textInverse |
#ffffff |
#111417 |
primaryContainer |
#cfead0 |
#1a3a1c |
onPrimaryContainer |
#0d2b12 |
#cfead0 |
secondaryContainer |
#dbe7ff |
#1a2a4a |
tertiaryContainer |
#efe7ff |
#2a1a4a |
dangerContainer |
#feecec |
#3a1a1a |
accentWarm |
#f5a97a |
#f5a97a (sin cambio) |
primary, secondary, tertiary, scanButton, success, danger, warning |
(sin cambios) | (sin cambios) |
4. Provider wrapper
Nuevo archivo: components/ThemeProvider.tsx
- Lee el
modedel themeStore y eluseColorScheme()del sistema - Calcula el tema efectivo (
lightodark) - Provee
colorsyisDarkvía React Context - Carga la preferencia guardada en AsyncStorage al montar
5. Integración en Root Layout
Modificar: app/_layout.tsx
- Envolver toda la app con
<ThemeProvider> - El
StatusBarse ajusta:style={isDark ? 'light' : 'auto'} - Los colores del Stack header se leen del contexto
6. Toggle en el perfil
Modificar: app/(tabs)/profile.tsx
Añadir en la pantalla de perfil (antes de "Configuración") un nuevo item de menú:
- Icono:
moon-outline/sunny-outlinesegún el tema actual - Texto: "Modo de pantalla"
- Al pulsar, mostrar un picker/modal con 3 opciones:
- Sistema (icono: phone-portrait) - respeta el SO
- Claro (icono: sunny) - fuerza light
- Oscuro (icono: moon) - fuerza dark
- El toggle se muestra inline (sin modal extra) tipo Segmented Control
7. Actualizar todos los screens y components
Cada archivo que importa colors debe cambiar a usar el hook useThemeColor():
| Archivo | Cambio |
|---|---|
app/_layout.tsx |
Usa ThemeProvider + context para header colors |
app/(tabs)/_layout.tsx |
Tab bar y header leen colors del context |
app/(tabs)/index.tsx |
const { colors } = useThemeColor() |
app/(tabs)/search.tsx |
const { colors } = useThemeColor() |
app/(tabs)/scan.tsx |
const { colors } = useThemeColor() |
app/(tabs)/alerts.tsx |
const { colors } = useThemeColor() |
app/(tabs)/profile.tsx |
const { colors, isDark } = useThemeColor() |
app/(tabs)/map.tsx |
const { colors } = useThemeColor() |
app/medicine/[id].tsx |
const { colors } = useThemeColor() |
app/pharmacy/[id].tsx |
const { colors } = useThemeColor() |
app/auth/login.tsx |
const { colors } = useThemeColor() |
app/auth/register.tsx |
const { colors } = useThemeColor() |
components/SearchBar.tsx |
const { colors } = useThemeColor() |
components/MedicineCard.tsx |
const { colors } = useThemeColor() |
components/LoadingSpinner.tsx |
const { colors } = useThemeColor() |
components/StockBadge.tsx |
Añade colores dark para badges |
components/BarcodeScanner.tsx |
const { colors } = useThemeColor() |
Nota sobre StyleSheet: Los StyleSheet.create() se ejecutan una vez. Para que los estilos se actualicen con el tema, las propiedades que dependen de colors deben aplicarse vía style inline o moverse a funciones que retornen estilos dinámicos. La estrategia más limpia es:
- Los colores de fondo/texto van como inline styles
- Las estructuras (flex, padding, borderRadius) se quedan en
StyleSheet.create
8. Colores hardcodeados
Los siguientes archivos tienen colores inline hardcodeados que necesitan variante dark:
StockBadge.tsx:#eaf7ec,#fff3cd,#feecec→ añadir tokens al temasearch.tsx: suggestions con#ffffff,#f0f7ff, etc. → usar colores del temaprofile.tsx: feedback#eaf7ec,#cfead0,#fecaca→ usar colores del temahome/index.tsx:rgba(255,255,255,0.2/0.3)para iconos → ajustar opacidad
9. app.json
Modificar: app.json
Cambiar "userInterfaceStyle": "light" → "userInterfaceStyle": "automatic"
Orden de ejecución
store/themeStore.ts(nuevo)hooks/useThemeColor.ts(nuevo)constants/theme.ts(añadirdarkColors)components/ThemeProvider.tsx(nuevo)app/_layout.tsx(envolver con ThemeProvider)app/(tabs)/_layout.tsx(colores dinámicos)app/(tabs)/profile.tsx(toggle de tema)- Todos los screens y components (migrar a hook)
app.json(userInterfaceStyle: automatic)- Corregir colores hardcodeados
Verificación
- Ejecutar
npx expo starty verificar que la app arranca sin errores - Probar en iOS/Android: el tema respeta la configuración del sistema
- Ir a Perfil > Modo de pantalla y cambiar entre Claro/Oscuro/Sistema
- Verificar que el cambio es inmediato sin recargar la app
- Cerrar y reabrir la app → la preferencia persiste
- Verificar que todos los screens se ven bien en ambos modos (sin texto invisible, sin fondos transparentes)
- Probar tablets ( responsive)