feat: add phone tab-shell for narrow viewports
At ≤720px, restyle existing rails as exclusive bottom sheets driven by a tab bar. Desktop layout stays unchanged; Clean View and Cockpit hide the chrome. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
759652207f
commit
1f5848c5ec
|
|
@ -5,6 +5,13 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Phone layout shell (`src/mobileShell.js`): at ≤720px, a bottom tab bar opens
|
||||
Search / Layers / Context / Display / Presets / More as exclusive sheets over
|
||||
the existing rails. Desktop layout is unchanged; Clean View and Cockpit hide
|
||||
the tab chrome.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Mapped-site outages show their scheduled retry countdown and distinguish
|
||||
|
|
|
|||
|
|
@ -2317,7 +2317,7 @@ silently demoting every later lookup for the session.
|
|||
- Map Source lives in the bottom Visual Presets tray. The left accordion contains no MAP STACK panel, and the `k` panel token that addressed it is gone from the share registry, so legacy `ui=k...` state takes the ordinary unknown-token skip.
|
||||
- A dock popover (Visual Presets, Location) auto-dismisses on mouse-away unless pinned. Focus inside the tray defers that dismissal only when the browser reports `:focus-visible` — keyboard focus and typed-into fields hold the tray open; a mouse-clicked tile does not, because Chromium focuses a `<button>` on press.
|
||||
- GEV MIC control is a glass capsule (var(--glass-bg), blur(24px) saturate(1.4), 999px radius; panel radius in error state).
|
||||
- The desktop right rail (`#right-context-rail`) owns `DISPLAY`, `CCTV`, its active parameter controls, and `GLOBAL CONTEXT` as one fixed responsive stack in that order. Its compact buttons use the same 176 px width as the left accordion and one consistent 50 px height, share the left stack's 52 px edge inset and measured top baseline across HUD variants, then constrain themselves against visible HUD/chrome rectangles and the remaining vertical corridor. `DISPLAY` is no longer draggable and legacy saved coordinates are ignored.
|
||||
- The desktop right rail (`#right-context-rail`) owns `DISPLAY`, `CCTV`, its active parameter controls, and `GLOBAL CONTEXT` as one fixed responsive stack in that order. Its compact buttons use the same 176 px width as the left accordion and one consistent 50 px height, share the left stack's 52 px edge inset and measured top baseline across HUD variants, then constrain themselves against visible HUD/chrome rectangles and the remaining vertical corridor. `DISPLAY` is no longer draggable and legacy saved coordinates are ignored. On viewports ≤720px, `body.mobile-shell` (`src/mobileShell.js`) restyles those same panels as exclusive bottom sheets driven by `#mobile-tab-bar` (Search / Layers / Context / Display / Presets / More); desktop rails are unchanged, and Clean View / Cockpit / scene-playback hide the tab chrome.
|
||||
- The right rail is labeled **DISPLAY** (formerly "MOVE") and groups, in order, HUD, DETECT, Bloom, Sharpen, 3D, Clean-UI (HUD + DETECT promoted to the top). Its expanded controls retain the same compact 176 px width as the right-side tabs instead of growing to the wider Context detail-card width. It starts expanded on first run and respects the user's later `v6` collapse choice. Collapses/expands with directional chevrons (`◀` collapsed, `▶` expanded).
|
||||
- Display and Context use matching 330 px expanded widths and matching compact tab dimensions. The parameter panel is part of Display's expanded content. DISPLAY may remain open beside one contextual panel; CCTV and Context are mutually exclusive. In Tactical HUD, expanding CCTV or Context hides the other contextual launcher while DISPLAY remains independently available. The most recently opened right-rail panel owns the constrained lane even when it appears later in DOM order; passive restoration and automatic disclosure do not replace that explicit owner. Minimal and other HUD layouts retain the collapsed launchers; when their active panel exceeds the measured corridor, the rail reserves sibling heights and gaps and scrolls the active panel internally.
|
||||
- `STYLE PRESETS` and `LOCATIONS` start collapsed, expand on intentional hover/click, and auto-collapse after hover leave delay.
|
||||
|
|
|
|||
33
index.html
33
index.html
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<title>God's Eye View</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
|
|
@ -908,6 +908,37 @@
|
|||
<p class="key-setup-note" data-key-setup-status role="status" aria-live="polite">The Google Maps key buys the photorealistic planet — everything else stacks on top.</p>
|
||||
</aside>
|
||||
|
||||
<div id="mobile-sheet-backdrop" hidden></div>
|
||||
<button type="button" id="mobile-modal-close" hidden aria-label="Close">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
<nav id="mobile-tab-bar" aria-label="Mobile panels">
|
||||
<button type="button" data-mobile-sheet="search" aria-pressed="false" aria-controls="location-bar">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">search</span>
|
||||
<span>Search</span>
|
||||
</button>
|
||||
<button type="button" data-mobile-sheet="layers" aria-pressed="false" aria-controls="data-panel">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">layers</span>
|
||||
<span>Layers</span>
|
||||
</button>
|
||||
<button type="button" data-mobile-sheet="context" aria-pressed="false" aria-controls="global-context-panel">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
|
||||
<span>Context</span>
|
||||
</button>
|
||||
<button type="button" data-mobile-sheet="display" aria-pressed="false" aria-controls="pp-toggles">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">tune</span>
|
||||
<span>Display</span>
|
||||
</button>
|
||||
<button type="button" data-mobile-sheet="presets" aria-pressed="false" aria-controls="control-panel">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">palette</span>
|
||||
<span>Presets</span>
|
||||
</button>
|
||||
<button type="button" data-mobile-sheet="more" aria-pressed="false" aria-controls="cctv-panel">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">more_horiz</span>
|
||||
<span>More</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Intelligence HUD Overlay -->
|
||||
<div id="intel-hud"></div>
|
||||
|
||||
|
|
|
|||
21
src/main.js
21
src/main.js
|
|
@ -81,6 +81,8 @@ async function init() {
|
|||
const googleApiKey = import.meta.env.GOOGLE_MAPS_API_KEY;
|
||||
if (googleApiKey) window.__GOOGLE_MAPS_API_KEY__ = googleApiKey;
|
||||
|
||||
const phoneLayout = window.matchMedia('(max-width: 720px)').matches;
|
||||
|
||||
// Create the Cesium viewer with minimal chrome
|
||||
const viewer = new Cesium.Viewer('cesiumContainer', {
|
||||
timeline: false,
|
||||
|
|
@ -108,7 +110,8 @@ async function init() {
|
|||
document.body.appendChild(el);
|
||||
return el;
|
||||
})(),
|
||||
msaaSamples: 4,
|
||||
// 4x MSAA is a known WebGL kill on phones; desktop keeps the current look.
|
||||
msaaSamples: phoneLayout ? 1 : 4,
|
||||
contextOptions: {
|
||||
webgl: {
|
||||
preserveDrawingBuffer: true,
|
||||
|
|
@ -148,10 +151,24 @@ async function init() {
|
|||
loaderStatus.textContent = googleApiKey || cesiumToken
|
||||
? 'Loading Google 3D Tiles...'
|
||||
: 'Loading the keyless globe...';
|
||||
const photoreal = await loadPhotorealisticTileset(Cesium, {
|
||||
// On phones a hung tileset request used to sit on the splash until the
|
||||
// browser showed a timeout page — race a bounded deadline against startup.
|
||||
const createTileset = loadPhotorealisticTileset(Cesium, {
|
||||
googleApiKey,
|
||||
cesiumToken,
|
||||
});
|
||||
const photoreal = phoneLayout
|
||||
? await Promise.race([
|
||||
createTileset,
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Google 3D Tiles timed out on this device')), 25_000);
|
||||
}),
|
||||
]).catch((tileError) => ({
|
||||
tileset: null,
|
||||
route: null,
|
||||
errors: [tileError],
|
||||
}))
|
||||
: await createTileset;
|
||||
const tileset = photoreal.tileset;
|
||||
if (tileset) {
|
||||
viewer.scene.primitives.add(tileset);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
/**
|
||||
* Phone tab-shell. Desktop (721px+) never sees this class or sheet state.
|
||||
* Panel DOM stays in the existing rails; CSS restyles the active sheet.
|
||||
*/
|
||||
|
||||
export const MOBILE_SHELL_MEDIA_QUERY = '(max-width: 720px)';
|
||||
export const MOBILE_SHELL_CLASS = 'mobile-shell';
|
||||
export const MOBILE_SHEET_ATTR = 'data-mobile-sheet';
|
||||
|
||||
export const MOBILE_SHEET_PANELS = Object.freeze({
|
||||
layers: Object.freeze(['data-panel']),
|
||||
context: Object.freeze(['global-context-panel']),
|
||||
display: Object.freeze(['pp-toggles']),
|
||||
more: Object.freeze(['cctv-panel', 'scene-panel', 'geo-status-panel']),
|
||||
search: Object.freeze(['location-bar']),
|
||||
presets: Object.freeze(['control-panel']),
|
||||
});
|
||||
|
||||
export const MOBILE_TAB_SHEETS = Object.freeze(['search', 'layers', 'context', 'display', 'presets', 'more']);
|
||||
|
||||
const EXCLUSIVE_BODY_CLASSES = Object.freeze([
|
||||
'cockpit-mode',
|
||||
'recording-mode',
|
||||
'scene-playback-mode',
|
||||
'tour-playback-mode',
|
||||
'ui-clean-view',
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {string} panelId
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function sheetForPanelId(panelId) {
|
||||
for (const [sheet, ids] of Object.entries(MOBILE_SHEET_PANELS)) {
|
||||
if (ids.includes(panelId)) return sheet;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether a panel collapse/expand should change the open sheet.
|
||||
* Restored desktop prefs never open a sheet. Missing mobile-shell is a no-op.
|
||||
*
|
||||
* @param {object} input
|
||||
* @param {boolean} input.mobileShellActive
|
||||
* @param {string|null} input.currentSheet
|
||||
* @param {string} input.panelId
|
||||
* @param {boolean} input.collapsed
|
||||
* @param {boolean} [input.restore=false]
|
||||
* @param {boolean} [input.explicit=false]
|
||||
* @returns {{ changed: boolean, sheet: string|null }}
|
||||
*/
|
||||
export function nextMobileSheetState({
|
||||
mobileShellActive,
|
||||
currentSheet,
|
||||
panelId,
|
||||
collapsed,
|
||||
restore = false,
|
||||
explicit = false,
|
||||
}) {
|
||||
if (!mobileShellActive) return { changed: false, sheet: currentSheet ?? null };
|
||||
if (restore) return { changed: false, sheet: currentSheet ?? null };
|
||||
const sheet = sheetForPanelId(panelId);
|
||||
if (!sheet) return { changed: false, sheet: currentSheet ?? null };
|
||||
if (!collapsed) {
|
||||
return { changed: currentSheet !== sheet, sheet };
|
||||
}
|
||||
if (explicit && currentSheet === sheet) return { changed: true, sheet: null };
|
||||
return { changed: false, sheet: currentSheet ?? null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply or clear the mobile-shell class from a matchMedia result.
|
||||
* Crossing 721px always drops the open sheet so desktop rails resume cleanly.
|
||||
*
|
||||
* @param {object} input
|
||||
* @param {boolean} input.matches
|
||||
* @param {Element|null} input.body
|
||||
* @returns {boolean} Whether mobile-shell is now active.
|
||||
*/
|
||||
export function applyMobileShellMatch({ matches, body }) {
|
||||
if (!body) return false;
|
||||
if (matches) {
|
||||
body.classList.add(MOBILE_SHELL_CLASS);
|
||||
return true;
|
||||
}
|
||||
body.classList.remove(MOBILE_SHELL_CLASS);
|
||||
body.removeAttribute(MOBILE_SHEET_ATTR);
|
||||
return false;
|
||||
}
|
||||
|
||||
function exclusiveSurfaceActive(body) {
|
||||
return EXCLUSIVE_BODY_CLASSES.some((name) => body.classList.contains(name));
|
||||
}
|
||||
|
||||
function currentSheetOf(body) {
|
||||
const value = body.getAttribute(MOBILE_SHEET_ATTR);
|
||||
return value && value in MOBILE_SHEET_PANELS ? value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync sheet state from setPanelCollapsed. No-op without body.mobile-shell.
|
||||
*
|
||||
* @param {string} panelId
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.collapsed]
|
||||
* @param {boolean} [options.restore=false]
|
||||
* @param {Document} [options.documentRef]
|
||||
*/
|
||||
export function syncMobileSheetFromPanel(panelId, {
|
||||
collapsed,
|
||||
restore = false,
|
||||
explicit = false,
|
||||
documentRef = globalThis.document,
|
||||
} = {}) {
|
||||
const body = documentRef?.body;
|
||||
if (!body?.classList.contains(MOBILE_SHELL_CLASS)) return;
|
||||
const next = nextMobileSheetState({
|
||||
mobileShellActive: true,
|
||||
currentSheet: currentSheetOf(body),
|
||||
panelId,
|
||||
collapsed: Boolean(collapsed),
|
||||
restore,
|
||||
explicit,
|
||||
});
|
||||
if (!next.changed) return;
|
||||
applySheet(body, next.sheet, documentRef);
|
||||
}
|
||||
|
||||
function resetSearchField(documentRef) {
|
||||
const input = documentRef.getElementById('location-search');
|
||||
const toggle = documentRef.getElementById('search-toggle');
|
||||
input?.classList.remove('expanded', 'searching');
|
||||
toggle?.classList.remove('is-open');
|
||||
toggle?.setAttribute('aria-expanded', 'false');
|
||||
if (toggle) toggle.title = 'Search any location';
|
||||
}
|
||||
|
||||
function applySheet(body, sheet, documentRef) {
|
||||
const tabBar = documentRef.getElementById('mobile-tab-bar');
|
||||
const backdrop = documentRef.getElementById('mobile-sheet-backdrop');
|
||||
const closeBtn = documentRef.getElementById('mobile-modal-close');
|
||||
if (sheet) {
|
||||
body.setAttribute(MOBILE_SHEET_ATTR, sheet);
|
||||
backdrop?.removeAttribute('hidden');
|
||||
closeBtn?.removeAttribute('hidden');
|
||||
} else {
|
||||
body.removeAttribute(MOBILE_SHEET_ATTR);
|
||||
backdrop?.setAttribute('hidden', '');
|
||||
closeBtn?.setAttribute('hidden', '');
|
||||
resetSearchField(documentRef);
|
||||
}
|
||||
tabBar?.querySelectorAll('[data-mobile-sheet]').forEach((button) => {
|
||||
const pressed = button.dataset.mobileSheet === sheet;
|
||||
button.setAttribute('aria-pressed', String(pressed));
|
||||
});
|
||||
}
|
||||
|
||||
function expandSheetPanels(sheet, setPanelCollapsed, documentRef = globalThis.document) {
|
||||
for (const panelId of MOBILE_SHEET_PANELS[sheet] || []) {
|
||||
setPanelCollapsed(panelId, false, {
|
||||
explicit: true,
|
||||
persist: false,
|
||||
syncShare: false,
|
||||
});
|
||||
}
|
||||
if (sheet === 'search') {
|
||||
const input = documentRef.getElementById('location-search');
|
||||
const toggle = documentRef.getElementById('search-toggle');
|
||||
input?.classList.add('expanded');
|
||||
toggle?.classList.add('is-open');
|
||||
toggle?.setAttribute('aria-expanded', 'true');
|
||||
if (toggle) toggle.title = 'Close search';
|
||||
}
|
||||
}
|
||||
|
||||
function collapseSheetPanels(sheet, setPanelCollapsed) {
|
||||
for (const panelId of MOBILE_SHEET_PANELS[sheet] || []) {
|
||||
setPanelCollapsed(panelId, true, {
|
||||
persist: false,
|
||||
syncShare: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire the tab bar, matchMedia gate, and exclusive-surface close.
|
||||
*
|
||||
* @param {object} input
|
||||
* @param {(panelId: string, collapsed: boolean, options?: object) => void} input.setPanelCollapsed
|
||||
* @param {Document} [input.documentRef]
|
||||
* @param {(query: string) => MediaQueryList} [input.matchMedia]
|
||||
* @returns {{ dispose: () => void, setSheet: (sheet: string|null) => void }}
|
||||
*/
|
||||
export function initMobileShell({
|
||||
setPanelCollapsed,
|
||||
documentRef = globalThis.document,
|
||||
matchMedia = globalThis.matchMedia?.bind(globalThis),
|
||||
} = {}) {
|
||||
const body = documentRef.body;
|
||||
const tabBar = documentRef.getElementById('mobile-tab-bar');
|
||||
const backdrop = documentRef.getElementById('mobile-sheet-backdrop');
|
||||
const removers = [];
|
||||
|
||||
const setSheet = (sheet) => {
|
||||
if (!body.classList.contains(MOBILE_SHELL_CLASS)) return;
|
||||
if (exclusiveSurfaceActive(body)) sheet = null;
|
||||
const previous = currentSheetOf(body);
|
||||
if (previous && previous !== sheet) {
|
||||
if (previous === 'search') resetSearchField(documentRef);
|
||||
collapseSheetPanels(previous, setPanelCollapsed);
|
||||
}
|
||||
applySheet(body, sheet, documentRef);
|
||||
if (sheet) expandSheetPanels(sheet, setPanelCollapsed, documentRef);
|
||||
};
|
||||
|
||||
const onMedia = (matches) => {
|
||||
const active = applyMobileShellMatch({ matches, body });
|
||||
if (tabBar) tabBar.setAttribute('aria-hidden', active ? 'false' : 'true');
|
||||
if (!active) {
|
||||
applySheet(body, null, documentRef);
|
||||
}
|
||||
};
|
||||
|
||||
const media = typeof matchMedia === 'function' ? matchMedia(MOBILE_SHELL_MEDIA_QUERY) : null;
|
||||
onMedia(Boolean(media?.matches));
|
||||
const mediaListener = (event) => onMedia(event.matches);
|
||||
media?.addEventListener?.('change', mediaListener);
|
||||
if (media && !media.addEventListener && media.addListener) media.addListener(mediaListener);
|
||||
removers.push(() => {
|
||||
media?.removeEventListener?.('change', mediaListener);
|
||||
media?.removeListener?.(mediaListener);
|
||||
});
|
||||
|
||||
tabBar?.querySelectorAll('[data-mobile-sheet]').forEach((button) => {
|
||||
const onClick = (event) => {
|
||||
event.preventDefault();
|
||||
const next = button.dataset.mobileSheet;
|
||||
setSheet(currentSheetOf(body) === next ? null : next);
|
||||
};
|
||||
button.addEventListener('click', onClick);
|
||||
removers.push(() => button.removeEventListener('click', onClick));
|
||||
});
|
||||
|
||||
const closeSheet = () => setSheet(null);
|
||||
backdrop?.addEventListener('click', closeSheet);
|
||||
removers.push(() => backdrop?.removeEventListener('click', closeSheet));
|
||||
|
||||
const closeBtn = documentRef.getElementById('mobile-modal-close');
|
||||
closeBtn?.addEventListener('click', closeSheet);
|
||||
removers.push(() => closeBtn?.removeEventListener('click', closeSheet));
|
||||
|
||||
const onKeydown = (event) => {
|
||||
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
||||
if (!body.classList.contains(MOBILE_SHELL_CLASS)) return;
|
||||
if (!currentSheetOf(body)) return;
|
||||
if (exclusiveSurfaceActive(body)) return;
|
||||
event.preventDefault();
|
||||
closeSheet();
|
||||
};
|
||||
documentRef.addEventListener('keydown', onKeydown);
|
||||
removers.push(() => documentRef.removeEventListener('keydown', onKeydown));
|
||||
|
||||
let touchStartY = null;
|
||||
const onTouchStart = (event) => {
|
||||
touchStartY = event.changedTouches?.[0]?.clientY ?? null;
|
||||
};
|
||||
const onTouchEnd = (event) => {
|
||||
const start = touchStartY;
|
||||
touchStartY = null;
|
||||
if (start == null) return;
|
||||
const endY = event.changedTouches?.[0]?.clientY;
|
||||
if (endY != null && endY - start > 56) closeSheet();
|
||||
};
|
||||
backdrop?.addEventListener('touchstart', onTouchStart, { passive: true });
|
||||
backdrop?.addEventListener('touchend', onTouchEnd, { passive: true });
|
||||
removers.push(() => {
|
||||
backdrop?.removeEventListener('touchstart', onTouchStart);
|
||||
backdrop?.removeEventListener('touchend', onTouchEnd);
|
||||
});
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (exclusiveSurfaceActive(body) && currentSheetOf(body)) closeSheet();
|
||||
});
|
||||
observer.observe(body, { attributes: true, attributeFilter: ['class'] });
|
||||
removers.push(() => observer.disconnect());
|
||||
|
||||
return {
|
||||
dispose() {
|
||||
for (const remove of removers.splice(0)) remove();
|
||||
},
|
||||
setSheet,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
MOBILE_SHELL_CLASS,
|
||||
MOBILE_SHELL_MEDIA_QUERY,
|
||||
MOBILE_SHEET_ATTR,
|
||||
MOBILE_SHEET_PANELS,
|
||||
MOBILE_TAB_SHEETS,
|
||||
applyMobileShellMatch,
|
||||
nextMobileSheetState,
|
||||
sheetForPanelId,
|
||||
syncMobileSheetFromPanel,
|
||||
} from './mobileShell.js';
|
||||
|
||||
const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||
const css = readFileSync(new URL('../style.css', import.meta.url), 'utf8');
|
||||
const ui = readFileSync(new URL('./ui.js', import.meta.url), 'utf8');
|
||||
|
||||
test('tab bar markup maps Search, Layers, Context, Display, Presets, and More', () => {
|
||||
const match = html.match(/<nav id="mobile-tab-bar"[\s\S]*?<\/nav>/);
|
||||
assert.ok(match, 'mobile tab bar is missing');
|
||||
assert.deepEqual([...MOBILE_TAB_SHEETS], ['search', 'layers', 'context', 'display', 'presets', 'more']);
|
||||
for (const sheet of MOBILE_TAB_SHEETS) {
|
||||
assert.match(match[0], new RegExp(`data-mobile-sheet="${sheet}"`));
|
||||
}
|
||||
assert.match(html, /viewport-fit=cover/);
|
||||
});
|
||||
|
||||
test('sheet ownership stays exclusive and complete', () => {
|
||||
assert.deepEqual(Object.keys(MOBILE_SHEET_PANELS), ['layers', 'context', 'display', 'more', 'search', 'presets']);
|
||||
assert.deepEqual([...MOBILE_SHEET_PANELS.layers], ['data-panel']);
|
||||
assert.deepEqual([...MOBILE_SHEET_PANELS.context], ['global-context-panel']);
|
||||
assert.deepEqual([...MOBILE_SHEET_PANELS.display], ['pp-toggles']);
|
||||
assert.deepEqual([...MOBILE_SHEET_PANELS.more], ['cctv-panel', 'scene-panel', 'geo-status-panel']);
|
||||
assert.deepEqual([...MOBILE_SHEET_PANELS.search], ['location-bar']);
|
||||
assert.deepEqual([...MOBILE_SHEET_PANELS.presets], ['control-panel']);
|
||||
assert.equal(sheetForPanelId('data-panel'), 'layers');
|
||||
assert.equal(sheetForPanelId('location-bar'), 'search');
|
||||
assert.equal(sheetForPanelId('control-panel'), 'presets');
|
||||
});
|
||||
|
||||
test('setPanelCollapsed hook is a no-op without mobile-shell', () => {
|
||||
assert.equal(
|
||||
nextMobileSheetState({
|
||||
mobileShellActive: false,
|
||||
currentSheet: null,
|
||||
panelId: 'global-context-panel',
|
||||
collapsed: false,
|
||||
}).changed,
|
||||
false,
|
||||
);
|
||||
const body = {
|
||||
classList: { contains: () => false },
|
||||
getAttribute: () => null,
|
||||
setAttribute() { throw new Error('desktop must not write sheet state'); },
|
||||
removeAttribute() { throw new Error('desktop must not write sheet state'); },
|
||||
};
|
||||
syncMobileSheetFromPanel('global-context-panel', {
|
||||
collapsed: false,
|
||||
documentRef: { body, getElementById: () => null },
|
||||
});
|
||||
});
|
||||
|
||||
test('restored desktop prefs do not open a phone sheet', () => {
|
||||
const next = nextMobileSheetState({
|
||||
mobileShellActive: true,
|
||||
currentSheet: null,
|
||||
panelId: 'global-context-panel',
|
||||
collapsed: false,
|
||||
restore: true,
|
||||
});
|
||||
assert.equal(next.changed, false);
|
||||
assert.equal(next.sheet, null);
|
||||
});
|
||||
|
||||
test('explicit expand opens the matching sheet and explicit collapse closes it', () => {
|
||||
const opened = nextMobileSheetState({
|
||||
mobileShellActive: true,
|
||||
currentSheet: null,
|
||||
panelId: 'data-panel',
|
||||
collapsed: false,
|
||||
});
|
||||
assert.deepEqual(opened, { changed: true, sheet: 'layers' });
|
||||
const closed = nextMobileSheetState({
|
||||
mobileShellActive: true,
|
||||
currentSheet: 'layers',
|
||||
panelId: 'data-panel',
|
||||
collapsed: true,
|
||||
explicit: true,
|
||||
});
|
||||
assert.deepEqual(closed, { changed: true, sheet: null });
|
||||
const housekeeping = nextMobileSheetState({
|
||||
mobileShellActive: true,
|
||||
currentSheet: 'layers',
|
||||
panelId: 'data-panel',
|
||||
collapsed: true,
|
||||
explicit: false,
|
||||
});
|
||||
assert.equal(housekeeping.changed, false);
|
||||
});
|
||||
|
||||
test('crossing 721px drops the shell class and the open sheet', () => {
|
||||
const attrs = new Map([[MOBILE_SHEET_ATTR, 'layers']]);
|
||||
const classes = new Set([MOBILE_SHELL_CLASS]);
|
||||
const body = {
|
||||
classList: {
|
||||
add: (name) => classes.add(name),
|
||||
remove: (name) => classes.delete(name),
|
||||
contains: (name) => classes.has(name),
|
||||
},
|
||||
setAttribute: (name, value) => attrs.set(name, value),
|
||||
removeAttribute: (name) => attrs.delete(name),
|
||||
getAttribute: (name) => attrs.get(name) ?? null,
|
||||
};
|
||||
assert.equal(applyMobileShellMatch({ matches: true, body }), true);
|
||||
assert.equal(applyMobileShellMatch({ matches: false, body }), false);
|
||||
assert.equal(classes.has(MOBILE_SHELL_CLASS), false);
|
||||
assert.equal(attrs.has(MOBILE_SHEET_ATTR), false);
|
||||
});
|
||||
|
||||
test('ui.js keeps the desktop setPanelCollapsed hook and 720px layout gate', () => {
|
||||
assert.match(ui, /import \{ initMobileShell, syncMobileSheetFromPanel \} from '\.\/mobileShell\.js'/);
|
||||
assert.match(ui, /syncMobileSheetFromPanel\(panelId, \{ collapsed: nextCollapsed, restore, explicit \}\)/);
|
||||
assert.match(ui, /window\.matchMedia\('\(max-width: 720px\)'\)\.matches/);
|
||||
assert.match(css, /\(max-width:\s*720px\)/);
|
||||
assert.equal(MOBILE_SHELL_MEDIA_QUERY, '(max-width: 720px)');
|
||||
});
|
||||
|
||||
test('phone boot keeps desktop MSAA and falls back if tiles hang', () => {
|
||||
const main = readFileSync(new URL('./main.js', import.meta.url), 'utf8');
|
||||
assert.match(main, /const phoneLayout = window\.matchMedia\('\(max-width: 720px\)'\)\.matches/);
|
||||
assert.match(main, /msaaSamples: phoneLayout \? 1 : 4/);
|
||||
assert.match(main, /Google 3D Tiles timed out on this device/);
|
||||
assert.match(main, /loadPhotorealisticTileset/);
|
||||
assert.match(main, /: await createTileset/);
|
||||
});
|
||||
|
||||
test('tab bar is display:none by default and only appears under mobile-shell at 720px', () => {
|
||||
assert.match(css, /#mobile-tab-bar,\s*\n#mobile-sheet-backdrop,\s*\n#mobile-modal-close \{\s*\n\s*display:\s*none;/);
|
||||
assert.match(html, /id="mobile-modal-close"/);
|
||||
const shellBlock = css.match(
|
||||
/\/\* ── Mobile tab-shell \(720px, body\.mobile-shell only\) ── \*\/\s*@media \(max-width: 720px\) \{([\s\S]*?)\n\}\n\n@media \(max-width: 720px\) and \(max-height: 500px\)/,
|
||||
);
|
||||
assert.ok(shellBlock, 'mobile-shell 720px block is missing');
|
||||
assert.match(shellBlock[1], /body\.mobile-shell #mobile-tab-bar \{/);
|
||||
assert.match(shellBlock[1], /display:\s*flex;/);
|
||||
assert.match(shellBlock[1], /body\.cockpit-mode\.mobile-shell #mobile-tab-bar/);
|
||||
assert.match(shellBlock[1], /body\.ui-clean-view\.mobile-shell #mobile-tab-bar/);
|
||||
assert.match(shellBlock[1], /body\.recording-mode\.mobile-shell #mobile-tab-bar/);
|
||||
assert.match(shellBlock[1], /z-index:\s*160/);
|
||||
assert.match(shellBlock[1], /#mobile-modal-close/);
|
||||
assert.match(shellBlock[1], /data-mobile-sheet='search'/);
|
||||
assert.match(shellBlock[1], /data-mobile-sheet='presets'/);
|
||||
});
|
||||
|
||||
test('every body.mobile-shell rule stays inside the 720px gate', () => {
|
||||
const src = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
let index = 0;
|
||||
let prelude = '';
|
||||
const mediaStack = [];
|
||||
const leaks = [];
|
||||
while (index < src.length) {
|
||||
const char = src[index];
|
||||
if (char === '{') {
|
||||
const head = prelude.replace(/\s+/g, ' ').trim();
|
||||
prelude = '';
|
||||
if (head.startsWith('@media')) {
|
||||
mediaStack.push(head.slice('@media'.length).trim());
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (head.startsWith('@')) {
|
||||
let level = 1;
|
||||
index += 1;
|
||||
while (index < src.length && level > 0) {
|
||||
if (src[index] === '{') level += 1;
|
||||
else if (src[index] === '}') level -= 1;
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (/\bbody\.mobile-shell\b/.test(head)) {
|
||||
const gated = mediaStack.some((condition) => condition.includes('max-width: 720px'));
|
||||
if (!gated) leaks.push(head);
|
||||
}
|
||||
const close = src.indexOf('}', index);
|
||||
index = close + 1;
|
||||
continue;
|
||||
}
|
||||
if (char === '}') {
|
||||
mediaStack.pop();
|
||||
prelude = '';
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
prelude += char;
|
||||
index += 1;
|
||||
}
|
||||
assert.deepEqual(leaks, [], `mobile-shell rules leaked outside 720px:\n ${leaks.join('\n ')}`);
|
||||
});
|
||||
|
||||
test('mobile-shell credit and dock offsets lift only under the shell selector', () => {
|
||||
assert.match(
|
||||
css,
|
||||
/body\.mobile-shell #command-dock \{\s*bottom:\s*calc\(3\.25rem \+ 8px\);/,
|
||||
);
|
||||
assert.match(
|
||||
css,
|
||||
/body\.mobile-shell #command-dock:has\(#location-bar:not\(\.collapsed\)\),\s*\n\s*body\.mobile-shell #command-dock:has\(#control-panel:not\(\.collapsed\)\) \{\s*\n\s*left:\s*0;\s*\n\s*transform:\s*none;/,
|
||||
);
|
||||
assert.match(
|
||||
css,
|
||||
/body\.mobile-shell:not\(\.ui-clean-view\):not\(\.recording-mode\) #cesium-credits,[\s\S]*?bottom:\s*calc\(2vh \+ 8\.5rem\);/,
|
||||
);
|
||||
});
|
||||
|
|
@ -120,6 +120,7 @@ import {
|
|||
resolveCockpitUtilityAnchor,
|
||||
resolveCockpitUtilityLayout,
|
||||
} from './cockpitUtilityLayout.js';
|
||||
import { initMobileShell, syncMobileSheetFromPanel } from './mobileShell.js';
|
||||
import {
|
||||
applyCockpitVisionStageIntensities,
|
||||
captureCockpitVisionBaseline,
|
||||
|
|
@ -2618,6 +2619,11 @@ export class StyleManager {
|
|||
this._initUI();
|
||||
this._initMapStackControl();
|
||||
this._initPanelChrome();
|
||||
this._mobileShell = initMobileShell({
|
||||
setPanelCollapsed: (panelId, collapsed, options) => {
|
||||
this.setPanelCollapsed(panelId, collapsed, options);
|
||||
},
|
||||
});
|
||||
this._initLeftPanelAdaptiveLayout();
|
||||
this._initRightPanelAdaptiveLayout();
|
||||
this._initRadioPanel();
|
||||
|
|
@ -7711,6 +7717,7 @@ export class StyleManager {
|
|||
if (priorRightOwner !== this._rightStackPreferredPanelId) {
|
||||
this._scheduleRightPanelLayout({ reconsiderAutoCollapse: true });
|
||||
}
|
||||
syncMobileSheetFromPanel(panelId, { collapsed: nextCollapsed, restore, explicit });
|
||||
return;
|
||||
}
|
||||
panelEl.classList.remove('layout-auto-collapsed');
|
||||
|
|
@ -7761,6 +7768,7 @@ export class StyleManager {
|
|||
reconsiderAutoCollapse: this._leftPanelStack?.contains(panelEl) === true,
|
||||
});
|
||||
if (syncShare) this.shareLinkManager?.onPanelStateChange?.();
|
||||
syncMobileSheetFromPanel(panelId, { collapsed: nextCollapsed, restore, explicit });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
673
style.css
673
style.css
|
|
@ -1418,7 +1418,10 @@ body.ui-clean-view #data-panel,
|
|||
body.ui-clean-view #cctv-panel,
|
||||
body.ui-clean-view #radio-panel,
|
||||
body.ui-clean-view #global-context-panel,
|
||||
body.ui-clean-view #scene-panel {
|
||||
body.ui-clean-view #scene-panel,
|
||||
body.ui-clean-view #mobile-tab-bar,
|
||||
body.ui-clean-view #mobile-sheet-backdrop,
|
||||
body.ui-clean-view #mobile-modal-close {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
|
|
@ -3843,7 +3846,8 @@ body.cockpit-mode :is(
|
|||
#title-bar, #style-indicator, #top-center-actions, #traffic-sync-chip, #cctv-sync-chip,
|
||||
#command-dock,
|
||||
#military-awareness-panel, #military-awareness-direction-overlay,
|
||||
#cockpit-entry
|
||||
#cockpit-entry,
|
||||
#mobile-tab-bar, #mobile-sheet-backdrop, #mobile-modal-close
|
||||
) { display: none !important; }
|
||||
body.cockpit-mode :is(#clear-selected-layers, #share-btn, #reset-globe-view) {
|
||||
display: none !important;
|
||||
|
|
@ -9520,3 +9524,668 @@ body.scene-playback-mode #key-setup {
|
|||
border-color: rgba(255, 170, 150, 0.65);
|
||||
outline: none;
|
||||
}
|
||||
/* Hidden on desktop and before JS applies body.mobile-shell. */
|
||||
#mobile-tab-bar,
|
||||
#mobile-sheet-backdrop,
|
||||
#mobile-modal-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Mobile tab-shell (720px, body.mobile-shell only) ── */
|
||||
@media (max-width: 720px) {
|
||||
body.mobile-shell {
|
||||
--mobile-tabbar-height: 3.25rem;
|
||||
--mobile-dock-height: 3.875rem;
|
||||
--mobile-chrome-bottom: calc(env(safe-area-inset-bottom, 0px) + var(--mobile-tabbar-height) + var(--mobile-dock-height));
|
||||
--mobile-modal-inset-top: max(12px, env(safe-area-inset-top, 0px));
|
||||
--mobile-modal-max-height: min(72dvh, calc(100dvh - var(--mobile-chrome-bottom) - var(--mobile-modal-inset-top) - 12px));
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-tab-bar {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 146;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
min-height: var(--mobile-tabbar-height);
|
||||
padding: 0 4px env(safe-area-inset-bottom, 0px);
|
||||
background: rgba(9, 18, 27, 0.92);
|
||||
border-top: 1px solid rgba(123, 189, 211, 0.22);
|
||||
box-shadow: 0 -8px 28px rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(24px) saturate(1.3);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.3);
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-tab-bar button {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
padding: 6px 4px;
|
||||
color: var(--text-secondary);
|
||||
background: none;
|
||||
border: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-tab-bar button .material-symbols-outlined {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-tab-bar button[aria-pressed='true'] {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-sheet-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 155;
|
||||
display: none;
|
||||
background: rgba(4, 8, 14, 0.42);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet] #mobile-sheet-backdrop:not([hidden]) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-modal-close {
|
||||
position: fixed;
|
||||
top: calc(var(--mobile-modal-inset-top) + 8px);
|
||||
right: 18px;
|
||||
z-index: 170;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
color: var(--text-primary);
|
||||
background: rgba(9, 18, 27, 0.92);
|
||||
border: 1px solid rgba(123, 189, 211, 0.28);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet] #mobile-modal-close:not([hidden]) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-modal-close .material-symbols-outlined {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
body.mobile-shell #title-bar {
|
||||
top: max(12px, env(safe-area-inset-top, 0px));
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
body.mobile-shell #title-bar .subtitle,
|
||||
body.mobile-shell #style-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #top-center-actions {
|
||||
top: max(10px, env(safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
body.mobile-shell #top-center-actions button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
body.mobile-shell #left-panel-stack {
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #right-context-rail {
|
||||
width: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #right-context-rail,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #right-context-rail,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock {
|
||||
z-index: 160;
|
||||
}
|
||||
|
||||
body.mobile-shell #left-panel-stack > [data-panel-id],
|
||||
body.mobile-shell #left-panel-stack > #data-panel.active,
|
||||
body.mobile-shell #right-context-rail > [data-panel-id],
|
||||
body.mobile-shell #pp-toggles {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet] .panel-collapse-btn,
|
||||
body.mobile-shell[data-mobile-sheet] .dock-pin-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack > #data-panel,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #right-context-rail > #global-context-panel,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #right-context-rail > #pp-toggles,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock > #control-panel {
|
||||
position: fixed;
|
||||
top: var(--mobile-modal-inset-top);
|
||||
right: 12px;
|
||||
bottom: auto;
|
||||
left: 12px;
|
||||
z-index: 160;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
visibility: visible;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
max-height: var(--mobile-modal-max-height);
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack > #data-panel.active {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #data-panel .panel-header,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #global-context-panel .panel-header,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles .pp-header-row,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #location-bar .location-toolbar,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #control-panel .panel-header,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack .panel-header {
|
||||
padding-right: 52px;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack > #data-panel,
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack > #data-panel.collapsed,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #right-context-rail > #global-context-panel,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #right-context-rail > #global-context-panel.collapsed,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles.collapsed,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar.collapsed,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock > #control-panel,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock > #control-panel.collapsed {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles .pp-header-row,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles .pp-toggle-group,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles #clean-view-toggle {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack > #data-panel .data-panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #right-context-rail > #global-context-panel .global-context-panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar .location-inner,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock > #control-panel .panel-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
max-height: var(--mobile-modal-max-height);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='display'] #right-context-rail > #pp-toggles,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #right-context-rail > #pp-toggles:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='display'] #right-context-rail.layout-focus > #pp-toggles:not(.collapsed) {
|
||||
box-sizing: border-box;
|
||||
flex: 0 1 auto;
|
||||
align-items: stretch;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
max-height: var(--mobile-modal-max-height);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 12px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles > .pp-header-row {
|
||||
flex: 0 0 auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles > .pp-toggle-group,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles > #clean-view-toggle,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles > #param-slider-panel {
|
||||
flex: 0 0 auto;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #data-toggles {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack {
|
||||
position: fixed;
|
||||
top: var(--mobile-modal-inset-top);
|
||||
right: 12px;
|
||||
bottom: auto;
|
||||
left: 12px;
|
||||
z-index: 160;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
visibility: visible;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-height: var(--mobile-modal-max-height);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan-y;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #data-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #cctv-panel,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #scene-panel,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #geo-status-panel,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #cctv-panel.collapsed,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #scene-panel.collapsed,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #geo-status-panel.collapsed,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #cctv-panel:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #scene-panel:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #geo-status-panel:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack.layout-focus > #cctv-panel:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack.layout-focus > #scene-panel:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack.layout-focus > #geo-status-panel:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack.layout-tail > #cctv-panel:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack.layout-tail > #scene-panel:not(.collapsed),
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack.layout-tail > #geo-status-panel:not(.collapsed) {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
visibility: visible;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack.layout-focus > [data-panel-id].collapsed {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #geo-status-panel,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #geo-status-panel.collapsed,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #geo-status-panel:not(.collapsed) {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='more'] #cctv-panel .cctv-panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #scene-panel .scene-panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #geo-status-panel .geo-status-panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #cctv-panel:not(.collapsed) .cctv-panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #scene-panel:not(.collapsed) .scene-panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #geo-status-panel:not(.collapsed) .geo-status-panel-inner {
|
||||
height: auto;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock > #location-bar,
|
||||
body.mobile-shell #command-dock > #control-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock > #control-panel {
|
||||
display: flex;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock .location-toolbar-label,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock #control-panel .panel-title {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar .location-city-row,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar .location-search-wrap,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock #location-search,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock #location-search.expanded {
|
||||
flex-basis: auto;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock #location-search,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock #location-search.expanded {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock > #control-panel .button-grid {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock #location-bar:not(.collapsed) .dock-popover-content,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock #control-panel:not(.collapsed) .dock-popover-content {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
width: auto;
|
||||
margin-bottom: 0;
|
||||
transform: translateY(0) scale(1);
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack > #data-panel,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #right-context-rail > #global-context-panel,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock > #control-panel,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #data-panel .data-panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #global-context-panel .global-context-panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #location-bar .location-inner,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #control-panel .panel-inner,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #cctv-panel,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack > #scene-panel {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] .data-toggle-row,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #cctv-frame,
|
||||
body.mobile-shell[data-mobile-sheet='more'] select {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock > #gev-voice-control {
|
||||
position: fixed;
|
||||
right: max(12px, env(safe-area-inset-right, 0px));
|
||||
bottom: calc(var(--mobile-chrome-bottom) + 12px);
|
||||
left: auto;
|
||||
z-index: 148;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock .gev-voice-heading,
|
||||
body.mobile-shell #command-dock .gev-voice-readout,
|
||||
body.mobile-shell #command-dock .gev-voice-help-tray,
|
||||
body.mobile-shell #command-dock .gev-mic-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock #gev-voice-button {
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock #gev-voice-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -18px;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock .gev-mic-orbit,
|
||||
body.mobile-shell #command-dock .gev-mic-orbit img {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock .gev-voice-visualizer {
|
||||
position: fixed;
|
||||
left: max(12px, env(safe-area-inset-left, 0px));
|
||||
right: auto;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
width: min(46vw, 11rem);
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: height 220ms ease, opacity 180ms ease;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock .gev-voice-visualizer-input {
|
||||
bottom: calc(var(--mobile-chrome-bottom) + 76px);
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock .gev-voice-visualizer-output {
|
||||
bottom: calc(var(--mobile-chrome-bottom) + 16px);
|
||||
}
|
||||
|
||||
body.mobile-shell #gev-voice-control[data-wave-input='open'] .gev-voice-visualizer-input,
|
||||
body.mobile-shell #gev-voice-control[data-wave-output='open'] .gev-voice-visualizer-output {
|
||||
height: 2rem;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-tab-bar button {
|
||||
padding: 6px 2px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock {
|
||||
bottom: calc(3.25rem + 8px);
|
||||
left: 0;
|
||||
transform: none;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #command-dock:has(#location-bar:not(.collapsed)),
|
||||
body.mobile-shell #command-dock:has(#control-panel:not(.collapsed)) {
|
||||
left: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
body.mobile-shell:not(.ui-clean-view):not(.recording-mode) #cesium-credits,
|
||||
body.mobile-shell:not(.ui-clean-view):not(.recording-mode):has(#intel-hud[data-variant='minimal'].active) #cesium-credits {
|
||||
bottom: calc(2vh + 8.5rem);
|
||||
left: 0.75rem;
|
||||
max-width: calc(100vw - 1.5rem);
|
||||
}
|
||||
|
||||
body.mobile-shell #location-mini-status {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #intel-hud .hud-summary-wrap {
|
||||
position: fixed;
|
||||
left: max(12px, env(safe-area-inset-left, 0px));
|
||||
right: calc(80px + 28px + env(safe-area-inset-right, 0px));
|
||||
bottom: calc(var(--mobile-chrome-bottom) + 12px);
|
||||
top: auto;
|
||||
z-index: 145;
|
||||
max-width: none;
|
||||
max-height: 80px;
|
||||
margin-top: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #intel-hud .hud-summary-label {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
body.mobile-shell #intel-hud .hud-summary {
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
line-clamp: 4;
|
||||
max-height: 64px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.mobile-shell #intel-hud .hud-bottom-left,
|
||||
body.mobile-shell #intel-hud #hud-mgrs,
|
||||
body.mobile-shell #intel-hud #hud-latlon,
|
||||
body.mobile-shell #intel-hud .hud-bottom-bar,
|
||||
body.mobile-shell #intel-hud[data-variant='tactical'] .hud-bottom-left,
|
||||
body.mobile-shell #intel-hud[data-variant='operator'] .hud-bottom-left,
|
||||
body.mobile-shell #intel-hud[data-variant='minimal'] .hud-bottom-left,
|
||||
body.mobile-shell #intel-hud[data-variant='operator'] .hud-bottom-bar,
|
||||
body.mobile-shell #intel-hud[data-variant='minimal'] .hud-bottom-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #intel-hud .hud-bottom-left,
|
||||
body.mobile-shell #intel-hud .hud-bottom-right,
|
||||
body.mobile-shell #intel-hud[data-variant='tactical'] .hud-bottom-left,
|
||||
body.mobile-shell #intel-hud[data-variant='tactical'] .hud-bottom-right,
|
||||
body.mobile-shell #intel-hud[data-variant='operator'] .hud-bottom-left,
|
||||
body.mobile-shell #intel-hud[data-variant='operator'] .hud-bottom-right,
|
||||
body.mobile-shell #intel-hud[data-variant='minimal'] .hud-bottom-left {
|
||||
bottom: calc(var(--mobile-chrome-bottom) + 0.75rem);
|
||||
}
|
||||
|
||||
body.mobile-shell #intel-hud .hud-bottom-bar,
|
||||
body.mobile-shell #intel-hud[data-variant='operator'] .hud-bottom-bar,
|
||||
body.mobile-shell #intel-hud[data-variant='minimal'] .hud-bottom-bar {
|
||||
bottom: calc(var(--mobile-chrome-bottom) + 3.4rem);
|
||||
}
|
||||
|
||||
body.mobile-shell.cockpit-mode #view-switcher {
|
||||
bottom: max(16px, env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
body.mobile-shell.cockpit-mode .cockpit-utility-controls {
|
||||
bottom: calc(152px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
body.ui-clean-view.mobile-shell #mobile-tab-bar,
|
||||
body.recording-mode.mobile-shell #mobile-tab-bar,
|
||||
body.cockpit-mode.mobile-shell #mobile-tab-bar,
|
||||
body.scene-playback-mode.mobile-shell #mobile-tab-bar,
|
||||
body.ui-clean-view.mobile-shell #mobile-sheet-backdrop,
|
||||
body.recording-mode.mobile-shell #mobile-sheet-backdrop,
|
||||
body.cockpit-mode.mobile-shell #mobile-sheet-backdrop,
|
||||
body.ui-clean-view.mobile-shell #mobile-modal-close,
|
||||
body.recording-mode.mobile-shell #mobile-modal-close,
|
||||
body.cockpit-mode.mobile-shell #mobile-modal-close,
|
||||
body.scene-playback-mode.mobile-shell #mobile-modal-close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) and (max-height: 500px) {
|
||||
body.mobile-shell #mobile-tab-bar button span:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile-shell #mobile-tab-bar {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
body.mobile-shell[data-mobile-sheet='layers'] #left-panel-stack > #data-panel,
|
||||
body.mobile-shell[data-mobile-sheet='context'] #right-context-rail > #global-context-panel,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #pp-toggles,
|
||||
body.mobile-shell[data-mobile-sheet='display'] #right-context-rail > #pp-toggles,
|
||||
body.mobile-shell[data-mobile-sheet='more'] #left-panel-stack,
|
||||
body.mobile-shell[data-mobile-sheet='search'] #command-dock > #location-bar,
|
||||
body.mobile-shell[data-mobile-sheet='presets'] #command-dock > #control-panel {
|
||||
top: var(--mobile-modal-inset-top);
|
||||
right: 12px;
|
||||
bottom: auto;
|
||||
left: 12px;
|
||||
width: auto;
|
||||
max-height: var(--mobile-modal-max-height);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue