mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 13:19: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>
454 lines
15 KiB
TypeScript
454 lines
15 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
|
|
import { useColorScheme, Platform } from 'react-native';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
// Theme-Varianten
|
|
export type ThemeVariant = 'lume' | 'nature' | 'stone' | 'ocean';
|
|
export type ColorMode = 'system' | 'light' | 'dark';
|
|
|
|
// Namen der Theme-Varianten für die UI
|
|
export const THEME_NAMES: Record<ThemeVariant, string> = {
|
|
lume: 'Lume',
|
|
nature: 'Nature',
|
|
stone: 'Stone',
|
|
ocean: 'Ocean',
|
|
};
|
|
|
|
// Theme-Farben-Definitionen
|
|
const THEME_COLORS: Record<ThemeVariant, { light: ThemeColors; dark: ThemeColors }> = {
|
|
lume: {
|
|
light: {
|
|
primary: '#f8d62b',
|
|
primaryButton: '#f8d62b',
|
|
primaryButtonText: '#000000',
|
|
secondary: '#D4B200',
|
|
secondaryButton: '#FFE9A3',
|
|
contentBackground: '#ffffff',
|
|
contentBackgroundHover: '#f5f5f5',
|
|
contentPageBackground: '#ffffff',
|
|
menuBackground: '#dddddd',
|
|
menuBackgroundHover: '#cccccc',
|
|
pageBackground: '#dddddd',
|
|
text: '#2c2c2c',
|
|
textSecondary: '#666666',
|
|
textTertiary: '#999999',
|
|
borderLight: '#f2f2f2',
|
|
border: '#e6e6e6',
|
|
borderStrong: '#cccccc',
|
|
error: '#e74c3c',
|
|
skeleton: 'rgba(128, 128, 128, 0.2)',
|
|
},
|
|
dark: {
|
|
primary: '#f8d62b',
|
|
primaryButton: '#7C6B16',
|
|
primaryButtonText: '#ffffff',
|
|
secondary: '#D4B200',
|
|
secondaryButton: '#1E1E1E',
|
|
contentBackground: '#1E1E1E',
|
|
contentBackgroundHover: '#333333',
|
|
contentPageBackground: '#121212',
|
|
menuBackground: '#252525',
|
|
menuBackgroundHover: '#333333',
|
|
pageBackground: '#121212',
|
|
text: '#ffffff',
|
|
textSecondary: '#cccccc',
|
|
textTertiary: '#999999',
|
|
borderLight: '#333333',
|
|
border: '#424242',
|
|
borderStrong: '#616161',
|
|
error: '#e74c3c',
|
|
skeleton: 'rgba(255, 255, 255, 0.1)',
|
|
},
|
|
},
|
|
nature: {
|
|
light: {
|
|
primary: '#4CAF50',
|
|
primaryButton: '#A08500',
|
|
primaryButtonText: '#ffffff',
|
|
secondary: '#81C784',
|
|
secondaryButton: '#F1F8E9',
|
|
contentBackground: '#F1F8E9',
|
|
contentBackgroundHover: '#E8F5E9',
|
|
contentPageBackground: '#ffffff',
|
|
menuBackground: '#E8F5E9',
|
|
menuBackgroundHover: '#C8E6C9',
|
|
pageBackground: '#FBFDF8',
|
|
text: '#1B5E20',
|
|
textSecondary: '#33691E',
|
|
textTertiary: '#558B2F',
|
|
borderLight: '#E8F5E9',
|
|
border: '#C8E6C9',
|
|
borderStrong: '#A5D6A7',
|
|
error: '#E57373',
|
|
skeleton: 'rgba(76, 175, 80, 0.1)',
|
|
},
|
|
dark: {
|
|
primary: '#4CAF50',
|
|
primaryButton: '#FF9500',
|
|
primaryButtonText: '#000000',
|
|
secondary: '#81C784',
|
|
secondaryButton: '#1E1E1E',
|
|
contentBackground: '#1E1E1E',
|
|
contentBackgroundHover: '#2E7D32',
|
|
contentPageBackground: '#121212',
|
|
menuBackground: '#252525',
|
|
menuBackgroundHover: '#2E7D32',
|
|
pageBackground: '#121212',
|
|
text: '#FFFFFF',
|
|
textSecondary: '#C8E6C9',
|
|
textTertiary: '#A5D6A7',
|
|
borderLight: '#1B5E20',
|
|
border: '#2E7D32',
|
|
borderStrong: '#388E3C',
|
|
error: '#CF6679',
|
|
skeleton: 'rgba(76, 175, 80, 0.1)',
|
|
},
|
|
},
|
|
stone: {
|
|
light: {
|
|
primary: '#607D8B',
|
|
primaryButton: '#FF9500',
|
|
primaryButtonText: '#000000',
|
|
secondary: '#90A4AE',
|
|
secondaryButton: '#ECEFF1',
|
|
contentBackground: '#ECEFF1',
|
|
contentBackgroundHover: '#E0E6EA',
|
|
contentPageBackground: '#ffffff',
|
|
menuBackground: '#E0E6EA',
|
|
menuBackgroundHover: '#CFD8DC',
|
|
pageBackground: '#F5F7F9',
|
|
text: '#263238',
|
|
textSecondary: '#37474F',
|
|
textTertiary: '#546E7A',
|
|
borderLight: '#ECEFF1',
|
|
border: '#CFD8DC',
|
|
borderStrong: '#B0BEC5',
|
|
error: '#EF5350',
|
|
skeleton: 'rgba(96, 125, 139, 0.1)',
|
|
},
|
|
dark: {
|
|
primary: '#78909C',
|
|
primaryButton: '#FF9500',
|
|
primaryButtonText: '#000000',
|
|
secondary: '#90A4AE',
|
|
secondaryButton: '#1E1E1E',
|
|
contentBackground: '#1E1E1E',
|
|
contentBackgroundHover: '#37474F',
|
|
contentPageBackground: '#121212',
|
|
menuBackground: '#252525',
|
|
menuBackgroundHover: '#37474F',
|
|
pageBackground: '#121212',
|
|
text: '#FFFFFF',
|
|
textSecondary: '#B0BEC5',
|
|
textTertiary: '#90A4AE',
|
|
borderLight: '#37474F',
|
|
border: '#455A64',
|
|
borderStrong: '#546E7A',
|
|
error: '#CF6679',
|
|
skeleton: 'rgba(120, 144, 156, 0.1)',
|
|
},
|
|
},
|
|
ocean: {
|
|
light: {
|
|
primary: '#039BE5',
|
|
primaryButton: '#FF9500',
|
|
primaryButtonText: '#000000',
|
|
secondary: '#4FC3F7',
|
|
secondaryButton: '#E1F5FE',
|
|
contentBackground: '#E1F5FE',
|
|
contentBackgroundHover: '#B3E5FC',
|
|
contentPageBackground: '#ffffff',
|
|
menuBackground: '#E1F5FE',
|
|
menuBackgroundHover: '#B3E5FC',
|
|
pageBackground: '#F5FCFF',
|
|
text: '#01579B',
|
|
textSecondary: '#0277BD',
|
|
textTertiary: '#0288D1',
|
|
borderLight: '#E1F5FE',
|
|
border: '#B3E5FC',
|
|
borderStrong: '#81D4FA',
|
|
error: '#EF5350',
|
|
skeleton: 'rgba(3, 155, 229, 0.1)',
|
|
},
|
|
dark: {
|
|
primary: '#039BE5',
|
|
primaryButton: '#FF9500',
|
|
primaryButtonText: '#000000',
|
|
secondary: '#4FC3F7',
|
|
secondaryButton: '#1E1E1E',
|
|
contentBackground: '#1E1E1E',
|
|
contentBackgroundHover: '#0277BD',
|
|
contentPageBackground: '#121212',
|
|
menuBackground: '#252525',
|
|
menuBackgroundHover: '#0277BD',
|
|
pageBackground: '#121212',
|
|
text: '#FFFFFF',
|
|
textSecondary: '#B3E5FC',
|
|
textTertiary: '#81D4FA',
|
|
borderLight: '#01579B',
|
|
border: '#0277BD',
|
|
borderStrong: '#0288D1',
|
|
error: '#CF6679',
|
|
skeleton: 'rgba(3, 155, 229, 0.1)',
|
|
},
|
|
},
|
|
};
|
|
|
|
// Storage-Keys für persistente Einstellungen
|
|
const STORAGE_KEYS = {
|
|
COLOR_MODE: '@theme/colorMode',
|
|
THEME_VARIANT: '@theme/themeVariant',
|
|
};
|
|
|
|
// Theme-Farben-Typ
|
|
export type ThemeColors = {
|
|
primary: string;
|
|
primaryButton: string;
|
|
primaryButtonText: string;
|
|
secondary: string;
|
|
secondaryButton: string;
|
|
contentBackground: string;
|
|
contentBackgroundHover: string;
|
|
contentPageBackground: string;
|
|
menuBackground: string;
|
|
menuBackgroundHover: string;
|
|
pageBackground: string;
|
|
text: string;
|
|
textSecondary: string;
|
|
textTertiary: string;
|
|
borderLight: string;
|
|
border: string;
|
|
borderStrong: string;
|
|
error: string;
|
|
skeleton: string;
|
|
};
|
|
|
|
// Theme-Kontext-Typ
|
|
type ThemeContextType = {
|
|
isDark: boolean;
|
|
colorMode: ColorMode;
|
|
setColorMode: (mode: ColorMode) => void;
|
|
themeVariant: ThemeVariant;
|
|
setThemeVariant: (variant: ThemeVariant) => void;
|
|
// Tailwind-Klassen-Generator
|
|
tw: (className: string) => string;
|
|
// Aktuelle Theme-Farben
|
|
colors: ThemeColors;
|
|
};
|
|
|
|
// Theme-Update-Kontext-Typ
|
|
type ThemeUpdateContextType = {
|
|
toggleTheme: () => void;
|
|
setColorMode: (mode: ColorMode) => void;
|
|
setThemeVariant: (variant: ThemeVariant) => void;
|
|
};
|
|
|
|
// Standard-Werte für den Kontext
|
|
const ThemeContext = createContext<ThemeContextType>({
|
|
isDark: false,
|
|
colorMode: 'system',
|
|
setColorMode: () => {},
|
|
themeVariant: 'lume',
|
|
setThemeVariant: () => {},
|
|
tw: (className) => className,
|
|
colors: THEME_COLORS.lume.light,
|
|
});
|
|
|
|
// Kontext für Theme-Update-Funktionen
|
|
const ThemeUpdateContext = createContext<ThemeUpdateContextType>({
|
|
toggleTheme: () => {},
|
|
setColorMode: () => {},
|
|
setThemeVariant: () => {},
|
|
});
|
|
|
|
/**
|
|
* Theme-Provider-Komponente
|
|
* Verwaltet den Theme-Zustand und stellt ihn der App zur Verfügung
|
|
*/
|
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const systemColorScheme = useColorScheme();
|
|
const [colorMode, setColorMode] = useState<ColorMode>('system');
|
|
const [themeVariant, setThemeVariant] = useState<ThemeVariant>('lume');
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
|
|
// Lade gespeicherte Einstellungen beim Start
|
|
useEffect(() => {
|
|
const loadSettings = async () => {
|
|
try {
|
|
const [savedColorMode, savedThemeVariant] = await Promise.all([
|
|
AsyncStorage.getItem(STORAGE_KEYS.COLOR_MODE),
|
|
AsyncStorage.getItem(STORAGE_KEYS.THEME_VARIANT),
|
|
]);
|
|
|
|
if (savedColorMode) setColorMode(savedColorMode as ColorMode);
|
|
if (savedThemeVariant) setThemeVariant(savedThemeVariant as ThemeVariant);
|
|
|
|
setIsInitialized(true);
|
|
} catch (error) {
|
|
console.error('Error loading theme settings:', error);
|
|
setIsInitialized(true);
|
|
}
|
|
};
|
|
loadSettings();
|
|
}, []);
|
|
|
|
// Speichere Einstellungen bei Änderungen
|
|
useEffect(() => {
|
|
if (!isInitialized) return;
|
|
|
|
const saveSettings = async () => {
|
|
try {
|
|
await Promise.all([
|
|
AsyncStorage.setItem(STORAGE_KEYS.COLOR_MODE, colorMode),
|
|
AsyncStorage.setItem(STORAGE_KEYS.THEME_VARIANT, themeVariant),
|
|
]);
|
|
} catch (error) {
|
|
console.error('Error saving theme settings:', error);
|
|
}
|
|
};
|
|
|
|
saveSettings();
|
|
}, [colorMode, themeVariant, isInitialized]);
|
|
|
|
// Bestimme den effektiven Farbmodus (system, light, dark)
|
|
const effectiveColorScheme = colorMode === 'system' ? systemColorScheme : colorMode;
|
|
const isDark = effectiveColorScheme === 'dark';
|
|
|
|
// Set dark class on document element for web platform
|
|
useEffect(() => {
|
|
if (Platform.OS === 'web' && typeof document !== 'undefined') {
|
|
if (isDark) {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
}
|
|
}, [isDark]);
|
|
|
|
// Berechne die aktuellen Theme-Farben
|
|
const currentColors = useMemo(() => {
|
|
return THEME_COLORS[themeVariant][isDark ? 'dark' : 'light'];
|
|
}, [themeVariant, isDark]);
|
|
|
|
// Cache für transformierte Klassen
|
|
const classCache = useMemo(() => new Map<string, string>(), []);
|
|
|
|
// Liste der unterstützten generischen Klassen für Validierung
|
|
const supportedGenericClasses = useMemo(() => [
|
|
'bg-primary', 'text-primary', 'border-primary',
|
|
'bg-secondary', 'text-secondary', 'border-secondary',
|
|
'bg-background', 'text-default', 'border-default',
|
|
'border-light', 'border-strong',
|
|
'bg-pageBackground', 'bg-contentPageBackground'
|
|
], []);
|
|
|
|
// Tailwind-Klassen-Generator mit Caching und Validierung
|
|
const tw = useCallback((className: string) => {
|
|
// Wenn die Klasse bereits im Cache ist und sich das Theme nicht geändert hat,
|
|
// gib den gecachten Wert zurück
|
|
const cacheKey = `${className}|${themeVariant}|${isDark}`;
|
|
if (classCache.has(cacheKey)) {
|
|
return classCache.get(cacheKey)!;
|
|
}
|
|
|
|
// Validiere die Klassen (nur im Entwicklungsmodus)
|
|
if (__DEV__) {
|
|
// Prüfe auf unbekannte generische Klassen, die mit bg-, text- oder border- beginnen
|
|
const classNames = className.split(' ');
|
|
classNames.forEach(cls => {
|
|
if ((cls.startsWith('bg-') || cls.startsWith('text-') || cls.startsWith('border-')) &&
|
|
!cls.includes('-primary') && !cls.includes('-secondary') &&
|
|
!cls.includes('-background') && !cls.includes('-pageBackground') && !cls.includes('-contentPageBackground') && !cls.includes('-default') &&
|
|
!cls.includes('-error') && !supportedGenericClasses.includes(cls)) {
|
|
console.warn(`Möglicherweise nicht unterstützte generische Klasse: ${cls}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Basis-Klassen
|
|
let classes = className;
|
|
|
|
// Ersetze generische Farb-Klassen durch Theme-spezifische
|
|
classes = classes
|
|
.replace(/\bbg-primary\b/g, `bg-${themeVariant}-primary`)
|
|
.replace(/\btext-primary\b/g, `text-${themeVariant}-primary`)
|
|
.replace(/\bborder-primary\b/g, `border-${themeVariant}-primary`)
|
|
.replace(/\bbg-secondary\b/g, `bg-${themeVariant}-secondary`)
|
|
.replace(/\btext-secondary\b/g, `text-${themeVariant}-secondary`)
|
|
.replace(/\bborder-secondary\b/g, `border-${themeVariant}-secondary`)
|
|
.replace(/\bbg-background\b/g, `bg-${themeVariant}-background`)
|
|
.replace(/\bbg-pageBackground\b/g, `bg-${themeVariant}-pageBackground`)
|
|
.replace(/\bbg-contentPageBackground\b/g, `bg-${themeVariant}-contentPageBackground`)
|
|
.replace(/\btext-default\b/g, `text-${themeVariant}-text`)
|
|
.replace(/\bborder-default\b/g, `border-${themeVariant}-border`)
|
|
.replace(/\bborder-light\b/g, `border-${themeVariant}-borderLight`)
|
|
.replace(/\bborder-strong\b/g, `border-${themeVariant}-borderStrong`);
|
|
|
|
// Füge Dark-Mode-Klassen hinzu, wenn im Dark-Mode
|
|
if (isDark) {
|
|
// Ersetze alle Theme-spezifischen Klassen durch ihre Dark-Mode-Varianten
|
|
classes = classes
|
|
.replace(new RegExp(`bg-${themeVariant}-primary`, 'g'), `bg-dark-${themeVariant}-primary`)
|
|
.replace(new RegExp(`text-${themeVariant}-primary`, 'g'), `text-dark-${themeVariant}-primary`)
|
|
.replace(new RegExp(`border-${themeVariant}-primary`, 'g'), `border-dark-${themeVariant}-primary`)
|
|
.replace(new RegExp(`bg-${themeVariant}-secondary`, 'g'), `bg-dark-${themeVariant}-secondary`)
|
|
.replace(new RegExp(`text-${themeVariant}-secondary`, 'g'), `text-dark-${themeVariant}-secondary`)
|
|
.replace(new RegExp(`border-${themeVariant}-secondary`, 'g'), `border-dark-${themeVariant}-secondary`)
|
|
.replace(new RegExp(`bg-${themeVariant}-background`, 'g'), `bg-dark-${themeVariant}-background`)
|
|
.replace(new RegExp(`bg-${themeVariant}-pageBackground`, 'g'), `bg-dark-${themeVariant}-pageBackground`)
|
|
.replace(new RegExp(`bg-${themeVariant}-contentPageBackground`, 'g'), `bg-dark-${themeVariant}-contentPageBackground`)
|
|
.replace(new RegExp(`text-${themeVariant}-text`, 'g'), `text-dark-${themeVariant}-text`)
|
|
.replace(new RegExp(`border-${themeVariant}-border`, 'g'), `border-dark-${themeVariant}-border`)
|
|
.replace(new RegExp(`border-${themeVariant}-borderLight`, 'g'), `border-dark-${themeVariant}-borderLight`)
|
|
.replace(new RegExp(`border-${themeVariant}-borderStrong`, 'g'), `border-dark-${themeVariant}-borderStrong`);
|
|
}
|
|
|
|
// Speichere das Ergebnis im Cache
|
|
classCache.set(cacheKey, classes);
|
|
|
|
return classes;
|
|
}, [themeVariant, isDark]);
|
|
|
|
// Toggle-Funktion für das Theme
|
|
const toggleTheme = useCallback(() => {
|
|
const newMode = isDark ? 'light' : 'dark';
|
|
setColorMode(newMode);
|
|
}, [isDark]);
|
|
|
|
// Kontext-Wert mit Memoization für Performance
|
|
const contextValue = useMemo(() => ({
|
|
isDark,
|
|
colorMode,
|
|
setColorMode,
|
|
themeVariant,
|
|
setThemeVariant,
|
|
tw,
|
|
colors: currentColors,
|
|
}), [isDark, colorMode, themeVariant, tw, currentColors]);
|
|
|
|
// Update-Kontext-Wert
|
|
const updateContextValue = useMemo(() => ({
|
|
toggleTheme,
|
|
setColorMode,
|
|
setThemeVariant,
|
|
}), [toggleTheme, setColorMode, setThemeVariant]);
|
|
|
|
// Zeige nichts während der Initialisierung
|
|
if (!isInitialized) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<ThemeContext.Provider value={contextValue}>
|
|
<ThemeUpdateContext.Provider value={updateContextValue}>
|
|
{children}
|
|
</ThemeUpdateContext.Provider>
|
|
</ThemeContext.Provider>
|
|
);
|
|
};
|
|
|
|
// Hook für den Zugriff auf das Theme
|
|
export const useTheme = () => useContext(ThemeContext);
|
|
|
|
// Hook für den Zugriff auf die Theme-Update-Funktionen
|
|
export const useThemeUpdate = () => useContext(ThemeUpdateContext);
|