- 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>
99 lines
2.9 KiB
TypeScript
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);
|
|
}
|