Theming - RumenDamyanov/js-chess GitHub Wiki

Theming

Comprehensive guide to creating, customizing, and implementing themes in the chess showcase.

Overview

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

Theme Architecture

CSS Custom Properties System

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);
}

Theme Structure

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;
}

Built-in Themes

Light Theme (Default)

/* 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;
}

Dark Theme

/* shared/styles/themes/dark.css */
[data-theme="dark"] {
  --color-background: #0d1117;
  --color-surface: #161b22;
  --chess-light-square: #eeeed2;
  --chess-dark-square: #769656;
}

High Contrast Theme

/* 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;
}

Classic Wood Theme

/* 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);
}

Neon Theme

/* 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);
}

Theme Manager

JavaScript Theme Controller

// 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();

Framework Implementations

Angular Theme Service

// 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 Theme Hook

// 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 Theme Composable

// 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 Component

Universal Theme Selector

<!-- 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 Styles

/* 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;
  }
}

Theme Persistence and Sync

User Preferences Storage

// 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;
  }
}

Advanced Theme Features

Automatic Theme Switching

// 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();
    }
  }
}

Theme Animation System

// 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;
  }
}

Testing Themes

Theme Testing Utilities

// 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;
  }
}

Performance Considerations

Efficient Theme Loading

// 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);
  }
}

Next Steps

⚠️ **GitHub.com Fallback** ⚠️