From 49fb2ca9f4c7aac48502db0cf952de6e703c2de2 Mon Sep 17 00:00:00 2001 From: nai <81188562+natiixnt@users.noreply.github.com> Date: Tue, 19 May 2026 16:04:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20UI=20overhaul=20=E2=80=94=20consoli?= =?UTF-8?q?dates=20#305-#309=20(keyboard=20shortcuts,=20perf=20monitor,=20?= =?UTF-8?q?toasts,=20theme,=20command=20palette,=20activity=20log,=20data?= =?UTF-8?q?=20export,=20mobile=20PWA,=20accessibility,=20i18n)=20(#620)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ui): add keyboard shortcuts, perf monitor, toast system, theme toggle, and WCAG accessibility - Keyboard shortcuts overlay (press ? for help, 1-8 for tabs, T for theme, P for perf) - Real-time performance monitor with FPS, memory, latency sparklines (draggable) - Enhanced toast notification system with stacking, auto-dismiss, progress bars - Dark/light theme toggle with localStorage persistence and system preference detection - WCAG accessibility: skip-to-content link, ARIA roles/attributes on tabs and panels, arrow key navigation in tab bar, focus-visible outlines - ESLint config for UI directory with security and quality rules * feat(ui): add command palette, activity log, data export, fullscreen mode, connection status - Command palette (Ctrl+K / Cmd+K) with fuzzy search across tabs and actions - Activity log panel (L key) with real-time console interception, filters, resizable - Data export utility (E key) for sensor data as JSON/CSV with dialog - Fullscreen mode (F key / F11) for visualization tabs with exit button - Connection status widget in header showing WebSocket state and reconnect * feat(ui): add mobile hamburger nav, PWA support, and 40 unit tests - Mobile hamburger navigation: slide-out drawer replacing tab bar on <768px, swipe-to-close, animated hamburger icon, auto-sync with tab manager - PWA manifest + service worker: installable dashboard, offline shell caching (cache-first for static, network-first for API), auto-cleanup of old caches - 40 unit tests for ToastManager, ThemeToggle, KeyboardShortcuts, PerfMonitor, TabManager - browser-based test runner at ui/tests/unit-tests.html - PWA meta tags: theme-color, apple-mobile-web-app-capable, manifest link - Icon generator page for creating PWA icons (ui/icons/generate.html) * feat(ui): add URL routing, onboarding tour, idle detection, notification center - Hash router: tabs are bookmarkable/shareable via URL (#demo, #sensing, etc.), syncs with TabManager, supports browser back/forward navigation - Onboarding tour: interactive 6-step first-run walkthrough with spotlight highlighting, step indicators, skip/back/next controls, localStorage persistence - Idle detection: pauses health polling and reduces CSS animations after 3 min of inactivity, resumes on user interaction, integrates with Page Visibility API - Notification center: bell icon in header with unread badge, event history panel with mark-read/clear, persists across page views via sessionStorage * feat(ui): add i18n (EN/PL), screenshot tool, settings panel, reduced motion, uptime clock - i18n: English/Polish translations with auto-detection, language selector in header, data-i18n attributes on dashboard elements, localStorage persistence - Screenshot tool (S key): captures active tab to clipboard or downloads PNG, flash effect, canvas rendering with watermark, fallback for tainted canvases - Quick settings panel (gear icon): reduced motion toggle, high contrast mode, compact layout mode, health polling toggle, clear data, reset onboarding - Uptime clock: current time + session duration in header - prefers-reduced-motion: system-level and manual toggle, disables all animations and transitions for vestibular accessibility - High contrast mode: WCAG AAA compliant colors for both light and dark themes - Compact mode: condensed layout for dense information display --- ui/.eslintrc.json | 33 + ui/app.js | 201 +++- ui/components/TabManager.js | 43 +- ui/icons/generate.html | 66 ++ ui/index.html | 68 +- ui/manifest.json | 25 + ui/style.css | 1741 +++++++++++++++++++++++++++++++ ui/sw.js | 124 +++ ui/tests/unit-tests.html | 472 +++++++++ ui/utils/activity-log.js | 181 ++++ ui/utils/command-palette.js | 311 ++++++ ui/utils/connection-status.js | 84 ++ ui/utils/data-export.js | 148 +++ ui/utils/fullscreen.js | 79 ++ ui/utils/i18n.js | 264 +++++ ui/utils/idle-manager.js | 83 ++ ui/utils/keyboard-shortcuts.js | 168 +++ ui/utils/mobile-nav.js | 171 +++ ui/utils/notification-center.js | 233 +++++ ui/utils/onboarding.js | 192 ++++ ui/utils/perf-monitor.js | 216 ++++ ui/utils/quick-settings.js | 191 ++++ ui/utils/router.js | 47 + ui/utils/screenshot.js | 160 +++ ui/utils/theme-toggle.js | 86 ++ ui/utils/toast.js | 150 +++ ui/utils/uptime-clock.js | 61 ++ 27 files changed, 5526 insertions(+), 72 deletions(-) create mode 100644 ui/.eslintrc.json create mode 100644 ui/icons/generate.html create mode 100644 ui/manifest.json create mode 100644 ui/sw.js create mode 100644 ui/tests/unit-tests.html create mode 100644 ui/utils/activity-log.js create mode 100644 ui/utils/command-palette.js create mode 100644 ui/utils/connection-status.js create mode 100644 ui/utils/data-export.js create mode 100644 ui/utils/fullscreen.js create mode 100644 ui/utils/i18n.js create mode 100644 ui/utils/idle-manager.js create mode 100644 ui/utils/keyboard-shortcuts.js create mode 100644 ui/utils/mobile-nav.js create mode 100644 ui/utils/notification-center.js create mode 100644 ui/utils/onboarding.js create mode 100644 ui/utils/perf-monitor.js create mode 100644 ui/utils/quick-settings.js create mode 100644 ui/utils/router.js create mode 100644 ui/utils/screenshot.js create mode 100644 ui/utils/theme-toggle.js create mode 100644 ui/utils/toast.js create mode 100644 ui/utils/uptime-clock.js diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json new file mode 100644 index 00000000..8e5a89e7 --- /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 a1c94ded..5c5bada6 100644 --- a/ui/app.js +++ b/ui/app.js @@ -10,6 +10,24 @@ 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 { CommandPalette } from './utils/command-palette.js'; +import { ActivityLog } from './utils/activity-log.js'; +import { DataExport } from './utils/data-export.js'; +import { FullscreenManager } from './utils/fullscreen.js'; +import { ConnectionStatus } from './utils/connection-status.js'; +import { MobileNav } from './utils/mobile-nav.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'; +import { i18n } from './utils/i18n.js'; +import { ScreenshotTool } from './utils/screenshot.js'; +import { UptimeClock } from './utils/uptime-clock.js'; +import { QuickSettings } from './utils/quick-settings.js'; class WiFiDensePoseApp { constructor() { @@ -30,10 +48,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 +188,118 @@ class WiFiDensePoseApp { } } + // Initialize enhancement modules + initializeEnhancements() { + // Toast notifications + toastManager.init(); + + // Connection status widget in header + this.connectionStatus = new ConnectionStatus(); + this.connectionStatus.init(); + + // Theme toggle + this.themeToggle = new ThemeToggle(); + this.themeToggle.init(); + + // Performance monitor + this.perfMonitor = new PerfMonitor(); + this.perfMonitor.init(); + + // Activity log + this.activityLog = new ActivityLog(); + this.activityLog.init(); + + // Data export + this.dataExport = new DataExport(); + this.dataExport.init(); + + // Fullscreen manager + this.fullscreenManager = new FullscreenManager(); + this.fullscreenManager.init(); + + // Command palette (Ctrl+K) + this.commandPalette = new CommandPalette(this); + this.commandPalette.init(); + + // Mobile navigation (hamburger menu for small screens) + this.mobileNav = new MobileNav(); + this.mobileNav.init(); + + // Notification center (bell icon in header) + this.notificationCenter = new NotificationCenter(); + this.notificationCenter.init(); + + // Screenshot tool + this.screenshotTool = new ScreenshotTool(); + this.screenshotTool.init(); + + // Uptime clock + this.uptimeClock = new UptimeClock(); + this.uptimeClock.init(); + + // Quick settings panel + this.quickSettings = new QuickSettings(this); + this.quickSettings.init(); + + // Internationalization (EN/PL) + i18n.init(); + + // Keyboard shortcuts (pass app reference for tab switching) + this.keyboardShortcuts = new KeyboardShortcuts(this); + this.keyboardShortcuts.register('l', 'Toggle activity log', () => { + document.dispatchEvent(new CustomEvent('toggle-activity-log')); + }); + this.keyboardShortcuts.register('e', 'Export sensor data', () => { + document.dispatchEvent(new CustomEvent('export-data')); + }); + this.keyboardShortcuts.register('f', 'Toggle fullscreen', () => { + document.dispatchEvent(new CustomEvent('toggle-fullscreen')); + }); + this.keyboardShortcuts.register('s', 'Take screenshot', () => { + document.dispatchEvent(new CustomEvent('take-screenshot')); + }); + this.keyboardShortcuts.init(); + + // Listen for show-shortcuts from command palette + document.addEventListener('show-shortcuts', () => { + this.keyboardShortcuts.showHelp(); + }); + + // Register PWA service worker + this.registerServiceWorker(); + + // 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(); + } + + // Register service worker for offline capability + registerServiceWorker() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('./sw.js').then(reg => { + console.info('Service worker registered:', reg.scope); + }).catch(err => { + console.warn('Service worker registration failed:', err); + }); + } + } + // Handle tab changes handleTabChange(newTab, oldTab) { console.log(`Tab changed from ${oldTab} to ${newTab}`); @@ -272,45 +405,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 +431,29 @@ 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.commandPalette) this.commandPalette.dispose(); + if (this.activityLog) this.activityLog.dispose(); + if (this.dataExport) this.dataExport.dispose(); + if (this.fullscreenManager) this.fullscreenManager.dispose(); + if (this.connectionStatus) this.connectionStatus.dispose(); + if (this.mobileNav) this.mobileNav.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(); + if (this.screenshotTool) this.screenshotTool.dispose(); + if (this.uptimeClock) this.uptimeClock.dispose(); + if (this.quickSettings) this.quickSettings.dispose(); + i18n.dispose(); + toastManager.dispose(); } // Public API diff --git a/ui/components/TabManager.js b/ui/components/TabManager.js index d559c2ea..c2d35297 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/icons/generate.html b/ui/icons/generate.html new file mode 100644 index 00000000..161ad7c6 --- /dev/null +++ b/ui/icons/generate.html @@ -0,0 +1,66 @@ + + +RuView Icon Generator + +

Open this file in a browser and right-click to save the canvas images as icon-192.png and icon-512.png

+ + + + + diff --git a/ui/index.html b/ui/index.html index a68dc799..857ebf2f 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3,40 +3,48 @@ + + + + WiFi DensePose: Human Tracking Through Walls + + + Skip to main content +
-
+ -