UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

130 lines (111 loc) • 4.32 kB
/** * @moduleName: Theme Management Service - UI Theme & Dark Mode System * @version: 2.0.0 * @since: 2025-07-21 * @lastUpdated: 2025-07-27 * @projectSummary: Enhanced MCP Quiz Server - Theme management with light/dark/system modes and smooth transitions * @techStack: TypeScript, Singleton Pattern, LocalStorage, CSS Classes, Media Queries * @dependency: ThemeConfig, ThemeMode, ThemeColor interfaces from types/index.js * @interModuleDependency: DOM manipulation, localStorage persistence, CSS theme variables * @requirementsTraceability: * {@link Requirements.REQ_CONFIG_003} (Theme System Management) * {@link Requirements.REQ_UI_003} (Advanced Settings) * @briefDescription: Singleton service managing theme preferences with system detection, persistence, and smooth transitions * @methods: setMode, setColor, toggleDarkMode, applyTheme, subscribe, initialize * @contributors: GitHub Copilot, Frontend Team * @examples: * - ThemeService.getInstance().setMode('dark') * - ThemeService.getInstance().toggleDarkMode() * - const unsubscribe = themeService.subscribe(theme => console.log(theme)) * @vulnerabilitiesAssessment: LocalStorage XSS prevention via JSON parsing, CSS injection prevention through class allowlist */ import { ThemeColor, ThemeConfig, ThemeMode } from '../types/index'; export class ThemeService { private static instance: ThemeService; private currentTheme: ThemeConfig; private listeners: Set<(theme: ThemeConfig) => void> = new Set(); private constructor() { this.currentTheme = this.loadThemeFromStorage(); this.initializeSystemThemeListener(); } static getInstance(): ThemeService { if (!ThemeService.instance) { ThemeService.instance = new ThemeService(); } return ThemeService.instance; } private loadThemeFromStorage(): ThemeConfig { const saved = localStorage.getItem('quiz-theme'); if (saved) { try { const parsed = JSON.parse(saved); // Ensure only mode is used for simplified theme system return { mode: parsed.mode || 'system', color: 'slate' }; } catch (e) { console.warn('Invalid theme data in localStorage'); } } return { mode: 'system', color: 'slate' }; } private saveThemeToStorage(): void { localStorage.setItem('quiz-theme', JSON.stringify(this.currentTheme)); } private initializeSystemThemeListener(): void { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (this.currentTheme.mode === 'system') { this.applyTheme(); } }); } getTheme(): ThemeConfig { return { ...this.currentTheme }; } setMode(mode: ThemeMode): void { this.currentTheme.mode = mode; this.applyTheme(); this.saveThemeToStorage(); this.notifyListeners(); } setColor(color: ThemeColor): void { this.currentTheme.color = color; this.applyTheme(); this.saveThemeToStorage(); this.notifyListeners(); } toggleDarkMode(): void { const newMode = this.currentTheme.mode === 'dark' ? 'light' : 'dark'; this.setMode(newMode); } applyTheme(): void { const html = document.documentElement; // Remove existing theme classes (simplified - only dark mode) html.classList.remove('dark'); // Apply dark/light mode based on setting if (this.currentTheme.mode === 'dark') { html.classList.add('dark'); } else if (this.currentTheme.mode === 'system') { const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; if (prefersDark) { html.classList.add('dark'); } } // Light mode: no class needed (default) // Trigger smooth transition document.body.classList.add('theme-transition'); setTimeout(() => { document.body.classList.remove('theme-transition'); }, 300); } subscribe(callback: (theme: ThemeConfig) => void): () => void { this.listeners.add(callback); return () => { this.listeners.delete(callback); }; } private notifyListeners(): void { this.listeners.forEach(callback => callback(this.getTheme())); } initialize(): void { this.applyTheme(); } }