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