feat(web): follow-finger drawer drag — open with a rightward swipe from anywhere on mobile #140
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#140
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The mobile drawer opens only from a 24px left edge zone (
web/src/components/AppShell.tsx:112-172). That zone collides with the OS edge gestures (iOS back swipe, Android gesture nav), which pre-empt it — on iOS the drawer is effectively hamburger-only. The gesture is also binary: 40px of travel snaps the drawer open with no physical feel.Decision (grilled 2026-07-11)
Replace the edge-zone snap gesture with a follow-finger drag that starts anywhere on the mobile viewport (<1024px). The OS keeps the edge; we take the surface.
Gesture model
.shell-railtranslates with the finger (inline transform,transition: none, visibility forced visible) and the scrim fades in proportionally to drag progress. The scrim must therefore be mountable in a "dragging" state, not only whendrawerOpen()is true.Arbitration (who gets the touch)
scrollWidth > clientWidth, overflow-x auto/scroll —.diff-scroll,.tool-body,.md-pre, table wraps) and hasscrollLeft > 0, leave the touch entirely to the page. AtscrollLeft === 0a rightward pull has nothing to scroll, so the drawer may claim it (mirrors iOS back-swipe vs carousel behavior).INTENT_PX), but when the drawer is closed only a rightward horizontal intent claims the touch (preventDefault); leftward horizontal drags always stay with the page.touchcancel→ animate the drawer back to closed. Where it doesn't, the edge still works naturally.touchcancelhandling is required regardless.Motion
prefers-reduced-motion: finger-tracking stays (user-driven motion), but the release settle uses the existing reduced-motion snap rule (base.css ~539) instead of animating..shell-rail(closed drawer leaves the a11y tree; see base.css:254-284) intact through the dragging state.Code shape & tests
web/src/lib/drawerGesture.ts(house pattern: pure module + colocated vitest test). It consumes touch events plus environment answers (drawer open?, horizontally-scrollable-ancestor-with-scrollLeft>0?, editable target?, drawer width) and emits decisions: ignore / track / drag-to(x, progress) / commit-open / commit-closed / cancel-reset.AppShell.tsxonly binds DOM listeners (touchstart/touchmovenon-passive/touchend/touchcancel), answers the environment queries, and applies transforms/classes.Acceptance
scrollLeft > 0) scroll back on rightward swipe instead of opening the drawer; atscrollLeft === 0the drawer opens.npm test(vitest) green with the new gesture state-machine suite.Landing audit — PR #143 → PASS
ci / nativesucceeded in 5m13s (vitest 643 tests incl. 21 new, lint, prettier, tsc, vite build). Relied on this signal; not re-run.main, no conflicts.afk/140carriesCloses #140.drawerGesture.tsstate-machine arbitration (scroller/editable/axis/rightward-when-closed/flick-vs-position) and AppShell touch wiring (identifier tracking, non-passive touchmove, touchcancel reset, batched settle) match the grilled #140 design.Verdict: PASS, awaiting free-text merge go-ahead.