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