Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions ui/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
201 changes: 163 additions & 38 deletions ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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');

Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
43 changes: 39 additions & 4 deletions ui/components/TabManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
Loading