@frank-auth/react
Version:
Flexible and customizable React UI components for Frank Authentication
397 lines (346 loc) • 11.6 kB
text/typescript
/**
* @frank-auth/react - Theme Configuration
*
* Advanced theme management system with support for light/dark modes,
* custom color palettes, and HeroUI integration.
*/
import {type BrandingConfig, type Theme, type ThemeMode, ThemeUtils,} from './types';
import {DEFAULT_COLOR_PALETTE, DEFAULT_THEME_CONFIG,} from './defaults';
import {generateColorPalette} from '../utils';
import type {ThemeColors} from '../types';
// ============================================================================
// Theme Utilities
// ============================================================================
/**
* Creates a dark theme variant from a light theme
*/
export function createDarkTheme(lightTheme: Theme): Theme {
const darkColors: ThemeColors = {
...lightTheme.colors,
// Invert background colors
background: '#0f172a',
foreground: '#f8fafc',
card: '#1e293b',
cardForeground: '#f8fafc',
popover: '#1e293b',
popoverForeground: '#f8fafc',
muted: '#334155',
mutedForeground: '#94a3b8',
accent: '#334155',
accentForeground: '#f8fafc',
border: '#334155',
input: '#334155',
ring: lightTheme.palette.primary.DEFAULT,
};
return {
...lightTheme,
mode: 'dark',
colors: darkColors,
palette: {
...lightTheme.palette,
// Adjust primary colors for dark mode
primary: {
...lightTheme.palette.primary,
DEFAULT: lightTheme.colors.primary[400],
foreground: '#000000',
},
// Adjust secondary colors for dark mode
secondary: {
...lightTheme.palette.secondary,
DEFAULT: lightTheme.colors.secondary[400],
foreground: '#000000',
},
}
};
}
// ============================================================================
// Predefined Themes
// ============================================================================
/**
* Blue theme (default)
*/
export const BLUE_THEME: Theme = {
...DEFAULT_THEME_CONFIG,
colors: {
...DEFAULT_THEME_CONFIG.colors,
primary: generateColorPalette('#3b82f6'),
},
palette: {
...DEFAULT_COLOR_PALETTE,
primary: generateColorPalette('#3b82f6'),
},
};
/**
* Purple theme
*/
export const PURPLE_THEME: Theme = {
...DEFAULT_THEME_CONFIG,
colors: {
...DEFAULT_THEME_CONFIG.colors,
primary: generateColorPalette('#8b5cf6'),
secondary: generateColorPalette('#6366f1'),
},
palette: {
...DEFAULT_COLOR_PALETTE,
primary: generateColorPalette('#8b5cf6'),
secondary: generateColorPalette('#6366f1'),
},
};
/**
* Green theme
*/
export const GREEN_THEME: Theme = {
...DEFAULT_THEME_CONFIG,
colors: {
...DEFAULT_THEME_CONFIG.colors,
primary: generateColorPalette('#10b981'),
secondary: generateColorPalette('#059669'),
},
palette: {
...DEFAULT_COLOR_PALETTE,
primary: generateColorPalette('#10b981'),
secondary: generateColorPalette('#059669'),
},
};
/**
* Orange theme
*/
export const ORANGE_THEME: Theme = {
...DEFAULT_THEME_CONFIG,
colors: {
...DEFAULT_THEME_CONFIG.colors,
primary: generateColorPalette('#f97316').DEFAULT,
secondary: generateColorPalette('#ea580c').DEFAULT,
},
palette: {
...DEFAULT_COLOR_PALETTE,
primary: generateColorPalette('#f97316'),
secondary: generateColorPalette('#ea580c'),
},
};
/**
* Pink theme
*/
export const PINK_THEME: Theme = {
...DEFAULT_THEME_CONFIG,
colors: {
...DEFAULT_THEME_CONFIG.colors,
primary: generateColorPalette('#ec4899').DEFAULT,
secondary: generateColorPalette('#db2777').DEFAULT,
},
palette: {
...DEFAULT_COLOR_PALETTE,
primary: generateColorPalette('#ec4899'),
secondary: generateColorPalette('#db2777'),
},
};
/**
* Available theme presets
*/
export const THEME_PRESETS = {
blue: BLUE_THEME,
purple: PURPLE_THEME,
green: GREEN_THEME,
orange: ORANGE_THEME,
pink: PINK_THEME,
} as const;
export type ThemePreset = keyof typeof THEME_PRESETS;
// ============================================================================
// Theme Manager Class
// ============================================================================
export class ThemeManager {
private currentTheme: Theme;
private systemPreference: ThemeMode;
private listeners: Set<(theme: Theme) => void> = new Set();
constructor(initialTheme?: Partial<Theme>) {
this.currentTheme = this.mergeTheme(DEFAULT_THEME_CONFIG, initialTheme);
this.systemPreference = this.detectSystemPreference();
// Listen for system theme changes
if (typeof window !== 'undefined') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => {
this.systemPreference = this.detectSystemPreference();
this.updateTheme();
});
}
}
/**
* Get current theme
*/
getTheme(): Theme {
return { ...this.currentTheme };
}
/**
* Set theme configuration
*/
setTheme(theme: Partial<Theme>): void {
this.currentTheme = this.mergeTheme(this.currentTheme, theme);
this.updateTheme();
}
/**
* Apply a theme preset
*/
applyPreset(preset: ThemePreset): void {
this.currentTheme = { ...THEME_PRESETS[preset] };
this.updateTheme();
}
/**
* Set theme mode
*/
setMode(mode: ThemeMode): void {
this.currentTheme.mode = mode;
this.updateTheme();
}
/**
* Get effective theme mode (resolves 'system' to 'light' or 'dark')
*/
getEffectiveMode(): 'light' | 'dark' {
if (this.currentTheme.mode === 'system') {
return this.systemPreference === 'dark' ? 'dark' : 'light';
}
return this.currentTheme.mode === 'dark' ? 'dark' : 'light';
}
/**
* Apply branding colors to theme
*/
applyBranding(branding: BrandingConfig): void {
if (branding.colors?.primary) {
this.currentTheme.palette.primary = generateColorPalette(branding.colors.primary);
}
if (branding.colors?.secondary) {
this.currentTheme.palette.secondary = generateColorPalette(branding.colors.secondary);
}
if (branding.fonts?.primary) {
this.currentTheme.typography.fontFamily.sans = [
branding.fonts.primary,
...this.currentTheme.typography.fontFamily.sans,
];
}
this.updateTheme();
}
/**
* Generate CSS variables for the current theme
*/
generateCSSVariables(): Record<string, string> {
const theme = this.getResolvedTheme();
return ThemeUtils.generateCSSVariables(theme);
}
/**
* Apply theme to DOM
*/
applyToDOM(): void {
if (typeof document === 'undefined') return;
const variables = this.generateCSSVariables();
const root = document.documentElement;
// Apply CSS variables
Object.entries(variables).forEach(([property, value]) => {
root.style.setProperty(property, value);
});
// Apply theme class
const effectiveMode = this.getEffectiveMode();
root.classList.remove('light', 'dark');
root.classList.add(effectiveMode);
}
/**
* Subscribe to theme changes
*/
subscribe(callback: (theme: Theme) => void): () => void {
this.listeners.add(callback);
return () => {
this.listeners.delete(callback);
};
}
// Private methods
private detectSystemPreference(): ThemeMode {
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
private mergeTheme(base: Theme, override?: Partial<Theme>): Theme {
if (!override) return { ...base };
return {
...base,
...override,
colors: { ...base.colors, ...override.colors },
typography: {
...base.typography,
...override.typography,
fontFamily: { ...base.typography.fontFamily, ...override.typography?.fontFamily },
fontSize: { ...base.typography.fontSize, ...override.typography?.fontSize },
fontWeight: { ...base.typography.fontWeight, ...override.typography?.fontWeight },
},
spacing: { ...base.spacing, ...override.spacing },
borderRadius: { ...base.borderRadius, ...override.borderRadius },
shadows: { ...base.shadows, ...override.shadows },
animations: {
...base.animations,
...override.animations,
duration: { ...base.animations.duration, ...override.animations?.duration },
timingFunction: { ...base.animations.timingFunction, ...override.animations?.timingFunction },
keyframes: { ...base.animations.keyframes, ...override.animations?.keyframes },
},
};
}
private getResolvedTheme(): Theme {
const effectiveMode = this.getEffectiveMode();
if (effectiveMode === 'dark' && this.currentTheme.mode !== 'dark') {
return createDarkTheme(this.currentTheme);
}
return this.currentTheme;
}
private updateTheme(): void {
this.applyToDOM();
this.listeners.forEach(callback => callback(this.currentTheme));
}
}
// ============================================================================
// Theme Hooks and Utilities
// ============================================================================
/**
* Create a theme manager instance
*/
export function createThemeManager(initialTheme?: Partial<Theme>): ThemeManager {
return new ThemeManager(initialTheme);
}
/**
* Get theme CSS for server-side rendering
*/
export function getThemeCSS(theme: Theme): string {
const manager = new ThemeManager(theme);
const variables = manager.generateCSSVariables();
return `:root {
${Object.entries(variables)
.map(([property, value]) => `${property}: ${value};`)
.join('\n ')}
}`;
}
/**
* Validate theme configuration
*/
export function validateTheme(theme: Partial<Theme>): boolean {
try {
// Basic validation - ensure required color properties exist
if (theme.colors) {
const requiredColors = ['primary', 'secondary', 'background', 'foreground'];
for (const color of requiredColors) {
if (!(color in theme.colors)) {
return false;
}
}
}
// Validate typography if provided
if (theme.typography) {
if (theme.typography.fontFamily && !Array.isArray(theme.typography.fontFamily.sans)) {
return false;
}
}
return true;
} catch {
return false;
}
}
// ============================================================================
// Export theme utilities
// ============================================================================
export {
DEFAULT_THEME_CONFIG,
};