- Simplified OrbitControls touch gesture configuration: - ONE: THREE.TOUCH.ROTATE (one-finger orbit) - TWO: THREE.TOUCH.DOLLY (pinch zoom only, no accidental pan) - THREE: THREE.TOUCH.PAN (three-finger pan) - Removed complex dynamic enablePan toggling that didn't work reliably with OrbitControls' internal touch processing - Added iOS Safari-specific touch improvements: - Double-tap zoom prevention - Touch action and user select CSS properties - -webkit-tap-highlight-color: transparent - Enhanced CSS for better mobile touch handling: - touch-action: none on scene container and canvas - -webkit-touch-callout: none - -webkit-user-select: none - user-select: none All mobile tests (29 tests) and quick-actions tests (22 tests) pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
359 lines
8.5 KiB
CSS
359 lines
8.5 KiB
CSS
/**
|
|
* Spaxel Dashboard - 3D Scene Styles
|
|
*
|
|
* 3D scene layout with responsive canvas sizing.
|
|
* Handles iOS Safari visual viewport quirks and bottom navigation spacing.
|
|
*
|
|
* WCAG 2.1 Touch Target Compliance:
|
|
* - All interactive elements meet minimum 44x44px touch target size
|
|
* - Touch targets are expanded using padding or pseudo-elements where needed
|
|
*/
|
|
|
|
/* ===== WCAG 2.1 Touch Target Compliance Utilities ===== */
|
|
|
|
/* Touch target expansion for small checkboxes */
|
|
.panel-form-checkbox input[type="checkbox"],
|
|
.pattern-checkbox input[type="checkbox"],
|
|
.timeline-category-checkbox input[type="checkbox"],
|
|
.sim-gdop-controls input[type="checkbox"] {
|
|
/* Ensure checkbox touch area is 44x44px minimum */
|
|
width: 18px;
|
|
height: 18px;
|
|
min-width: 18px;
|
|
min-height: 18px;
|
|
position: relative;
|
|
}
|
|
|
|
/* Expand checkbox hit area with pseudo-element */
|
|
.panel-form-checkbox input[type="checkbox"]::before,
|
|
.pattern-checkbox input[type="checkbox"]::before,
|
|
.timeline-category-checkbox input[type="checkbox"]::before,
|
|
.sim-gdop-controls input[type="checkbox"]::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
/* Expand small toggle switches to 44px height */
|
|
.toggle-switch {
|
|
min-height: 44px;
|
|
}
|
|
|
|
.toggle-switch .slider {
|
|
min-height: 44px;
|
|
height: 44px;
|
|
}
|
|
|
|
.toggle-switch .slider:before {
|
|
width: 36px;
|
|
height: 36px;
|
|
top: var(--space-1);
|
|
left: var(--space-1);
|
|
}
|
|
|
|
.toggle-switch input:checked + .slider:before {
|
|
transform: translateX(22px);
|
|
}
|
|
|
|
/* Expand slider thumb touch area */
|
|
.panel-form-group input[type="range"] {
|
|
height: 44px; /* Full touch target height */
|
|
background: transparent;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.panel-form-group input[type="range"]::-webkit-slider-runnable-track {
|
|
height: 6px;
|
|
background: var(--bg-hover);
|
|
border-radius: var(--radius-control);
|
|
margin: 19px; /* Center track within 44px height */
|
|
}
|
|
|
|
.panel-form-group input[type="range"]::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 32px;
|
|
height: 32px;
|
|
background: var(--blue-10);
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
margin-top: -13px; /* Center thumb on track */
|
|
transition: transform 0.1s;
|
|
}
|
|
|
|
.panel-form-group input[type="range"]::-webkit-slider-thumb:hover {
|
|
transform: scale(1.15);
|
|
}
|
|
|
|
.panel-form-group input[type="range"]::-moz-range-track {
|
|
height: 6px;
|
|
background: var(--bg-hover);
|
|
border-radius: var(--radius-control);
|
|
}
|
|
|
|
.panel-form-group input[type="range"]::-moz-range-thumb {
|
|
width: 32px;
|
|
height: 32px;
|
|
background: var(--blue-10);
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
/* Expand close button touch targets to 44x44px */
|
|
.panel-close,
|
|
.modal-close,
|
|
.panel-modal-close,
|
|
.sim-close-btn {
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
width: 44px;
|
|
height: 44px;
|
|
font-size: var(--text-3xl);
|
|
line-height: 44px;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Expand context menu items to 44px minimum height */
|
|
.context-item,
|
|
.blob-context-menu-item {
|
|
min-height: 44px;
|
|
padding: var(--space-3) var(--space-4);
|
|
}
|
|
|
|
/* Expand link list items to 44px minimum */
|
|
.link-item {
|
|
min-height: 44px;
|
|
padding: var(--space-3) var(--space-4);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
/* Ensure all buttons meet 44x44px minimum */
|
|
.panel-btn,
|
|
.modal-btn,
|
|
.btn,
|
|
.sim-btn,
|
|
.alert-banner-btn,
|
|
.timeline-filter-toggle,
|
|
.timeline-load-more-btn,
|
|
.view-btn,
|
|
#floorplan-btn,
|
|
.node-identify-btn {
|
|
min-height: 44px;
|
|
min-width: 44px;
|
|
padding: var(--space-3) var(--space-5);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
/* Small buttons: expand with padding while keeping appearance compact */
|
|
.btn-sm,
|
|
.timeline-feedback-btn,
|
|
.timeline-seek-btn {
|
|
min-height: 44px;
|
|
min-width: 44px;
|
|
padding: 14px var(--space-3); /* Extra vertical padding to reach 44px */
|
|
}
|
|
|
|
/* Volume tools buttons */
|
|
.volume-tools button {
|
|
min-height: 44px;
|
|
min-width: 44px;
|
|
padding: var(--space-3) var(--space-4);
|
|
}
|
|
|
|
/* Hamburger tabs - ensure 44px minimum */
|
|
.hamburger-tab {
|
|
min-height: 44px;
|
|
min-width: 44px;
|
|
}
|
|
|
|
/* Hamburger items - already 44px, ensure explicit */
|
|
.hamburger-item {
|
|
min-height: 44px;
|
|
padding: var(--space-3) var(--space-4);
|
|
}
|
|
|
|
/* Toast dismiss button */
|
|
.toast-dismiss {
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
width: 44px;
|
|
height: 44px;
|
|
font-size: var(--text-xl);
|
|
}
|
|
|
|
/* Action items */
|
|
.action-item .action-remove {
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
padding: var(--space-3) var(--space-2);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
/* Sim item delete button */
|
|
.sim-item-delete {
|
|
min-height: 44px;
|
|
min-width: 44px;
|
|
padding: var(--space-3) var(--space-4);
|
|
}
|
|
|
|
/* Form controls */
|
|
.panel-form-group input[type="text"],
|
|
.panel-form-group input[type="number"],
|
|
.panel-form-group input[type="password"],
|
|
.panel-form-group input[type="email"],
|
|
.panel-form-group input[type="url"],
|
|
.panel-form-group select,
|
|
.panel-form-group textarea,
|
|
.panel-input,
|
|
.form-control,
|
|
.timeline-filter-select,
|
|
.timeline-search,
|
|
.timeline-date-input {
|
|
min-height: 44px;
|
|
padding: var(--space-3) var(--space-4);
|
|
}
|
|
|
|
/* ===== Scene Container (3D Canvas) — grid child =====
|
|
#scene-container lives inside .app-main (the grid main area).
|
|
It fills its parent via width/height: 100% instead of position:fixed. */
|
|
#scene-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
touch-action: none;
|
|
z-index: 1;
|
|
/* Mobile-specific touch handling improvements */
|
|
-webkit-touch-callout: none;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
/* Prevent iOS Safari from delaying single taps */
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
/* iOS Safari-specific handling for visual viewport */
|
|
@supports (-webkit-touch-callout: none) {
|
|
/* iOS Safari with visual viewport API */
|
|
#scene-container {
|
|
/* Use dynamic viewport height on iOS (handles address bar) */
|
|
height: 100dvh;
|
|
}
|
|
}
|
|
|
|
/* iOS Safari orientation change handling */
|
|
@supports (-webkit-touch-callout: none) {
|
|
body {
|
|
-webkit-text-size-adjust: 100%;
|
|
}
|
|
}
|
|
|
|
/* Ensure canvas doesn't overflow during transitions */
|
|
#scene-container canvas {
|
|
display: block;
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
touch-action: none;
|
|
/* Mobile-specific touch handling improvements */
|
|
-webkit-touch-callout: none;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
/* Handle visual viewport resize events (iOS Safari keyboard, address bar) */
|
|
.visual-viewport-resizing #scene-container {
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ===== Status Bar — grid header via layout.css =====
|
|
When #status-bar is a direct child of .app-shell--live, it is the
|
|
grid header. Uses position:sticky to stay visible during scroll. */
|
|
#status-bar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
/* ===== Alert Banner — full-width overlay ===== */
|
|
#alert-banner {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 5000;
|
|
}
|
|
|
|
/* ===== Orientation-specific adjustments ===== */
|
|
|
|
/* Landscape orientation */
|
|
@media screen and (orientation: landscape) {
|
|
#scene-container {
|
|
height: 100%;
|
|
height: 100dvh;
|
|
}
|
|
}
|
|
|
|
/* Portrait orientation */
|
|
@media screen and (orientation: portrait) {
|
|
#scene-container {
|
|
height: 100%;
|
|
height: 100dvh;
|
|
}
|
|
}
|
|
|
|
/* ===== Responsive breakpoints ===== */
|
|
|
|
/* Small mobile devices */
|
|
@media screen and (max-width: 600px) {
|
|
#scene-container {
|
|
height: 100%;
|
|
height: 100dvh;
|
|
}
|
|
}
|
|
|
|
/* Tablets and larger */
|
|
@media screen and (min-width: 601px) {
|
|
#scene-container {
|
|
height: 100%;
|
|
height: 100dvh;
|
|
}
|
|
}
|
|
|
|
/* ===== Safe area insets for notched devices ===== */
|
|
@supports (padding: max(0px)) {
|
|
body {
|
|
padding-top: env(safe-area-inset-top);
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
|
|
#scene-container {
|
|
padding-left: env(safe-area-inset-left);
|
|
padding-right: env(safe-area-inset-right);
|
|
}
|
|
|
|
#scene-container {
|
|
height: calc(100% - env(safe-area-inset-top) - env(safe-area-inset-bottom));
|
|
height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
|
|
}
|
|
|
|
}
|
|
|
|
/* ===== Prevent overscroll/bounce on iOS ===== */
|
|
@supports (-webkit-touch-callout: none) {
|
|
#scene-container {
|
|
-webkit-overflow-scrolling: touch;
|
|
overscroll-behavior: none;
|
|
}
|
|
}
|
|
|
|
/* ===== Loading state ===== */
|
|
#scene-container.loading {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|