mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 23:59:39 +02:00
Projects included: - maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing) - manacore (Expo mobile + SvelteKit web + Astro landing) - manadeck (NestJS backend + Expo mobile + SvelteKit web) - memoro (Expo mobile + SvelteKit web + Astro landing) This commit preserves the current state before monorepo restructuring. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
173 lines
No EOL
5 KiB
TypeScript
173 lines
No EOL
5 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
|
import { Appearance, ColorSchemeName, Platform } from 'react-native';
|
|
import { ThemeName, ThemeMode, ThemeColors } from '~/types/theme';
|
|
import { themes } from '~/themes';
|
|
|
|
interface ThemeState {
|
|
theme: ThemeName;
|
|
mode: ThemeMode;
|
|
systemColorScheme: ColorSchemeName;
|
|
}
|
|
|
|
interface ThemeContextValue extends ThemeState {
|
|
// Actions
|
|
setTheme: (theme: ThemeName) => void;
|
|
setMode: (mode: ThemeMode) => void;
|
|
toggleMode: () => void;
|
|
setSystemColorScheme: (scheme: ColorSchemeName) => void;
|
|
// Computed values
|
|
isDark: boolean;
|
|
currentColors: ThemeColors;
|
|
rootClassName: string;
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
|
|
|
const STORAGE_KEY = 'theme-storage';
|
|
|
|
// Helper functions
|
|
const getIsDark = (mode: ThemeMode, systemColorScheme: ColorSchemeName): boolean => {
|
|
if (mode === 'system') {
|
|
return systemColorScheme === 'dark';
|
|
}
|
|
return mode === 'dark';
|
|
};
|
|
|
|
const getCurrentColors = (theme: ThemeName, isDark: boolean): ThemeColors => {
|
|
const themeData = themes[theme];
|
|
return isDark ? themeData.dark : themeData.light;
|
|
};
|
|
|
|
const getRootClassName = (theme: ThemeName, isDark: boolean): string => {
|
|
const themeClass = `theme-${theme}`;
|
|
const modeClass = isDark ? 'dark' : '';
|
|
return [themeClass, modeClass].filter(Boolean).join(' ');
|
|
};
|
|
|
|
// Storage helpers
|
|
const storage = {
|
|
getItem: async (): Promise<Partial<ThemeState> | null> => {
|
|
try {
|
|
if (Platform.OS === 'web') {
|
|
if (typeof window !== 'undefined' && window.localStorage) {
|
|
const str = window.localStorage.getItem(STORAGE_KEY);
|
|
return str ? JSON.parse(str) : null;
|
|
}
|
|
} else {
|
|
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
const str = await AsyncStorage.getItem(STORAGE_KEY);
|
|
return str ? JSON.parse(str) : null;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading theme:', error);
|
|
}
|
|
return null;
|
|
},
|
|
setItem: async (value: Partial<ThemeState>) => {
|
|
try {
|
|
const str = JSON.stringify(value);
|
|
if (Platform.OS === 'web') {
|
|
if (typeof window !== 'undefined' && window.localStorage) {
|
|
window.localStorage.setItem(STORAGE_KEY, str);
|
|
}
|
|
} else {
|
|
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
await AsyncStorage.setItem(STORAGE_KEY, str);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving theme:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const ThemeStoreProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
const [theme, setThemeState] = useState<ThemeName>('default');
|
|
const [mode, setModeState] = useState<ThemeMode>('system');
|
|
const [systemColorScheme, setSystemColorScheme] = useState<ColorSchemeName>(
|
|
Appearance.getColorScheme()
|
|
);
|
|
|
|
// Load persisted state on mount
|
|
useEffect(() => {
|
|
storage.getItem().then((persisted) => {
|
|
if (persisted) {
|
|
if (persisted.theme) setThemeState(persisted.theme);
|
|
if (persisted.mode) setModeState(persisted.mode);
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
// Listen to system theme changes
|
|
useEffect(() => {
|
|
const subscription = Appearance.addChangeListener(({ colorScheme }) => {
|
|
setSystemColorScheme(colorScheme);
|
|
});
|
|
return () => subscription?.remove();
|
|
}, []);
|
|
|
|
// Persist changes
|
|
const persistState = useCallback((theme: ThemeName, mode: ThemeMode) => {
|
|
storage.setItem({ theme, mode });
|
|
}, []);
|
|
|
|
// Actions
|
|
const setTheme = useCallback((newTheme: ThemeName) => {
|
|
setThemeState(newTheme);
|
|
persistState(newTheme, mode);
|
|
}, [mode, persistState]);
|
|
|
|
const setMode = useCallback((newMode: ThemeMode) => {
|
|
setModeState(newMode);
|
|
persistState(theme, newMode);
|
|
}, [theme, persistState]);
|
|
|
|
const toggleMode = useCallback(() => {
|
|
const newMode = mode === 'light' ? 'dark' : mode === 'dark' ? 'system' : 'light';
|
|
setMode(newMode);
|
|
}, [mode, setMode]);
|
|
|
|
// Computed values
|
|
const isDark = getIsDark(mode, systemColorScheme);
|
|
const currentColors = getCurrentColors(theme, isDark);
|
|
const rootClassName = getRootClassName(theme, isDark);
|
|
|
|
const value: ThemeContextValue = {
|
|
theme,
|
|
mode,
|
|
systemColorScheme,
|
|
setTheme,
|
|
setMode,
|
|
toggleMode,
|
|
setSystemColorScheme,
|
|
isDark,
|
|
currentColors,
|
|
rootClassName,
|
|
};
|
|
|
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
};
|
|
|
|
// Hook to use the theme store
|
|
export const useThemeStore = () => {
|
|
const context = useContext(ThemeContext);
|
|
if (!context) {
|
|
throw new Error('useThemeStore must be used within ThemeStoreProvider');
|
|
}
|
|
return context;
|
|
};
|
|
|
|
// Selector hooks for backward compatibility
|
|
export const useIsDark = () => {
|
|
const { isDark } = useThemeStore();
|
|
return isDark;
|
|
};
|
|
|
|
export const useCurrentColors = () => {
|
|
const { currentColors } = useThemeStore();
|
|
return currentColors;
|
|
};
|
|
|
|
export const useRootClassName = () => {
|
|
const { rootClassName } = useThemeStore();
|
|
return rootClassName;
|
|
}; |