feat: Implement dual theme system with purple/pink and high contrast modes
Added comprehensive theme switching system with two color palettes: **New Default Theme: Purple/Pink Gradient** - Primary: #8b52ff (vibrant purple) → #d194e6 (light purple) - Secondary: #ff7094 (pink) → #ffb3e4 (light pink) - Modern, energetic gradient aesthetic - Created purpleTheme.ts with full MUI theme configuration **High Contrast Theme: Warm Peach (Original)** - Renamed maternalTheme to highContrastTheme - Larger text sizes (7.5% increase for accessibility) - Stronger shadows and bolder fonts - Optimized for readability and accessibility **Theme Infrastructure:** 1. **ThemeContext** (contexts/ThemeContext.tsx) - Created theme context provider with useThemeContext hook - Theme modes: 'standard' (purple) | 'highContrast' (peach) - localStorage persistence with key: maternal_theme_preference - Prevents flash of wrong theme on load 2. **ThemeRegistry Updates** (components/ThemeRegistry.tsx) - Wrapped with ThemeContextProvider - Dynamic theme switching via useThemeContext - Maintains backward compatibility 3. **Settings Page Toggle** (app/settings/page.tsx) - Added theme switcher in Preferences section - Switch control with descriptive labels - Explains High Contrast benefits - Instant theme switching without reload 4. **CSS Variables** (app/globals.css) - Added --color-1 through --color-5 for purple gradient - Added --warm-1 through --warm-5 for peach palette - Available for custom styling **Features:** ✅ Instant theme switching (no page reload) ✅ Persistent preference across sessions ✅ Accessibility-focused high contrast mode ✅ Modern purple/pink gradient as default ✅ Smooth transitions between themes ✅ All existing components work with both themes **Files Created:** - contexts/ThemeContext.tsx - styles/themes/purpleTheme.ts - styles/themes/highContrastTheme.ts **Files Modified:** - components/ThemeRegistry.tsx - app/settings/page.tsx - app/globals.css Resolves: High Priority Feature #12 - Secondary Color Palette & Accessibility Toggle 🎉 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,20 @@
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
/* Purple/Pink Gradient Theme (Default) */
|
||||
--color-1: #8b52ff;
|
||||
--color-2: #d194e6;
|
||||
--color-3: #f2a6f2;
|
||||
--color-4: #ffb3e4;
|
||||
--color-5: #ff7094;
|
||||
|
||||
/* Warm Peach Theme (High Contrast) */
|
||||
--warm-1: #FFB6C1;
|
||||
--warm-2: #FFDAB9;
|
||||
--warm-3: #FFE4E1;
|
||||
--warm-4: #FFD4CC;
|
||||
--warm-5: #FF8B7D;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
@@ -20,10 +20,12 @@ import { TimeFormatSelector } from '@/components/settings/TimeFormatSelector';
|
||||
import { PhotoUpload } from '@/components/common/PhotoUpload';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useThemeContext } from '@/contexts/ThemeContext';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { t } = useTranslation('settings');
|
||||
const { user, logout, refreshUser } = useAuth();
|
||||
const { themeMode, setThemeMode } = useThemeContext();
|
||||
const [name, setName] = useState(user?.name || '');
|
||||
const [photoUrl, setPhotoUrl] = useState(user?.photoUrl || '');
|
||||
const [timezone, setTimezone] = useState(user?.timezone || 'UTC');
|
||||
@@ -194,6 +196,25 @@ export default function SettingsPage() {
|
||||
<TimeZoneSelector value={timezone} onChange={setTimezone} />
|
||||
<Divider />
|
||||
<TimeFormatSelector value={timeFormat} onChange={setTimeFormat} />
|
||||
<Divider />
|
||||
<Box>
|
||||
<Typography variant="subtitle1" fontWeight="600" gutterBottom>
|
||||
Theme Mode
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
High Contrast mode uses larger text and stronger colors for better readability
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={themeMode === 'highContrast'}
|
||||
onChange={(e) => setThemeMode(e.target.checked ? 'highContrast' : 'standard')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
}
|
||||
label={themeMode === 'highContrast' ? 'High Contrast Mode' : 'Standard Mode'}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -3,19 +3,31 @@
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
|
||||
import { maternalTheme } from '@/styles/themes/maternalTheme';
|
||||
import { AuthProvider } from '@/lib/auth/AuthContext';
|
||||
import { ThemeContextProvider, useThemeContext } from '@/contexts/ThemeContext';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
function ThemeProviderWrapper({ children }: { children: ReactNode }) {
|
||||
const { currentTheme } = useThemeContext();
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={currentTheme}>
|
||||
<CssBaseline />
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeRegistry({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<AppRouterCacheProvider>
|
||||
<ThemeProvider theme={maternalTheme}>
|
||||
<CssBaseline />
|
||||
<AuthProvider>
|
||||
<ThemeContextProvider>
|
||||
<ThemeProviderWrapper>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</ThemeProviderWrapper>
|
||||
</ThemeContextProvider>
|
||||
</AppRouterCacheProvider>
|
||||
);
|
||||
}
|
||||
|
||||
65
maternal-web/contexts/ThemeContext.tsx
Normal file
65
maternal-web/contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import { purpleTheme } from '@/styles/themes/purpleTheme';
|
||||
import { highContrastTheme } from '@/styles/themes/highContrastTheme';
|
||||
|
||||
export type ThemeMode = 'standard' | 'highContrast';
|
||||
|
||||
interface ThemeContextType {
|
||||
themeMode: ThemeMode;
|
||||
toggleTheme: () => void;
|
||||
setThemeMode: (mode: ThemeMode) => void;
|
||||
currentTheme: Theme;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
const THEME_STORAGE_KEY = 'maternal_theme_preference';
|
||||
|
||||
export function ThemeContextProvider({ children }: { children: ReactNode }) {
|
||||
const [themeMode, setThemeModeState] = useState<ThemeMode>('standard');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Load theme preference from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY) as ThemeMode | null;
|
||||
if (savedTheme === 'highContrast' || savedTheme === 'standard') {
|
||||
setThemeModeState(savedTheme);
|
||||
}
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Save theme preference to localStorage when it changes
|
||||
const setThemeMode = (mode: ThemeMode) => {
|
||||
setThemeModeState(mode);
|
||||
localStorage.setItem(THEME_STORAGE_KEY, mode);
|
||||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newMode = themeMode === 'standard' ? 'highContrast' : 'standard';
|
||||
setThemeMode(newMode);
|
||||
};
|
||||
|
||||
const currentTheme = themeMode === 'highContrast' ? highContrastTheme : purpleTheme;
|
||||
|
||||
// Prevent flash of wrong theme
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ themeMode, toggleTheme, setThemeMode, currentTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useThemeContext() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useThemeContext must be used within a ThemeContextProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
127
maternal-web/styles/themes/highContrastTheme.ts
Normal file
127
maternal-web/styles/themes/highContrastTheme.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
// High Contrast Warm Peach theme - Original theme, now accessibility option
|
||||
export const highContrastTheme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#FFB6C1', // Light pink/rose
|
||||
light: '#FFE4E1', // Misty rose
|
||||
dark: '#DB7093', // Pale violet red
|
||||
},
|
||||
secondary: {
|
||||
main: '#FFDAB9', // Peach puff
|
||||
light: '#FFE5CC',
|
||||
dark: '#FFB347', // Deep peach
|
||||
},
|
||||
background: {
|
||||
default: '#FFF9F5', // Warm white
|
||||
paper: '#FFFFFF',
|
||||
},
|
||||
text: {
|
||||
primary: '#2D3748', // Dark gray - 4.5:1+ contrast
|
||||
secondary: '#4A5568', // Darker gray for better contrast (7:1+ on white)
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
h1: {
|
||||
fontSize: '2.15rem', // 7.5% larger for accessibility
|
||||
fontWeight: 600,
|
||||
},
|
||||
h2: {
|
||||
fontSize: '1.88rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h3: {
|
||||
fontSize: '1.61rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h4: {
|
||||
fontSize: '1.34rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h5: {
|
||||
fontSize: '1.21rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h6: {
|
||||
fontSize: '1.07rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
body1: {
|
||||
fontSize: '1.07rem', // Slightly larger for readability
|
||||
},
|
||||
body2: {
|
||||
fontSize: '0.94rem',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 16,
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 24,
|
||||
textTransform: 'none',
|
||||
minHeight: 48, // Touch target size
|
||||
fontSize: '1.07rem', // Larger text
|
||||
fontWeight: 600, // Bolder text for high contrast
|
||||
paddingLeft: 24,
|
||||
paddingRight: 24,
|
||||
},
|
||||
sizeLarge: {
|
||||
minHeight: 56,
|
||||
fontSize: '1.21rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiInputBase-root': {
|
||||
minHeight: 48,
|
||||
borderRadius: 16,
|
||||
},
|
||||
'& .MuiInputBase-input': {
|
||||
fontSize: '1.07rem', // Larger input text
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 20,
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.12)', // Stronger shadow for contrast
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 16,
|
||||
},
|
||||
elevation1: {
|
||||
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
elevation2: {
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.12)',
|
||||
},
|
||||
elevation3: {
|
||||
boxShadow: '0 6px 30px rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 12,
|
||||
fontWeight: 600, // Bolder for high contrast
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
144
maternal-web/styles/themes/purpleTheme.ts
Normal file
144
maternal-web/styles/themes/purpleTheme.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
'use client';
|
||||
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
// Purple/Pink gradient theme - New default theme
|
||||
export const purpleTheme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#8b52ff', // Vibrant purple
|
||||
light: '#d194e6', // Light purple
|
||||
dark: '#6b3cc9', // Dark purple
|
||||
},
|
||||
secondary: {
|
||||
main: '#ff7094', // Pink
|
||||
light: '#ffb3e4', // Light pink
|
||||
dark: '#e6537a', // Dark pink
|
||||
},
|
||||
background: {
|
||||
default: '#faf8ff', // Very light purple tint
|
||||
paper: '#ffffff',
|
||||
},
|
||||
text: {
|
||||
primary: '#2D3748', // Dark gray - 4.5:1+ contrast
|
||||
secondary: '#4A5568', // Darker gray for better contrast
|
||||
},
|
||||
success: {
|
||||
main: '#10b981',
|
||||
light: '#6ee7b7',
|
||||
dark: '#047857',
|
||||
},
|
||||
warning: {
|
||||
main: '#f59e0b',
|
||||
light: '#fbbf24',
|
||||
dark: '#d97706',
|
||||
},
|
||||
error: {
|
||||
main: '#ef4444',
|
||||
light: '#fca5a5',
|
||||
dark: '#dc2626',
|
||||
},
|
||||
info: {
|
||||
main: '#3b82f6',
|
||||
light: '#93c5fd',
|
||||
dark: '#2563eb',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
h1: {
|
||||
fontSize: '2rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h2: {
|
||||
fontSize: '1.75rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h3: {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h4: {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h5: {
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h6: {
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
body1: {
|
||||
fontSize: '1rem',
|
||||
},
|
||||
body2: {
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 16,
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 24,
|
||||
textTransform: 'none',
|
||||
minHeight: 48, // Touch target size
|
||||
fontSize: '1rem',
|
||||
fontWeight: 500,
|
||||
paddingLeft: 24,
|
||||
paddingRight: 24,
|
||||
},
|
||||
sizeLarge: {
|
||||
minHeight: 56,
|
||||
fontSize: '1.125rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiInputBase-root': {
|
||||
minHeight: 48,
|
||||
borderRadius: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 20,
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 16,
|
||||
},
|
||||
elevation1: {
|
||||
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
elevation2: {
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
||||
},
|
||||
elevation3: {
|
||||
boxShadow: '0 6px 30px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 12,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user