The mobile layout had the shape of a mail app and none of the handling. A row is clicked, right-clicked and dragged into a folder — on a touchscreen two of those three do not exist, so the gestures people reach for first did nothing at all.
Four gestures, touch-only
A mouse keeps drag-to-folder untouched; everything here is gated on (pointer: coarse).
Swipe a row sideways to act on it. Each direction is a setting (Settings → Appearance → Swiping), defaulting to right = archive, left = delete. The strip revealed behind the row names what will actually happen in the folder it is happening in — "Delete forever" out of Deleted Items, "Not spam" inside Junk Mail — and where an action is a no-op (archiving out of the archive, reporting your own drafts as spam) the row will not move that way at all.
Hold a row to select it. Selection was reachable already, by aiming at a checkbox beside an avatar, which is not how anyone selects mail on a phone. The selection toolbar gained an overflow menu at the same time: report spam, mark unread and label are hide-mobile with nowhere else to go, so touch selection could not reach them.
Hold a folder for the menu its ⋮ button opens.
Pull the list to refresh, and drag in from the left edge of a conversation to go back. The toolbar button and back arrow both stay — a gesture with no visible control is one only the people who already know about it can use.
Haptic confirmation on every threshold crossing, where the device has a motor.
Where the logic lives
web/src/lib/touch.ts holds the arithmetic, away from the components and under test, because the numbers are the whole thing. The axis lock is deliberately biased towards the vertical: a scroll misread as a swipe grabs the list out from under the reader, while a swipe misread as a scroll costs a second attempt. web/src/lib/swipe.ts resolves a setting plus a folder into the label, icon and tone the strip shows.
Two older layout bugs, found on a 390px screen
Both predate this work and are fixed here because they are what a phone actually looks like:
.app is a grid with only its rows named, so it took an implicit auto column sized to the top bar's min-content — about 470px. On a 390px phone every message row ran 84px off the right of the glass with its date beyond the edge. The column is now minmax(0, 1fr), and .searchbar gets min-width: 0 so it shrinks instead of setting the width for everything below it.
Full-screen surfaces measure in dvh rather than vh (a full-screen composer put Send behind the mobile address bar), and the tab bar, drawer, dialogs and compose button now keep clear of the notch and home indicator.
Checks
407 tests pass, including new coverage of the gesture maths and the swipe descriptors. One of them caught a real defect while being written: the resistance curve past the threshold had no ceiling, so a hard flick accumulated travel with nowhere to show.
Typecheck and production build clean.
Driven by hand at 390×844 against the mock server, with (pointer: coarse) stubbed: swipe left and right in Inbox, Archive, Junk Mail and Deleted Items; a below-threshold swipe snapping back with no action; a diagonal drag staying a scroll; a direction set to "Nothing" refusing to move; long press opening selection without also opening the conversation; the folder menu; pull-to-refresh; and the edge-swipe back.
The mobile layout had the shape of a mail app and none of the handling. A row is clicked, right-clicked and dragged into a folder — on a touchscreen two of those three do not exist, so the gestures people reach for first did nothing at all.
## Four gestures, touch-only
A mouse keeps drag-to-folder untouched; everything here is gated on `(pointer: coarse)`.
- **Swipe a row sideways to act on it.** Each direction is a setting (Settings → Appearance → Swiping), defaulting to right = archive, left = delete. The strip revealed behind the row names what will actually happen *in the folder it is happening in* — "Delete forever" out of Deleted Items, "Not spam" inside Junk Mail — and where an action is a no-op (archiving out of the archive, reporting your own drafts as spam) the row will not move that way at all.
- **Hold a row to select it.** Selection was reachable already, by aiming at a checkbox beside an avatar, which is not how anyone selects mail on a phone. The selection toolbar gained an overflow menu at the same time: report spam, mark unread and label are `hide-mobile` with nowhere else to go, so touch selection could not reach them.
- **Hold a folder** for the menu its ⋮ button opens.
- **Pull the list to refresh**, and **drag in from the left edge of a conversation** to go back. The toolbar button and back arrow both stay — a gesture with no visible control is one only the people who already know about it can use.
Haptic confirmation on every threshold crossing, where the device has a motor.
## Where the logic lives
`web/src/lib/touch.ts` holds the arithmetic, away from the components and under test, because the numbers are the whole thing. The axis lock is deliberately biased towards the vertical: a scroll misread as a swipe grabs the list out from under the reader, while a swipe misread as a scroll costs a second attempt. `web/src/lib/swipe.ts` resolves a setting plus a folder into the label, icon and tone the strip shows.
## Two older layout bugs, found on a 390px screen
Both predate this work and are fixed here because they are what a phone actually looks like:
- `.app` is a grid with only its rows named, so it took an implicit `auto` column sized to the top bar's min-content — about 470px. On a 390px phone every message row ran 84px off the right of the glass with its date beyond the edge. The column is now `minmax(0, 1fr)`, and `.searchbar` gets `min-width: 0` so it shrinks instead of setting the width for everything below it.
- Full-screen surfaces measure in `dvh` rather than `vh` (a full-screen composer put Send behind the mobile address bar), and the tab bar, drawer, dialogs and compose button now keep clear of the notch and home indicator.
## Checks
- 407 tests pass, including new coverage of the gesture maths and the swipe descriptors. One of them caught a real defect while being written: the resistance curve past the threshold had no ceiling, so a hard flick accumulated travel with nowhere to show.
- Typecheck and production build clean.
- Driven by hand at 390×844 against the mock server, with `(pointer: coarse)` stubbed: swipe left and right in Inbox, Archive, Junk Mail and Deleted Items; a below-threshold swipe snapping back with no action; a diagonal drag staying a scroll; a direction set to "Nothing" refusing to move; long press opening selection without also opening the conversation; the folder menu; pull-to-refresh; and the edge-swipe back.
**Merged** 2026-08-31 as coffey-labs/ihasmail@c987ecccf3e7
<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.
The mobile layout had the shape of a mail app and none of the handling. A row is clicked, right-clicked and dragged into a folder — on a touchscreen two of those three do not exist, so the gestures people reach for first did nothing at all.
Four gestures, touch-only
A mouse keeps drag-to-folder untouched; everything here is gated on
(pointer: coarse).hide-mobilewith nowhere else to go, so touch selection could not reach them.Haptic confirmation on every threshold crossing, where the device has a motor.
Where the logic lives
web/src/lib/touch.tsholds the arithmetic, away from the components and under test, because the numbers are the whole thing. The axis lock is deliberately biased towards the vertical: a scroll misread as a swipe grabs the list out from under the reader, while a swipe misread as a scroll costs a second attempt.web/src/lib/swipe.tsresolves a setting plus a folder into the label, icon and tone the strip shows.Two older layout bugs, found on a 390px screen
Both predate this work and are fixed here because they are what a phone actually looks like:
.appis a grid with only its rows named, so it took an implicitautocolumn sized to the top bar's min-content — about 470px. On a 390px phone every message row ran 84px off the right of the glass with its date beyond the edge. The column is nowminmax(0, 1fr), and.searchbargetsmin-width: 0so it shrinks instead of setting the width for everything below it.dvhrather thanvh(a full-screen composer put Send behind the mobile address bar), and the tab bar, drawer, dialogs and compose button now keep clear of the notch and home indicator.Checks
(pointer: coarse)stubbed: swipe left and right in Inbox, Archive, Junk Mail and Deleted Items; a below-threshold swipe snapping back with no action; a diagonal drag staying a scroll; a direction set to "Nothing" refusing to move; long press opening selection without also opening the conversation; the folder menu; pull-to-refresh; and the edge-swipe back.Merged 2026-08-31 as coffey-labs/ihasmail@c987ecccf3
Rebuilt from: git history, session transcript.