WellNuo/contexts/ThemeContext.tsx
Sergei 610104090a Add dark mode support with theme toggle
- Create ThemeContext with light/dark/system mode support
- Add DarkColors palette for dark mode UI
- Extend Colors object with full dark theme variants
- Update useThemeColor hook to use ThemeContext
- Add useThemeColors, useResolvedTheme, useIsDarkMode hooks
- Update RootLayout (native and web) with ThemeProvider
- Add theme toggle UI in ProfileDrawer settings
- Theme preference persisted to AsyncStorage
- Add comprehensive tests for ThemeContext and hooks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 10:01:07 -08:00

99 lines
2.9 KiB
TypeScript

import AsyncStorage from '@react-native-async-storage/async-storage';
import React, { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';
import { useColorScheme as useSystemColorScheme } from 'react-native';
export type ThemeMode = 'light' | 'dark' | 'system';
export type ResolvedTheme = 'light' | 'dark';
const THEME_STORAGE_KEY = '@wellnuo:theme_preference';
interface ThemeContextType {
themeMode: ThemeMode;
resolvedTheme: ResolvedTheme;
isDark: boolean;
setThemeMode: (mode: ThemeMode) => Promise<void>;
toggleTheme: () => Promise<void>;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const systemColorScheme = useSystemColorScheme();
const [themeMode, setThemeModeState] = useState<ThemeMode>('system');
const [isLoaded, setIsLoaded] = useState(false);
// Load saved theme preference on mount
useEffect(() => {
const loadThemePreference = async () => {
try {
const savedTheme = await AsyncStorage.getItem(THEME_STORAGE_KEY);
if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark' || savedTheme === 'system')) {
setThemeModeState(savedTheme as ThemeMode);
}
} catch (error) {
console.warn('Failed to load theme preference:', error);
} finally {
setIsLoaded(true);
}
};
loadThemePreference();
}, []);
// Resolve the actual theme based on mode and system preference
const resolvedTheme: ResolvedTheme =
themeMode === 'system' ? (systemColorScheme === 'dark' ? 'dark' : 'light') : themeMode;
const isDark = resolvedTheme === 'dark';
// Set theme mode and persist to storage
const setThemeMode = useCallback(async (mode: ThemeMode) => {
try {
await AsyncStorage.setItem(THEME_STORAGE_KEY, mode);
setThemeModeState(mode);
} catch (error) {
console.warn('Failed to save theme preference:', error);
// Still update state even if storage fails
setThemeModeState(mode);
}
}, []);
// Toggle between light and dark (skips system)
const toggleTheme = useCallback(async () => {
const newMode: ThemeMode = resolvedTheme === 'light' ? 'dark' : 'light';
await setThemeMode(newMode);
}, [resolvedTheme, setThemeMode]);
// Prevent flash by not rendering until theme is loaded
if (!isLoaded) {
return null;
}
return (
<ThemeContext.Provider
value={{
themeMode,
resolvedTheme,
isDark,
setThemeMode,
toggleTheme,
}}
>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// For components that need optional theme access (during initialization)
export function useThemeOptional() {
return useContext(ThemeContext);
}