feat: implement comprehensive theming system

- Add ThemeContext with support for dark, gray, light, and custom themes
- Create theme switching UI in Settings with theme selector
- Add custom color editor for custom theme mode
- Update styles.css with theme-specific CSS variables
- Add theme storage API methods for persistence
- Update syntax highlighting to match selected theme
- Wrap App with ThemeProvider for global theme access

The theming system allows users to switch between predefined themes
or create their own custom theme with live color editing.
This commit is contained in:
Mufeed VH
2025-07-28 15:05:46 +05:30
parent 4ddb6a1995
commit c87d36e118
10 changed files with 809 additions and 192 deletions

View File

@@ -0,0 +1,182 @@
import React, { createContext, useState, useContext, useCallback, useEffect } from 'react';
import { api } from '../lib/api';
export type ThemeMode = 'dark' | 'gray' | 'light' | 'custom';
export interface CustomThemeColors {
background: string;
foreground: string;
card: string;
cardForeground: string;
primary: string;
primaryForeground: string;
secondary: string;
secondaryForeground: string;
muted: string;
mutedForeground: string;
accent: string;
accentForeground: string;
destructive: string;
destructiveForeground: string;
border: string;
input: string;
ring: string;
}
interface ThemeContextType {
theme: ThemeMode;
customColors: CustomThemeColors;
setTheme: (theme: ThemeMode) => Promise<void>;
setCustomColors: (colors: Partial<CustomThemeColors>) => Promise<void>;
isLoading: boolean;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const THEME_STORAGE_KEY = 'theme_preference';
const CUSTOM_COLORS_STORAGE_KEY = 'theme_custom_colors';
// Default custom theme colors (based on current dark theme)
const DEFAULT_CUSTOM_COLORS: CustomThemeColors = {
background: 'oklch(0.12 0.01 240)',
foreground: 'oklch(0.98 0.01 240)',
card: 'oklch(0.14 0.01 240)',
cardForeground: 'oklch(0.98 0.01 240)',
primary: 'oklch(0.98 0.01 240)',
primaryForeground: 'oklch(0.12 0.01 240)',
secondary: 'oklch(0.16 0.01 240)',
secondaryForeground: 'oklch(0.98 0.01 240)',
muted: 'oklch(0.16 0.01 240)',
mutedForeground: 'oklch(0.65 0.01 240)',
accent: 'oklch(0.16 0.01 240)',
accentForeground: 'oklch(0.98 0.01 240)',
destructive: 'oklch(0.6 0.2 25)',
destructiveForeground: 'oklch(0.98 0.01 240)',
border: 'oklch(0.16 0.01 240)',
input: 'oklch(0.16 0.01 240)',
ring: 'oklch(0.98 0.01 240)',
};
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setThemeState] = useState<ThemeMode>('dark');
const [customColors, setCustomColorsState] = useState<CustomThemeColors>(DEFAULT_CUSTOM_COLORS);
const [isLoading, setIsLoading] = useState(true);
// Load theme preference and custom colors from storage
useEffect(() => {
const loadTheme = async () => {
try {
// Load theme preference
const savedTheme = await api.getSetting(THEME_STORAGE_KEY);
if (savedTheme) {
const themeMode = savedTheme as ThemeMode;
setThemeState(themeMode);
applyTheme(themeMode, customColors);
}
// Load custom colors
const savedColors = await api.getSetting(CUSTOM_COLORS_STORAGE_KEY);
if (savedColors) {
const colors = JSON.parse(savedColors) as CustomThemeColors;
setCustomColorsState(colors);
if (theme === 'custom') {
applyTheme('custom', colors);
}
}
} catch (error) {
console.error('Failed to load theme settings:', error);
} finally {
setIsLoading(false);
}
};
loadTheme();
}, []);
// Apply theme to document
const applyTheme = useCallback((themeMode: ThemeMode, colors: CustomThemeColors) => {
const root = document.documentElement;
// Remove all theme classes
root.classList.remove('theme-dark', 'theme-gray', 'theme-light', 'theme-custom');
// Add new theme class
root.classList.add(`theme-${themeMode}`);
// If custom theme, apply custom colors as CSS variables
if (themeMode === 'custom') {
Object.entries(colors).forEach(([key, value]) => {
const cssVarName = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
root.style.setProperty(cssVarName, value);
});
} else {
// Clear custom CSS variables when not using custom theme
Object.keys(colors).forEach((key) => {
const cssVarName = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
root.style.removeProperty(cssVarName);
});
}
}, []);
const setTheme = useCallback(async (newTheme: ThemeMode) => {
try {
setIsLoading(true);
// Apply theme immediately
setThemeState(newTheme);
applyTheme(newTheme, customColors);
// Save to storage
await api.saveSetting(THEME_STORAGE_KEY, newTheme);
} catch (error) {
console.error('Failed to save theme preference:', error);
} finally {
setIsLoading(false);
}
}, [customColors, applyTheme]);
const setCustomColors = useCallback(async (colors: Partial<CustomThemeColors>) => {
try {
setIsLoading(true);
const newColors = { ...customColors, ...colors };
setCustomColorsState(newColors);
// Apply immediately if custom theme is active
if (theme === 'custom') {
applyTheme('custom', newColors);
}
// Save to storage
await api.saveSetting(CUSTOM_COLORS_STORAGE_KEY, JSON.stringify(newColors));
} catch (error) {
console.error('Failed to save custom colors:', error);
} finally {
setIsLoading(false);
}
}, [theme, customColors, applyTheme]);
const value: ThemeContextType = {
theme,
customColors,
setTheme,
setCustomColors,
isLoading,
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
export const useThemeContext = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useThemeContext must be used within a ThemeProvider');
}
return context;
};