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.

254 lines (217 loc) • 9.1 kB
/** * @moduleName: Sidebar Toggle Component - Collapsible Navigation Control * @version: 1.0.0 * @since: 2025-07-20 * @lastUpdated: 2025-07-27 * @projectSummary: Enhanced MCP Quiz Server - Sidebar management component for expandable navigation with smooth animations * @techStack: TypeScript, Component Architecture, CSS Transitions, LocalStorage * @dependency: Component base class, SettingsService for persistence * @interModuleDependency: SettingsService for sidebar state persistence, CSS transition classes * @requirementsTraceability: * {@link Requirements.REQ_UI_001} (Sidebar Navigation System) * {@link Requirements.REQ_CONFIG_001} (Local Settings Management) * @testCoverage: Tested through navigation-toggle.spec.ts, phase2-integration.spec.ts E2E tests * @testType: e2e, integration * @testFramework: playwright * @briefDescription: Component managing sidebar collapse/expand functionality with smooth animations and persistent state * @methods: toggleSidebar, setSidebarState, initializeSidebarState, updateUI, bindEvents * @contributors: GitHub Copilot, AI Assistant, Frontend Team * @examples: * - new SidebarToggle().mount() // Initializes sidebar functionality * - sidebarToggle.setSidebarState(true) // Programmatically expand sidebar * @vulnerabilitiesAssessment: Low risk - DOM manipulation only, secure state persistence via SettingsService */ import { SettingsService } from '../services/SettingsService'; import { Component } from './Component'; export class SidebarToggle extends Component { private static instance: SidebarToggle; private settingsService: SettingsService; private sidebar: HTMLElement; private mainContent: HTMLElement; private floatingToggle: HTMLElement; private sidebarToggleBtn: HTMLElement; private sidebarToggleIcon: HTMLElement; private expanded: boolean = true; private eventsbound: boolean = false; private isToggling: boolean = false; constructor() { super('#sidebar'); this.settingsService = SettingsService.getInstance(); SidebarToggle.instance = this; this.sidebar = document.querySelector('#sidebar') as HTMLElement; this.mainContent = document.querySelector('#main-content') as HTMLElement; this.floatingToggle = document.querySelector('#floating-sidebar-toggle') as HTMLElement; this.sidebarToggleBtn = document.querySelector('#sidebar-toggle') as HTMLElement; this.sidebarToggleIcon = this.sidebarToggleBtn?.querySelector('i[data-lucide]') as HTMLElement; // Debug: Validate DOM elements are found console.log('SidebarToggle Debug:', { sidebar: !!this.sidebar, mainContent: !!this.mainContent, toggleBtn: !!this.sidebarToggleBtn, floatingToggle: !!this.floatingToggle, sidebarClasses: this.sidebar?.className, }); } /** * Get the singleton instance of SidebarToggle */ static getInstance(): SidebarToggle | null { return SidebarToggle.instance || null; } /** * Check if current viewport is mobile (width < 768px) */ private isMobileViewport(): boolean { return window.innerWidth < 768; } /** * Auto-hide sidebar on mobile when quiz starts * This provides a better UX by maximizing content area on small screens */ autoHideOnMobileForQuiz(): void { if (this.isMobileViewport() && this.expanded) { console.log('🔀 Auto-hiding sidebar on mobile for quiz start'); this.setSidebarState(false); } } protected render(): void { // Component uses existing HTML structure, no rendering needed } protected onMount(): void { this.initializeSidebarState(); this.bindEvents(); } protected bindEvents(): void { // Prevent multiple event bindings if (this.eventsbound) return; this.eventsbound = true; // Sidebar header toggle button this.sidebarToggleBtn?.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); console.log('Sidebar toggle button clicked'); this.handleToggleClick(); }); // Floating toggle button this.floatingToggle?.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); console.log('Floating toggle button clicked'); this.handleToggleClick(); }); // Keyboard shortcut (Ctrl+B or Cmd+B) document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'b') { e.preventDefault(); e.stopPropagation(); console.log('Keyboard shortcut triggered'); this.handleToggleClick(); } }); } private handleToggleClick(): void { // Prevent rapid successive clicks if (this.isToggling) { console.log('Toggle already in progress, ignoring click'); return; } this.isToggling = true; this.toggleSidebar(); // Reset toggle lock after a short delay setTimeout(() => { this.isToggling = false; }, 100); } private initializeSidebarState(): void { // Get saved state from settings or default to expanded const settings = this.settingsService.getSettings(); this.expanded = settings.ui.sidebarExpanded !== false; // Default to true if not set this.updateUI(); } private toggleSidebar(): void { console.log('🔄 toggleSidebar called, current expanded:', this.expanded); this.expanded = !this.expanded; console.log('🔄 toggleSidebar new expanded:', this.expanded); this.setSidebarState(this.expanded); } private setSidebarState(expanded: boolean): void { this.expanded = expanded; // Save state to settings this.settingsService.updateUISettings({ sidebarExpanded: expanded }); this.updateUI(); } private updateUI(): void { console.log('updateUI called, expanded:', this.expanded); console.log('DOM elements:', { sidebar: !!this.sidebar, mainContent: !!this.mainContent, sidebarClasses: this.sidebar?.className, }); if (!this.sidebar || !this.mainContent) { console.error('Critical: Sidebar or main content elements not found!'); return; } if (this.expanded) { // Expanded state - Show sidebar console.log('Setting expanded state...'); this.sidebar.classList.remove('hidden'); console.log('After setting expanded - Sidebar classes:', this.sidebar.className); // Adjust main content layout to account for visible sidebar this.mainContent.classList.remove('main-sidebar-collapsed'); this.mainContent.classList.add('main-sidebar-expanded'); console.log('After setting expanded - Main content classes:', this.mainContent.className); // Hide floating toggle when sidebar is visible if (this.floatingToggle) { this.floatingToggle.classList.add('hidden'); } // Update toggle button icon and tooltip if (this.sidebarToggleIcon) { this.sidebarToggleIcon.setAttribute('data-lucide', 'panel-left-close'); } if (this.sidebarToggleBtn) { this.sidebarToggleBtn.title = 'Hide sidebar (Ctrl+B)'; } } else { // Collapsed state - Hide sidebar completely console.log('Setting collapsed state...'); this.sidebar.classList.add('hidden'); console.log('After setting collapsed - Sidebar classes:', this.sidebar.className); // Adjust main content layout to fill full width when sidebar is hidden this.mainContent.classList.remove('main-sidebar-expanded'); this.mainContent.classList.add('main-sidebar-collapsed'); console.log('After setting collapsed - Main content classes:', this.mainContent.className); // Show floating toggle when sidebar is hidden if (this.floatingToggle) { this.floatingToggle.classList.remove('hidden'); } // Update toggle button icon (though it won't be visible when sidebar is hidden) if (this.sidebarToggleIcon) { this.sidebarToggleIcon.setAttribute('data-lucide', 'panel-left-open'); } if (this.sidebarToggleBtn) { this.sidebarToggleBtn.title = 'Show sidebar (Ctrl+B)'; } } // Refresh Lucide icons after changing data-lucide attributes if ((window as any).lucide) { (window as any).lucide.createIcons(); } // Handle wide mode adjustments const isWideMode = this.mainContent.classList.contains('wide-mode'); if (isWideMode && this.expanded) { this.mainContent.classList.add('sidebar-expanded'); } else { this.mainContent.classList.remove('sidebar-expanded'); } } // Public method to get current state public getSidebarExpanded(): boolean { return this.expanded; } // Public method to set state programmatically public setSidebar(expanded: boolean): void { this.setSidebarState(expanded); } }