Theming - RumenDamyanov/js-chess GitHub Wiki
Comprehensive guide to creating, customizing, and implementing themes in the chess showcase.
The chess showcase supports extensive theming capabilities including:
- Multiple built-in themes
- Custom theme creation
- Dark/light mode support
- Color scheme customization
- Board and piece styling
- User preference persistence
- Accessibility-compliant themes
The theming system is built on CSS custom properties for maximum flexibility:
/* shared/styles/themes/base.css */
:root {
/* Core color palette */
--color-primary: #646cff;
--color-primary-variant: #4c51bf;
--color-secondary: #22c55e;
--color-error: #ef4444;
--color-warning: #f59e0b;
--color-success: #10b981;
/* Neutral colors */
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-surface-variant: #e9ecef;
--color-outline: #dee2e6;
--color-shadow: rgba(0, 0, 0, 0.1);
/* Text colors */
--color-on-background: #212529;
--color-on-surface: #495057;
--color-on-surface-variant: #6c757d;
--color-on-primary: #ffffff;
--color-on-secondary: #000000;
/* Chess-specific colors */
--chess-light-square: #f0d9b5;
--chess-dark-square: #b58863;
--chess-border: #8b7355;
--chess-coords: #8b7355;
/* Piece colors */
--chess-white-piece: #ffffff;
--chess-black-piece: #000000;
--chess-piece-shadow: rgba(0, 0, 0, 0.3);
/* Interactive states */
--chess-selected: var(--color-primary);
--chess-valid-move: var(--color-success);
--chess-capture: var(--color-error);
--chess-check: var(--color-warning);
--chess-last-move: #ffd93d;
/* Dimensions */
--chess-board-border-radius: 8px;
--chess-board-border-width: 2px;
--chess-square-border-radius: 0px;
/* Typography */
--font-family-ui: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-family-mono: 'JetBrains Mono', 'Fira Code', monospace;
--font-family-chess: 'Chess Merida', serif;
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--transition-slow: 400ms ease;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
Each theme is defined as a CSS file that overrides base variables:
/* shared/styles/themes/dark.css */
[data-theme="dark"] {
/* Core colors for dark mode */
--color-background: #0d1117;
--color-surface: #161b22;
--color-surface-variant: #21262d;
--color-outline: #30363d;
--color-shadow: rgba(0, 0, 0, 0.3);
/* Text colors */
--color-on-background: #f0f6fc;
--color-on-surface: #e6edf3;
--color-on-surface-variant: #7d8590;
/* Chess board for dark theme */
--chess-light-square: #eeeed2;
--chess-dark-square: #769656;
--chess-border: #586e5a;
--chess-coords: #b8c5b9;
/* Piece colors adjusted for dark background */
--chess-white-piece: #f0f0f0;
--chess-black-piece: #2d2d2d;
--chess-piece-shadow: rgba(0, 0, 0, 0.5);
/* Interactive states */
--chess-selected: #4493f8;
--chess-valid-move: #3fb950;
--chess-capture: #f85149;
--chess-check: #d29922;
--chess-last-move: #bb800a;
}
/* shared/styles/themes/light.css */
[data-theme="light"] {
/* Uses base variables with light-specific overrides */
--color-primary: #0969da;
--chess-light-square: #f0d9b5;
--chess-dark-square: #b58863;
}
/* shared/styles/themes/dark.css */
[data-theme="dark"] {
--color-background: #0d1117;
--color-surface: #161b22;
--chess-light-square: #eeeed2;
--chess-dark-square: #769656;
}
/* shared/styles/themes/high-contrast.css */
[data-theme="high-contrast"] {
--color-background: #ffffff;
--color-surface: #ffffff;
--color-on-background: #000000;
--color-on-surface: #000000;
--chess-light-square: #ffffff;
--chess-dark-square: #000000;
--chess-white-piece: #000000;
--chess-black-piece: #ffffff;
--chess-selected: #ff0000;
--chess-valid-move: #00ff00;
--chess-capture: #ff0000;
--chess-check: #ffff00;
/* Enhanced borders for clarity */
--chess-board-border-width: 4px;
--chess-square-border-radius: 0px;
}
/* shared/styles/themes/wood.css */
[data-theme="wood"] {
--chess-light-square: #f4e4bc;
--chess-dark-square: #8b4513;
--chess-border: #654321;
/* Wood texture background */
--chess-board-background:
linear-gradient(45deg, #8b4513 25%, transparent 25%),
linear-gradient(-45deg, #8b4513 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #8b4513 75%),
linear-gradient(-45deg, transparent 75%, #8b4513 75%);
--chess-board-background-size: 4px 4px;
--chess-board-border-radius: 12px;
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
}
[data-theme="wood"] .chess-board {
background: var(--chess-board-background);
background-size: var(--chess-board-background-size);
box-shadow: var(--shadow-lg);
}
/* shared/styles/themes/neon.css */
[data-theme="neon"] {
--color-background: #000011;
--color-surface: #001122;
--color-primary: #00ffff;
--color-secondary: #ff00ff;
--chess-light-square: #001133;
--chess-dark-square: #000022;
--chess-border: #00ffff;
--chess-white-piece: #00ffff;
--chess-black-piece: #ff00ff;
--chess-piece-shadow: 0 0 10px currentColor;
--chess-selected: #ffff00;
--chess-valid-move: #00ff00;
--chess-capture: #ff0000;
/* Neon glow effects */
--neon-glow: 0 0 5px currentColor, 0 0 10px currentColor;
}
[data-theme="neon"] .chess-piece {
filter: drop-shadow(var(--neon-glow));
}
[data-theme="neon"] .chess-board {
border: 2px solid var(--chess-border);
box-shadow: 0 0 20px var(--chess-border);
}
// shared/theming/ThemeManager.js
export class ThemeManager {
constructor() {
this.themes = new Map();
this.currentTheme = 'light';
this.userPreferences = this.loadPreferences();
this.registerBuiltInThemes();
this.detectSystemPreferences();
this.applyTheme(this.userPreferences.theme || this.getSystemTheme());
}
registerBuiltInThemes() {
this.themes.set('light', {
name: 'Light',
description: 'Clean and bright theme',
category: 'default',
cssFile: 'themes/light.css'
});
this.themes.set('dark', {
name: 'Dark',
description: 'Easy on the eyes for low light',
category: 'default',
cssFile: 'themes/dark.css'
});
this.themes.set('high-contrast', {
name: 'High Contrast',
description: 'Maximum contrast for accessibility',
category: 'accessibility',
cssFile: 'themes/high-contrast.css'
});
this.themes.set('wood', {
name: 'Classic Wood',
description: 'Traditional wooden chess board',
category: 'classic',
cssFile: 'themes/wood.css'
});
this.themes.set('neon', {
name: 'Neon',
description: 'Futuristic glowing theme',
category: 'fun',
cssFile: 'themes/neon.css'
});
}
detectSystemPreferences() {
// Detect dark mode preference
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeQuery.addEventListener('change', (e) => {
if (!this.userPreferences.theme) {
this.applyTheme(e.matches ? 'dark' : 'light');
}
});
// Detect high contrast preference
const highContrastQuery = window.matchMedia('(prefers-contrast: high)');
highContrastQuery.addEventListener('change', (e) => {
if (e.matches && !this.userPreferences.theme) {
this.applyTheme('high-contrast');
}
});
// Detect reduced motion preference
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
reducedMotionQuery.addEventListener('change', (e) => {
document.documentElement.style.setProperty(
'--transition-fast',
e.matches ? '0ms' : '150ms ease'
);
document.documentElement.style.setProperty(
'--transition-normal',
e.matches ? '0ms' : '250ms ease'
);
document.documentElement.style.setProperty(
'--transition-slow',
e.matches ? '0ms' : '400ms ease'
);
});
}
getSystemTheme() {
if (window.matchMedia('(prefers-contrast: high)').matches) {
return 'high-contrast';
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
async applyTheme(themeId) {
if (!this.themes.has(themeId)) {
console.warn(`Theme "${themeId}" not found`);
return;
}
const theme = this.themes.get(themeId);
// Remove existing theme classes
this.themes.forEach((_, id) => {
document.documentElement.removeAttribute('data-theme');
});
// Apply new theme
document.documentElement.setAttribute('data-theme', themeId);
// Load theme CSS if not already loaded
await this.loadThemeCSS(theme.cssFile);
this.currentTheme = themeId;
this.userPreferences.theme = themeId;
this.savePreferences();
// Emit theme change event
this.emitThemeChange(themeId, theme);
}
async loadThemeCSS(cssFile) {
const linkId = `theme-css-${cssFile.replace(/[^a-zA-Z0-9]/g, '-')}`;
if (document.getElementById(linkId)) {
return; // Already loaded
}
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.id = linkId;
link.rel = 'stylesheet';
link.href = cssFile;
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
}
registerCustomTheme(id, themeConfig) {
this.themes.set(id, {
name: themeConfig.name,
description: themeConfig.description,
category: themeConfig.category || 'custom',
cssFile: themeConfig.cssFile,
variables: themeConfig.variables
});
// If variables are provided, inject them directly
if (themeConfig.variables) {
this.injectThemeVariables(id, themeConfig.variables);
}
}
injectThemeVariables(themeId, variables) {
const styleId = `theme-variables-${themeId}`;
let styleElement = document.getElementById(styleId);
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
const cssText = `[data-theme="${themeId}"] {\n${
Object.entries(variables)
.map(([key, value]) => ` ${key}: ${value};`)
.join('\n')
}\n}`;
styleElement.textContent = cssText;
}
getAvailableThemes() {
return Array.from(this.themes.entries()).map(([id, theme]) => ({
id,
...theme
}));
}
getCurrentTheme() {
return this.currentTheme;
}
getThemesByCategory() {
const categories = {};
this.themes.forEach((theme, id) => {
const category = theme.category || 'default';
if (!categories[category]) {
categories[category] = [];
}
categories[category].push({ id, ...theme });
});
return categories;
}
// Custom theme builder
createCustomTheme(baseThemeId, overrides) {
const baseTheme = this.themes.get(baseThemeId);
if (!baseTheme) {
throw new Error(`Base theme "${baseThemeId}" not found`);
}
const customId = `custom-${Date.now()}`;
const customTheme = {
name: overrides.name || `Custom ${baseTheme.name}`,
description: overrides.description || 'User-created custom theme',
category: 'custom',
variables: { ...overrides.variables }
};
this.registerCustomTheme(customId, customTheme);
return customId;
}
loadPreferences() {
try {
const stored = localStorage.getItem('chess-theme-preferences');
return stored ? JSON.parse(stored) : {};
} catch (error) {
console.warn('Failed to load theme preferences:', error);
return {};
}
}
savePreferences() {
try {
localStorage.setItem('chess-theme-preferences', JSON.stringify(this.userPreferences));
} catch (error) {
console.warn('Failed to save theme preferences:', error);
}
}
emitThemeChange(themeId, theme) {
const event = new CustomEvent('themeChanged', {
detail: { themeId, theme }
});
document.dispatchEvent(event);
}
// Theme customization helpers
updateThemeVariable(variable, value) {
document.documentElement.style.setProperty(variable, value);
}
getThemeVariable(variable) {
return getComputedStyle(document.documentElement).getPropertyValue(variable);
}
resetToSystemTheme() {
delete this.userPreferences.theme;
this.savePreferences();
this.applyTheme(this.getSystemTheme());
}
}
// Global theme manager instance
export const themeManager = new ThemeManager();
// angular-chess/src/app/services/theme.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ThemeManager } from '../../../shared/theming/ThemeManager';
export interface Theme {
id: string;
name: string;
description: string;
category: string;
}
@Injectable({
providedIn: 'root'
})
export class ThemeService {
private themeManager: ThemeManager;
private currentTheme$ = new BehaviorSubject<string>('light');
private availableThemes$ = new BehaviorSubject<Theme[]>([]);
constructor() {
this.themeManager = new ThemeManager();
this.currentTheme$.next(this.themeManager.getCurrentTheme());
this.availableThemes$.next(this.themeManager.getAvailableThemes());
// Listen for theme changes
document.addEventListener('themeChanged', (event: any) => {
this.currentTheme$.next(event.detail.themeId);
});
}
getCurrentTheme(): Observable<string> {
return this.currentTheme$.asObservable();
}
getAvailableThemes(): Observable<Theme[]> {
return this.availableThemes$.asObservable();
}
async setTheme(themeId: string): Promise<void> {
await this.themeManager.applyTheme(themeId);
}
getThemesByCategory(): { [category: string]: Theme[] } {
return this.themeManager.getThemesByCategory();
}
createCustomTheme(baseThemeId: string, overrides: any): string {
const customId = this.themeManager.createCustomTheme(baseThemeId, overrides);
this.availableThemes$.next(this.themeManager.getAvailableThemes());
return customId;
}
updateThemeVariable(variable: string, value: string): void {
this.themeManager.updateThemeVariable(variable, value);
}
resetToSystemTheme(): void {
this.themeManager.resetToSystemTheme();
}
}
// react-chess/src/hooks/useTheme.js
import { useState, useEffect, useCallback } from 'react';
import { themeManager } from '../../shared/theming/ThemeManager';
export function useTheme() {
const [currentTheme, setCurrentTheme] = useState(themeManager.getCurrentTheme());
const [availableThemes, setAvailableThemes] = useState(themeManager.getAvailableThemes());
useEffect(() => {
const handleThemeChange = (event) => {
setCurrentTheme(event.detail.themeId);
};
document.addEventListener('themeChanged', handleThemeChange);
return () => document.removeEventListener('themeChanged', handleThemeChange);
}, []);
const setTheme = useCallback(async (themeId) => {
await themeManager.applyTheme(themeId);
}, []);
const createCustomTheme = useCallback((baseThemeId, overrides) => {
const customId = themeManager.createCustomTheme(baseThemeId, overrides);
setAvailableThemes(themeManager.getAvailableThemes());
return customId;
}, []);
const updateVariable = useCallback((variable, value) => {
themeManager.updateThemeVariable(variable, value);
}, []);
const getThemesByCategory = useCallback(() => {
return themeManager.getThemesByCategory();
}, []);
const resetToSystemTheme = useCallback(() => {
themeManager.resetToSystemTheme();
}, []);
return {
currentTheme,
availableThemes,
setTheme,
createCustomTheme,
updateVariable,
getThemesByCategory,
resetToSystemTheme
};
}
// vue-chess/src/composables/useTheme.js
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { themeManager } from '../../shared/theming/ThemeManager';
export function useTheme() {
const currentTheme = ref(themeManager.getCurrentTheme());
const availableThemes = ref(themeManager.getAvailableThemes());
const handleThemeChange = (event) => {
currentTheme.value = event.detail.themeId;
};
onMounted(() => {
document.addEventListener('themeChanged', handleThemeChange);
});
onUnmounted(() => {
document.removeEventListener('themeChanged', handleThemeChange);
});
const setTheme = async (themeId) => {
await themeManager.applyTheme(themeId);
};
const createCustomTheme = (baseThemeId, overrides) => {
const customId = themeManager.createCustomTheme(baseThemeId, overrides);
availableThemes.value = themeManager.getAvailableThemes();
return customId;
};
const updateVariable = (variable, value) => {
themeManager.updateThemeVariable(variable, value);
};
const getThemesByCategory = () => {
return themeManager.getThemesByCategory();
};
const resetToSystemTheme = () => {
themeManager.resetToSystemTheme();
};
return {
currentTheme,
availableThemes,
setTheme,
createCustomTheme,
updateVariable,
getThemesByCategory,
resetToSystemTheme
};
}
<!-- Theme Selector Template -->
<div class="theme-selector">
<div class="theme-selector-header">
<h3>Choose Theme</h3>
<button class="reset-btn" onclick="resetToSystemTheme()">
Reset to System
</button>
</div>
<div class="theme-categories">
<!-- Default Themes -->
<div class="theme-category">
<h4>Default</h4>
<div class="theme-grid">
<div class="theme-option" data-theme="light">
<div class="theme-preview light-preview"></div>
<span>Light</span>
</div>
<div class="theme-option" data-theme="dark">
<div class="theme-preview dark-preview"></div>
<span>Dark</span>
</div>
</div>
</div>
<!-- Accessibility Themes -->
<div class="theme-category">
<h4>Accessibility</h4>
<div class="theme-grid">
<div class="theme-option" data-theme="high-contrast">
<div class="theme-preview high-contrast-preview"></div>
<span>High Contrast</span>
</div>
</div>
</div>
<!-- Fun Themes -->
<div class="theme-category">
<h4>Decorative</h4>
<div class="theme-grid">
<div class="theme-option" data-theme="wood">
<div class="theme-preview wood-preview"></div>
<span>Classic Wood</span>
</div>
<div class="theme-option" data-theme="neon">
<div class="theme-preview neon-preview"></div>
<span>Neon</span>
</div>
</div>
</div>
</div>
<!-- Custom Theme Builder -->
<div class="custom-theme-builder">
<h4>Create Custom Theme</h4>
<form class="theme-builder-form">
<div class="form-group">
<label for="base-theme">Base Theme:</label>
<select id="base-theme" name="baseTheme">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="wood">Wood</option>
</select>
</div>
<div class="color-inputs">
<div class="form-group">
<label for="light-squares">Light Squares:</label>
<input type="color" id="light-squares" name="lightSquares" value="#f0d9b5">
</div>
<div class="form-group">
<label for="dark-squares">Dark Squares:</label>
<input type="color" id="dark-squares" name="darkSquares" value="#b58863">
</div>
<div class="form-group">
<label for="accent-color">Accent Color:</label>
<input type="color" id="accent-color" name="accentColor" value="#646cff">
</div>
</div>
<button type="submit" class="create-theme-btn">Create Theme</button>
</form>
</div>
</div>
/* Theme Selector Component Styles */
.theme-selector {
padding: 1.5rem;
background: var(--color-surface);
border-radius: 8px;
border: 1px solid var(--color-outline);
max-width: 600px;
}
.theme-selector-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.theme-selector-header h3 {
margin: 0;
font-size: 1.25rem;
color: var(--color-on-surface);
}
.reset-btn {
padding: 0.5rem 1rem;
background: var(--color-surface-variant);
border: 1px solid var(--color-outline);
border-radius: 4px;
color: var(--color-on-surface);
cursor: pointer;
font-size: 0.875rem;
transition: all var(--transition-fast);
}
.reset-btn:hover {
background: var(--color-primary);
color: var(--color-on-primary);
}
.theme-category {
margin-bottom: 2rem;
}
.theme-category h4 {
margin: 0 0 1rem 0;
font-size: 1rem;
color: var(--color-on-surface-variant);
font-weight: 600;
}
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1rem;
}
.theme-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
border: 2px solid var(--color-outline);
border-radius: 8px;
cursor: pointer;
transition: all var(--transition-fast);
}
.theme-option:hover {
border-color: var(--color-primary);
background: var(--color-surface-variant);
}
.theme-option.selected {
border-color: var(--color-primary);
background: var(--color-primary-container);
}
.theme-preview {
width: 60px;
height: 60px;
border-radius: 4px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
overflow: hidden;
border: 1px solid var(--color-outline);
}
/* Theme preview styles */
.light-preview {
background: linear-gradient(45deg, #f0d9b5 25%, #b58863 25%, #b58863 50%, #f0d9b5 50%);
}
.dark-preview {
background: linear-gradient(45deg, #eeeed2 25%, #769656 25%, #769656 50%, #eeeed2 50%);
}
.high-contrast-preview {
background: linear-gradient(45deg, #ffffff 25%, #000000 25%, #000000 50%, #ffffff 50%);
}
.wood-preview {
background: linear-gradient(45deg, #f4e4bc 25%, #8b4513 25%, #8b4513 50%, #f4e4bc 50%);
}
.neon-preview {
background: linear-gradient(45deg, #001133 25%, #000022 25%, #000022 50%, #001133 50%);
border: 1px solid #00ffff;
box-shadow: 0 0 10px #00ffff33;
}
.theme-option span {
font-size: 0.875rem;
color: var(--color-on-surface);
text-align: center;
}
/* Custom Theme Builder */
.custom-theme-builder {
border-top: 1px solid var(--color-outline);
padding-top: 1.5rem;
margin-top: 1.5rem;
}
.custom-theme-builder h4 {
margin: 0 0 1rem 0;
font-size: 1rem;
color: var(--color-on-surface-variant);
}
.theme-builder-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.color-inputs {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.875rem;
color: var(--color-on-surface-variant);
font-weight: 500;
}
.form-group input,
.form-group select {
padding: 0.5rem;
border: 1px solid var(--color-outline);
border-radius: 4px;
background: var(--color-surface);
color: var(--color-on-surface);
font-size: 0.875rem;
}
.form-group input[type="color"] {
height: 40px;
padding: 0.25rem;
cursor: pointer;
}
.create-theme-btn {
padding: 0.75rem 1.5rem;
background: var(--color-primary);
color: var(--color-on-primary);
border: none;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
align-self: flex-start;
}
.create-theme-btn:hover {
background: var(--color-primary-variant);
}
/* Responsive design */
@media (max-width: 768px) {
.theme-selector {
padding: 1rem;
}
.theme-grid {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.75rem;
}
.theme-preview {
width: 50px;
height: 50px;
}
.color-inputs {
grid-template-columns: 1fr;
}
.theme-selector-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
}
// shared/theming/ThemeStorage.js
export class ThemeStorage {
constructor() {
this.storageKey = 'chess-theme-preferences';
this.cloudSyncEnabled = false;
}
savePreferences(preferences) {
try {
// Local storage
localStorage.setItem(this.storageKey, JSON.stringify(preferences));
// Cloud sync if enabled
if (this.cloudSyncEnabled) {
this.syncToCloud(preferences);
}
return true;
} catch (error) {
console.error('Failed to save theme preferences:', error);
return false;
}
}
loadPreferences() {
try {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : this.getDefaultPreferences();
} catch (error) {
console.error('Failed to load theme preferences:', error);
return this.getDefaultPreferences();
}
}
getDefaultPreferences() {
return {
theme: null, // null means use system preference
customThemes: [],
autoSwitchTime: null, // For automatic day/night switching
followSystemTheme: true
};
}
async syncToCloud(preferences) {
// Implementation for cloud synchronization
// This would integrate with your backend API
try {
const response = await fetch('/api/user/theme-preferences', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getAuthToken()}`
},
body: JSON.stringify(preferences)
});
return response.ok;
} catch (error) {
console.error('Failed to sync theme preferences to cloud:', error);
return false;
}
}
async loadFromCloud() {
try {
const response = await fetch('/api/user/theme-preferences', {
headers: {
'Authorization': `Bearer ${this.getAuthToken()}`
}
});
if (response.ok) {
const cloudPreferences = await response.json();
// Merge with local preferences
const localPreferences = this.loadPreferences();
const mergedPreferences = { ...localPreferences, ...cloudPreferences };
this.savePreferences(mergedPreferences);
return mergedPreferences;
}
} catch (error) {
console.error('Failed to load theme preferences from cloud:', error);
}
return this.loadPreferences();
}
getAuthToken() {
// Return user authentication token for cloud sync
return localStorage.getItem('auth-token');
}
enableCloudSync() {
this.cloudSyncEnabled = true;
}
disableCloudSync() {
this.cloudSyncEnabled = false;
}
}
// shared/theming/AutoThemeSwitcher.js
export class AutoThemeSwitcher {
constructor(themeManager) {
this.themeManager = themeManager;
this.enabled = false;
this.schedule = {
lightStart: '06:00',
darkStart: '18:00'
};
this.checkInterval = null;
}
enable(schedule = null) {
if (schedule) {
this.schedule = { ...this.schedule, ...schedule };
}
this.enabled = true;
this.checkAndApplyTheme();
// Check every minute
this.checkInterval = setInterval(() => {
this.checkAndApplyTheme();
}, 60000);
}
disable() {
this.enabled = false;
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
checkAndApplyTheme() {
if (!this.enabled) return;
const now = new Date();
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
const shouldUseDarkTheme = this.isTimeInRange(
currentTime,
this.schedule.darkStart,
this.schedule.lightStart
);
const targetTheme = shouldUseDarkTheme ? 'dark' : 'light';
if (this.themeManager.getCurrentTheme() !== targetTheme) {
this.themeManager.applyTheme(targetTheme);
}
}
isTimeInRange(current, start, end) {
const currentMinutes = this.timeToMinutes(current);
const startMinutes = this.timeToMinutes(start);
const endMinutes = this.timeToMinutes(end);
if (startMinutes < endMinutes) {
// Same day range
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
} else {
// Overnight range
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
}
}
timeToMinutes(timeString) {
const [hours, minutes] = timeString.split(':').map(Number);
return hours * 60 + minutes;
}
setSchedule(lightStart, darkStart) {
this.schedule = { lightStart, darkStart };
if (this.enabled) {
this.checkAndApplyTheme();
}
}
}
// shared/theming/ThemeAnimator.js
export class ThemeAnimator {
constructor() {
this.isAnimating = false;
this.animationDuration = 300;
}
async animateThemeChange(fromTheme, toTheme) {
if (this.isAnimating) return;
this.isAnimating = true;
try {
// Create transition overlay
const overlay = this.createTransitionOverlay();
document.body.appendChild(overlay);
// Fade out
await this.fadeOut(overlay);
// Apply new theme
await this.themeManager.applyTheme(toTheme);
// Fade in
await this.fadeIn(overlay);
// Remove overlay
document.body.removeChild(overlay);
} finally {
this.isAnimating = false;
}
}
createTransitionOverlay() {
const overlay = document.createElement('div');
overlay.className = 'theme-transition-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: var(--color-background);
opacity: 0;
z-index: 9999;
pointer-events: none;
transition: opacity ${this.animationDuration}ms ease;
`;
return overlay;
}
fadeOut(overlay) {
return new Promise(resolve => {
requestAnimationFrame(() => {
overlay.style.opacity = '1';
setTimeout(resolve, this.animationDuration);
});
});
}
fadeIn(overlay) {
return new Promise(resolve => {
requestAnimationFrame(() => {
overlay.style.opacity = '0';
setTimeout(resolve, this.animationDuration);
});
});
}
setAnimationDuration(duration) {
this.animationDuration = duration;
}
}
// shared/testing/ThemeTestUtils.js
export class ThemeTestUtils {
static async testThemeContrast(themeId) {
// Apply theme
await themeManager.applyTheme(themeId);
// Get color values
const backgroundColor = getComputedStyle(document.documentElement)
.getPropertyValue('--color-background').trim();
const textColor = getComputedStyle(document.documentElement)
.getPropertyValue('--color-on-background').trim();
// Calculate contrast ratio
const ratio = this.calculateContrastRatio(backgroundColor, textColor);
return {
backgroundColor,
textColor,
contrastRatio: ratio,
passesAA: ratio >= 4.5,
passesAAA: ratio >= 7
};
}
static calculateContrastRatio(color1, color2) {
const rgb1 = this.hexToRgb(color1);
const rgb2 = this.hexToRgb(color2);
const l1 = this.getLuminance(rgb1);
const l2 = this.getLuminance(rgb2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
static hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
static getLuminance({ r, g, b }) {
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
static async testAllThemes() {
const themes = themeManager.getAvailableThemes();
const results = {};
for (const theme of themes) {
results[theme.id] = await this.testThemeContrast(theme.id);
}
return results;
}
}
// shared/theming/ThemeLoader.js
export class ThemeLoader {
constructor() {
this.loadedThemes = new Set();
this.preloadQueue = [];
}
async preloadTheme(themeId) {
if (this.loadedThemes.has(themeId)) {
return; // Already loaded
}
const theme = themeManager.themes.get(themeId);
if (!theme) return;
// Add to preload queue
this.preloadQueue.push(themeId);
// Load CSS in background
if (theme.cssFile) {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = theme.cssFile;
document.head.appendChild(link);
}
this.loadedThemes.add(themeId);
}
async preloadCommonThemes() {
// Preload system themes
await Promise.all([
this.preloadTheme('light'),
this.preloadTheme('dark'),
this.preloadTheme('high-contrast')
]);
}
getLoadedThemes() {
return Array.from(this.loadedThemes);
}
}
- Design System - Component styling system
- Accessibility - Accessible theme implementation
- Performance - Theme loading optimization
- Responsive Design - Theme adaptation for different screens