Swipe the calendar sideways to step a day or a month #199

Closed
opened 2026-09-02 05:13:30 +00:00 by jcoffey-dev · 0 comments
Owner

The calendar had next/previous as toolbar buttons and n/p, and nothing for a thumb — while every other finger-driven surface in the app already answers to a swipe.

Scope

Day and month only. Those are the two views where a period is a page. Week and agenda scroll through a range rather than turning to the next one, so a sideways flick would not obviously mean anything there. Dragging left pulls the next period in from the right, the way paper and every phone do it.

Three deliberate refusals

It draws nothing while the finger moves. The row swipe slides open because the strip underneath has to name which of six actions is coming; stepping a calendar has two outcomes and the direction of the finger already says which. Translating the grid would also break the sticky day header, since a transform makes a containing block. The threshold is reported by the vibration motor instead — which is what the haptics are for.

It does not start on an event (.ev-chip, .ev-block, .agenda-ev). Which gesture was meant is decidable at the moment the finger lands and only then, so drag-to-reschedule stays available to build later without having to be untangled from this first.

It asks for a longer drag than a row swipe. Not because the consequence is bigger — stepping back undoes it, while a swiped row has already been archived. It is because this gesture has no way to change its mind: a row reveals what it will do and can be let go early, and offers Undo after. This shows nothing and offers nothing, so the distance is the only chance to not mean it.

A correction worth noting

My first threshold was max(72, min(150, w * 0.22)), which I wrote claiming it was stricter than the row swipe. A test comparing the two directly proved it was the opposite — 79px against the row's 96px at phone width, so the whole-screen gesture was the easier one to trigger by accident. It is now max(80, min(180, w * 0.3)), and the test asserts the relationship across five widths rather than trusting the arithmetic.

The stated reason was wrong too, and is fixed in the comment: the justification is the absent reveal and absent Undo, not the size of the consequence.

Reuse

Shares lockAxis with the row swipe, so it keeps the same bias towards the vertical — the day grid scrolls through the hours, and a scroll misread as a swipe throws the reader into another day. Gated on (pointer: coarse) like every other gesture, not on screen width. Toolbar buttons and n/p stay.

Testing

10 unit tests over the pure decision functions: direction, the threshold boundary exactly, the floor and ceiling, scaling across widths, the comparison against the row threshold, and the axis-lock cases the day grid depends on.

Suite green: web 613, server 110, typecheck clean.

Not verified on a real touchscreen — the gesture logic is tested, but the feel of it (threshold, haptic timing) is worth a thumb before merge.

Merged 2026-09-01 as coffey-labs/ihasmail@ec8e2ad2d0

Rebuilt from: git history, session transcript.

The calendar had next/previous as toolbar buttons and `n`/`p`, and nothing for a thumb — while every other finger-driven surface in the app already answers to a swipe. ## Scope **Day and month only.** Those are the two views where a period is a page. Week and agenda scroll through a range rather than turning to the next one, so a sideways flick would not obviously mean anything there. Dragging left pulls the next period in from the right, the way paper and every phone do it. ## Three deliberate refusals **It draws nothing while the finger moves.** The row swipe slides open because the strip underneath has to name which of six actions is coming; stepping a calendar has two outcomes and the direction of the finger already says which. Translating the grid would also break the sticky day header, since a transform makes a containing block. The threshold is reported by the vibration motor instead — which is what the haptics are for. **It does not start on an event** (`.ev-chip`, `.ev-block`, `.agenda-ev`). Which gesture was meant is decidable at the moment the finger lands and only then, so drag-to-reschedule stays available to build later without having to be untangled from this first. **It asks for a longer drag than a row swipe.** Not because the consequence is bigger — stepping back undoes it, while a swiped row has already been archived. It is because this gesture has no way to change its mind: a row reveals what it will do and can be let go early, and offers Undo after. This shows nothing and offers nothing, so the distance is the only chance to not mean it. ## A correction worth noting My first threshold was `max(72, min(150, w * 0.22))`, which I wrote *claiming* it was stricter than the row swipe. A test comparing the two directly proved it was the opposite — 79px against the row's 96px at phone width, so the whole-screen gesture was the easier one to trigger by accident. It is now `max(80, min(180, w * 0.3))`, and the test asserts the relationship across five widths rather than trusting the arithmetic. The stated reason was wrong too, and is fixed in the comment: the justification is the absent reveal and absent Undo, not the size of the consequence. ## Reuse Shares `lockAxis` with the row swipe, so it keeps the same bias towards the vertical — the day grid scrolls through the hours, and a scroll misread as a swipe throws the reader into another day. Gated on `(pointer: coarse)` like every other gesture, not on screen width. Toolbar buttons and `n`/`p` stay. ## Testing 10 unit tests over the pure decision functions: direction, the threshold boundary exactly, the floor and ceiling, scaling across widths, the comparison against the row threshold, and the axis-lock cases the day grid depends on. Suite green: web 613, server 110, typecheck clean. Not verified on a real touchscreen — the gesture logic is tested, but the feel of it (threshold, haptic timing) is worth a thumb before merge. **Merged** 2026-09-01 as coffey-labs/ihasmail@ec8e2ad2d00f <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.