Skip to content

v0.98.0 — Mobile play shell

Shipped 2026-09-20 — merged to main as d6cf7f71, tagged v0.98.0. Built on version/0.98.0, thirteen commits. The eighth of the releases the near-term roadmap sequences from the August 2026 heuristic evaluation; the roadmap page is mobile-play-shell, part two of the navigation refactor and the whole of the evaluation's Mobile Improvements project (its bugs shipped in v0.90.0). The design reference is Style Guide 3.1.0; the help page is Playing on a Phone.

"Not too many changes relative to current", in the evaluation's words: MobileContextBar (v0.75.0) was already a fixed bottom bar with a play tab, an impersonation flow and a chat overlay. This release makes its model explicit — three tabs, Map / Me / Chat, each taking the whole screen above the bar, with up to two slots per tab — moves that model into a store, retires the mobile top nav (the desktop top bar it mirrored went in v0.97.0), and chooses the shell by input rather than width. Decision M-D1 (the Initiative Tracker stays a Chat sub-view) at its recommendation.

The model

  • mobileShellStore (M.1) — tab, one drawer at a time (each owned by a tab; opening one selects its tab, selecting a tab closes it), and chooser (Change Character's request for the chooser while somebody is impersonated). The rules are pure functions beside it: meView (nobody impersonated → the chooser, always) and slotsFor (Map → Actions and Palette; Me → Change and Stop while impersonating; Chat → View — never more than two). Stop returns to Map; targeting mode resets everything.
  • The bar renders it (M.2) — no useState left but the nav drawer's flag. A role="tablist" of the three tabs with aria-selected from the store and the Chat tab's unread badge; the selected tab's slots to the right, a caret per sheet (aria-expanded, disabled when the slot is) or a button per action.

The screens and the sheets

Under Layout/MobileShell/:

  • TabBody — a tab's whole screen: fixed, portalled, from the top to the bar, role="tabpanel", in the MOBILE_OVERLAY band. Map has none: the map is beneath everything.
  • MobileDrawer — a bottom sheet in the MOBILE_MENU band whose frame and overlay end at the bar (inline styles, since Mantine's inset: 0 ties a class), as tall as its content up to 70vh; outside tap dismisses.
  • Map (M.3) — Actions: the v0.96.0 list grouped by power, a sheet instead of a centred modal. Palette: ToolPalette gains a vertical embed ({ vertical: true, onPick }) — every tool named, Find a token by name, Undo / Redo, the layers as a list with the current one marked, Edit Layers — with no overflow plan consulted; a pick closes the sheet. The floating palette on a phone is gone.
  • Me (M.4) — MeChooser ("Select your character": touch a token on the map, or pick from the tokens you may play) or the sheet: CharacterContext at full width with the sheet as the one scroller (measured: the only scrolling element in the tab), or the token sheet for a character-less token. The header is v0.42.0's switcher; no close button — Map is a tab.
  • Chat (M.5) — the chat context filling the tab, the header naming the view with the voice-transcript control and Chat Settings; ChatViewDrawer is a list of the four views with the current one checked, built for chat scope's channels to append to.

Retiring the top nav

  • MobileTopNav deleted (M.6). The nub renders on a phone too — from the viewport's top edge, centred on the viewport, up to 80vw. Off /play the bar is one leftmost slot, Menu, opening MobileNavDrawer: LeftRail in a new variant="drawer" (always wide, in flow, no toggle, no width published, closing on any navigation) — a phone has the same rail the desktop has. With the top nav went what only it kept alive: NavSections (T.1's section menus), SideNav (v0.24.0's drawer content, with its Characters badge and NotificationsBellMenu over pending invitations — Messages, v0.62.0, announce both), the dead TopNav. retiredChrome.guard holds all five. HoverTokenPortrait reads LEFT_NAV_RAIL_WIDTH (60) instead of the retired SIDENAV_COLLAPSED_WIDTH (44).

Gestures, input, chrome

  • Gestures (M.7) — no swipe between tabs (a guard fails a touch handler in the shell). The back gesture: pushDrawerEntry pushes one history entry per open sheet — the same URL, the router's state kept, plus a marker — and closes the sheet when it is popped; a sheet closed any other way pops its own entry. Four tests on a fake window.
  • Chosen by input (M.8) — getScreenSize(width, hasHover): a pointer that cannot hover gets small (the shell) at any width; one that can gets the desktop down to BREAKPOINTS.small, now 768 (was 1024). UI.tsx reads useIsMobile() instead of its own width query. Landscape: the bar stays at the bottom, the tab fills the rest, the sheet's columns are its own container rule.
  • Bottom chrome generalised (M.9) — aboveBottomChrome(extraPx) reads --vtt-bottom-chrome for the End Turn button, the token bars, the mobile token-context button, the multi-select bar and the action bubbles, so each is right whether or not the bar is mounted; MOBILE_CONTEXT_BAR_HEIGHT has no reader outside the bar. Every popup the theme defaults opens above the shell's bands; mobileShellBands.test.ts holds the order for both themes.

Finishing

  • Guards (M.10) — seven bar guards deleted with the useState they read, ten with the retired chrome; MobileContextBar.contract, MobileShell.contract, mobileShellBands, screenOwners.guard (the three tabs are the only screen owners) and drawerHistory added; about twenty re-anchored.
  • Verification (M.11) — in Chrome at 390px and 740×390 through a same-origin iframe (the window would not resize), with requestAnimationFrame polyfilled because a background tab fires none and Mantine's transitions need it. A physical-device pass is still owed — real touch, hover: none, the Android back gesture, the keyboard over the chat composer.
  • Style Guide 3.1.0 (M.12) — a minor bump: the Mobile section, the shell's bands in the Layers table. Help: Playing on a Phone, and Core Concepts points to it.

Decisions

M-D1 at its recommendation. Made here: the Actions sheet keeps the grouped list rather than "the same component the Character Context's Actions section uses" (that is the module-rendered sheet section, not a list); a modal covers the bar rather than sitting below its band, since a bar tappable above a modal's overlay would close nothing; and the shell is input-first with a 768px floor for a hovering pointer, so a narrow desktop window is not left on the desktop layout with nowhere to put a sidebar.