Three mobile problems, all measured in a real browser at drawer width before and after.
Folder tree → drill-down under 768px
The tree spent its width on depth. Measured in the 300px drawer, each level costs 16px of indent and the twisty walks right with it:
depth
twisty x
label width
0
8px
149px
2
40px
117px
4
72px
85px
So a deep folder was both hard to expand — an 18px twisty holding a 14px chevron — and unreadable once expanded. A miss landed on the row, which is a <Link>, so the wrong tap also cost a navigation.
Under 768px the tree is now a drill-down: one level at a time, no indent, a back row above it, and a chevron at the right edge that is the same size in the same place on every row. Tapping the row still opens the folder; only the chevron changes what the list shows. The folder you drilled into is listed with its children, because it is still a folder you can open. Unread counts follow: a folder you have not drilled into carries its subtree's total, and speaks only for itself once you are inside it.
The tree is unchanged above 768px. A wide sidebar can afford the indent, and dragging a folder onto another folder is still the only way to reparent one — there is no "Move to…" in the folder menu — which needs both folders on screen at once. Worth closing separately; touch users cannot reparent a folder today either.
Touch targets
Every control was under the 44px a fingertip covers:
control
was
now
hamburger
36×36, 6px from the bezel
44×44 hit area, 12px in
search filter
30×30
44×44
theme / settings / account
36×36
44×44
folder twisty
18×18
44×44
row ⋮
24×24
36×44
sidebar rows
36px tall
44px tall
Controls keep the size they draw at and gain a transparent hit area, since growing the boxes would reflow a top bar with no room to give. Rows grow for real: a 44px target inside a 36px row reaches into its neighbours. The ⋮ is held to 36px wide on purpose — at a full 44 its hit area overlapped the drill chevron's by 4px (200–244 against 240–284), so its right edge silently drilled instead. They now abut exactly at x=240.
All of it keys off pointer: coarse rather than a width, following the reasoning already in ui/misc.tsx about useIsTouch: width decides the layout, the pointer decides what has to be aimed at. The drill-down keys off width, because that is a layout that does not fit rather than a target that is small.
Pinch on the message list
.msg-row set touch-action: pan-y to feed the swipe gesture the horizontal movement the browser is not using. But naming any value drops every gesture not named, zoom included — so pinch worked on an open message and died on the list, which reads as the zoom being broken at random rather than as a rule about rows. pan-y pinch-zoom keeps the swipe and gives the zoom back.
Tests
Five new tests in web/src/views/mail/__tests__/mailbox-drilldown.test.tsx covering the wide-screen tree being untouched, walking in and out, the subtree unread counts, and the drawer opening at the level of the folder being read. npm test 472 passing, npm run lint and npm run i18n:check clean; the one new string is translated in all nine languages.
Three mobile problems, all measured in a real browser at drawer width before and after.
## Folder tree → drill-down under 768px
The tree spent its width on depth. Measured in the 300px drawer, each level costs 16px of indent and the twisty walks right with it:
| depth | twisty x | label width |
|---|---|---|
| 0 | 8px | 149px |
| 2 | 40px | 117px |
| 4 | 72px | **85px** |
So a deep folder was both hard to expand — an 18px twisty holding a 14px chevron — and unreadable once expanded. A miss landed on the row, which is a `<Link>`, so the wrong tap also cost a navigation.
Under 768px the tree is now a drill-down: one level at a time, no indent, a back row above it, and a chevron at the right edge that is the same size in the same place on every row. Tapping the row still opens the folder; only the chevron changes what the list shows. The folder you drilled into is listed with its children, because it is still a folder you can open. Unread counts follow: a folder you have not drilled into carries its subtree's total, and speaks only for itself once you are inside it.
**The tree is unchanged above 768px.** A wide sidebar can afford the indent, and dragging a folder onto another folder is still the only way to reparent one — there is no "Move to…" in the folder menu — which needs both folders on screen at once. Worth closing separately; touch users cannot reparent a folder today either.
## Touch targets
Every control was under the 44px a fingertip covers:
| control | was | now |
|---|---|---|
| hamburger | 36×36, 6px from the bezel | 44×44 hit area, 12px in |
| search filter | 30×30 | 44×44 |
| theme / settings / account | 36×36 | 44×44 |
| folder twisty | 18×18 | 44×44 |
| row ⋮ | 24×24 | 36×44 |
| sidebar rows | 36px tall | 44px tall |
Controls keep the size they draw at and gain a transparent hit area, since growing the boxes would reflow a top bar with no room to give. Rows grow for real: a 44px target inside a 36px row reaches into its neighbours. The ⋮ is held to 36px wide on purpose — at a full 44 its hit area overlapped the drill chevron's by 4px (200–244 against 240–284), so its right edge silently drilled instead. They now abut exactly at x=240.
All of it keys off `pointer: coarse` rather than a width, following the reasoning already in `ui/misc.tsx` about `useIsTouch`: width decides the layout, the pointer decides what has to be aimed at. The drill-down keys off width, because that is a layout that does not fit rather than a target that is small.
## Pinch on the message list
`.msg-row` set `touch-action: pan-y` to feed the swipe gesture the horizontal movement the browser is not using. But naming any value drops every gesture not named, zoom included — so pinch worked on an open message and died on the list, which reads as the zoom being broken at random rather than as a rule about rows. `pan-y pinch-zoom` keeps the swipe and gives the zoom back.
## Tests
Five new tests in `web/src/views/mail/__tests__/mailbox-drilldown.test.tsx` covering the wide-screen tree being untouched, walking in and out, the subtree unread counts, and the drawer opening at the level of the folder being read. `npm test` 472 passing, `npm run lint` and `npm run i18n:check` clean; the one new string is translated in all nine languages.
**Merged** 2026-08-31 as coffey-labs/ihasmail@20929ddc2c0a
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Three mobile problems, all measured in a real browser at drawer width before and after.
Folder tree → drill-down under 768px
The tree spent its width on depth. Measured in the 300px drawer, each level costs 16px of indent and the twisty walks right with it:
So a deep folder was both hard to expand — an 18px twisty holding a 14px chevron — and unreadable once expanded. A miss landed on the row, which is a
<Link>, so the wrong tap also cost a navigation.Under 768px the tree is now a drill-down: one level at a time, no indent, a back row above it, and a chevron at the right edge that is the same size in the same place on every row. Tapping the row still opens the folder; only the chevron changes what the list shows. The folder you drilled into is listed with its children, because it is still a folder you can open. Unread counts follow: a folder you have not drilled into carries its subtree's total, and speaks only for itself once you are inside it.
The tree is unchanged above 768px. A wide sidebar can afford the indent, and dragging a folder onto another folder is still the only way to reparent one — there is no "Move to…" in the folder menu — which needs both folders on screen at once. Worth closing separately; touch users cannot reparent a folder today either.
Touch targets
Every control was under the 44px a fingertip covers:
Controls keep the size they draw at and gain a transparent hit area, since growing the boxes would reflow a top bar with no room to give. Rows grow for real: a 44px target inside a 36px row reaches into its neighbours. The ⋮ is held to 36px wide on purpose — at a full 44 its hit area overlapped the drill chevron's by 4px (200–244 against 240–284), so its right edge silently drilled instead. They now abut exactly at x=240.
All of it keys off
pointer: coarserather than a width, following the reasoning already inui/misc.tsxaboutuseIsTouch: width decides the layout, the pointer decides what has to be aimed at. The drill-down keys off width, because that is a layout that does not fit rather than a target that is small.Pinch on the message list
.msg-rowsettouch-action: pan-yto feed the swipe gesture the horizontal movement the browser is not using. But naming any value drops every gesture not named, zoom included — so pinch worked on an open message and died on the list, which reads as the zoom being broken at random rather than as a rule about rows.pan-y pinch-zoomkeeps the swipe and gives the zoom back.Tests
Five new tests in
web/src/views/mail/__tests__/mailbox-drilldown.test.tsxcovering the wide-screen tree being untouched, walking in and out, the subtree unread counts, and the drawer opening at the level of the folder being read.npm test472 passing,npm run lintandnpm run i18n:checkclean; the one new string is translated in all nine languages.Merged 2026-08-31 as coffey-labs/ihasmail@20929ddc2c
Rebuilt from: git history, session transcript.