spaxel/dashboard/css/expert.css
jedarden eea920de19 feat: add expert mode CSS and improve mobile viewport handling
- Add expert.css for expert mode specific styles
- Improve mobile orientation change handling with visualViewport API support
- Add iOS Safari visual viewport resize handling for better mobile experience
- Add getViewportDimensions() function preferring visualViewport API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 02:48:55 -04:00

250 lines
6.3 KiB
CSS

/**
* Spaxel Dashboard - Expert Mode Styles
*
* Expert mode 3D scene layout with responsive canvas sizing.
* Handles iOS Safari visual viewport quirks and bottom navigation spacing.
*/
/* ===== Scene Container (3D Canvas) ===== */
#scene-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
/* Full viewport height by default */
height: 100vh;
/* Fallback for browsers that don't support viewport units */
height: 100%;
z-index: 1;
}
/* Canvas height adjustment when simple mode navigation is visible */
body.simple-mode #scene-container {
/* Simple mode has 56px bottom nav, subtract from viewport height */
height: calc(100vh - 56px);
}
/* 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;
}
body.simple-mode #scene-container {
/* Subtract bottom navigation bar height */
height: calc(100dvh - 56px);
}
}
/* iOS Safari orientation change handling */
/* Prevent iOS Safari from scaling content on orientation change */
@supports (-webkit-touch-callout: none) {
body {
/* Prevent automatic text size adjustment on orientation change */
-webkit-text-size-adjust: 100%;
}
}
/* Ensure canvas doesn't overflow during transitions */
#scene-container canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
/* Handle visual viewport resize events (iOS Safari keyboard, address bar) */
.visual-viewport-resizing #scene-container {
/* Prevent layout shift during visual viewport changes */
pointer-events: none;
}
/* ===== Status Bar ===== */
#status-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 40px;
background: rgba(0, 0, 0, 0.9);
z-index: 100;
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
/* Adjust scene container when status bar is visible */
#status-bar ~ #scene-container {
top: 40px;
height: calc(100vh - 40px);
}
/* Simple mode navigation bar adjustment */
body.simple-mode .simple-quick-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 56px;
z-index: 100;
}
/* ===== Mode Toggle Bar ===== */
.mode-toggle-bar {
position: fixed;
top: 40px;
left: 0;
right: 0;
height: 44px;
z-index: 90;
}
/* Adjust scene container when mode toggle bar is visible */
.mode-toggle-bar ~ #scene-container,
body:not(.simple-mode) .mode-toggle-bar ~ #scene-container {
top: 84px; /* 40px status bar + 44px mode toggle bar */
height: calc(100vh - 84px);
}
/* Both status bar and mode toggle bar present */
#status-bar .mode-toggle-bar ~ #scene-container {
top: 84px;
height: calc(100vh - 84px);
}
/* ===== Alert Banner ===== */
#alert-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 5000;
}
/* Adjust scene container when alert banner is visible */
#alert-banner.visible ~ #scene-container {
top: 60px; /* Approximate alert banner height */
height: calc(100vh - 60px);
}
/* ===== Orientation-specific adjustments ===== */
/* Landscape orientation */
@media screen and (orientation: landscape) {
#scene-container {
/* In landscape, ensure full height is used */
height: 100vh;
height: 100dvh;
}
body.simple-mode #scene-container {
height: calc(100vh - 56px);
height: calc(100dvh - 56px);
}
/* Adjust for landscape status bar */
#status-bar ~ #scene-container {
height: calc(100vh - 40px);
height: calc(100dvh - 40px);
}
/* Adjust for landscape mode toggle bar */
.mode-toggle-bar ~ #scene-container {
height: calc(100vh - 44px);
height: calc(100dvh - 44px);
}
}
/* Portrait orientation */
@media screen and (orientation: portrait) {
#scene-container {
height: 100vh;
height: 100dvh;
}
body.simple-mode #scene-container {
height: calc(100vh - 56px);
height: calc(100dvh - 56px);
}
}
/* ===== Responsive breakpoints ===== */
/* Small mobile devices */
@media screen and (max-width: 600px) {
#scene-container {
height: 100vh;
height: 100dvh;
}
body.simple-mode #scene-container {
height: calc(100vh - 56px);
height: calc(100dvh - 56px);
}
/* On small mobile, status bar and mode toggle may stack */
#status-bar ~ .mode-toggle-bar ~ #scene-container {
top: 84px;
height: calc(100vh - 84px);
height: calc(100dvh - 84px);
}
}
/* Tablets and larger */
@media screen and (min-width: 601px) {
#scene-container {
height: 100vh;
height: 100dvh;
}
}
/* ===== Safe area insets for notched devices ===== */
@supports (padding: max(0px)) {
#scene-container {
/* Use safe-area-inset for devices with notches (iPhone X+) */
padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
/* Adjust height calculation to account for safe areas */
#scene-container {
height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
}
body.simple-mode #scene-container {
/* Simple mode: also subtract bottom nav height */
height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 56px);
height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 56px);
}
}
/* ===== Prevent overscroll/bounce on iOS ===== */
@supports (-webkit-touch-callout: none) {
#scene-container {
/* Prevent rubber-band scrolling effect */
-webkit-overflow-scrolling: touch;
overscroll-behavior: none;
}
}
/* ===== Loading state ===== */
#scene-container.loading {
opacity: 0.5;
pointer-events: none;
transition: opacity 0.3s ease;
}
/* ===== Hide scene container in simple mode ===== */
body.simple-mode #scene-container {
display: none;
}
/* ===== Expert mode specific styles ===== */
body:not(.simple-mode) #scene-container {
display: block;
}