Implement mobile-responsive expert mode for Spaxel dashboard

- Add hamburger menu for mobile panel navigation
- Create bottom sheet panels that slide from bottom on mobile
- Implement touch-optimized UI with 44x44px minimum tap targets
- Add mobile-specific panel content for Fleet Status, Zones, Triggers, Settings
- Support drag-to-close gesture on bottom sheets
- Maintain existing desktop panel behavior
- Integrate with existing systems (Viz3D, SpaxelPanels, SpatialQuickActions)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-06 06:24:47 -04:00
parent 8cfd0699c6
commit c7d37b834d
3 changed files with 1196 additions and 0 deletions

View file

@ -28,6 +28,7 @@
<link rel="stylesheet" href="css/quick-actions.css">
<link rel="stylesheet" href="css/briefing.css">
<link rel="stylesheet" href="css/simulator.css">
<link rel="stylesheet" href="static/css/mobile.css">
<style>
* {
margin: 0;
@ -4495,5 +4496,7 @@
}
})();
</script>
<!-- Mobile Responsive Expert Mode -->
<script type="module" src="static/js/mobile.js"></script>
</body>
</html>

View file

@ -0,0 +1,482 @@
/* ============================================
Mobile-Responsive Expert Mode Styles
============================================ */
/* Hamburger menu button in status bar */
.mobile-hamburger {
display: none;
position: absolute;
left: var(--space-3);
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
width: 44px;
height: 44px;
cursor: pointer;
padding: 10px;
z-index: 101;
}
.mobile-hamburger span {
display: block;
width: 24px;
height: 2px;
background: var(--text-primary);
margin: 5px 0;
transition: background var(--transition-fast);
border-radius: 2px;
}
.mobile-hamburger:hover span {
background: var(--color-primary);
}
.mobile-hamburger:active span {
background: var(--color-primary-hover);
}
/* Active state (X icon) */
.mobile-hamburger.active span:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.mobile-hamburger.active span:nth-child(2) {
opacity: 0;
}
.mobile-hamburger.active span:nth-child(3) {
transform: rotate(-45deg) translate(7px, -6px);
}
/* Mobile navigation overlay */
.mobile-nav-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--overlay-strong);
z-index: 998;
opacity: 0;
transition: opacity var(--transition-fast);
}
.mobile-nav-overlay.visible {
opacity: 1;
}
/* Mobile navigation panel */
.mobile-nav-panel {
display: none;
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 280px;
max-width: 85vw;
background: var(--bg-card);
z-index: 999;
transform: translateX(-100%);
transition: transform var(--transition-normal);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
box-shadow: 2px 0 16px var(--shadow-xl);
}
.mobile-nav-panel.visible {
transform: translateX(0);
}
.mobile-nav-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-bottom: 1px solid var(--border-default);
min-height: 56px;
}
.mobile-nav-panel-title {
font-size: var(--text-lg);
font-weight: var(--fw-heading);
color: var(--text-primary);
}
.mobile-nav-close {
background: transparent;
border: none;
width: 44px;
height: 44px;
cursor: pointer;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
border-radius: var(--radius-control);
transition: background var(--transition-fast);
}
.mobile-nav-close:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
/* Mobile nav section headers */
.mobile-nav-section {
border-bottom: 1px solid var(--border-subtle);
}
.mobile-nav-section-header {
padding: var(--space-3) var(--space-4);
font-size: var(--text-xs);
font-weight: var(--fw-heading);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Mobile nav list items */
.mobile-nav-list {
list-style: none;
padding: 0;
margin: 0;
}
.mobile-nav-item {
border-bottom: 1px solid var(--border-subtle);
}
.mobile-nav-item:last-child {
border-bottom: none;
}
.mobile-nav-link {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: var(--space-3) var(--space-4);
min-height: 48px;
background: transparent;
border: none;
color: var(--text-primary);
font-size: var(--text-sm);
font-family: var(--font-body);
text-align: left;
cursor: pointer;
transition: background var(--transition-fast);
-webkit-tap-highlight-color: transparent;
}
.mobile-nav-link:hover {
background: var(--bg-hover);
}
.mobile-nav-link:active {
background: var(--bg-active);
}
.mobile-nav-link-icon {
display: flex;
align-items: center;
gap: var(--space-3);
}
.mobile-nav-link-chevron {
color: var(--text-muted);
font-size: 18px;
}
/* Bottom sheet panel */
.mobile-bottom-sheet {
display: none;
position: fixed;
left: 0;
right: 0;
bottom: 0;
max-height: 70vh;
background: var(--bg-card);
border-radius: var(--radius-card) var(--radius-card) 0 0;
box-shadow: 0 -4px 24px var(--shadow-xl);
z-index: 997;
transform: translateY(100%);
transition: transform var(--transition-normal);
overflow: hidden;
-webkit-overflow-scrolling: touch;
}
.mobile-bottom-sheet.visible {
transform: translateY(0);
}
/* Bottom sheet drag handle */
.mobile-bottom-sheet-handle {
width: 100%;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
flex-shrink: 0;
touch-action: none;
}
.mobile-bottom-sheet-handle:active {
cursor: grabbing;
}
.mobile-bottom-sheet-handle-bar {
width: 40px;
height: 4px;
background: var(--border-default);
border-radius: 2px;
}
/* Bottom sheet header */
.mobile-bottom-sheet-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border-subtle);
min-height: 48px;
}
.mobile-bottom-sheet-title {
font-size: var(--text-base);
font-weight: var(--fw-heading);
color: var(--text-primary);
}
.mobile-bottom-sheet-close {
background: transparent;
border: none;
width: 44px;
height: 44px;
cursor: pointer;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
border-radius: var(--radius-control);
transition: background var(--transition-fast);
}
.mobile-bottom-sheet-close:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
/* Bottom sheet content */
.mobile-bottom-sheet-content {
padding: var(--space-3);
overflow-y: auto;
max-height: calc(70vh - 80px);
}
/* Mobile list items */
.mobile-list {
list-style: none;
padding: 0;
margin: 0;
}
.mobile-list-item {
padding: var(--space-3);
border-bottom: 1px solid var(--border-subtle);
min-height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
}
.mobile-list-item:last-child {
border-bottom: none;
}
.mobile-list-item-label {
font-size: var(--text-sm);
color: var(--text-primary);
}
.mobile-list-item-value {
font-size: var(--text-sm);
color: var(--text-secondary);
}
.mobile-list-item-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: var(--radius-pill);
font-size: var(--text-xs);
font-weight: var(--fw-medium);
}
.mobile-list-item-badge--success {
background: var(--green-9);
color: var(--green-2);
}
.mobile-list-item-badge--warning {
background: var(--yellow-9);
color: var(--yellow-2);
}
.mobile-list-item-badge--error {
background: var(--red-9);
color: var(--red-2);
}
.mobile-list-item-badge--info {
background: var(--blue-9);
color: var(--blue-2);
}
/* Mobile zone items */
.mobile-zone-item {
padding: var(--space-3);
border-bottom: 1px solid var(--border-subtle);
border-left: 3px solid transparent;
min-height: 44px;
}
.mobile-zone-item--armed {
border-left-color: var(--color-arming-armed);
background: rgba(var(--color-arming-armed-rgb), 0.1);
}
.mobile-zone-item--disarmed {
border-left-color: var(--color-arming-disarmed);
}
.mobile-zone-item-name {
font-size: var(--text-sm);
font-weight: var(--fw-medium);
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.mobile-zone-item-details {
display: flex;
justify-content: space-between;
font-size: var(--text-xs);
color: var(--text-secondary);
}
/* Mobile trigger items */
.mobile-trigger-item {
padding: var(--space-3);
border-bottom: 1px solid var(--border-subtle);
min-height: 44px;
}
.mobile-trigger-item-name {
font-size: var(--text-sm);
font-weight: var(--fw-medium);
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.mobile-trigger-item-condition {
font-size: var(--text-xs);
color: var(--text-secondary);
}
/* Mobile settings items */
.mobile-settings-item {
padding: var(--space-3);
border-bottom: 1px solid var(--border-subtle);
min-height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
}
.mobile-settings-label {
font-size: var(--text-sm);
color: var(--text-primary);
}
.mobile-settings-toggle {
width: 52px;
height: 32px;
background: var(--slate-7);
border-radius: var(--radius-pill);
position: relative;
cursor: pointer;
transition: background var(--transition-fast);
}
.mobile-settings-toggle--on {
background: var(--color-primary);
}
.mobile-settings-toggle-knob {
position: absolute;
top: 4px;
left: 4px;
width: 24px;
height: 24px;
background: white;
border-radius: 50%;
transition: transform var(--transition-fast);
box-shadow: 0 2px 4px var(--shadow-md);
}
.mobile-settings-toggle--on .mobile-settings-toggle-knob {
transform: translateX(20px);
}
/* Mobile breakpoint: show hamburger and mobile nav */
@media (max-width: 767px) {
.mobile-hamburger {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.mobile-nav-overlay,
.mobile-nav-panel,
.mobile-bottom-sheet {
display: block;
}
/* Adjust status bar to account for hamburger */
.live-status-bar {
padding-left: calc(var(--space-3) + 44px);
}
/* Hide desktop panels on mobile */
.live-panel--left,
.live-panel--right,
.live-panel--presence,
#node-panel,
#presence-panel {
display: none !important;
}
}
/* Extra small devices */
@media (max-width: 374px) {
.mobile-nav-panel {
width: 100%;
max-width: 100%;
}
}
/* Safe area support for notched devices */
@supports (padding: max(0px)) {
.mobile-bottom-sheet {
padding-bottom: env(safe-area-inset-bottom);
}
.mobile-nav-panel {
padding-top: env(safe-area-inset-top);
}
}

View file

@ -0,0 +1,711 @@
/**
* Spaxel Dashboard - Mobile Expert Mode
*
* Mobile-responsive enhancements for expert mode:
* - Hamburger menu for panel navigation
* - Bottom sheet panels for mobile
* - Touch-optimized interactions
* - No hover-dependent UI
*/
(function() {
'use strict';
// ============================================
// State
// ============================================
let hamburgerMenu = null;
let mobileNavOpen = false;
let activeBottomSheet = null;
// ============================================
// Configuration
// ============================================
const MOBILE_BREAKPOINT = 768; // px
const BOTTOM_SHEET_MAX_HEIGHT = '70vh';
const BOTTOM_SHEET_MIN_HEIGHT = '40vh';
// ============================================
// Hamburger Menu
// ============================================
/**
* Create hamburger menu button
*/
function createHamburgerMenu() {
if (document.getElementById('mobile-hamburger')) {
return;
}
const hamburger = document.createElement('button');
hamburger.id = 'mobile-hamburger';
hamburger.className = 'mobile-hamburger';
hamburger.setAttribute('aria-label', 'Open menu');
hamburger.setAttribute('aria-expanded', 'false');
hamburger.innerHTML = `
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
`;
hamburger.addEventListener('click', toggleMobileNav);
// Insert into status bar
const statusBar = document.getElementById('status-bar');
if (statusBar) {
statusBar.insertBefore(hamburger, statusBar.firstChild);
}
hamburgerMenu = hamburger;
console.log('[Mobile] Hamburger menu created');
}
/**
* Toggle mobile navigation
*/
function toggleMobileNav() {
mobileNavOpen = !mobileNavOpen;
if (mobileNavOpen) {
openMobileNav();
} else {
closeMobileNav();
}
}
/**
* Open mobile navigation
*/
function openMobileNav() {
mobileNavOpen = true;
if (hamburgerMenu) {
hamburgerMenu.classList.add('active');
hamburgerMenu.setAttribute('aria-expanded', 'true');
}
// Close any open bottom sheets
closeActiveBottomSheet();
// Create mobile nav overlay
let navOverlay = document.getElementById('mobile-nav-overlay');
if (!navOverlay) {
navOverlay = document.createElement('div');
navOverlay.id = 'mobile-nav-overlay';
navOverlay.className = 'mobile-nav-overlay';
navOverlay.addEventListener('click', closeMobileNav);
document.body.appendChild(navOverlay);
}
// Create mobile nav panel
let navPanel = document.getElementById('mobile-nav-panel');
if (!navPanel) {
navPanel = createMobileNavPanel();
document.body.appendChild(navPanel);
}
// Show navigation
requestAnimationFrame(() => {
navOverlay.classList.add('visible');
navPanel.classList.add('visible');
});
// Disable OrbitControls
if (window.Viz3D && window.Viz3D.controls) {
const controls = window.Viz3D.controls();
if (controls) controls.enabled = false;
}
}
/**
* Close mobile navigation
*/
function closeMobileNav() {
mobileNavOpen = false;
if (hamburgerMenu) {
hamburgerMenu.classList.remove('active');
hamburgerMenu.setAttribute('aria-expanded', 'false');
}
const navOverlay = document.getElementById('mobile-nav-overlay');
const navPanel = document.getElementById('mobile-nav-panel');
if (navOverlay) navOverlay.classList.remove('visible');
if (navPanel) navPanel.classList.remove('visible');
// Re-enable OrbitControls
if (window.Viz3D && window.Viz3D.controls) {
const controls = window.Viz3D.controls();
if (controls) controls.enabled = true;
}
}
/**
* Create mobile navigation panel
*/
function createMobileNavPanel() {
const panel = document.createElement('div');
panel.id = 'mobile-nav-panel';
panel.className = 'mobile-nav-panel';
// Panel sections
const sections = [
{
title: 'Panels',
items: [
{ id: 'fleet-status', label: 'Fleet Status', icon: '📡', action: openFleetStatus },
{ id: 'zones', label: 'Zones', icon: '🏠', action: openZonesPanel },
{ id: 'triggers', label: 'Automation', icon: '⚡', action: openTriggersPanel },
{ id: 'settings', label: 'Settings', icon: '⚙️', action: openSettingsPanel }
]
},
{
title: 'View',
items: [
{ id: 'reset-view', label: 'Reset View', icon: '🎯', action: resetView },
{ id: 'toggle-layers', label: 'Toggle Layers', icon: '👁️', action: toggleLayers },
{ id: 'simple-mode', label: 'Simple Mode', icon: '📱', action: switchToSimpleMode }
]
}
];
let html = '<div class="mobile-nav-content">';
sections.forEach(section => {
html += `
<div class="mobile-nav-section">
<h3 class="mobile-nav-section-title">${section.title}</h3>
<div class="mobile-nav-items">
`;
section.items.forEach(item => {
html += `
<button class="mobile-nav-item" data-action="${item.id}">
<span class="mobile-nav-icon">${item.icon}</span>
<span class="mobile-nav-label">${item.label}</span>
</button>
`;
});
html += `
</div>
</div>
`;
});
html += '</div>';
panel.innerHTML = html;
// Add event listeners
panel.querySelectorAll('.mobile-nav-item').forEach(item => {
item.addEventListener('click', (e) => {
const actionId = e.currentTarget.dataset.action;
handleMobileNavAction(actionId);
});
});
return panel;
}
/**
* Handle mobile navigation actions
*/
function handleMobileNavAction(actionId) {
closeMobileNav();
// Small delay to allow nav to close before opening panel
setTimeout(() => {
switch (actionId) {
case 'fleet-status':
openFleetStatus();
break;
case 'zones':
openZonesPanel();
break;
case 'triggers':
openTriggersPanel();
break;
case 'settings':
openSettingsPanel();
break;
case 'reset-view':
resetView();
break;
case 'toggle-layers':
toggleLayers();
break;
case 'simple-mode':
if (window.SpaxelRouter) {
window.SpaxelRouter.navigate('simple');
}
break;
}
}, 300);
}
// ============================================
// Panel Actions
// ============================================
function openFleetStatus() {
if (window.SpaxelPanels) {
openBottomSheet('fleet-status', 'Fleet Status', createFleetStatusContent());
}
}
function openZonesPanel() {
if (window.SpaxelPanels) {
openBottomSheet('zones', 'Zones', createZonesContent());
}
}
function openTriggersPanel() {
if (window.SpaxelPanels) {
openBottomSheet('triggers', 'Automation Triggers', createTriggersContent());
}
}
function openSettingsPanel() {
if (window.SpaxelPanels) {
openBottomSheet('settings', 'Settings', createSettingsContent());
}
}
function resetView() {
if (window.Viz3D && window.Viz3D.resetView) {
window.Viz3D.resetView();
}
}
function toggleLayers() {
if (window.Viz3D && window.Viz3D.toggleLayers) {
window.Viz3D.toggleLayers();
}
}
// ============================================
// Bottom Sheet Panels (Mobile)
// ============================================
/**
* Open bottom sheet panel on mobile
*/
function openBottomSheet(id, title, content) {
if (!isMobile()) {
// On desktop, use regular sidebar
window.SpaxelPanels.openSidebar({
title: title,
content: content,
width: '360px',
position: 'right'
});
return;
}
// Close existing bottom sheet
closeActiveBottomSheet();
// Create bottom sheet
const sheet = document.createElement('div');
sheet.id = `bottom-sheet-${id}`;
sheet.className = 'mobile-bottom-sheet';
sheet.innerHTML = `
<div class="bottom-sheet-handle"></div>
<div class="bottom-sheet-header">
<h2 class="bottom-sheet-title">${title}</h2>
<button class="bottom-sheet-close" aria-label="Close">&times;</button>
</div>
<div class="bottom-sheet-content"></div>
`;
// Add content
const contentEl = sheet.querySelector('.bottom-sheet-content');
if (typeof content === 'string') {
contentEl.innerHTML = content;
} else if (content instanceof HTMLElement) {
contentEl.appendChild(content);
} else if (typeof content === 'function') {
const result = content(contentEl);
if (result instanceof HTMLElement) {
contentEl.innerHTML = '';
contentEl.appendChild(result);
}
}
// Close button handler
sheet.querySelector('.bottom-sheet-close').addEventListener('click', closeActiveBottomSheet);
// Handle drag to close
setupBottomSheetDrag(sheet);
// Add to DOM
document.body.appendChild(sheet);
// Disable OrbitControls
if (window.Viz3D && window.Viz3D.controls) {
const controls = window.Viz3D.controls();
if (controls) controls.enabled = false;
}
// Show with animation
requestAnimationFrame(() => {
sheet.classList.add('visible');
});
activeBottomSheet = sheet;
console.log('[Mobile] Bottom sheet opened:', id);
}
/**
* Close active bottom sheet
*/
function closeActiveBottomSheet() {
if (!activeBottomSheet) return;
const sheet = activeBottomSheet;
sheet.classList.remove('visible');
setTimeout(() => {
if (sheet.parentNode) {
sheet.parentNode.removeChild(sheet);
}
// Re-enable OrbitControls
if (window.Viz3D && window.Viz3D.controls) {
const controls = window.Viz3D.controls();
if (controls) controls.enabled = true;
}
activeBottomSheet = null;
}, 300);
}
/**
* Set up bottom sheet drag to close
*/
function setupBottomSheetDrag(sheet) {
const handle = sheet.querySelector('.bottom-sheet-handle');
const content = sheet.querySelector('.bottom-sheet-content');
let startY = 0;
let currentY = 0;
let isDragging = false;
handle.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY;
isDragging = true;
}, { passive: true });
handle.addEventListener('touchmove', (e) => {
if (!isDragging) return;
currentY = e.touches[0].clientY;
const deltaY = currentY - startY;
// Only allow dragging down
if (deltaY > 0) {
const translateY = Math.min(deltaY, window.innerHeight * 0.7);
sheet.style.transform = `translateY(${translateY}px)`;
}
}, { passive: true });
handle.addEventListener('touchend', () => {
if (!isDragging) return;
isDragging = false;
const deltaY = currentY - startY;
// If dragged more than 100px down, close the sheet
if (deltaY > 100) {
closeActiveBottomSheet();
} else {
// Reset position
sheet.style.transform = '';
}
});
}
// ============================================
// Content Generators
// ============================================
function createFleetStatusContent(container) {
// Fetch fleet status
fetch('/api/nodes')
.then(response => response.json())
.then(nodes => {
if (nodes.length === 0) {
container.innerHTML = '<div class="mobile-empty">No nodes found</div>';
return;
}
let html = '<div class="mobile-list">';
nodes.forEach(node => {
const statusClass = node.status === 'online' ? 'status-online' :
node.status === 'stale' ? 'status-stale' : 'status-offline';
html += `
<div class="mobile-list-item" data-mac="${node.mac}">
<div class="mobile-list-status ${statusClass}"></div>
<div class="mobile-list-content">
<div class="mobile-list-title">${node.name || node.mac}</div>
<div class="mobile-list-subtitle">${node.role || 'Unknown'} ${node.firmware_version || 'Unknown'}</div>
</div>
<button class="mobile-list-action" aria-label="Options"></button>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
// Add click handlers
container.querySelectorAll('.mobile-list-item').forEach(item => {
const mac = item.dataset.mac;
item.addEventListener('click', () => {
// Open node details
if (window.SpatialQuickActions) {
const node = nodes.find(n => n.mac === mac);
if (node) {
window.SpatialQuickActions.show(
window.innerWidth / 2,
window.innerHeight / 2,
'node',
node
);
}
}
});
});
})
.catch(error => {
console.error('[Mobile] Error fetching fleet status:', error);
container.innerHTML = '<div class="mobile-error">Failed to load fleet status</div>';
});
}
function createZonesContent(container) {
// Fetch zones
fetch('/api/zones')
.then(response => response.json())
.then(zones => {
if (zones.length === 0) {
container.innerHTML = '<div class="mobile-empty">No zones defined</div>';
return;
}
let html = '<div class="mobile-list">';
zones.forEach(zone => {
const occupancy = zone.occupancy || 0;
const people = zone.people || [];
html += `
<div class="mobile-list-item" data-zone-id="${zone.id}">
<div class="mobile-list-icon">🏠</div>
<div class="mobile-list-content">
<div class="mobile-list-title">${zone.name}</div>
<div class="mobile-list-subtitle">${occupancy} person${occupancy !== 1 ? 's' : ''}</div>
</div>
<button class="mobile-list-action" aria-label="Options"></button>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
// Add click handlers
container.querySelectorAll('.mobile-list-item').forEach(item => {
const zoneId = item.dataset.zoneId;
item.addEventListener('click', () => {
const zone = zones.find(z => z.id == zoneId);
if (zone && window.SpatialQuickActions) {
window.SpatialQuickActions.show(
window.innerWidth / 2,
window.innerHeight / 2,
'zone',
zone
);
}
});
});
})
.catch(error => {
console.error('[Mobile] Error fetching zones:', error);
container.innerHTML = '<div class="mobile-error">Failed to load zones</div>';
});
}
function createTriggersContent(container) {
// Fetch triggers
fetch('/api/triggers')
.then(response => response.json())
.then(triggers => {
if (triggers.length === 0) {
container.innerHTML = '<div class="mobile-empty">No automation triggers</div>';
return;
}
let html = '<div class="mobile-list">';
triggers.forEach(trigger => {
const enabledClass = trigger.enabled ? 'trigger-enabled' : 'trigger-disabled';
html += `
<div class="mobile-list-item" data-trigger-id="${trigger.id}">
<div class="mobile-list-icon ${enabledClass}"></div>
<div class="mobile-list-content">
<div class="mobile-list-title">${trigger.name}</div>
<div class="mobile-list-subtitle">${trigger.condition || 'Unknown condition'}</div>
</div>
<button class="mobile-list-action" aria-label="Options"></button>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
// Add click handlers
container.querySelectorAll('.mobile-list-item').forEach(item => {
const triggerId = item.dataset.triggerId;
item.addEventListener('click', () => {
const trigger = triggers.find(t => t.id == triggerId);
if (trigger && window.SpatialQuickActions) {
window.SpatialQuickActions.show(
window.innerWidth / 2,
window.innerHeight / 2,
'trigger',
trigger
);
}
});
});
})
.catch(error => {
console.error('[Mobile] Error fetching triggers:', error);
container.innerHTML = '<div class="mobile-error">Failed to load triggers</div>';
});
}
function createSettingsContent() {
return `
<div class="mobile-list">
<div class="mobile-list-item" data-setting="detection">
<div class="mobile-list-icon">🎯</div>
<div class="mobile-list-content">
<div class="mobile-list-title">Detection</div>
<div class="mobile-list-subtitle">Thresholds & sensitivity</div>
</div>
<button class="mobile-list-action"></button>
</div>
<div class="mobile-list-item" data-setting="display">
<div class="mobile-list-icon">🎨</div>
<div class="mobile-list-content">
<div class="mobile-list-title">Display</div>
<div class="mobile-list-subtitle">Appearance & layers</div>
</div>
<button class="mobile-list-action"></button>
</div>
<div class="mobile-list-item" data-setting="notifications">
<div class="mobile-list-icon">🔔</div>
<div class="mobile-list-content">
<div class="mobile-list-title">Notifications</div>
<div class="mobile-list-subtitle">Alerts & push</div>
</div>
<button class="mobile-list-action"></button>
</div>
<div class="mobile-list-item" data-setting="integrations">
<div class="mobile-list-icon">🔗</div>
<div class="mobile-list-content">
<div class="mobile-list-title">Integrations</div>
<div class="mobile-list-subtitle">Home Assistant, MQTT</div>
</div>
<button class="mobile-list-action"></button>
</div>
<div class="mobile-list-item" data-setting="help">
<div class="mobile-list-icon"></div>
<div class="mobile-list-content">
<div class="mobile-list-title">Help & Troubleshooting</div>
<div class="mobile-list-subtitle">Guides & diagnostics</div>
</div>
<button class="mobile-list-action"></button>
</div>
</div>
`;
}
// ============================================
// Utilities
// ============================================
/**
* Check if on mobile device
*/
function isMobile() {
return window.innerWidth < MOBILE_BREAKPOINT ||
('ontouchstart' in window && navigator.maxTouchPoints > 0);
}
/**
* Update hamburger menu visibility based on screen size
*/
function updateHamburgerVisibility() {
if (!hamburgerMenu) return;
if (isMobile()) {
hamburgerMenu.style.display = 'flex';
} else {
hamburgerMenu.style.display = 'none';
closeMobileNav();
}
}
// ============================================
// Public API
// ============================================
window.MobileExpertMode = {
init: init,
openNav: openMobileNav,
closeNav: closeMobileNav,
openBottomSheet: openBottomSheet,
closeBottomSheet: closeActiveBottomSheet,
isMobile: isMobile
};
// ============================================
// Initialization
// ============================================
function init() {
console.log('[Mobile] Initializing mobile expert mode...');
// Create hamburger menu
createHamburgerMenu();
// Update visibility on resize
window.addEventListener('resize', updateHamburgerVisibility);
updateHamburgerVisibility();
// Handle orientation changes
window.addEventListener('orientationchange', () => {
setTimeout(updateHamburgerVisibility, 100);
});
console.log('[Mobile] Mobile expert mode initialized');
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
console.log('[Mobile] Module loaded');
})();