feat: dark / light theme almost complete

This commit is contained in:
Eric Gullickson
2025-12-25 20:32:38 -06:00
parent 1fd77cd757
commit 50baec390f
18 changed files with 380 additions and 170 deletions

View File

@@ -0,0 +1,140 @@
/**
* @ai-summary Theme context for managing light/dark mode across the app
* Uses same localStorage key as useSettings for consistency
* Applies Tailwind dark class to document root
*/
import { createContext, useContext, useEffect, useMemo, useState, useCallback, ReactNode } from 'react';
import { ThemeProvider as MuiThemeProvider, Theme } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import { md3LightTheme, md3DarkTheme } from './md3Theme';
// Same storage key as useSettingsPersistence for consistency
const SETTINGS_STORAGE_KEY = 'motovaultpro-mobile-settings';
interface ThemeContextValue {
isDarkMode: boolean;
toggleDarkMode: () => void;
setDarkMode: (value: boolean) => void;
theme: Theme;
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
interface ThemeProviderProps {
children: ReactNode;
}
// Read dark mode preference from localStorage (synced with useSettings)
const getStoredDarkMode = (): boolean => {
try {
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
if (stored) {
const settings = JSON.parse(stored);
return settings.darkMode ?? false;
}
} catch {
// Ignore parse errors
}
return false;
};
// Update dark mode in localStorage while preserving other settings
const setStoredDarkMode = (darkMode: boolean): void => {
try {
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
const settings = stored ? JSON.parse(stored) : {};
settings.darkMode = darkMode;
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
} catch {
// Ignore storage errors
}
};
export const ThemeProvider = ({ children }: ThemeProviderProps) => {
const [isDarkMode, setIsDarkMode] = useState<boolean>(getStoredDarkMode);
const [isInitialized, setIsInitialized] = useState(false);
// Initialize on mount
useEffect(() => {
setIsDarkMode(getStoredDarkMode());
setIsInitialized(true);
}, []);
// Apply dark class to document root for Tailwind
useEffect(() => {
const root = document.documentElement;
if (isDarkMode) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}, [isDarkMode]);
// Listen for storage changes from other tabs/components
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === SETTINGS_STORAGE_KEY && e.newValue) {
try {
const settings = JSON.parse(e.newValue);
if (typeof settings.darkMode === 'boolean') {
setIsDarkMode(settings.darkMode);
}
} catch {
// Ignore parse errors
}
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, []);
const setDarkMode = useCallback((value: boolean) => {
setIsDarkMode(value);
setStoredDarkMode(value);
}, []);
const toggleDarkMode = useCallback(() => {
setDarkMode(!isDarkMode);
}, [isDarkMode, setDarkMode]);
const theme = useMemo(() => {
return isDarkMode ? md3DarkTheme : md3LightTheme;
}, [isDarkMode]);
const value = useMemo(
() => ({
isDarkMode,
toggleDarkMode,
setDarkMode,
theme,
}),
[isDarkMode, toggleDarkMode, setDarkMode, theme]
);
// Prevent flash of wrong theme during SSR/initial load
if (!isInitialized) {
return null;
}
return (
<ThemeContext.Provider value={value}>
<MuiThemeProvider theme={theme}>
<CssBaseline />
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextValue => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
// Re-export for convenience
export { md3LightTheme, md3DarkTheme } from './md3Theme';

View File

@@ -1,35 +1,29 @@
/**
* @ai-summary Material Design 3 theme configuration for MotoVaultPro
* Supports both light and dark modes
*/
import { createTheme, alpha } from '@mui/material/styles';
import { createTheme, alpha, ThemeOptions } from '@mui/material/styles';
// Brand color from mockup
// Brand colors
const primaryHex = '#7A212A';
export const md3Theme = createTheme({
palette: {
mode: 'light',
primary: {
main: primaryHex
},
secondary: {
main: alpha(primaryHex, 0.8)
},
background: {
default: '#F8F5F3',
paper: '#FFFFFF'
},
},
shape: {
borderRadius: 16
// Dark theme palette (Ferrari-inspired)
const nero = '#231F1C'; // Nero Daytona - page base
const avus = '#F2F3F6'; // Bianco Avus - primary text
const titanio = '#A8B8C0'; // Grigio Titanio - secondary text
// Shared theme options
const baseThemeOptions: ThemeOptions = {
shape: {
borderRadius: 16
},
typography: {
fontFamily:
'Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif',
h3: {
fontWeight: 700,
letterSpacing: -0.5
h3: {
fontWeight: 700,
letterSpacing: -0.5
},
},
components: {
@@ -37,8 +31,6 @@ export const md3Theme = createTheme({
styleOverrides: {
root: {
borderRadius: 20,
boxShadow:
'0 1px 2px rgba(16,24,40,.04), 0 4px 16px rgba(16,24,40,.06)',
},
},
},
@@ -54,8 +46,8 @@ export const md3Theme = createTheme({
textTransform: 'none',
fontWeight: 600,
paddingInline: 18,
'&:hover': {
backgroundColor: alpha(theme.palette.primary.main, 0.18)
'&:hover': {
backgroundColor: alpha(theme.palette.primary.main, 0.18)
},
}),
},
@@ -71,6 +63,47 @@ export const md3Theme = createTheme({
},
],
},
MuiBottomNavigationAction: {
styleOverrides: {
root: {
minWidth: 0,
paddingTop: 8,
paddingBottom: 8,
'&.Mui-selected': {
color: primaryHex
},
},
},
},
},
};
// Light theme
export const md3LightTheme = createTheme({
...baseThemeOptions,
palette: {
mode: 'light',
primary: { main: primaryHex },
secondary: { main: alpha(primaryHex, 0.8) },
background: {
default: '#F8F5F3',
paper: '#FFFFFF'
},
text: {
primary: '#1a1a1a',
secondary: '#666666',
},
},
components: {
...baseThemeOptions.components,
MuiCard: {
styleOverrides: {
root: {
borderRadius: 20,
boxShadow: '0 1px 2px rgba(16,24,40,.04), 0 4px 16px rgba(16,24,40,.06)',
},
},
},
MuiBottomNavigation: {
styleOverrides: {
root: {
@@ -80,17 +113,48 @@ export const md3Theme = createTheme({
},
},
},
MuiBottomNavigationAction: {
},
});
// Dark theme
export const md3DarkTheme = createTheme({
...baseThemeOptions,
palette: {
mode: 'dark',
primary: { main: primaryHex },
secondary: { main: alpha(primaryHex, 0.8) },
background: {
default: nero,
paper: '#1D1A18'
},
text: {
primary: avus,
secondary: titanio,
},
},
components: {
...baseThemeOptions.components,
MuiCard: {
styleOverrides: {
root: {
minWidth: 0,
paddingTop: 8,
paddingBottom: 8,
'&.Mui-selected': {
color: primaryHex
},
borderRadius: 20,
boxShadow: '0 1px 2px rgba(0,0,0,.2), 0 4px 16px rgba(0,0,0,.3)',
backgroundColor: 'rgba(255,255,255,0.05)',
border: '1px solid rgba(255,255,255,0.1)',
},
},
},
MuiBottomNavigation: {
styleOverrides: {
root: {
borderTop: '1px solid rgba(255,255,255,.1)',
background: alpha(nero, 0.9),
backdropFilter: 'blur(8px)',
},
},
},
},
});
});
// Default export for backward compatibility
export const md3Theme = md3LightTheme;