From cad15b73cbeadb7f53cd2af804b032861a41c39e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 16:58:14 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH --- mobile/main.js | 35 ++++++++++++++++++----------------- mobile/ui/globeChrome.js | 36 ++++++++++++++++++++++++++++++++++-- mobile/ui/sheet.js | 18 ++++++++++++++++++ 3 files changed, 70 insertions(+), 19 deletions(-) diff --git a/mobile/main.js b/mobile/main.js index 08b9dc39c..543e1d81d 100644 --- a/mobile/main.js +++ b/mobile/main.js @@ -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; diff --git a/mobile/ui/globeChrome.js b/mobile/ui/globeChrome.js index 38acfaa97..dd0019401 100644 --- a/mobile/ui/globeChrome.js +++ b/mobile/ui/globeChrome.js @@ -120,7 +120,7 @@ export function createGlobeChrome({ const top = h(`
——
- +
@@ -409,7 +409,30 @@ export function createGlobeChrome({ `; } + /** 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 = `
${icon('layers')} +

Layers

${on} on · ${fmtNum(count)} contacts
`; + grid.body.innerHTML = `
Look
+
${LENSES.map((l) => ``).join('')}
+ ${FAMILIES.map((f) => { + const x = st[f.id]; + const layers = [...x.layers].sort( + (a, b) => (b.available !== false) - (a.available !== false), + ); + return `
${icon(FAMILY_ICON[f.id])}${f.name}${x.on}/${x.total} · ${fmtNum(x.count)}
+
${layers.map(tileHtml).join('')}
`; + }).join('')} +

Counts are live from each feed; dotted means stale.

`; + } + 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(); diff --git a/mobile/ui/sheet.js b/mobile/ui/sheet.js index 9da845cc6..eee686756 100644 --- a/mobile/ui/sheet.js +++ b/mobile/ui/sheet.js @@ -19,6 +19,16 @@ const DEFAULT_DETENTS = { * heights?: Recordnumber>, 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(); } });