docs: add native ui and ux audit

This commit is contained in:
Linus Rath
2026-10-15 01:19:00 +02:00
parent e19885dc72
commit 2a47913d70
+550
View File
@@ -0,0 +1,550 @@
# Native UI and UX audit, 2026-10
Scope: every screen, sheet, dialog, banner and gesture of the Android app, on a phone and a tablet, in
portrait and landscape. Started from `e19885d` (0.1.75).
## Method
- Devices (emulators):
- phone `Bulwark_Pixel7_API35`, 411x914 dp;
- tablet `Pixel_Tablet_API_34`, 1280x800 dp.
The tablet's natural orientation is landscape, so `user_rotation 1` is tablet portrait.
- Cells used below:
| Cell | Device and orientation |
|---|---|
| PP | phone portrait |
| PL | phone landscape |
| TL | tablet landscape |
| TP | tablet portrait |
"all" means all four.
- Build: release APK from `C:\rn`, with the portrait lock removed for the audit only.
- Data: Stalwart 0.16 on VM 106, seeded with:
- 520+ inbox mails, a 16-message thread, HTML-only newsletters, a wide table, all attachment types
and an inline cid image;
- RTL mail (Arabic, Hebrew), a read-receipt request, very long subjects and names;
- nested folders;
- recurring, all-day, multi-day, overlapping, declined and cancelled events, and tasks;
- 65 contacts with and without photos, plus two groups;
- files and folders, and an active vacation reply.
The phone ran as `usera@`, the tablet as a separate `uxaudit@` account with the same seed.
- Every area was captured in all four cells in light English (tablet) and dark English (phone).
Then a second pass ran with German and font scale 1.3, on PP and TL, with the tablet in dark.
- Four read-only code passes (mail list, thread and compose, calendar, contacts/files/settings/login)
gave the `file:line` references. Device findings were checked against them.
- Screenshots are in the session scratchpad (`shots/q_<screen>.png`, a 4-cell strip: PP, PL, TL, TP;
`shots/d_<screen>.png` for the German/1.3 pass). They are not committed.
## Inventory
### Navigation
- Root native stack (`App.tsx`):
- MainTabs
- EmailThread
- EmailSource
- Compose (modal)
- ContactDetail
- ContactForm (modal)
- GroupDetail
- Scheduled
- UnifiedInbox
- AddAccount (modal)
- Bottom tabs: Mail, Calendar, Contacts, Files, Settings. A tab the server lacks is dimmed and does
nothing.
- Above the tabs:
- UpdateBanner
- OfflineCacheBanner
- PushOnboardingPrompt (floating card)
- AppIconBadge (no UI)
- On the focused stack screen: ToastHost and UndoSnackbar (none on Compose).
- Entry points:
- deep links: `bulwarkmobile://`, webmail permalinks, `mailto:`, and sign-in links
(`bulwarkmail://pair`, `connect`);
- Android share intents into Compose;
- notification taps (mail push, calendar reminders);
- widgets (inbox, agenda), which deep-link.
### Login (`LoginScreen` + `screens/login/*`)
- Steps: choose → email → (discovery) confirm or server → confirm → OAuth hand-off in the browser, or
password (+ TOTP when the server asks) → signing in.
- Side paths:
- QR scan modal (camera permission, blocked → open settings);
- paste-link modal;
- external sign-in link confirm alert;
- refused-link notice;
- Add account mode with a close button.
### Mail
- **Mail list** (`EmailListScreen`):
- Header: menu, folder title, import, logo.
- Search row: select-all, search with recent searches and people suggestions, sort toggle,
filter.
- Filter chips.
- Banners: empty folder, offline, failed outbox, unreachable accounts.
- States: loading, error + retry, no mailboxes, empty / no results.
- Gestures: pull to refresh, endless scroll, long-press to select, swipe actions (instant or
reveal).
- Selection header: star, read, tag, move, spam, archive, delete.
- Compose FAB.
- Sheets: filter modal (with date pickers), MoveSheet, TagSheet, attachment preview.
- **Sidebar drawer** (`SidebarDrawer`):
- Account switcher and account menu: add, set default, sign out, sign out of all.
- Unified Inbox, Scheduled.
- Unified Mailbox section, folders tree with unread counts and long-press folder sheet, shared
accounts, tags.
- Nested ActionSheet and NamePrompt.
- **Unified inbox** (`UnifiedInboxScreen`): its own header, search, selection, swipe, account dots,
pull to refresh, load more.
- **Scheduled** (`ScheduledScreen`): list, per-item actions (send now, reschedule, edit, cancel),
reschedule presets, custom date-time picker.
### Thread view (`EmailThreadScreen` + `components/email/*`)
- Layout:
- Horizontal pager over the list.
- Toolbar: back, delete, archive (≥400 dp), unread (≥340 dp), star, more.
- Subject with star.
- Single message, or conversation cards (collapsed rows, expanded cards with Reply / Reply all /
Forward pills).
- Message header: sender and recipients open the address sheet; "Show details".
- Attachments: preview, open, save and share; "Download all".
- Banners: blocked images, truncated body, read receipt, unsubscribe, calendar invitation (RSVP,
add to calendar).
- Quick reply box.
- Bottom bar: Prev, three configurable actions, Next.
- More sheet: archive, read, move, tag, spam, view mode, sender, forward as attachment, source,
export.
- Email source screen.
### Compose (`ComposeScreen`)
- Header: close, title + draft status, attach, schedule, send.
- Fields:
- From (IdentitySheet, From options);
- To / Cc / Bcc with chips, autocomplete and a chip menu;
- Subject.
- Attachments: sheet with photos, camera, files; chips with progress.
- Body: rich editor WebView or plain text.
- Format bar: templates, read receipt, plain/rich, B/I/U/S, colours, H1/H2, lists, quote, table,
alignment, link, image, clear, undo/redo.
- Modals: link, colour palette, table, schedule sheet, date-time pickers, template placeholders,
TemplateSheet.
- Flows:
- draft autosave;
- close guard (cancel / discard / save draft);
- empty-subject and forgotten-attachment checks;
- undo send through UndoSnackbar;
- scheduled-send confirmation.
### Calendar (`CalendarScreen` + `components/calendar/*`)
- Views: month (free scroll or paged), week (free scroll or paged), 3 days, day, schedule (agenda).
- Header: menu, month title with month dropdown, prev/next (paged only), today, tasks.
- FAB: event, or a menu with Event / Task.
- Sheets and dialogs:
- EventDetailSheet: RSVP, edit, duplicate, .ics, link, delete.
- EventModal (editor): RecurrenceEditor, ParticipantInput, pickers.
- RecurrenceScopeDialog.
- CalendarSidebarDrawer: views, refresh, calendars with a long-press panel, new calendar,
subscriptions, import.
- TasksSheet, ICalImportSheet, ICalSubscriptionSheet, CalendarEditSheet, CalendarShareSheet.
- Gestures:
- tap a slot for a draft, then tap the draft to create;
- long-press a slot to create;
- tap a month cell to open the day;
- free scroll with snapping.
- There is no drag-to-move or drag-to-resize, so there is nothing to break mid-drag.
### Contacts
- `ContactsScreen`:
- Header: drawer, count, search, import, add (long-press: contact or group).
- Address book / group chips.
- Long-press to select.
- Selection header: select all, export, move, tag, delete.
- Pull to refresh.
- `ContactsSidebarDrawer`: books, shared books, groups, tags with rename.
- `ContactDetailScreen`:
- hero and quick actions (email, call, SMS, share);
- sections: email, phone, address, work, personal, online, keys, categories, related, members,
notes, address book;
- recent mail and events;
- More sheet (duplicate, add to group, move, export, delete).
- `ContactFormScreen`: person, organisation or group, with many collapsible sections and a date
picker.
- `GroupDetailScreen`: members, add or remove.
- Sheets: AddressBookPicker, TagAssign, ContactPicker, ContactImport.
### Files (`FilesScreen`)
- Header: back, search, new folder, upload, grid/list.
- Breadcrumb, sort chip, upload progress.
- List or 3-column grid; long-press to select (download zip, delete).
- Per-item ⋮ sheet: preview, open with, download, share, move, duplicate, rename, delete.
- Folder picker, sort sheet, prompt modal, share sheet, preview modal.
### Settings (`SettingsScreen` + `components/settings/*`)
- One screen that swaps between a searchable section list and a page.
- Pages:
- account, language, notifications
- appearance, layout
- reading, composing, identities, vacation, filters (rule modal, sieve editor), templates,
folders, tags, downloads
- security, encryption (disabled), content & senders
- calendar and contacts (with device sync), files, sidebar apps
- about & data, themes, plugins, updates
- Sign out.
### Other
- PushOnboardingPrompt, OfflineBanner, OfflineCacheBanner, UpdateBanner.
- Toasts: swipe to dismiss, action button.
- UndoSnackbar: undo, send now.
- Android widgets (out of scope for fixes): inbox and agenda.
## Findings
Severity: **P1** broken or blocking, **P2** clearly wrong or clumsy, **P3** polish. Each finding
lists the cells where it occurs, the screenshot, the code location and the intended fix. A tick
carries the commit that fixed it.
### Cross-cutting
- [ ] **U-001 · P1 · PL TL TP · The app is locked to portrait and has no large-screen layout.**
- Where: `app.config.js:34`, `AndroidManifest.xml:28`.
- Android 16 ignores the lock on screens 600 dp and wider, so tablets and foldables rotate anyway.
- Fix: remove the lock in both places. Keep `configChanges` (and add `smallestScreenSize` for
split-screen and foldables). Then do phase 2.
- [ ] **U-002 · P2 · iPad, landscape · No RN `<Modal>` sets `supportedOrientations`.**
- Where: 56 modals in 40 files.
- iOS presents these portrait-only and forces a rotation.
- Fix: pass all orientations everywhere.
- [ ] **U-003 · P2 · TL TP · Every screen is the phone layout stretched to 1280 dp.**
- Affected: mail rows, thread body, compose, contacts, files, settings, login.
- Screenshots: `q_mail_list`, `q_thread_receipt`, `q_contacts`, `q_settings`, `t_login_confirm`.
- Fix: the adaptive layout plan below.
- [ ] **U-004 · P2 · TL TP · Sheets and floating cards span the full width.**
- Affected: bottom sheets, toasts, the undo bar and the push prompt.
- Screenshots: `q_event_detail`, `q_mail_list` (push prompt).
- Fix: on medium and expanded widths, cap them at about 560 dp and centre them.
- [ ] **U-005 · P2 · PL · Left and right safe-area insets are ignored.**
- Most screens use `SafeAreaView edges={['top']}` and position sheets `left: 0, right: 0`.
- A side navigation bar or a camera cutout covers the content.
- Fix: add the side edges to screen roots, and inset sheets and the FABs.
- [ ] **U-006 · P2 · all · The Files header pads a fixed 60 px instead of the status-bar inset.**
- Where: `FilesScreen.tsx:1438`.
- It is too tall in landscape and wrong on any other status bar height.
- [ ] **U-007 · P2 · all · Uppercase section labels get clipped on Android.**
- Example: "WORK" shows as "WOR", "ADDRESS BOOK" as "ADDRESS" (contact detail, English).
- Cause: `textTransform: 'uppercase'` with `letterSpacing` is measured before the transform.
13 places.
- Where: `ContactDetailScreen.tsx:976`.
- Fix: upper-case the string with `toLocaleUpperCase` instead.
- [ ] **U-008 · P2 · all · After a cold start in German, the tab labels and the folder title stay
English.** The search placeholder is German. Screenshot `t_layo`.
- Fix: re-render the tab labels and the title when the locale store hydrates.
- [ ] **U-009 · P2 · TL TP (font 1.3) · Tab labels truncate with plenty of room.**
- Example: "E-…", "Kalen…", "Einstellu…" in 256 dp-wide tabs (`q_de_mail`).
- The label sits beside the icon with a fixed font size and no width.
- Fix: let the label size itself; on wide screens use the side rail (phase 2).
- [ ] **U-010 · P2 · all (font 1.3) · The mail search field clips the top of its text.**
- Where: fixed height in `EmailListScreen.tsx:1885-1912`.
- Fix: use `minHeight` and vertical padding.
- [ ] **U-011 · P2 · dark · The dark palette doesn't match the webmail's standard theme.**
- The app uses a blue primary and zinc greys. The webmail uses a white `#fafafa` primary with
`#171717` text, a `#0a0a0a` ground, neutral greys, `#1c1c1c` popover and a `#262626` muted
surface.
- Where: `src/theme/tokens.ts` `DARK_COLORS`. APP.md lists this as open.
- Fix: port the webmail dark tokens.
- [ ] **U-012 · P2 · light · The mail header logo is invisible on the light theme.**
- It always loads `Bulwark Logo White.png` (`EmailListScreen.tsx:1105`). Screenshot `q_mail_list`
TL.
- Fix: pick the logo by the resolved theme.
- [ ] **U-013 · P2 · light · TagSheet reads the static dark palette.**
- Where: `TagSheet.tsx:7,101,124`. Tag colours are wrong on light.
- Fix: use `c.tags`.
- [ ] **U-014 · P2 · all · `Button` has no `accessibilityRole` and no disabled or busy state.**
- Where: `Button.tsx:35`. This affects every dialog and prompt button.
- [ ] **U-015 · P2 · all · Many icon buttons are under 48 dp.**
- Mail search clear 36, recent-search remove 28, filter chip remove 32, attachment chips about 22,
drawer chevron 32.
- Viewer: card pills about 36, collapse handle 18, quick-reply Send 28, subject star 34.
- Compose: recipient chip X 28, Send 36, Cc/Bcc toggles 28.
- Files: header 44, row ⋮ 26.
- Settings: account icon buttons 28, toggle 24 tall.
- Login: header 32, show-password 20.
- Sheet close buttons are 28 everywhere.
- Lists with `file:line` are in the code passes; the fix commits name the places.
- Fix: a shared 48 dp target (box plus `hitSlop`), applied screen by screen.
- [ ] **U-016 · P3 · all · Sheet and dialog scrims are a literal `rgba(0,0,0,0.5)` in about 20
files.** Fix: an `overlay` token.
- [ ] **U-017 · P3 · all · Some confirmations use the system alert, others the app's `Dialog`.**
Examples: compose discard, event delete, folder empty. This is a style mismatch.
- [ ] **U-018 · P3 · all · Changing the system font size while the app is open recreates the activity
and resets navigation to the mail list.** Text measured before the change can stay clipped until
the next restart ("Layo" for "Layout").
### Mail list, drawer, unified inbox, scheduled
- [ ] **U-020 · P2 · all · Back with an active search exits the app instead of clearing the
search.**
- Repro: search "Offsite", press Enter, then Back.
- Where: `EmailListScreen.tsx` has no back handler for search or selection.
- Fix: `useBackWhileFocused` that clears the selection, then the search.
- [ ] **U-021 · P2 · PL · Phone landscape shows about 1.5 rows.**
- The 56 dp header, the 56 dp search row and the 64 dp tab bar take most of the 411 dp.
- The compose FAB covers the second row. Screenshot `q_mail_list` PL.
- Fix: in compact height, merge the title and search into one row and drop the tab labels.
- [ ] **U-022 · P2 · PL (font 1.3) · Rows cut the sender to three letters and clip the time.**
- Example: "Us…", "17:4" (`q_de_mail` PL).
- Where: the row header in `EmailListScreen.tsx` around 158-250.
- Fix: let the time keep its width (`flexShrink: 0`) and let the name shrink.
- [ ] **U-023 · P2 · PP · The undo bar sits on top of the compose FAB for 5 s after every archive or
delete.**
- Where: `UndoSnackbar.tsx:161`, `EmailListScreen.tsx:2262`.
- Fix: lift the FAB while the bar shows.
- [ ] **U-024 · P2 · all · Swipe and bulk actions on the list fail silently.**
- Archive, spam, read, star, move and delete are called as `void ...` with no catch, so a server
rejection becomes an unhandled promise. The selection also stays.
- Where: `EmailListScreen.tsx:604-651, 715-799`; `handleImport` `:492`.
- Fix: report failures through `toast.error` as the viewer does.
- [ ] **U-025 · P2 · all · Sign out and "Sign out of all accounts" run with no confirmation.**
- Where: drawer `SidebarDrawer.tsx:930,943`, settings `SettingsScreen.tsx:497`.
- Fix: a destructive `Dialog`.
- [ ] **U-026 · P2 · all · The drawer's folder-settings icon only closes the drawer.**
- Where: `SidebarDrawer.tsx:1048`, `navigate('MainTabs')`.
- Fix: open Settings → Folders.
- [ ] **U-027 · P2 · all · The filter modal runs a query per keystroke and has no keyboard
handling.**
- Where: `EmailListScreen.tsx:1507-1550`.
- Fix: debounce like the main search; add next/search return keys.
- [ ] **U-028 · P2 · PP PL · The unified inbox and Scheduled lists end under the gesture bar.**
- Where: `UnifiedInboxScreen.tsx:384`, `ScheduledScreen.tsx:278` (top edge only, fixed padding).
- [ ] **U-029 · P2 · all · Unified inbox rows ignore the font-size, density and preview settings.**
- Where: `UnifiedInboxScreen.tsx:576`.
- Its load and open failures are silent (`:103-112`, `:179-206`).
- [ ] **U-030 · P3 · all · The menu button keeps a grey focus box after the drawer closes.**
Screenshot `q_search_results`.
- [ ] **U-031 · P3 · PP · The selection header holds up to 7 buttons; its count has no
`numberOfLines`.** Where: `EmailListScreen.tsx:982-1066`.
- [ ] **U-032 · P3 · all · Folders under a hidden empty parent show flattened at the top level.**
- Example: "Q4 contracts" without Clients / ACME / 2026 (`q_drawer` PP).
- This happens with "hide empty folders" on. To check before changing anything.
### Thread view
- [ ] **U-040 · P1 · PL TL TP · After a rotation the pager is misaligned.**
- Half of the neighbouring page shows and the open message is cut off at the right edge.
- It happens when rotating on the thread, and when rotating on Compose opened from it
(`q_thread_long` PL, `q_compose_close` TL).
- Where: `EmailThreadScreen.tsx:265-280`; the pager keeps its old pixel offset.
- Fix: re-scroll to the active index after a width change, without animation.
- [ ] **U-041 · P2 · all · A long thread opens at the oldest message.**
- The newest (opened) message is expanded but off screen on a phone (`q_thread_long` PP).
- Fix: scroll the opened message into view once the cards are laid out.
- [ ] **U-042 · P1 · all · Quick reply "More options" throws away the typed text.**
- Where: `EmailThreadScreen.tsx:1185`.
- Fix: pass the text as `prefillBody` to the reply composer.
- [ ] **U-043 · P2 · PL · Phone landscape leaves almost no room for the body.**
- Toolbar with labels, subject, sender block, attachments, banner and the labelled bottom bar
fill the 411 dp (`q_thread_receipt` PL).
- Fix: icon-only toolbar and bottom bar in compact height.
- [ ] **U-044 · P2 · PP (German, 1.3) · The toolbar overflows and pushes More off screen.**
- "Stern hinzufügen" is long (`d_de_thread`).
- Fix: drop the toolbar labels when they don't fit; keep the a11y labels.
- [ ] **U-045 · P3 · German · Prev/Next read "Zurück"/"Weiter".**
- "Zurück" reads as Back. "Allen antworten" wraps in the bottom bar.
- Fix: "Vorherige"/"Nächste" (locale), and a single line with shrinking text.
- [ ] **U-046 · P2 · all · The tag sheet doesn't scroll.**
- With many tags, or in landscape, rows can't be reached.
- Where: `EmailThreadScreen.tsx:1472-1486`.
- [ ] **U-047 · P2 · TL TP · Plain-text and fluid HTML bodies run 150+ characters per line.**
- Fix: a readable max width for the body on wide panes.
- [ ] **U-048 · P3 · all · The toolbar star always says "Star", even when the message is starred.**
- Where: `EmailThreadScreen.tsx:746`.
- [ ] **U-049 · P3 · PL TL · Attachment image preview is sized from the window width, so it is
taller than the screen in landscape.** Where: `AttachmentPreviewModal.tsx:112`.
### Compose
- [ ] **U-060 · P1 · PL · With the keyboard open in landscape, the whole compose body is blank.**
- Only the header stays, so you can't see what you type (`tri.png`).
- Where: the keyboard padding is computed from `Dimensions.get('screen').height`
(`ComposeScreen.tsx:400-425`), which doesn't follow the rotation.
- Fix: compute the overlap from the window, against the keyboard frame.
- [ ] **U-061 · P2 · all · An untouched reply autosaves a draft and asks "Save or discard?" on
close.**
- The signature insertion counts as an edit (`d_de_compose`). Opening and closing a reply should
leave nothing behind.
- Where: `ComposeScreen.tsx:1116-1184`, `:1331-1444`.
- [ ] **U-062 · P2 · all · Recipient and subject fields have no return-key flow.**
- Fix: To/Cc/Bcc use `next`; an empty submit moves on; Subject `next` focuses the body.
- Where: `ComposeScreen.tsx:2228-2395`.
- [ ] **U-063 · P2 · all (German, 1.3) · Field labels wrap.** "Betreff" shows as "Betref/f": the
label column is a fixed width.
- [ ] **U-064 · P3 · PP · The subject field shows the end of a long subject.**
- Example: "…ning 2027: venue…". The cursor was put at the end on mount.
- [ ] **U-065 · P2 · all · Format-bar buttons have no accessibility labels.**
- Where: `ComposeScreen.tsx:2441-2513`; the toolbar button also has no role or state.
- [ ] **U-066 · P2 · PL · Text inputs use Android's full-screen landscape keyboard.**
- The autocomplete and the composer are hidden behind it.
- Fix: `disableFullscreenUI` on recipients, subject, plain body and quick reply.
- [ ] **U-067 · P2 · TL TP · Compose fills the tablet screen.** Fix: a centred panel (phase 2).
### Calendar
- [ ] **U-080 · P1 · PL · Month view shows two week rows and cuts off the rest.**
- Rows keep a 72 dp minimum and the list is sized to six of them (`q_calendar_default` PL).
- Where: `MonthScrollView.tsx:127-128,250`, `MonthView.tsx:427-438`.
- Fix: size the list to the real height and let rows shrink.
- [ ] **U-081 · P1 · TL TP · Rotating on month or week jumps back a month, and week view goes
blank.**
- After a rotation, month view showed September instead of October. Week view showed no day
headers and no events until Today was tapped (`q_calendar_default` TP, `q_cal_week` TL).
- Where: `MonthScrollView` samples the visible month at a fixed 40% of a stale height;
`TimeGridScrollView` keeps the old `scrollX` after its list remounts.
- Fix: keep the focused date across the remount and reset the header offset.
- [ ] **U-082 · P1 · PL · The event sheet's action row (Edit, Duplicate, .ics, Delete) is below the
screen.**
- Screenshot `p_event_land`.
- Where: `EventDetailSheet.tsx:609,643` (body `maxHeight: 400`, no `flexShrink`).
- [ ] **U-083 · P1 · all · Saving an event fails silently.**
- `handleSave` has `try/finally` without catch: the spinner stops, the editor stays, nothing is
said.
- Where: `EventModal.tsx:301-368`.
- [ ] **U-084 · P2 · all · Several deletes have no confirmation or undo.**
- Deleting a single event (`CalendarScreen.tsx:632`, `EventModal.tsx:371`).
- Deleting a task (`TasksSheet.tsx:427`).
- Unsubscribing from a feed, which also deletes its calendar (`ICalSubscriptionSheet.tsx:278`).
- [ ] **U-085 · P2 · all · Cancelling the recurrence-scope dialog after editing a recurring event
throws the edits away.** Where: `CalendarScreen.tsx:786-790`.
- [ ] **U-086 · P2 · TL TP · Wide screens get wider columns, not more days.** 3 days stays 3 and
day stays 1. Fix: phase 2.
- [ ] **U-087 · P2 · PL · The month dropdown is a fixed 346 dp tall and sized from the window.**
- It overflows in landscape and lands on a half page after rotation.
- Where: `MonthDropdown.tsx:23,52,72`.
- [ ] **U-088 · P3 · all · Times and dates use English formats in other languages.**
- Hour labels hard-code AM/PM (`TimeGridScrollView.tsx:437,640`, `WeekView.tsx:234,318`).
- Dates use US-order patterns (`EventDetailSheet.tsx:96-108`, `EventModal.tsx:525,545`,
`TasksSheet.tsx:212`).
- [ ] **U-089 · P3 · all · ICalSubscriptionSheet and RecurrenceEditor are English only.** From
`docs/parity/05-calendar.md`.
- [ ] **U-090 · P3 · PP (font 1.3) · Month chips show only the start time, no title.**
- [ ] **U-091 · P3 · all · An event that starts above the visible hours shows as an untitled
block.**
### Contacts
- [ ] **U-100 · P1 · all · Deleting a contact or group crashes the detail screen.**
- Hooks run after an early return: `ContactDetailScreen.tsx:149` before `:216`, and
`GroupDetailScreen.tsx:58` before `:130`.
- The delete empties the store, the screen re-renders with fewer hooks, and React throws. A
contact removed by a sync does the same.
- [ ] **U-101 · P2 · PL · The address-book chip row is clipped at the bottom.** Screenshot
`q_contacts` PL.
- [ ] **U-102 · P2 · all · Back doesn't leave selection or search; it leaves the tab.**
- Where: `ContactsScreen.tsx`. Files and Settings already handle this.
- [ ] **U-103 · P2 · all · The contact form's discard check only covers the on-screen back.**
- Hardware back drops edits silently.
- Where: `ContactFormScreen.tsx:764`. Fix: `usePreventRemove`.
- [ ] **U-104 · P2 · all · Contact form fields have no next or submit chaining.** Where:
`ContactFormScreen.tsx:1197-1283`.
- [ ] **U-105 · P2 · TL TP · List and detail are separate full-screen pages.** Fix: phase 2.
### Files
- [ ] **U-110 · P2 · all · The grid has a fixed 3 columns.**
- The last row stretches its cells, so the columns don't line up. Tablet cells are huge
(`q_files_grid`).
- Where: `FilesScreen.tsx:989,1606`.
- [ ] **U-111 · P2 · all · Dates and times ignore the app's language and the 12/24 h setting.**
- Example: "10:47 PM" next to 24 h times everywhere else.
- Where: `FilesScreen.tsx:127-132`.
- [ ] **U-112 · P2 · all · Removing someone's share access has no confirmation.** Where:
`ShareSheet.tsx:213`.
- [ ] **U-113 · P2 · TL TP · There is no side-by-side preview.** Fix: phase 2.
### Settings
- [ ] **U-120 · P2 · all · Deleting a tag has no confirmation, while the reset next to it does.**
Where: `KeywordSettings.tsx:48,122`.
- [ ] **U-121 · P2 · PP · The vacation subject input is a fixed 240 dp.**
- It squeezes its label into five lines.
- Where: `VacationSettings.tsx:434`; the same pattern in `AccountSecuritySettings.tsx:234`.
- [ ] **U-122 · P2 · light · The amber banner in filters and the sieve editor is unreadable on
light.**
- Light yellow text on a faint brown fill.
- Where: `FilterSettings.tsx:515-519`, `SieveEditorSheet.tsx:208-212`.
- [ ] **U-123 · P2 · TL TP · The section list and the page replace each other.** Fix: phase 2.
- [ ] **U-124 · P3 · all · The language list is in neither name nor code order.** Example: Česky,
Slovenčina, Dansk.
### Login
- [ ] **U-130 · P2 · TL TP · Login steps stretch to the full width.**
- Fields and buttons are 1248 dp wide (`t_login_confirm`).
- Fix: a centred column of about 480 dp.
- [ ] **U-131 · P2 · all · Small targets: show-password 20 dp, header back 32 dp, email suggestion
chips about 26 dp.**
- [ ] **U-132 · P2 · all · The password step's return keys don't move on.**
- Username has no `next`. With TOTP on, the password field shows Next and does nothing.
- Where: `PasswordStep.tsx:56,78`.
- [ ] **U-133 · P2 · all · There is no way out while "Signing in" hangs.** Hardware back is
swallowed. Where: `SigningInStep.tsx`, `LoginScreen.tsx:165`.
## Adaptive layout plan
**Breakpoints** come from the window width (`useWindowDimensions`), not the device type, so
split-screen, freeform windows and foldables work too:
| Class | Width | Typical |
|---|---|---|
| compact | < 600 dp | phones in portrait, narrow split-screen |
| medium | 600–839 dp | tablet portrait, unfolded foldables |
| expanded | ≥ 840 dp | tablet landscape |
A second flag, `shortHeight` (window height < 480 dp), covers phone landscape (914x411 dp). By
width alone that would be "expanded", but two panes and a rail in 411 dp of height are worse than
one pane. So a short window keeps the compact layouts and only compresses chrome: icon-only bars,
a merged header row, no tab labels.
A small hook, `src/lib/use-layout.ts`, returns `{ widthClass, shortHeight, isSplit }` and a pure
`layoutFor(width, height)` that can be tested.
**Navigation.** The phone stack stays as it is. A split view is a layout inside a tab, not a second
navigator:
- Medium and expanded put the tab bar on the left as a rail (`tabBarPosition: 'left'`, material
variant), like the webmail's navigation rail.
- Stack routes still exist and are still used on compact widths. On medium and expanded, the list
screens render the detail in a right-hand pane instead of pushing, and the detail screens' bodies
are shared components that take props instead of route params.
- If the window narrows while a pane shows something, the pane's selection is pushed as the normal
stack route, so nothing is lost.
**Per area:**
| Area | compact | medium | expanded |
|---|---|---|---|
| Mail | list → push thread | list (360 dp) + thread pane | persistent folder sidebar (280 dp) + list + thread |
| Calendar | as today | wider grid, 3-day shows 5 days | week shows 7 wide columns, 3-day shows 7, day shows 2 |
| Contacts | list → push detail | list + detail pane | list + detail pane |
| Files | list / grid | grid columns from width (about 140 dp each) | grid columns from width + preview pane when a file is selected |
| Settings | list ↔ page | section list (320 dp) + page | section list + page |
| Compose | full screen | centred panel, max 720 dp, dimmed backdrop | same |
| Sheets, dialogs, toasts | full width at the bottom | max 560 dp, centred | max 560 dp, centred |
| Login | as today | centred column, max 480 dp | same |
The thread, contact detail and settings pages cap their reading width (about 760 dp for the
mail body, 640 dp for forms) inside wide panes.
**State across rotation.** The activity keeps its state through `configChanges` (orientation,
screen size, smallest screen size, layout). Panes keep their selection in component state or the
store. Lists that depend on width remount with their focused index or date carried over.