canoejs
Version:
A lightweight, widget-based UI framework
231 lines (215 loc) • 5.31 kB
text/typescript
import { Canoe } from "../../canoe";
export interface Theme {
name: string;
colors: {
primary: string;
secondary: string;
success: string;
danger: string;
warning: string;
info: string;
light: string;
dark: string;
white: string;
black: string;
gray: {
50: string;
100: string;
200: string;
300: string;
400: string;
500: string;
600: string;
700: string;
800: string;
900: string;
};
};
typography: {
fontFamily: string;
fontSize: {
xs: string;
sm: string;
base: string;
lg: string;
xl: string;
'2xl': string;
'3xl': string;
'4xl': string;
'5xl': string;
};
fontWeight: {
light: string;
normal: string;
medium: string;
semibold: string;
bold: string;
};
};
spacing: {
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
'2xl': string;
'3xl': string;
};
borderRadius: {
none: string;
sm: string;
md: string;
lg: string;
xl: string;
full: string;
};
shadows: {
sm: string;
md: string;
lg: string;
xl: string;
};
}
export const defaultTheme: Theme = {
name: 'default',
colors: {
primary: '#3b82f6',
secondary: '#6b7280',
success: '#10b981',
danger: '#ef4444',
warning: '#f59e0b',
info: '#06b6d4',
light: '#f8fafc',
dark: '#1e293b',
white: '#ffffff',
black: '#000000',
gray: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
},
},
typography: {
fontFamily: 'system-ui, -apple-system, sans-serif',
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
'5xl': '3rem',
},
fontWeight: {
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
'2xl': '3rem',
'3xl': '4rem',
},
borderRadius: {
none: '0',
sm: '0.125rem',
md: '0.375rem',
lg: '0.5rem',
xl: '0.75rem',
full: '9999px',
},
shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
},
};
export const darkTheme: Theme = {
...defaultTheme,
name: 'dark',
colors: {
...defaultTheme.colors,
primary: '#60a5fa',
secondary: '#9ca3af',
light: '#1e293b',
dark: '#f8fafc',
white: '#000000',
black: '#ffffff',
},
};
export class ThemeProvider {
private static currentTheme: Theme = defaultTheme;
private static themeChangeCallbacks: ((theme: Theme) => void)[] = [];
static getTheme(): Theme {
return this.currentTheme;
}
static setTheme(theme: Theme): void {
this.currentTheme = theme;
this.applyTheme(theme);
this.themeChangeCallbacks.forEach(callback => callback(theme));
}
static onThemeChange(callback: (theme: Theme) => void): void {
this.themeChangeCallbacks.push(callback);
}
static applyTheme(theme: Theme): void {
const root = document.documentElement;
// Apply CSS custom properties
Object.entries(theme.colors).forEach(([key, value]) => {
if (typeof value === 'object') {
Object.entries(value).forEach(([subKey, subValue]) => {
root.style.setProperty(`--color-${key}-${subKey}`, subValue);
});
} else {
root.style.setProperty(`--color-${key}`, value);
}
});
Object.entries(theme.typography).forEach(([key, value]) => {
if (typeof value === 'object') {
Object.entries(value).forEach(([subKey, subValue]) => {
root.style.setProperty(`--font-${key}-${subKey}`, subValue);
});
} else {
root.style.setProperty(`--font-${key}`, value);
}
});
Object.entries(theme.spacing).forEach(([key, value]) => {
root.style.setProperty(`--spacing-${key}`, value);
});
Object.entries(theme.borderRadius).forEach(([key, value]) => {
root.style.setProperty(`--radius-${key}`, value);
});
Object.entries(theme.shadows).forEach(([key, value]) => {
root.style.setProperty(`--shadow-${key}`, value);
});
// Add theme class to body
document.body.className = document.body.className
.replace(/theme-\w+/g, '')
.trim();
document.body.classList.add(`theme-${theme.name}`);
}
static toggleDarkMode(): void {
const isDark = this.currentTheme.name === 'dark';
this.setTheme(isDark ? defaultTheme : darkTheme);
}
}
// Initialize theme on load
if (typeof window !== 'undefined') {
ThemeProvider.applyTheme(defaultTheme);
}