yiekheng 8023c8f357 feat: bidirectional swipe — left=Delete, right=Archive/Pause; reminders list too
Two follow-ups on the activity-row swipe work:

1. SwipeableRow now supports BOTH directions
   ----------------------------------------
   The component grew a `leftActions` slot alongside the existing
   right shelf. Drag the row right to pull the left shelf into view
   (non-destructive action: Archive, Pause, etc.); drag left to pull
   the right shelf into view (destructive: Delete). Past
   REVEAL_THRESHOLD (60 px) the corresponding shelf locks open;
   below it, snaps closed. Each shelf is opt-in — omit a slot and
   the row only swipes one direction.

   - `computeSwipeNext` and the new `snapPosition` helper take a
     `{ leftActions, rightActions }` flag pair so the math knows
     which directions are valid. Drags toward a missing shelf get
     clamped to 0 instead of fully open.

   - Activity rows wired as iOS-Mail-style: leading edge (right
     swipe) = Archive/Restore (amber), trailing edge (left swipe)
     = Delete (destructive red).

   - Tests grew to 16 cases covering: snap-to-closed below threshold
     either way, snap-to-open at/past threshold either way, clamps
     don't escape the shelf width, missing-shelf rows don't snap to
     a non-existent open state, baseOffset-aware reverse-drag math,
     and SSR markup contracts (data-testid, data-state="closed",
     translateX(0px), aria-hidden=true on closed shelves, no
     orphaned shelf wrapper when only one slot is provided).

   Also fixed a `-0` slip in the clamp branch (`-maxRight` is `-0`
   when maxRight is 0) so call-site equality checks behave.

2. Reminders list rows are swipeable too
   ----------------------------------------
   /reminders page now wraps each row in SwipeableRow:

   - Left swipe → Delete (always available, destructive).
   - Right swipe → Pause (when status is "active") OR Restart
     (when "paused" or "ended"). Other lifecycle states (failed)
     omit the right shelf entirely; the row only swipes one way.

   Each shelf button is a tiny `<form>` posting to the existing
   server action (delete / pause / restart) — no client-side state
   beyond the swipe gesture. Page revalidates after the action,
   list re-renders, row redraws in its new state.

   Reused the same shelf-button visual language as the activity
   tab (color-coded action, icon + label, dark-mode pairs) via a
   tiny inline `ReminderShelfButton` helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:24:55 +08:00
..