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.

129 lines (113 loc) • 4.48 kB
/** * @moduleName: Theme Selector Component - UI Theme Switching Interface * @version: 2.1.0 * @since: 2025-07-21 * @lastUpdated: 2025-07-27 * @projectSummary: Enhanced MCP Quiz Server - Theme selection dropdown with light/dark/system modes * @techStack: TypeScript, Component Architecture, DOM Events, CSS Classes * @dependency: Component base class, ThemeService, ThemeConfig interfaces * @interModuleDependency: ThemeService for state management, header dropdown UI * @requirementsTraceability: * {@link Requirements.REQ_CONFIG_003} (Theme System Management) * {@link Requirements.REQ_UI_003} (Advanced Settings Interface) * @briefDescription: Component managing theme selection dropdown with light, dark, and system preference options * @methods: render, bindEvents, updateThemeButtons, toggleDropdown, selectTheme * @contributors: GitHub Copilot, Frontend Theme Team * @examples: * - const themeSelector = new ThemeSelector() * - themeSelector.render() // Initializes dropdown functionality * @vulnerabilitiesAssessment: DOM element validation, theme XSS prevention through allowlist, event cleanup */ import { ThemeService } from '../services/ThemeService'; import { ThemeConfig, ThemeMode } from '../types/index'; import { Component } from './Component'; export class ThemeSelector extends Component { private themeService: ThemeService; private themeMenuBtn: HTMLElement; private themeDropdown: HTMLElement; private unsubscribe: (() => void) | null = null; constructor() { super('body'); // We'll manage multiple elements this.themeService = ThemeService.getInstance(); this.themeMenuBtn = document.querySelector('#theme-menu-btn') as HTMLElement; this.themeDropdown = document.querySelector('#theme-dropdown') as HTMLElement; } protected onMount(): void { this.unsubscribe = this.themeService.subscribe(theme => this.onThemeChange(theme)); this.themeService.initialize(); } protected onUnmount(): void { this.unsubscribe?.(); } protected render(): void { // Initial render is handled by onThemeChange } protected bindEvents(): void { // Theme menu toggle this.themeMenuBtn?.addEventListener('click', e => { e.stopPropagation(); this.themeDropdown?.classList.toggle('hidden'); }); // Close theme menu document.querySelector('#close-theme-menu')?.addEventListener('click', () => { this.themeDropdown?.classList.add('hidden'); }); // Close theme menu when clicking outside document.addEventListener('click', e => { if ( !this.themeDropdown?.contains(e.target as Node) && !this.themeMenuBtn?.contains(e.target as Node) ) { this.themeDropdown?.classList.add('hidden'); } }); // Theme option buttons (Light/Dark/System) document.querySelectorAll('.theme-option').forEach(btn => { btn.addEventListener('click', () => { const mode = (btn as HTMLElement).dataset.theme as ThemeMode; if (mode) { this.themeService.setMode(mode); this.updateActiveTheme(mode); this.themeDropdown?.classList.add('hidden'); } }); }); } private onThemeChange(theme: ThemeConfig): void { this.updateActiveTheme(theme.mode); this.updateThemeButtonIcon(theme.mode); } private updateActiveTheme(currentMode: ThemeMode): void { document.querySelectorAll('.theme-option').forEach(btn => { const mode = (btn as HTMLElement).dataset.theme; if (mode === currentMode) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); } private updateThemeButtonIcon(mode: ThemeMode): void { const icon = this.themeMenuBtn?.querySelector('i[data-lucide]'); if (icon) { // Update icon based on current theme switch (mode) { case 'light': icon.setAttribute('data-lucide', 'sun'); break; case 'dark': icon.setAttribute('data-lucide', 'moon'); break; case 'system': icon.setAttribute('data-lucide', 'monitor'); break; default: icon.setAttribute('data-lucide', 'palette'); } // Refresh Lucide icons if ((window as any).lucide) { (window as any).lucide.createIcons(); } } } }