From 2a47913d7057a8d7551fc00c9a1ab3afadfcf63a Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:19:00 +0200 Subject: [PATCH] docs: add native ui and ux audit --- docs/ux-audit-2026-10.md | 550 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 docs/ux-audit-2026-10.md diff --git a/docs/ux-audit-2026-10.md b/docs/ux-audit-2026-10.md new file mode 100644 index 00000000..505d2b0a --- /dev/null +++ b/docs/ux-audit-2026-10.md @@ -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_.png`, a 4-cell strip: PP, PL, TL, TP; + `shots/d_.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 `` 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.