mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
feat(ui): phone layout with a bottom tab bar and one panel sheet at a time
At 720px and narrower the globe stays full screen and a bottom tab bar (Layers, Cameras, Context, Search, More) opens one bottom sheet at a time, presenting the existing rail and dock panels in place. Sheets borrow panels without persisting or sharing them and put back what they changed. Cockpit, recording and clean view close the sheet; scene playback keeps STOP reachable. Focus moves into the sheet and back to its tab, the required attribution stays visible above the voice control and any sheet, and the page uses safe-area insets. Wider viewports are unchanged. Adapted from the phone tab-shell proposed in #203 by @blearymoth.
This commit is contained in:
1 parent
224745bd0a
commit
fff16f3884
17 files changed
+1405
-20
No files matched your search
@@ -0,0 +1,415 @@
|
||||
/* ── Phone shell (≤720px, body.mobile-shell; src/ui/mobileShell.js) ─────────
|
||||
* The globe is full screen. A bottom tab bar opens one sheet; the sheet shows
|
||||
* the panels of that tab from the rails or dock, everything else stays hidden.
|
||||
*/
|
||||
:root {
|
||||
--mobile-tab-height: 58px;
|
||||
--mobile-bottom-inset: env(safe-area-inset-bottom, 0px);
|
||||
--mobile-top-inset: env(safe-area-inset-top, 0px);
|
||||
--mobile-tab-space: calc(
|
||||
var(--mobile-tab-height) + var(--mobile-bottom-inset)
|
||||
);
|
||||
--mobile-sheet-max-height: min(62vh, calc(100vh - 168px));
|
||||
--mobile-dock-sheet-max-height: 34vh;
|
||||
--mobile-rail-under-dock-max-height: 40vh;
|
||||
}
|
||||
|
||||
/* Dynamic viewport units keep sheets inside the area a mobile browser's
|
||||
toolbars leave visible. */
|
||||
@supports (height: 1dvh) {
|
||||
:root {
|
||||
--mobile-sheet-max-height: min(62dvh, calc(100dvh - 168px));
|
||||
--mobile-dock-sheet-max-height: 34dvh;
|
||||
--mobile-rail-under-dock-max-height: 40dvh;
|
||||
}
|
||||
}
|
||||
|
||||
#mobile-tab-bar,
|
||||
#mobile-sheet-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-tab-bar {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 170;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
height: var(--mobile-tab-space);
|
||||
padding: 0 max(4px, env(safe-area-inset-right, 0px))
|
||||
var(--mobile-bottom-inset) max(4px, env(safe-area-inset-left, 0px));
|
||||
background: rgba(8, 14, 22, 0.94);
|
||||
border-top: 1px solid var(--glass-border);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
body.mobile-shell .mobile-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
padding: 6px 0 4px;
|
||||
color: var(--text-secondary);
|
||||
background: none;
|
||||
border: 0;
|
||||
font: 500 10px/1.2 var(--font-sans);
|
||||
letter-spacing: 0.04em;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body.mobile-shell .mobile-tab-icon {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
body.mobile-shell .mobile-tab[aria-expanded='true'] {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
body.mobile-shell .mobile-tab:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet] #mobile-sheet-backdrop:not([hidden]) {
|
||||
position: fixed;
|
||||
inset: 0 0 var(--mobile-tab-space);
|
||||
z-index: 155;
|
||||
display: block;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
/* ── Top bar: logo left, globe actions right ───────────────────────────── */
|
||||
body.mobile-shell #title-bar {
|
||||
top: calc(var(--mobile-top-inset) + 12px);
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
body.mobile-shell #title-bar h1 > :not(.title-logo),
|
||||
body.mobile-shell #title-bar .subtitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #top-center-actions {
|
||||
top: calc(var(--mobile-top-inset) + 10px);
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
/* ── HUD: one compact readout under the top bar ────────────────────────── */
|
||||
body.mobile-shell #intel-hud .hud-edge,
|
||||
body.mobile-shell #intel-hud .hud-corner,
|
||||
body.mobile-shell #intel-hud .hud-top-bar,
|
||||
body.mobile-shell #intel-hud .hud-bottom-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #intel-hud .hud-bottom-left {
|
||||
top: calc(var(--mobile-top-inset) + 62px);
|
||||
bottom: auto;
|
||||
left: 16px;
|
||||
font-size: 9px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ── Panels: hidden unless their tab's sheet is open ───────────────────── */
|
||||
body.mobile-shell #left-panel-stack,
|
||||
body.mobile-shell #right-context-rail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack,
|
||||
body.mobile-shell[data-mobile-sheet='cameras'] #right-context-rail,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #right-context-rail,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #right-context-rail {
|
||||
position: fixed;
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: var(--mobile-tab-space);
|
||||
left: 0;
|
||||
z-index: 160;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
max-height: var(--mobile-sheet-max-height);
|
||||
padding: 14px 10px 10px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
pointer-events: auto;
|
||||
background: rgba(8, 14, 22, 0.96);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-bottom: 0;
|
||||
border-radius: 18px 18px 0 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #left-panel-stack > *,
|
||||
body.mobile-shell #right-context-rail > *,
|
||||
/* Rail rules show expanded Weather and Recent Imagery by id; on phones they
|
||||
belong to the Context sheet only. */
|
||||
body.mobile-shell #right-context-rail > #weather-panel,
|
||||
body.mobile-shell #right-context-rail > #recent-imagery-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Rail-scoped ids so a rail's own hiding rules (for example the Tactical HUD
|
||||
hiding collapsed siblings) never take a panel out of an open sheet. */
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack > #data-panel,
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack > #scene-panel,
|
||||
body.mobile-shell[data-mobile-sheet='cameras']
|
||||
#right-context-rail
|
||||
> #cctv-panel,
|
||||
body.mobile-shell[data-mobile-sheet='cameras']
|
||||
#right-context-rail
|
||||
> #street-level-panel,
|
||||
body.mobile-shell[data-mobile-sheet='context']
|
||||
#right-context-rail
|
||||
> #global-context-panel,
|
||||
body.mobile-shell[data-mobile-sheet='context']
|
||||
#right-context-rail
|
||||
> #weather-panel:not([hidden]),
|
||||
body.mobile-shell[data-mobile-sheet='context']
|
||||
#right-context-rail
|
||||
> #recent-imagery-panel:not([hidden]),
|
||||
body.mobile-shell[data-mobile-sheet='more'] #right-context-rail > #pp-toggles {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* ── Dock: voice floats above the tab bar; search and presets are sheets ─ */
|
||||
body.mobile-shell #command-dock {
|
||||
/* The dock's tray rules size voice by this basis; a column makes it a height. */
|
||||
--dock-voice-width: auto;
|
||||
top: auto;
|
||||
bottom: calc(var(--mobile-tab-space) + 10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
padding: 0 10px;
|
||||
pointer-events: none;
|
||||
background: none;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
/* The desktop dock centres itself with left: 50% and a translate under
|
||||
:has() selectors that outrank any phone selector; on phones it spans the
|
||||
width, so these few geometry properties must win. */
|
||||
body.mobile-shell #command-dock {
|
||||
right: 0 !important;
|
||||
left: 0 !important;
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
transform: none !important;
|
||||
translate: none !important;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body.mobile-shell #location-bar,
|
||||
body.mobile-shell #control-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet] #command-dock {
|
||||
z-index: 165;
|
||||
bottom: calc(var(--mobile-tab-space) + var(--mobile-sheet-height, 0px) + 8px);
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #command-dock > #control-panel {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
display: flex;
|
||||
flex: none;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Search and More: the dock becomes the top of the sheet, flush with the rail
|
||||
sheet below it (More) or alone above the tab bar (Search). */
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #command-dock {
|
||||
bottom: calc(var(--mobile-tab-space) + var(--mobile-sheet-height, 0px));
|
||||
align-items: stretch;
|
||||
max-height: var(--mobile-dock-sheet-max-height);
|
||||
padding: 14px 10px 8px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
pointer-events: auto;
|
||||
background: rgba(8, 14, 22, 0.96);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-bottom: 0;
|
||||
border-radius: 18px 18px 0 0;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='more'] #right-context-rail {
|
||||
max-height: var(--mobile-rail-under-dock-max-height);
|
||||
padding-top: 4px;
|
||||
border-top: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Presets and Location show their popover inline instead of as a floating
|
||||
tray; the dock scrolls, so a tray above it would be clipped. */
|
||||
body.mobile-shell[data-mobile-sheet='more']
|
||||
#command-dock
|
||||
#control-panel
|
||||
.panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='more']
|
||||
#command-dock
|
||||
#control-panel-popover,
|
||||
body.mobile-shell[data-mobile-sheet='search']
|
||||
#command-dock
|
||||
#location-bar
|
||||
.location-inner,
|
||||
body.mobile-shell[data-mobile-sheet='search']
|
||||
#command-dock
|
||||
#location-bar-popover {
|
||||
position: static;
|
||||
inset: auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* The tab names the sheet; the tray toggle and pin have nothing to do here. */
|
||||
body.mobile-shell[data-mobile-sheet='more'] #command-dock #control-panel-toggle,
|
||||
body.mobile-shell[data-mobile-sheet='more']
|
||||
#command-dock
|
||||
#control-panel
|
||||
.dock-pin-btn,
|
||||
body.mobile-shell[data-mobile-sheet='search']
|
||||
#command-dock
|
||||
#location-bar-toggle,
|
||||
body.mobile-shell[data-mobile-sheet='search']
|
||||
#command-dock
|
||||
#location-bar
|
||||
.panel-collapse-btn,
|
||||
body.mobile-shell[data-mobile-sheet='search']
|
||||
#command-dock
|
||||
#location-bar
|
||||
.dock-pin-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='more'] #style-buttons {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Search: the field takes the full width; the place pills scroll beneath it. */
|
||||
body.mobile-shell[data-mobile-sheet='search'] #location-bar .location-city-row {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='search']
|
||||
#location-bar
|
||||
.location-search-wrap {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='search'] #location-search {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='search'] #location-bar .location-pills {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* With a sheet open, voice steps aside; Search and More own the dock. */
|
||||
body.mobile-shell[data-mobile-sheet] #command-dock > #gev-voice-control {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock #gev-voice-control {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
flex: none;
|
||||
align-self: center;
|
||||
width: min(280px, 80vw);
|
||||
height: 5.35rem;
|
||||
margin: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* ── Credits and corner chips clear the tab bar ────────────────────────── */
|
||||
/* While the tab bar shows, the credit line sits above the voice bar. The
|
||||
dock's placements (including the Minimal HUD's :has() variant) outrank any
|
||||
phone selector, so the offset must win. Clean view keeps its own corner. */
|
||||
body.mobile-shell:not(.recording-mode):not(.cockpit-mode):not(.ui-clean-view)
|
||||
#cesium-credits {
|
||||
right: auto;
|
||||
bottom: calc(var(--mobile-tab-space) + 106px) !important;
|
||||
left: 10px;
|
||||
max-width: calc(100vw - 20px);
|
||||
transform-origin: left bottom;
|
||||
scale: 0.8;
|
||||
}
|
||||
|
||||
/* An open sheet lifts the required attribution above everything it covers. */
|
||||
body.mobile-shell[data-mobile-sheet]:not(.recording-mode):not(
|
||||
.cockpit-mode
|
||||
):not(.ui-clean-view)
|
||||
#cesium-credits {
|
||||
bottom: calc(var(--mobile-sheet-cover, 0px) + 6px) !important;
|
||||
z-index: 166;
|
||||
}
|
||||
|
||||
/* The view switcher clears the tab bar and the voice bar, opposite the credit. */
|
||||
body.mobile-shell:not(.cockpit-mode) #view-switcher {
|
||||
right: 12px;
|
||||
bottom: calc(var(--mobile-tab-space) + 106px);
|
||||
}
|
||||
|
||||
body.mobile-shell #corner-actions {
|
||||
top: calc(var(--mobile-top-inset) + 60px);
|
||||
right: 12px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
/* Full-screen modes hide the phone chrome like the desktop chrome. Scene
|
||||
playback keeps it: the Scenes panel (Layers tab) holds its STOP control. */
|
||||
body.mobile-shell.ui-clean-view #mobile-tab-bar,
|
||||
body.mobile-shell.recording-mode #mobile-tab-bar,
|
||||
body.mobile-shell.cockpit-mode #mobile-tab-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell.ui-clean-view #command-dock,
|
||||
body.mobile-shell.cockpit-mode #command-dock {
|
||||
bottom: calc(var(--mobile-bottom-inset) + 10px);
|
||||
}
|
||||
Reference in new issue
Block a user