Skip to content

feat(ui,desktop): swipe an Inbox row right to mark it read or unread - #3641

Merged
luke-agent[bot] merged 4 commits into
mainfrom
luke/feat-inbox-swipe
Oct 18, 2026
Merged

luke-agent[bot] merged 4 commits into
mainfrom
luke/feat-inbox-swipe

Conversation

@dastratakos

@dastratakos dastratakos commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

PR 3 of 3 for the Inbox list rows: the two-stage swipe from docs/proposals/inbox-threads.md §4 (approved by Dean). It builds on #3633 (unread and marking read), now merged.

@luke/ui ListRow swipe (list-row.tsx, use-row-swipe.ts, list-row.css). A row can offer one act on a right swipe: { label, icon, onAct }. The swipe has two stages, like Mail:

  • Short swipe (past 24px). On release the row rests open, 72px out, on the act's accent button. A click on that button acts once and closes the row.
  • Long swipe (past 60% of the row). The row arms: the button brightens and its label rides the row's edge. Release acts with no click. Pull back under the 60% line before letting go and the row rests open instead.
  • Pointer and trackpad. A pointer drag captures the pointer; any press still counts as a click until it has moved 5px. A two-finger trackpad swipe drives the same motion through horizontal wheel deltas. The wheel listener isn't passive, so a sideways swipe doesn't also scroll the list. A wheel swipe settles once the wheel has rested for 140ms, timed by requestAnimationFrame's own clock, the way use-row-drag.ts times things. No setTimeout.
  • Closing. An open row closes on a press or a wheel anywhere else (which includes starting to swipe another row), on a click on the row itself (which opens nothing), or when swiped back under 24px.
  • What does nothing. Swiping left from rest does nothing. A press that moves more up or down than sideways is left to the list to scroll.
  • Never acts:
    • when the pointer is cancelled (pointercancel);
    • when the act changes mid-swipe because the list redrew the row with another act (e.g. Read became Unread). The label is read again at release. An open row whose act changes closes;
    • when the row is unmounted, since its swipe goes with it.
  • No accidental opens. The click that a swipe's release produces is swallowed, so a swipe never opens the item.
  • Motion (docs/DESIGN.md):
    • The row's face moves only by transform and settles on --duration-fast / --spring-fast. While the finger or wheel is moving it follows with no transition.
    • The button behind it shows and hides only by opacity. It stays hidden at rest, so its edge never shows round the face's rounded corners, and it hides only after the face has slid home.
    • Reduced motion and capture runs hold the settling still through those tokens.
  • The face stays opaque. It's drawn on the list's ground with the row's hover or current fill over it, so it covers the button wherever it stands.
  • The button is pointer-only. It takes no Tab stop, since the same act is reachable from the keyboard through the hover button, the row's menu, and U.

The Inbox (inbox-list.tsx). Rows offer the swipe as Read (MailOpen) on an unread item and Unread (Mail) on a read one. It marks the item through the same act as the hover button, the menu, and U.

Bundle budget. The swipe adds about 1.6KB gzipped to the panel's up-front graph. This PR no longer touches apps/desktop/bundle-budget.json. An earlier push re-recorded the panel's baseline, because main stood 33 bytes under its ceiling. Main has since re-recorded every baseline (#3305), so the panel now has its 5% room again, and this PR builds within it.

Screenshots

Captured with Playwright from the jsdom fixtures and the built styles.css. The swipe states were set on the rendered rows: Backfill rests open, and Retry is swiped long and armed.

Dark Light
Swipe, dark Swipe, light

Evidence

  • Typecheck: @luke/ui and @luke/desktop, 0 errors.
  • Lint: biome clean on apps/desktop/src and packages/ui/src.
  • knip: clean.
  • Tests: pnpm exec vitest run packages/ui plus the desktop-inbox, desktop-tasks, desktop-home, person-page, desktop-factory, and automations-page suites. 78 files, 689 tests, all passing.
  • Test count: +14.
    • list-row.test.ts (+13):
      • a short swipe rests open, and its button acts once;
      • a long swipe arms and acts on release, while pulling back under the line rests open instead;
      • releasing under 24px, swiping left, or pressing up/down closes without acting, and a press that never moved still opens the row;
      • a cancelled pointer never acts;
      • redrawing with another act mid-swipe doesn't act, and an open row closes when its act changes;
      • an open row closes on a press elsewhere, or on a click on itself, which opens nothing;
      • a trackpad wheel swipe: a short one rests open and a long one acts once the wheel rests, while a vertical wheel is the list's;
      • a row with no swipe draws none;
      • from Devin's review: an open row's own acts take their click;
      • a trackpad swipe interrupted by a press does nothing;
      • a mostly-vertical wheel scrolls the list during a swipe;
      • a pointer swipe interrupted from elsewhere does nothing when released;
      • a keyboard click is never swallowed after an interrupted swipe.
    • desktop-inbox.test.ts (+1): a long swipe marks an unread item read, and a short swipe opens on Unread for a read item, whose button marks it. Neither opens the item.
  • Two test helpers (in desktop-inbox.test.ts and desktop-tasks.test.ts) now find a row's press as li button.luke-list-row-main, because a swiped row's press stands inside its sliding face.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged (LUKE_UPDATE_FIXTURES not run, or diff explained line by line). (CI)
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (manual; root AGENTS.md "Testing")
  • Gateway envelope goldens unchanged. (CI)
  • No new as type assertion outside the allowlisted files. (CI via anti-slop)
  • No Effect.runPromise/runSync/runFork outside the runtime edges and the shims tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-run-promise-outside-edges)
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch outside the files tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-raw-async-primitives)
  • No Effect, Stream, or Layer written as a statement and dropped; a description a caller walks past runs nothing. (CI via pnpm discarded-effect)
  • Test count equals the pre-PR count or the delta is listed. (manual: vitest run --reporter=json)
  • Each hand-rolled file the PR replaces is deleted, or the part that remains is a permanent adaptor named in root AGENTS.md's "The permanent adaptors" with the reason it stays.
  • Every CLAUDE.md/AGENTS.md sentence naming a changed part is edited; root pair stays byte-identical. (CI for pair existence; manual for wording)
  • PRIVACY.md unchanged, or the change is a product decision called out in the PR body.
  • Package package.json deps match what the sources import by bare specifier; effect via catalog:. (CI)
  • Barrel/door rule: no Node-reaching Effect module (@effect/platform-node, effect/sql) behind a barrel the renderer or a web function opens. (CI: existing renderer node: grep plus a new grep for those specifiers in renderer bundles)

Notes

  • Needs a hand test on the Mac. CI is Linux only and jsdom has no real trackpad, so the two-finger swipe's feel on a real trackpad can't be checked here: its momentum, the 140ms rest, and the delta sign under natural scrolling (it follows the design show, x - deltaX). The thresholds (24px, 60%) are the show's starting values.

Made with Luke


Devin Review


Summary by cubic

Adds a two-stage swipe to Inbox list rows so an item can be marked read or unread without opening it. A short swipe right leaves the row resting open on a Read or Unread button; swiping past 60% of the row arms it and marks the item on release. Pointer drags and trackpad two-finger swipes both drive it, via a new swipe prop on @luke/ui's ListRow.

  • The act is the same one the hover button, the row's menu, and U use.
  • The swipe button is pointer-only and takes no Tab stop, since the act stays reachable from the keyboard.
  • A cancelled pointer, an act that changed mid-swipe, a swipe left, a press up or down, and a press or wheel elsewhere never act; pressing an open row's own acts still performs them.
  • A swipe's release never opens the item; pressing or wheeling anywhere else closes an open row, a mostly vertical wheel stays the list's to scroll, and only a pointer's click is swallowed, so a keyboard's click still lands after an interrupted swipe.

Written for commit 8acf4bd. Summary will update on new commits.

View guided diff Turn on auto-fix

@vercel

vercel Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
luke-web Ready Ready Preview Oct 18, 2026 6:10pm UTC

Request Review

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@luke-agent
luke-agent Bot force-pushed the luke/feat-inbox-swipe branch from 2a4e3b4 to bc9e69f Compare October 18, 2026 17:03
@luke-agent
luke-agent Bot force-pushed the luke/feat-inbox-read-unread branch from 97c5d12 to 99c92f4 Compare October 18, 2026 17:03
devin-ai-integration[bot]

This comment was marked as resolved.

@luke-agent
luke-agent Bot force-pushed the luke/feat-inbox-swipe branch from 178c0b6 to 59eb1b3 Compare October 18, 2026 17:20
Base automatically changed from luke/feat-inbox-read-unread to main October 18, 2026 17:35
@luke-agent
luke-agent Bot force-pushed the luke/feat-inbox-swipe branch from 59eb1b3 to 0d4a06a Compare October 18, 2026 17:36
@luke-agent
luke-agent Bot enabled auto-merge October 18, 2026 17:36
@luke-agent
luke-agent Bot added this pull request to the merge queue Oct 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 18, 2026
@luke-agent
luke-agent Bot force-pushed the luke/feat-inbox-swipe branch from 0d4a06a to 26b1ed1 Compare October 18, 2026 17:56
@luke-agent
luke-agent Bot enabled auto-merge October 18, 2026 17:57
devin-ai-integration[bot]

This comment was marked as resolved.

@luke-agent
luke-agent Bot added this pull request to the merge queue Oct 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 18, 2026
dastratakos and others added 4 commits October 18, 2026 18:05
A ListRow may offer one swipe right (`swipe`), two-stage as Mail's: past
24px a release leaves the row open on the act's button, which a click
presses; past 60% of the row it arms, the button brighter and its label
riding the row's edge, and a release acts with no click. A pointer drag
and a trackpad's two-finger swipe (horizontal wheel deltas, let go once
the wheel rests on the frames' own clock) both drive it. A press or a
wheel elsewhere, a click on the open row, or swiping it back closes it;
swiping left does nothing. A cancelled pointer never acts, and nor does a
swipe whose act changed under it as the list redrew the row. The face
moves on transform and the button shows on opacity, timed from the motion
tokens, so reduced motion holds the settling still.

The Inbox's rows offer it as Read or Unread, marking the item through the
same act as the hover button, the menu, and U.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…elds to a press and to scrolling

A press on the row's own acts is theirs, never a swipe's; a press or a
wheel anywhere interrupts a trackpad's swipe still waiting for its wheel to
rest, which then does nothing; and a wheel more up or down than sideways
always scrolls the list, a swipe under way or not.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…en let go

Closing a row lets go of a pointer's swipe still held, releasing its
capture and swallowing its release's click, so a press or a wheel
elsewhere mid-drag leaves the act undone.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
Only a pointer's click follows a swipe's release; a keyboard's click,
which no press came before, always lands, so an interrupted swipe let go
off its row leaves the row's Enter working.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
@luke-agent
luke-agent Bot force-pushed the luke/feat-inbox-swipe branch from 26b1ed1 to 8acf4bd Compare October 18, 2026 18:07
@luke-agent
luke-agent Bot enabled auto-merge October 18, 2026 18:07
@luke-agent
luke-agent Bot added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 593f261 Oct 18, 2026
12 checks passed

This branch was successfully deployed

1 active deployment
Preview — 8acf4bd7 Deployed Oct 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant