diff --git a/build/panel.js b/build/panel.js index e10d0bbca..062475866 100644 --- a/build/panel.js +++ b/build/panel.js @@ -26,6 +26,8 @@ export function panelBuildConfig(config) { cssCodeSplit: false, rollupOptions: { ...config.build?.rollupOptions, + // The panel is the app alone: one page, so one inlined script. + input: undefined, output: { inlineDynamicImports: true }, }, }, diff --git a/build/vite.js b/build/vite.js index 4f2703e88..280687bab 100644 --- a/build/vite.js +++ b/build/vite.js @@ -1,9 +1,16 @@ +import { fileURLToPath } from 'node:url'; import { applicationHtmlPlugin } from './application-html.js'; import cesium from 'vite-plugin-cesium'; import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js'; import { embedFramingPlugin } from './embed-framing.js'; import { panelBuildPlugin } from './panel.js'; +/** HTML documents a production build emits: the app and its mobile shell. */ +export const BROWSER_PAGES = Object.freeze({ + main: fileURLToPath(new URL('../index.html', import.meta.url)), + mobile: fileURLToPath(new URL('../mobile/index.html', import.meta.url)), +}); + /** * Content-Security-Policy for every document the dev/preview server serves. * No inline script is permitted, and the only foreign script origins are the @@ -113,6 +120,9 @@ export function createBrowserViteConfig({ mapillaryToken ?? '', ), }, - build: { chunkSizeWarningLimit: 1500 }, + build: { + chunkSizeWarningLimit: 1500, + rollupOptions: { input: { ...BROWSER_PAGES } }, + }, }; } diff --git a/mobile/index.html b/mobile/index.html index 1c2120579..44b5190e7 100644 --- a/mobile/index.html +++ b/mobile/index.html @@ -22,7 +22,7 @@ - + diff --git a/mobile/styles/globe-ui.css b/mobile/styles/globe-ui.css index 0418cf879..f872ad6fc 100644 --- a/mobile/styles/globe-ui.css +++ b/mobile/styles/globe-ui.css @@ -82,11 +82,65 @@ animation: view-in var(--dur-3) var(--spring) both; } -.app.is-world .gc-readout, -.app.is-world .gc-attr { +.app.is-world .gc-readout { visibility: hidden; } +/* Attribution stays on screen whenever the globe is (provider terms). */ +.app.is-world .gc-attr { + top: calc(var(--safe-t) + 60px); +} + +.gc-attr.has-onscreen { + color: var(--ink-2); +} + +.gc-labels { + position: absolute; + inset: 0; + pointer-events: none !important; + overflow: hidden; + z-index: 0; +} + +.gc-labels span { + position: absolute; + left: 0; + top: 0; + display: inline-flex; + align-items: center; + gap: 4px; + margin: -9px 0 0 8px; + height: 18px; + padding: 0 6px 0 5px; + border-radius: 9px; + background: rgba(5, 7, 11, 0.58); + font-family: var(--mono); + font-size: 0.625rem; + font-weight: 600; + color: var(--ink); + white-space: nowrap; + will-change: transform; +} + +.gc-labels i { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--fam, var(--ink-3)); +} + +.tile.soon { + opacity: 0.5; + background: transparent; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.tile.soon .tile-status { + font-style: normal; + color: var(--ink-4); +} + /* Lens popover */ .lens-pop { position: absolute; @@ -735,6 +789,12 @@ color: var(--fam); } +/* The headline number belongs to the peek detent; taller detents show it in + the labelled metrics row instead of twice. */ +.brief-sheet:not([data-detent='peek']) .peek-metric { + display: none; +} + .peek-metric small { font-size: 0.75rem; font-weight: 650; diff --git a/mobile/ui/feed.js b/mobile/ui/feed.js index 7505da684..095ee9518 100644 --- a/mobile/ui/feed.js +++ b/mobile/ui/feed.js @@ -136,7 +136,11 @@ export function createFeed( } return all .filter((s) => !isReconstruction(s) && !isAlwaysOn(s)) - .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); + .sort( + (a, b) => + (b.severity ?? 0) - (a.severity ?? 0) || + new Date(b.updatedAt) - new Date(a.updatedAt), + ); } function render() { diff --git a/mobile/ui/globeChrome.js b/mobile/ui/globeChrome.js index 0536560aa..5bb4f8c03 100644 --- a/mobile/ui/globeChrome.js +++ b/mobile/ui/globeChrome.js @@ -96,6 +96,68 @@ export function createGlobeChrome({ onTime: (d) => globe.setTime(d), }); + /* ---------- HTML labels (engine draws none; ≤ 40, DESIGN.md §5) ---------- */ + const labelLayer = h('
'); + chrome.prepend(labelLayer); + let labelRaf = 0; + const KIND_FAM_L = { + aircraft: 'sky', + helicopter: 'sky', + military: 'sky', + vessel: 'sea', + ship: 'sea', + satellite: 'orbit', + station: 'orbit', + launch: 'orbit', + earthquake: 'hazards', + quake: 'hazards', + fire: 'hazards', + cyclone: 'hazards', + camera: 'eyes', + cctv: 'eyes', + }; + function drawLabels() { + labelRaf = 0; + if (document.hidden || getComputedStyle(chrome).visibility === 'hidden') + return; + const list = globe.labels(40); + const W = chrome.clientWidth; + const html = list + .map((l) => { + const flip = l.x > W - 9 * Math.min(24, l.title.length) - 40; + const tf = `translate(${Math.round(l.x)}px,${Math.round(l.y)}px)${flip ? ' translateX(calc(-100% - 16px))' : ''}`; + return `${esc(l.title)}`; + }) + .join(''); + if (labelLayer.innerHTML !== html) labelLayer.innerHTML = html; + } + const queueLabels = () => { + if (!labelRaf) labelRaf = requestAnimationFrame(drawLabels); + }; + globe.on('camera', queueLabels); + globe.on('layers', queueLabels); + setInterval(queueLabels, 1000); + + /** Provider credits flagged onScreen (e.g. Google 3D Tiles) must stay visible. */ + let attrKey = ''; + function refreshAttribution() { + const list = globe.credits() || []; + const onScreen = list + .filter((c) => c.onScreen) + .map((c) => c.text || String(c.html || '').replace(/<[^>]+>/g, '')); + const text = ['Esri · Maxar · Earthstar · NASA', ...onScreen].join(' · '); + if (text === attrKey) return; + attrKey = text; + attribution.textContent = text; + attribution.classList.toggle('has-onscreen', onScreen.length > 0); + } + let attrTimer = 0; + globe.on('camera', () => { + clearTimeout(attrTimer); + attrTimer = setTimeout(refreshAttribution, 300); + }); + setTimeout(refreshAttribution, 500); + /* ---------- camera readout ---------- */ globe.on('camera', (c) => { if (!c) return; @@ -141,7 +203,7 @@ export function createGlobeChrome({ for (const l of layers) { const s = st[l.family]; if (!s) continue; - s.total += 1; + if (l.available !== false) s.total += 1; s.layers.push(l); if (l.on) { s.on += 1; @@ -216,6 +278,12 @@ export function createGlobeChrome({ grid.on('close', () => (gridFamily = null)); function tileHtml(l) { + if (l.available === false) + return ``; return `