diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json new file mode 100644 index 000000000..8e5a89e77 --- /dev/null +++ b/ui/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "env": { + "browser": true, + "es2022": true + }, + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "no-undef": "error", + "no-var": "error", + "prefer-const": "warn", + "eqeqeq": ["error", "always"], + "no-eval": "error", + "no-implied-eval": "error", + "no-new-func": "error", + "no-script-url": "error", + "no-alert": "warn", + "no-console": ["warn", { "allow": ["warn", "error", "info"] }], + "curly": ["warn", "multi-line"], + "no-throw-literal": "error", + "prefer-template": "warn", + "no-duplicate-imports": "error" + }, + "ignorePatterns": [ + "node_modules/", + "mobile/", + "vendor/", + "*.min.js" + ] +} diff --git a/ui/app.js b/ui/app.js index a1c94ded1..33d95bb40 100644 --- a/ui/app.js +++ b/ui/app.js @@ -10,6 +10,14 @@ import { wsService } from './services/websocket.service.js'; import { healthService } from './services/health.service.js'; import { sensingService } from './services/sensing.service.js'; import { backendDetector } from './utils/backend-detector.js'; +import { KeyboardShortcuts } from './utils/keyboard-shortcuts.js'; +import { PerfMonitor } from './utils/perf-monitor.js'; +import { toastManager } from './utils/toast.js'; +import { ThemeToggle } from './utils/theme-toggle.js'; +import { Router } from './utils/router.js'; +import { Onboarding } from './utils/onboarding.js'; +import { IdleManager } from './utils/idle-manager.js'; +import { NotificationCenter } from './utils/notification-center.js'; class WiFiDensePoseApp { constructor() { @@ -30,10 +38,13 @@ class WiFiDensePoseApp { // Initialize UI components this.initializeComponents(); - + + // Initialize enhancements + this.initializeEnhancements(); + // Set up global event listeners this.setupEventListeners(); - + this.isInitialized = true; console.log('WiFi DensePose UI initialized successfully'); @@ -167,6 +178,48 @@ class WiFiDensePoseApp { } } + // Initialize enhancement modules (keyboard shortcuts, perf monitor, toast, theme) + initializeEnhancements() { + // Toast notifications + toastManager.init(); + + // Theme toggle + this.themeToggle = new ThemeToggle(); + this.themeToggle.init(); + + // Performance monitor + this.perfMonitor = new PerfMonitor(); + this.perfMonitor.init(); + + // Notification center (bell icon in header) + this.notificationCenter = new NotificationCenter(); + this.notificationCenter.init(); + + // Keyboard shortcuts (pass app reference for tab switching) + this.keyboardShortcuts = new KeyboardShortcuts(this); + this.keyboardShortcuts.init(); + + // URL hash router (bookmarkable tabs) + this.router = new Router(this); + this.router.init(); + + // Idle detection (pause updates when inactive) + this.idleManager = new IdleManager(); + this.idleManager.onIdle(() => { + healthService.stopHealthMonitoring(); + console.info('[App] Paused health monitoring (idle)'); + }); + this.idleManager.onActive(() => { + healthService.startHealthMonitoring(); + console.info('[App] Resumed health monitoring (active)'); + }); + this.idleManager.init(); + + // Onboarding tour (first-run walkthrough) + this.onboarding = new Onboarding(this); + this.onboarding.init(); + } + // Handle tab changes handleTabChange(newTab, oldTab) { console.log(`Tab changed from ${oldTab} to ${newTab}`); @@ -272,45 +325,17 @@ class WiFiDensePoseApp { }); } - // Show backend status notification + // Show backend status notification (uses enhanced toast system) showBackendStatus(message, type) { - // Create status notification if it doesn't exist - let statusToast = document.getElementById('backendStatusToast'); - if (!statusToast) { - statusToast = document.createElement('div'); - statusToast.id = 'backendStatusToast'; - statusToast.className = 'backend-status-toast'; - document.body.appendChild(statusToast); - } - - statusToast.textContent = message; - statusToast.className = `backend-status-toast ${type}`; - statusToast.classList.add('show'); - - // Auto-hide success messages, keep warnings and errors longer - const timeout = type === 'success' ? 3000 : 8000; - setTimeout(() => { - statusToast.classList.remove('show'); - }, timeout); + const toastType = type === 'success' ? 'success' : 'warning'; + toastManager[toastType](message, { + duration: type === 'success' ? 3000 : 8000 + }); } - // Show global error message + // Show global error message (uses enhanced toast system) showGlobalError(message) { - // Create error toast if it doesn't exist - let errorToast = document.getElementById('globalErrorToast'); - if (!errorToast) { - errorToast = document.createElement('div'); - errorToast.id = 'globalErrorToast'; - errorToast.className = 'error-toast'; - document.body.appendChild(errorToast); - } - - errorToast.textContent = message; - errorToast.classList.add('show'); - - setTimeout(() => { - errorToast.classList.remove('show'); - }, 5000); + toastManager.error(message, { duration: 6000 }); } // Clean up resources @@ -326,9 +351,19 @@ class WiFiDensePoseApp { // Disconnect all WebSocket connections wsService.disconnectAll(); - + // Stop health monitoring healthService.dispose(); + + // Dispose enhancements + if (this.keyboardShortcuts) this.keyboardShortcuts.dispose(); + if (this.perfMonitor) this.perfMonitor.dispose(); + if (this.themeToggle) this.themeToggle.dispose(); + if (this.router) this.router.dispose(); + if (this.onboarding) this.onboarding.dispose(); + if (this.idleManager) this.idleManager.dispose(); + if (this.notificationCenter) this.notificationCenter.dispose(); + toastManager.dispose(); } // Public API diff --git a/ui/components/TabManager.js b/ui/components/TabManager.js index d559c2eac..c2d352976 100644 --- a/ui/components/TabManager.js +++ b/ui/components/TabManager.js @@ -19,6 +19,33 @@ export class TabManager { tab.addEventListener('click', () => this.switchTab(tab)); }); + // Arrow key navigation within tab bar (WCAG) + const nav = this.container.querySelector('.nav-tabs'); + if (nav) { + nav.addEventListener('keydown', (e) => { + const buttonTabs = this.tabs.filter(t => t.tagName === 'BUTTON' && !t.disabled); + const currentIndex = buttonTabs.indexOf(document.activeElement); + if (currentIndex === -1) return; + + let nextIndex = -1; + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + nextIndex = (currentIndex + 1) % buttonTabs.length; + } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + nextIndex = (currentIndex - 1 + buttonTabs.length) % buttonTabs.length; + } else if (e.key === 'Home') { + nextIndex = 0; + } else if (e.key === 'End') { + nextIndex = buttonTabs.length - 1; + } + + if (nextIndex >= 0) { + e.preventDefault(); + buttonTabs[nextIndex].focus(); + this.switchTab(buttonTabs[nextIndex]); + } + }); + } + // Activate first tab if none active const activeTab = this.tabs.find(tab => tab.classList.contains('active')); if (activeTab) { @@ -36,14 +63,22 @@ export class TabManager { return; } - // Update tab states + // Update tab states and ARIA attributes this.tabs.forEach(tab => { - tab.classList.toggle('active', tab === tabElement); + const isActive = tab === tabElement; + tab.classList.toggle('active', isActive); + if (tab.hasAttribute('aria-selected')) { + tab.setAttribute('aria-selected', String(isActive)); + } }); - // Update content visibility + // Update content visibility and ARIA this.tabContents.forEach(content => { - content.classList.toggle('active', content.id === tabId); + const isActive = content.id === tabId; + content.classList.toggle('active', isActive); + if (content.hasAttribute('role')) { + content.setAttribute('aria-hidden', String(!isActive)); + } }); // Update active tab diff --git a/ui/index.html b/ui/index.html index a68dc7990..2d7f31e38 100644 --- a/ui/index.html +++ b/ui/index.html @@ -7,34 +7,37 @@
+ + Skip to main content +Human Tracking Through Walls Using WiFi Signals