feat: make expert mode mobile-responsive with touch orbit/pan/zoom

- Add touch-action: none to scene container to prevent default browser touch behaviors
- Configure OrbitControls for touch gestures (one-finger rotate, two-finger pan, pinch zoom)
- Add comprehensive mobile responsive CSS with media queries
- Add mobile menu toggle button to show/hide panels on small screens
- Add touch-specific optimizations for larger touch targets (44px minimum)
- Optimize panel layouts for mobile (collapsible panels, adjusted spacing)
- Disable hover effects on touch devices, use active states instead
- Add touch event handlers to prevent accidental panel collapse

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-10 04:33:13 -04:00
parent 81d5aa3a49
commit e0d0879598
4 changed files with 392 additions and 3 deletions

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
6b22ba65acb3e583cdd7cb786d7104402f85a495
b583990d434dd472d8bac8cfb60f3129ff93e56a

View file

@ -38,6 +38,7 @@
#scene-container {
width: 100vw;
height: 100vh;
touch-action: none; /* Prevent default touch behaviors for OrbitControls */
}
/* Status bar at top */
@ -2181,6 +2182,297 @@
background: rgba(255, 255, 255, 0.15);
}
/* ===== Mobile Menu Toggle Button ===== */
#mobile-menu-btn {
display: none;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ccc;
font-size: 14px;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
#mobile-menu-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
#mobile-menu-btn svg {
width: 18px;
height: 18px;
vertical-align: middle;
}
/* ===== Mobile Responsive Styles ===== */
@media (max-width: 768px) {
/* Show mobile menu button */
#mobile-menu-btn {
display: block;
order: -1; /* Move to beginning of status bar */
}
/* Status bar - compact on mobile */
#status-bar {
height: auto;
flex-wrap: wrap;
padding: 8px;
gap: 8px;
font-size: 12px;
}
.status-item {
font-size: 11px;
}
/* Hide less important status items on mobile */
.status-item:has(#fps-counter),
.status-item:has(#link-count) {
display: none;
}
/* Node panel - collapsible on mobile */
#node-panel {
top: auto;
bottom: 0;
left: 0;
right: 0;
width: auto;
max-height: 50vh;
border-radius: 12px 12px 0 0;
transform: translateY(calc(100% - 40px));
transition: transform 0.3s ease;
}
#node-panel.expanded {
transform: translateY(0);
}
/* Chart panel - smaller on mobile */
#chart-panel {
width: calc(100% - 16px);
right: 8px;
left: 8px;
bottom: 8px;
height: 200px;
}
/* Presence panel - hide on mobile (use timeline instead) */
#presence-panel {
display: none;
}
/* Buttons - smaller on mobile */
.view-btn, #gdop-toggle-btn, #fresnel-toggle-btn, #room-editor-btn,
#floorplan-btn, #simulator-btn, #pause-live-btn, #ble-btn,
#settings-btn, #add-node-btn, #add-virtual-btn {
padding: 4px 8px;
font-size: 11px;
}
/* Detection quality gauge - compact */
#detection-quality {
padding: 2px 6px;
}
#quality-gauge-container {
width: 24px;
height: 24px;
}
#quality-value {
font-size: 7px;
}
#quality-label {
display: none;
}
/* Room editor panel - full width on mobile */
#room-editor-panel {
top: auto;
bottom: 0;
left: 0;
right: 0;
width: auto;
border-radius: 12px 12px 0 0;
transform: translateY(calc(100% - 40px));
transition: transform 0.3s ease;
}
#room-editor-panel.expanded {
transform: translateY(0);
}
/* GDOP legend - smaller on mobile */
#gdop-legend {
left: 8px;
bottom: 220px;
padding: 8px;
}
#gdop-gradient {
width: 100px;
}
/* Toast notifications - full width on mobile */
#toast-container {
bottom: 220px;
left: 8px;
right: 8px;
transform: none;
}
.toast {
font-size: 13px;
padding: 10px 16px;
}
/* Wizard - full screen on mobile */
#wizard-card {
width: 100%;
height: 100vh;
max-height: 100vh;
border-radius: 0;
padding: 20px;
}
/* Onboarding illustration - smaller on mobile */
.esp32-illustration {
max-width: 200px;
}
/* Anomaly banner - smaller on mobile */
.anomaly-banner {
padding: 16px;
margin: 0 8px;
}
/* Replay control bar - compact on mobile */
.replay-control-bar {
width: calc(100% - 16px);
padding: 10px 16px;
flex-wrap: wrap;
justify-content: center;
}
.replay-info {
order: -1;
width: 100%;
text-align: center;
margin-bottom: 8px;
}
/* Timeline - full screen on mobile */
#timeline-view {
padding: 0;
}
.timeline-header {
padding: 12px;
}
.timeline-filter-bar {
padding: 12px;
}
/* Simulator panel - full screen on mobile */
.simulator-panel {
width: 100%;
height: 100vh;
border-radius: 0;
}
/* Briefing card - full width on mobile */
#briefing-card {
width: calc(100% - 16px);
margin: 8px;
}
/* Quick actions - larger touch targets on mobile */
.quick-action-item {
min-height: 48px;
padding: 12px 16px;
}
/* Command palette - full width on mobile */
.command-palette-overlay {
padding: 8px;
}
.command-palette {
max-width: 100%;
}
/* Morning briefing - adjust spacing */
#morning-briefing-panel {
width: calc(100% - 16px);
margin: 8px;
}
}
/* Touch device optimizations */
@media (hover: none) and (pointer: coarse) {
/* Larger touch targets for touch devices */
.view-btn, #gdop-toggle-btn, #fresnel-toggle-btn, #room-editor-btn,
#floorplan-btn, #simulator-btn, #pause-live-btn, #ble-btn,
#settings-btn, #add-node-btn, #add-virtual-btn {
min-height: 44px;
min-width: 44px;
padding: 8px 12px;
}
/* Node items - larger touch targets */
.node-item {
padding: 12px 8px;
min-height: 44px;
}
/* Link items - larger touch targets */
.link-item {
padding: 10px 8px;
min-height: 44px;
}
/* Toast notifications - larger on touch devices */
.toast {
padding: 14px 20px;
font-size: 14px;
}
/* Replay controls - larger touch targets */
.replay-btn {
width: 44px;
height: 44px;
}
/* Timeline events - larger touch targets */
.timeline-event-item {
padding: 16px;
}
/* Disable hover effects on touch devices */
.node-item:hover,
.link-item:hover,
.view-btn:hover,
#gdop-toggle-btn:hover,
#fresnel-toggle-btn:hover,
.room-field input:hover {
background: initial;
}
/* Use active states instead */
.node-item:active,
.link-item:active,
.view-btn:active,
#gdop-toggle-btn:active,
#fresnel-toggle-btn:active {
background: rgba(255, 255, 255, 0.15);
}
}
/* ===== Replay Control Bar ===== */
#pause-live-btn {
background: rgba(255, 193, 7, 0.15);
@ -2526,6 +2818,13 @@
<span id="ws-status-text">Disconnected</span>
<span id="ws-reconnect-spinner"></span>
</div>
<button id="mobile-menu-btn" onclick="toggleMobilePanels()" aria-label="Toggle panels">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div class="status-item">
<span>Nodes: <strong id="node-count">0</strong></span>
</div>
@ -3040,6 +3339,79 @@
</div>
</div>
<script>
// Mobile panel toggle functionality
function toggleMobilePanels() {
var nodePanel = document.getElementById('node-panel');
var roomEditor = document.getElementById('room-editor-panel');
var chartPanel = document.getElementById('chart-panel');
// Toggle node panel
if (nodePanel) {
nodePanel.classList.toggle('expanded');
}
// Toggle room editor panel
if (roomEditor) {
roomEditor.classList.toggle('expanded');
}
// On mobile, hide chart panel when panels are shown, show when hidden
if (chartPanel) {
var isExpanded = nodePanel && nodePanel.classList.contains('expanded');
chartPanel.style.display = isExpanded ? 'none' : 'block';
}
}
// Auto-collapse panels on mobile when clicking outside
if (window.matchMedia('(max-width: 768px)').matches) {
document.addEventListener('click', function(e) {
var nodePanel = document.getElementById('node-panel');
var roomEditor = document.getElementById('room-editor-panel');
var menuBtn = document.getElementById('mobile-menu-btn');
// If clicking outside panels and menu button
if (menuBtn && !menuBtn.contains(e.target)) {
var clickedInsideNodePanel = nodePanel && nodePanel.contains(e.target);
var clickedInsideRoomEditor = roomEditor && roomEditor.contains(e.target);
if (!clickedInsideNodePanel && !clickedInsideRoomEditor) {
if (nodePanel) nodePanel.classList.remove('expanded');
if (roomEditor) roomEditor.classList.remove('expanded');
}
}
});
}
// Handle touch events on panels to prevent accidental collapse
function setupPanelTouchHandlers(panelId) {
var panel = document.getElementById(panelId);
if (!panel) return;
var touchStartY = 0;
panel.addEventListener('touchstart', function(e) {
touchStartY = e.touches[0].clientY;
}, { passive: true });
panel.addEventListener('touchmove', function(e) {
// Allow scrolling within the panel
var currentY = e.touches[0].clientY;
var deltaY = currentY - touchStartY;
// If scrolling down and at top, or scrolling up and at bottom
if ((deltaY > 0 && this.scrollTop === 0) ||
(deltaY < 0 && this.scrollTop + this.clientHeight >= this.scrollHeight)) {
// Allow the scroll
}
}, { passive: true });
}
// Initialize touch handlers for panels when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
setupPanelTouchHandlers('node-panel');
setupPanelTouchHandlers('room-editor-panel');
});
// Keep view-preset buttons in sync with active state
['perspective','topdown','follow'].forEach(function(p) {
document.getElementById('view-' + p).addEventListener('click', function() {

View file

@ -110,6 +110,23 @@
controls.maxDistance = 50;
controls.maxPolarAngle = Math.PI / 2 + 0.3; // Allow slight below-ground view
// Touch-specific optimizations for mobile
controls.rotateSpeed = 0.8; // Rotation speed
controls.zoomSpeed = 1.2; // Zoom speed for pinch gesture
controls.panSpeed = 0.8; // Pan speed for two-finger drag
controls.enablePan = true; // Enable pan (two-finger drag)
controls.enableZoom = true; // Enable zoom (pinch gesture)
controls.enableRotate = true; // Enable rotate (one-finger drag)
// Touch gesture configuration
// One finger: rotate (orbit)
// Two fingers: pan
// Pinch: zoom
controls.touches = {
ONE: THREE.TOUCH.ROTATE, // One-finger touch rotates the camera
TWO: THREE.TOUCH.DOLLY_PAN // Two-finger touch zooms and pans
};
// Grid helper (XZ plane, Y-up)
gridHelper = new THREE.GridHelper(
CONFIG.gridWidth,