fix(mobile): never trap navigation; top-right button opens all layers

Tab switches no longer run inside a View Transition, which the globe
and camera could stall so taps on the tab bar went unanswered, and a
capture enter/leave failure can no longer abort a switch. Escape closes
the topmost open sheet wherever focus is. The top-right globe button
opens one Layers sheet with the visual looks and every layer family.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
This commit is contained in:
Claude committed 2026-10-23 16:58:14 +00:00
1 parent 65fec98c94
commit cad15b73cb
3 files changed
+70 -19

No files matched your search

+18 -17
View File
@@ -159,13 +159,16 @@ function applyTab(tab) {
else el.hidden = id !== tab && !(two && tab === 'capture' && id === prev);
}
tabbar.select(tab);
if (tab === 'capture') {
globe?.pause();
capture?.enter();
} else {
if (prev === 'capture') capture?.leave();
updateGlobeActivity();
// A camera or sensor failure must never trap someone in Capture.
try {
if (tab === 'capture') {
globe?.pause();
capture?.enter();
} else if (prev === 'capture') capture?.leave();
} catch (err) {
console.warn('capture', err);
}
if (tab !== 'capture') updateGlobeActivity();
if (tab === 'you') you?.render?.();
if (tab === 'rooms') threads?.refresh?.();
if (tab === 'live' && two) chrome?.renderLeadView();
@@ -180,16 +183,14 @@ function selectTab(tab) {
if (player?.current) player.close();
if (world?.current && !isTwoPane()) world.close();
if (narrative?.active) narrative.stop();
const vt = document.startViewTransition && !reducedMotion();
if (vt) document.startViewTransition(() => applyTab(tab));
else {
applyTab(tab);
const v = views[tab];
if (v && !v.hidden) {
v.classList.remove('view-enter');
void v.offsetWidth;
v.classList.add('view-enter');
}
// Switch synchronously. A View Transition waits on frames the globe and
// the camera can hold up, which left taps on the tab bar unanswered.
applyTab(tab);
const v = views[tab];
if (v && !v.hidden && !reducedMotion()) {
v.classList.remove('view-enter');
void v.offsetWidth;
v.classList.add('view-enter');
}
}
@@ -1298,7 +1299,7 @@ function buildDirector(g) {
// Keyboard: Escape closes the topmost thing.
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
if (e.key !== 'Escape' || e.defaultPrevented) return;
if (document.querySelector('.sheet-layer.is-modal:not([hidden])')) return;
if (dossier?.isOpen) return dossier.close();
if (player?.current) return;
+34 -2
View File
@@ -120,7 +120,7 @@ export function createGlobeChrome({
const top = h(`<div class="gc-top">
<div class="gc-readout"><span class="tele gc-alt">—</span><span class="tele gc-ll">—</span></div>
<div class="gc-buttons">
<button class="icon-btn glass-strong liquid" type="button" data-act="lens" aria-label="Visual lens" aria-haspopup="true">${icon('lens')}</button>
<button class="icon-btn glass-strong liquid" type="button" data-act="lens" aria-label="Layers" aria-haspopup="dialog">${icon('layers')}</button>
<button class="icon-btn glass-strong liquid gc-compass" type="button" data-act="home" aria-label="Reset view to the whole planet"><span class="compass-n">${icon('north')}</span></button>
<button class="icon-btn glass-strong liquid" type="button" data-act="credits" aria-label="Map data attribution">${icon('info')}</button>
</div>
@@ -409,7 +409,30 @@ export function createGlobeChrome({
</button>`;
}
/** Every family in one sheet, with the visual looks on top. */
function renderAllGrid() {
const st = familyState();
const active = store.get('lens') || 'cinematic';
const on = Object.values(st).reduce((a, x) => a + x.on, 0);
const count = Object.values(st).reduce((a, x) => a + x.count, 0);
grid.sheet.dataset.family = 'all';
grid.head.innerHTML = `<div class="grid-head"><span class="grid-ic">${icon('layers')}</span>
<div><h2 class="sheet-title">Layers</h2><div class="sheet-sub num">${on} on · ${fmtNum(count)} contacts</div></div></div>`;
grid.body.innerHTML = `<div class="section-h">Look</div>
<div class="lens-row" data-hscroll>${LENSES.map((l) => `<button type="button" class="lens-card ${l.id === active ? 'on' : ''}" data-lens3="${l.id}"><span class="lens-sw" style="background:${l.sw}"></span>${l.name}</button>`).join('')}</div>
${FAMILIES.map((f) => {
const x = st[f.id];
const layers = [...x.layers].sort(
(a, b) => (b.available !== false) - (a.available !== false),
);
return `<div class="section-h" data-family="${f.id}"><span class="lead-fam">${icon(FAMILY_ICON[f.id])}${f.name}</span><small class="num">${x.on}/${x.total} · ${fmtNum(x.count)}</small></div>
<div class="tiles">${layers.map(tileHtml).join('')}</div>`;
}).join('')}
<p class="footnote" style="padding:14px 2px 0">Counts are live from each feed; dotted means stale.</p>`;
}
function renderGrid(fam) {
if (fam === 'all') return renderAllGrid();
const st = familyState()[fam];
const f = FAMILIES.find((x) => x.id === fam);
grid.sheet.dataset.family = fam;
@@ -427,6 +450,15 @@ export function createGlobeChrome({
}
grid.root.addEventListener('click', (e) => {
const look = e.target.closest('[data-lens3]');
if (look) {
haptic('tick');
setLens(look.dataset.lens3);
$$('[data-lens3]', grid.root).forEach((b) =>
b.classList.toggle('on', b === look),
);
return;
}
const soon = e.target.closest('[data-soon]');
if (soon) {
toast(
@@ -506,8 +538,8 @@ export function createGlobeChrome({
top.addEventListener('click', (e) => {
const act = e.target.closest('[data-act]')?.dataset.act;
if (act === 'lens') {
lensPop.hidden = !lensPop.hidden;
haptic('tick');
openGrid('all');
} else if (act === 'home') {
haptic('tick');
hidePick();
+18
View File
@@ -19,6 +19,16 @@ const DEFAULT_DETENTS = {
* heights?: Record<string,(H:number)=>number>, closeButton?: boolean,
* }} o
*/
/** Open dismissible sheets, newest last: Escape closes the top one even
* when focus is elsewhere (on the globe, or nowhere after a click). */
const escStack = [];
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape' || e.defaultPrevented || !escStack.length) return;
if (e.target.closest?.('input, textarea, [contenteditable]')) return;
e.preventDefault();
escStack[escStack.length - 1]();
});
export function createSheet(o) {
const ev = emitter();
const detents = o.detents || ['peek', 'medium', 'large'];
@@ -89,6 +99,11 @@ export function createSheet(o) {
measure();
root.hidden = false;
isOpen = true;
if (o.modal || o.dismissible) {
const i = escStack.indexOf(close);
if (i >= 0) escStack.splice(i, 1);
escStack.push(close);
}
if (!current || root.dataset.closing) {
y = maxH() + 40;
apply(false);
@@ -112,6 +127,8 @@ export function createSheet(o) {
function close() {
if (!isOpen) return;
isOpen = false;
const i = escStack.indexOf(close);
if (i >= 0) escStack.splice(i, 1);
root.dataset.closing = '1';
y = maxH() + 40;
apply(true, false);
@@ -260,6 +277,7 @@ export function createSheet(o) {
root.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && (o.modal || o.dismissible)) {
e.stopPropagation();
e.preventDefault();
close();
}
});