Readur/frontend/src/contexts/ThemeContext.tsx

271 lines
9.1 KiB
TypeScript

import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { createTheme, Theme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import { PaletteMode } from '@mui/material';
import { modernTokens } from '../theme';
interface ThemeContextType {
mode: PaletteMode;
toggleTheme: () => void;
modernTokens: typeof modernTokens;
glassEffect: (alphaValue?: number) => object;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
interface ThemeProviderProps {
children: ReactNode;
}
// Glassmorphism effect that adapts to theme mode
const createGlassEffect = (mode: PaletteMode) => (alphaValue: number = 0.1) => ({
background: mode === 'light'
? `rgba(255, 255, 255, ${alphaValue})`
: `rgba(30, 30, 30, ${alphaValue})`,
backdropFilter: 'blur(10px)',
border: mode === 'light'
? '1px solid rgba(255, 255, 255, 0.2)'
: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: mode === 'light'
? modernTokens.shadows.glass
: '0 8px 32px 0 rgba(0, 0, 0, 0.37)',
});
const createAppTheme = (mode: PaletteMode): Theme => {
return createTheme({
palette: {
mode,
primary: {
main: modernTokens.colors.primary[500],
light: modernTokens.colors.primary[300],
dark: modernTokens.colors.primary[700],
50: modernTokens.colors.primary[50],
100: modernTokens.colors.primary[100],
200: modernTokens.colors.primary[200],
300: modernTokens.colors.primary[300],
400: modernTokens.colors.primary[400],
500: modernTokens.colors.primary[500],
600: modernTokens.colors.primary[600],
700: modernTokens.colors.primary[700],
800: modernTokens.colors.primary[800],
900: modernTokens.colors.primary[900],
},
secondary: {
main: modernTokens.colors.secondary[500],
light: modernTokens.colors.secondary[300],
dark: modernTokens.colors.secondary[700],
50: modernTokens.colors.secondary[50],
100: modernTokens.colors.secondary[100],
200: modernTokens.colors.secondary[200],
300: modernTokens.colors.secondary[300],
400: modernTokens.colors.secondary[400],
500: modernTokens.colors.secondary[500],
600: modernTokens.colors.secondary[600],
700: modernTokens.colors.secondary[700],
800: modernTokens.colors.secondary[800],
900: modernTokens.colors.secondary[900],
},
background: {
default: mode === 'light' ? '#fafafa' : '#121212',
paper: mode === 'light' ? '#ffffff' : '#1e1e1e',
},
text: {
primary: mode === 'light' ? '#333333' : '#f8fafc',
secondary: mode === 'light' ? '#666666' : '#cbd5e1',
},
success: {
main: modernTokens.colors.success[500],
light: modernTokens.colors.success[50],
dark: modernTokens.colors.success[600],
50: modernTokens.colors.success[50],
100: mode === 'light' ? '#dcfce7' : '#14532d',
200: mode === 'light' ? '#bbf7d0' : '#166534',
300: mode === 'light' ? '#86efac' : '#15803d',
400: mode === 'light' ? '#4ade80' : '#16a34a',
500: modernTokens.colors.success[500],
600: modernTokens.colors.success[600],
700: mode === 'light' ? '#15803d' : '#4ade80',
800: mode === 'light' ? '#166534' : '#86efac',
900: mode === 'light' ? '#14532d' : '#dcfce7',
},
warning: {
main: modernTokens.colors.warning[500],
light: modernTokens.colors.warning[50],
dark: modernTokens.colors.warning[600],
50: modernTokens.colors.warning[50],
100: mode === 'light' ? '#fef3c7' : '#78350f',
200: mode === 'light' ? '#fde68a' : '#92400e',
300: mode === 'light' ? '#fcd34d' : '#b45309',
400: mode === 'light' ? '#fbbf24' : '#d97706',
500: modernTokens.colors.warning[500],
600: modernTokens.colors.warning[600],
700: mode === 'light' ? '#b45309' : '#fbbf24',
800: mode === 'light' ? '#92400e' : '#fcd34d',
900: mode === 'light' ? '#78350f' : '#fef3c7',
},
error: {
main: modernTokens.colors.error[500],
light: modernTokens.colors.error[50],
dark: modernTokens.colors.error[600],
50: modernTokens.colors.error[50],
100: mode === 'light' ? '#fee2e2' : '#7f1d1d',
200: mode === 'light' ? '#fecaca' : '#991b1b',
300: mode === 'light' ? '#fca5a5' : '#b91c1c',
400: mode === 'light' ? '#f87171' : '#dc2626',
500: modernTokens.colors.error[500],
600: modernTokens.colors.error[600],
700: mode === 'light' ? '#b91c1c' : '#f87171',
800: mode === 'light' ? '#991b1b' : '#fca5a5',
900: mode === 'light' ? '#7f1d1d' : '#fee2e2',
},
info: {
main: modernTokens.colors.info[500],
light: modernTokens.colors.info[50],
dark: modernTokens.colors.info[600],
50: modernTokens.colors.info[50],
100: mode === 'light' ? '#dbeafe' : '#1e3a8a',
200: mode === 'light' ? '#bfdbfe' : '#1e40af',
300: mode === 'light' ? '#93c5fd' : '#1d4ed8',
400: mode === 'light' ? '#60a5fa' : '#2563eb',
500: modernTokens.colors.info[500],
600: modernTokens.colors.info[600],
700: mode === 'light' ? '#1d4ed8' : '#60a5fa',
800: mode === 'light' ? '#1e40af' : '#93c5fd',
900: mode === 'light' ? '#1e3a8a' : '#dbeafe',
},
divider: mode === 'light' ? 'rgba(0, 0, 0, 0.12)' : 'rgba(255, 255, 255, 0.12)',
},
typography: {
fontFamily: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'sans-serif',
].join(','),
h4: {
fontWeight: 600,
},
h5: {
fontWeight: 600,
},
h6: {
fontWeight: 600,
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
borderRadius: 8,
},
},
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: 12,
boxShadow: mode === 'light'
? '0 2px 8px rgba(0,0,0,0.1)'
: '0 2px 8px rgba(0,0,0,0.3)',
backgroundColor: mode === 'light' ? '#ffffff' : '#1e1e1e',
},
},
},
MuiPaper: {
styleOverrides: {
root: {
borderRadius: 8,
backgroundColor: mode === 'light' ? '#ffffff' : '#1e1e1e',
},
},
},
MuiAppBar: {
styleOverrides: {
root: {
backgroundColor: mode === 'light'
? 'rgba(255, 255, 255, 0.95)'
: 'rgba(30, 30, 30, 0.95)',
backdropFilter: 'blur(20px)',
},
},
},
MuiDrawer: {
styleOverrides: {
paper: {
backgroundColor: mode === 'light' ? '#ffffff' : '#1e1e1e',
borderRight: mode === 'light'
? '1px solid rgba(0, 0, 0, 0.12)'
: '1px solid rgba(255, 255, 255, 0.12)',
},
},
},
MuiTextField: {
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)',
},
'&:hover fieldset': {
borderColor: mode === 'light' ? 'rgba(0, 0, 0, 0.87)' : 'rgba(255, 255, 255, 0.87)',
},
},
},
},
},
},
});
};
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [mode, setMode] = useState<PaletteMode>(() => {
const savedMode = localStorage.getItem('themeMode');
if (savedMode === 'light' || savedMode === 'dark') {
return savedMode;
}
// Default to system preference or light mode
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
const toggleTheme = () => {
const newMode = mode === 'light' ? 'dark' : 'light';
setMode(newMode);
localStorage.setItem('themeMode', newMode);
};
const theme = createAppTheme(mode);
const glassEffect = createGlassEffect(mode);
// Listen for system theme changes
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
// Only update if user hasn't manually set a preference
if (!localStorage.getItem('themeMode')) {
setMode(e.matches ? 'dark' : 'light');
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
return (
<ThemeContext.Provider value={{ mode, toggleTheme, modernTokens, glassEffect }}>
<MuiThemeProvider theme={theme}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
);
};