Skip to content
Source
Sign inSign up
Open History to see the last commit

mobile-layout.md

59 lines3.8 KB

1# Mobile layout
2​
3The mobile app runs on phones, foldables, tablets and resizable iPad windows. One rule keeps
4that manageable: each layer below makes one decision, and no layer makes another's. A feature
5screen never asks which platform or window width it is on.
6​
7![Wide window as five stacked layers: router, workspace shell, feature screens, design system, native](./images/mobile-workspace-split.png)
8​
9![A compact Android screen as the same five layers](./images/mobile-layers.png)
10​
11## The layers
12​
131. **Router.** The [root stack](../../apps/mobile/src/Stack.tsx) is the layout state: what is
14 selected, what is pushed, which sheet is open. Rotating, folding or resizing renders the same
15 stack differently. Nothing remembers which column a page was in. Each route declares what it
16 is with a [preset](../../apps/mobile/src/features/layout/route-options.ts) (`pageRoute`,
17 `sheetRoute`, `modalRoute`, `immersiveRoute`); the preset owns detents, corner radius, header
18 appearance and the Android fallback. See [Mobile navigation](./mobile-navigation.md).
192. **Workspace shell.** [`features/layout`](../../apps/mobile/src/features/layout/) owns placement
20 and widths, from the width class in [`layout.ts`](../../apps/mobile/src/lib/layout.ts):
21 - _Expanded_: sidebar, content and inspector columns.
22 - _Medium_ (600–720pt wide): the sidebar sits beside a list and floats over a page.
23 - _Compact_ (under 600pt either way): the sidebar is the root, and pages push.
24 - Where the inspector column does not fit, the inspector is a sheet.
25​
26 The workspace tabs, dock and toolbars live in `features/layout/workspace`. Stacks nested in a
27 sheet or popover, and screens rendered under a route of their own, also come from here.
28​
293. **Feature screens.** A screen places content by role. It draws one `AppBar` (or `SheetHeader`
30 for a form sheet), renders its page, declares its inspector panes with `<Inspector>`, and asks
31 for them with `showInspector()`. The shell decides between column and sheet. Choosing a route,
32 a component, header options or an inset by width or platform here is the bug this structure
33 exists to prevent.
344. **Design system.** Every pane, page, sheet and modal has an `AppBar` with the same contract on
35 both platforms; a sheet or modal passes `onClose`.
36 [`PaneContext`](../../apps/mobile/src/components/ui/Pane.tsx) tells the bar which column it is
37 in, so it adds that column's controls, such as the sidebar toggle. Where content meets the bar
38 comes from [`useBarInset`](../../apps/mobile/src/components/ui/useBarInset.ts). Chrome
39 branches by platform only here.
405. **Native.** On iOS 26, the columns are `UISplitViewController` columns with a navigation bar
41 each. On Android and on iPadOS 18–25, the shell draws the same columns in JS from the window
42 and hinge metrics. Android never shows the native stack header; every bar is drawn in-tree.
43​
44Platform splits live in layers 1, 2 and 4, as `.ios` and `.android` files.
45​
46## Traps
47​
48- iPadOS 18–25 runs the JS columns, and the iOS deployment target is 18. Code that looks
49 Android-only in the JS column path is still live on those iPads.
50- Android renders `Home` twice: in the sidebar column and as the root of the content stack. The
51 sidebar copy is never the focused route, so hooks that gate on focus (such as Back handling)
52 must also accept `usePaneRole() === "sidebar"`.
53- Modals and full-screen pages are presented over the columns and render with no pane role, so
54 they get no column chrome.
55- On iOS each inspector pane is a native screen of its own, and only the active one is attached.
56 A pane hidden with opacity still has its native bar, toolbar and scroll view in the window,
57 and UIKit shows them over the visible pane.
58- A route opened from the inspector sheet must wait for the sheet to close, through
59 `InspectorSheetContext`. Presenting over a closing sheet races its dismissal on iOS.