You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found reconciling the 15 September 2026 prototype drop (4beb98b2) against lib/. Owner ruling, 16 September 2026: the whole gesture layer ships in v1. The v1 Readiness Audit.html was not updated for this drop and does not mention it; this ticket is the ruling's home, and the surface tickets cite it.
The app has no horizontal swipe anywhere. This is the foundation the four surfaces below need, and it exists so a gesture fix lands once instead of four times — the debt the design paid for twice (its notes §27, §33).
Why one implementation
Every screen in the prototype that forked the gesture cost the same bug twice. The app should not repeat that: one gesture, one direction contract, one deck affordance, one hint controller.
Direction is a contract. Left advances. Right goes back, or sets aside. Destructive actions get no swipe at all — the same motion must never mean "keep for later" here and "delete" there.
What to build
The gesture. Commit threshold and maximum drag per caller. A blocked direction damps to 22% rather than going dead — an element that resists reads as empty, one that freezes reads as broken. A committed swipe leaves the screen with a slight tilt before the content changes; landing returns it to centre with the incoming content already in place, so nothing flies in from the edge the old one left through.
A blocked direction must explain itself on the gesture, not on the movement. Any caption for a blocked swipe is driven by the undamped finger distance, not the damped offset — keying it to the damped value gave 13px of travel at 0.3 opacity for a 60px swipe, which was reported as the list being completely broken while the gesture worked the whole time. Full label opacity by 30px of finger travel.
The deck stack. One card per side that has a card, at the caller's own radius, rising from 13px/0.955 to 0/1 as the drag goes its way and driven to full size during the exit. It renders in a box the size of the card, as a sibling of the transformed element — not inside its transform. Slivers sit at full opacity with a 52%-accent edge: a first attempt at 0.55 opacity over a 7% fill measured 1.09:1 against the page and was invisible, and by then it was the only affordance left. Depth comes from offset and scale, which cost no contrast.
The first-run hint. Nudge twice, with a caption. It stops when the gesture is used, not after a count — a cap only ever fires on the learner who has not learned the gesture, who needs the explanation more the third time, not less. The used-flag persists, is replayable from one debug toggle, and is readable by callers so a standing affordance can dim against it (bright under the hint, then quiet once used). Reduced motion drops the flight and the nudge.
What ports, and what does not
The drop's §25–§28 are a chain of DOM pointer-event races — implicit pointer capture, listeners attached in an effect, a ref written during render. Flutter's gesture arena has none of them, so do not transliterate the fixes. What ports is the rule they were found by: nothing a gesture depends on may wait for a frame, and the geometry and contrast findings, which are platform-neutral.
Two findings do port directly:
The whole row is the target. A tap target sized to the text left the row's padding and its trailing column answering neither the tap nor the drag — most of the row's area.
No control inside a control. A row that is itself a button prunes a nested button from the accessibility tree, and on the dictionary row that nested button is the only way to un-save.
Acceptance criteria
One gesture widget carries the direction contract, the damped block and the fly-off commit, with the arithmetic in a sibling pure-Dart file and unit-tested without pumping widgets — HorizontalSwipe over swipe_geometry.dart, which imports dart:math and nothing else; swipe_geometry_test.dart pumps no widget
The deck stack and the hint controller are separate, reusable, and honour MediaQuery.disableAnimations — SwipeDeckStack, and SwipeHint / SwipeNudge / SwipeHintCaption, each in its own file; reduced motion drops the flight and the nudge and keeps the spring home, covered in horizontal_swipe_test.dart and swipe_hint_test.dart
A blocked direction's caption is driven by undamped travel, proven by a test that a blocked swipe reaches full caption opacity — horizontal_swipe_test.dart, "reaches full caption opacity on a 60px swipe": the same drag reports travel −60 and offset −13.2
The four consuming surfaces, which are their own tickets, and swipe-to-grade on flashcards — the design considered and rejected it as a new feature needing a second pile, a re-drill loop and a results screen reporting recall.
Found reconciling the 15 September 2026 prototype drop (
4beb98b2) againstlib/. Owner ruling, 16 September 2026: the whole gesture layer ships in v1. Thev1 Readiness Audit.htmlwas not updated for this drop and does not mention it; this ticket is the ruling's home, and the surface tickets cite it.The app has no horizontal swipe anywhere. This is the foundation the four surfaces below need, and it exists so a gesture fix lands once instead of four times — the debt the design paid for twice (its notes §27, §33).
Why one implementation
Every screen in the prototype that forked the gesture cost the same bug twice. The app should not repeat that: one gesture, one direction contract, one deck affordance, one hint controller.
Direction is a contract. Left advances. Right goes back, or sets aside. Destructive actions get no swipe at all — the same motion must never mean "keep for later" here and "delete" there.
What to build
The gesture. Commit threshold and maximum drag per caller. A blocked direction damps to 22% rather than going dead — an element that resists reads as empty, one that freezes reads as broken. A committed swipe leaves the screen with a slight tilt before the content changes; landing returns it to centre with the incoming content already in place, so nothing flies in from the edge the old one left through.
A blocked direction must explain itself on the gesture, not on the movement. Any caption for a blocked swipe is driven by the undamped finger distance, not the damped offset — keying it to the damped value gave 13px of travel at 0.3 opacity for a 60px swipe, which was reported as the list being completely broken while the gesture worked the whole time. Full label opacity by 30px of finger travel.
The deck stack. One card per side that has a card, at the caller's own radius, rising from 13px/0.955 to 0/1 as the drag goes its way and driven to full size during the exit. It renders in a box the size of the card, as a sibling of the transformed element — not inside its transform. Slivers sit at full opacity with a 52%-accent edge: a first attempt at 0.55 opacity over a 7% fill measured 1.09:1 against the page and was invisible, and by then it was the only affordance left. Depth comes from offset and scale, which cost no contrast.
The first-run hint. Nudge twice, with a caption. It stops when the gesture is used, not after a count — a cap only ever fires on the learner who has not learned the gesture, who needs the explanation more the third time, not less. The used-flag persists, is replayable from one debug toggle, and is readable by callers so a standing affordance can dim against it (bright under the hint, then quiet once used). Reduced motion drops the flight and the nudge.
What ports, and what does not
The drop's §25–§28 are a chain of DOM pointer-event races — implicit pointer capture, listeners attached in an effect, a ref written during render. Flutter's gesture arena has none of them, so do not transliterate the fixes. What ports is the rule they were found by: nothing a gesture depends on may wait for a frame, and the geometry and contrast findings, which are platform-neutral.
Two findings do port directly:
Acceptance criteria
HorizontalSwipeoverswipe_geometry.dart, which importsdart:mathand nothing else;swipe_geometry_test.dartpumps no widgetMediaQuery.disableAnimations—SwipeDeckStack, andSwipeHint/SwipeNudge/SwipeHintCaption, each in its own file; reduced motion drops the flight and the nudge and keeps the spring home, covered inhorizontal_swipe_test.dartandswipe_hint_test.darthorizontal_swipe_test.dart, "reaches full caption opacity on a 60px swipe": the same drag reportstravel−60 andoffset−13.2dart format,flutter analyze, the metrics gate and the suite pass, with a changelog bullet — every CI job green on One horizontal swipe, one direction contract: the gesture, the deck stack and the first-run hint #621 (analyze & test,iOS build,format,comments,changelog)Out of scope
The four consuming surfaces, which are their own tickets, and swipe-to-grade on flashcards — the design considered and rejected it as a new feature needing a second pile, a re-drill loop and a results screen reporting recall.