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
text/typescript
/**
* @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();
}
}