Repository navigation
feat(core): swipe actions come from createSwipe and run on the shared drag path; a trackpad's swipe follows the fingers (spec:AST-057 DEC-8; spec:AST-094 FR43, DEC-24) - #8690
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
This pull request needs a spec decision before it can be approved: spec:AST-057 on main lists two-finger trackpad swipes as a non-goal and its AR2 keeps the swipe off pointers that can hover, while the DEC-7 cited here exists only in the open #8646, whose FR4 would also make a pen swipe, which this change forbids. An engineering owner records that decision by commenting /approve-spec 6df8dd02f0913da1c01e72453a6415dffd3b9cec on this pull request, and the author may do so as an engineering owner. Whatever that decision, two changes are needed:
- Land or retarget the dependency pull requests, so this diff carries only its own 381 lines instead of 53,279.
- When a trackpad has opened a row, a click on its content should close it without running the row's action, as a finger's tap already does.
[Automated review]
| // A trackpad's horizontal scroll swipes the row as a finger does, and | ||
| // a mouse wheel's horizontal notches the same way (`spec:AST-057` | ||
| // DEC-7): the same contact, moving with the content. | ||
| allowedScrollTypes: ['continuous', 'discrete'], |
There was a problem hiding this comment.
Spec hold: spec:AST-057 on main lists two-finger trackpad swipes as a non-goal (line 138), and its AR2 keeps the gesture's paint, tab order and accessibility tree untouched on a pointer that can hover (lines 250–252). The DEC-7 cited here exists only in the open #8646, so this opt-in waits on an engineering owner's decision on that record.
| rootRef, | ||
| () => | ||
| new TapGestureRecognizer({ | ||
| allowedPointerTypes: ['touch'], |
There was a problem hiding this comment.
Now that a trackpad can open a row, this dismissal still takes only a finger. In Chromium, a click on the content of a trackpad-opened row reaches the row's onClick and the row stays open, while a finger's tap closes it and activates nothing (Item.test.tsx:1178). Letting any pointer's press dismiss while the row is open, and swallowing its click through the click guard, would match the finger path; a test for a wheel-opened row would pin it.
There was a problem hiding this comment.
Fixed in 08e6916: the dismissing tap now takes every pointer and the click guard swallows the click, so a mouse click on a trackpad-opened row closes it; tests added in both engines.
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
There was a problem hiding this comment.
This pull request needs a spec decision before it can be approved: spec:AST-057 on main lists two-finger trackpad swipes as a non-goal and keeps the swipe off pointers that can hover (AR2, DEC-4), while the DEC-7 cited here exists only in the open #8646, whose FR4 would also make a pen swipe, which this change forbids. An engineering owner records that decision by landing the spec:AST-057 amendment with the pen settled, or ruling otherwise, and commenting /approve-spec 08e6916d0e698009bca4d50efdd10c6824a0c381 on this pull request; the author may do so as an engineering owner. Whatever that decision, two changes are needed:
- Land or retarget the dependency pull requests, so this diff carries its own 458 lines instead of 53,360.
- Under Reduce Motion on a fine pointer, a trackpad or wheel swipe's settle should complete at once, as
design:reduced-motionDR3 requires, instead of cross-fading like a touch release (useSwipeAction.ts:777,:834).
[Automated review]
| // Under Reduce Motion the animator cross-fades or lands at once | ||
| // (design:reduced-motion); what a release started always cross-fades. | ||
| const touchRelease = cause === 'release'; |
There was a problem hiding this comment.
Under Reduce Motion this treats every release as a touch release, so when a trackpad or wheel gesture ends on a fine primary pointer the row's settle cross-fades for up to 200 ms. design:reduced-motion DR3 requires any other transition on a fine pointer to complete at once (DR2's exception covers only the release of a touch drag), and #8646's FR5 asks the same of a trackpad-started swipe. Passing touchRelease only when a finger's contact ended the drag, here and in the slide-out fade at line 834, with a Reduce Motion test for a wheel-opened row, would fix it.
There was a problem hiding this comment.
The trackpad, wheel and pen swipe now matches spec:AST-057 on main (FR4, FR5, DEC-7), but approval needs an engineering owner’s decision on two things this head adds without current spec: the new component:BottomSheet element job for the input hold’s shield (BottomSheet.spec.md, lines 232–236), and whether a mouse or pen press on an open row’s own content should close it and activate nothing (useSwipeAction.ts, lines 1308–1332). An engineering owner records that decision by commenting /approve-spec bfef65e146dc5a8089b53dd84dd6c6dfafb1d1bd on this pull request, or by ruling otherwise in a comment; the author may do so as an engineering owner. Whatever that decision, two changes are needed:
- Land or retarget the dependency pull requests, so this diff carries its own 637 lines instead of 54,294.
- In the
swipeActionsdoc and JSDoc, say a swipe-only verb is unreachable “by keyboard and by a mouse drag” and that an open row closes on a press elsewhere (Item.doc.mjs:202,Item.tsx:280and:283).
[Automated review]
| rootRef, | ||
| () => | ||
| new TapGestureRecognizer({ | ||
| name: 'item-swipe-dismiss', |
There was a problem hiding this comment.
Spec hold: with no pointer gate, a mouse or pen press on an open row’s own content now closes it and its click is swallowed. spec:AST-057 on main states no content-press dismissal for any pointer (FR7 lists a pointer outside, Escape, firing an entry and unmount; DEC-7 covers reveal and fire), so this waits on an engineering owner’s decision.
| * the side's panel; a mouse never starts the drag, and a mostly vertical | ||
| * drag stays the scroller's. The gesture is | ||
| * a touch accelerator and never a row's only path to a verb: a verb | ||
| * reachable only by swipe is unreachable by keyboard and by mouse, so every |
There was a problem hiding this comment.
A mouse’s horizontal wheel now reaches a swipe-only verb (DEC-7), and a press from any pointer outside closes an open row (FR7). Saying “unreachable by keyboard and by a mouse drag”, as spec:AST-057 asks the docs to, and “on a press elsewhere” on line 283 would match; the same two phrases are in Item.doc.mjs:202.
| - while a modal sheet enters under a finger, the layer's input hold adds a | ||
| shield inside the dialog: a transparent cover of the viewport that takes | ||
| a tap or a drag landing during the entrance (`spec:AST-094` FR35). It is | ||
| removed when the sheet lands. A non-modal sheet adds no shield: it holds | ||
| only input on itself, and the page beside it stays live. |
There was a problem hiding this comment.
Spec hold: this adds a named element job that the record on main does not have, and its verification row fails an element in the open sheet’s frame with no named job. An engineering owner approves or rejects it, ideally with the pull request that adds the shield.
There was a problem hiding this comment.
The pen, trackpad and wheel swipe matches spec:AST-057 on main (FR4, FR5, DEC-7), but approval needs an engineering owner's decision on two things this head adds without current spec: whether a mouse or pen press on an open row's own content should close it and activate nothing (useSwipeAction.ts, lines 1308–1333), and the new component:BottomSheet element job for the input hold's shield (BottomSheet.spec.md, lines 232–236). An engineering owner records that decision by commenting /approve-spec 90bf79fdcdaa1bba8793bb0ba4d610bae4c3977b on this pull request, or by ruling otherwise in a comment; the author may do so as an engineering owner. Whatever that decision, three changes are needed:
- Land or retarget the dependency pull requests, so this diff carries its own 644 lines instead of 54,742.
- In the
swipeActionsdoc and JSDoc, say a swipe-only verb is unreachable "by keyboard and by a mouse drag" and that an open row closes on a press elsewhere (Item.tsxlines 280 and 283,Item.doc.mjslines 202 and 257). - Add the physical-device check
spec:AST-057's verification asks for: a real trackpad (a slow swipe and a quick flick) and a real pen on the swipe-actions story.
[Automated review]
| rootRef, | ||
| () => | ||
| new TapGestureRecognizer({ | ||
| name: 'item-swipe-dismiss', |
There was a problem hiding this comment.
Spec hold: with no pointer gate, a mouse or pen press on an open row's own content now closes it and its click is swallowed. spec:AST-057 on main states no content-press dismissal for any pointer (FR7 lists a pointer outside, Escape, firing an entry and unmount; AR3 keeps the primary's activation), so this waits on an engineering owner's decision.
| * the side's panel; a mouse never starts the drag, and a mostly vertical | ||
| * drag stays the scroller's. The gesture is | ||
| * a touch accelerator and never a row's only path to a verb: a verb | ||
| * reachable only by swipe is unreachable by keyboard and by mouse, so every |
There was a problem hiding this comment.
A mouse's horizontal wheel now reaches a swipe-only verb (DEC-7). spec:AST-057 asks the docs to say "unreachable by keyboard and by a mouse drag"; the same phrase is in Item.doc.mjs:202 and :257.
| * entry must also be reachable through the row's own content (a menu or a | ||
| * button in `endContent`). | ||
| * button in `endContent`). An open row closes on a tap on its own content, | ||
| * which activates nothing and paints no press, on a touch elsewhere, when |
There was a problem hiding this comment.
FR7 closes an open row on "a pointer landing anywhere outside it", which this head does for every pointer; "on a press elsewhere" would match here and in Item.doc.mjs:202.
| - while a modal sheet enters under a finger, the layer's input hold adds a | ||
| shield inside the dialog: a transparent cover of the viewport that takes | ||
| a tap or a drag landing during the entrance (`spec:AST-094` FR35). It is | ||
| removed when the sheet lands. A non-modal sheet adds no shield: it holds | ||
| only input on itself, and the page beside it stays live. |
There was a problem hiding this comment.
Spec hold: this names an element job the record on main does not have, and its verification row fails an element in the open sheet's frame with no named job. An engineering owner approves or rejects it, ideally with the pull request that adds the shield.
There was a problem hiding this comment.
The pen, trackpad and wheel swipe matches spec:AST-057 on main (FR4–FR6, DEC-7), but approval needs an engineering owner's decision on three spec deltas this head carries without current authority: a mouse or pen press on an open row's own content closing it without activating anything (useSwipeAction.ts, lines 1300–1324), the new component:BottomSheet element job for the entrance shield (BottomSheet.spec.md, lines 233–237), and the new spec:AST-094 FR41 rule for claimed mouse and pen drags (docs/specs/AST-094-touch-presentations/spec.md, lines 471–475). An engineering owner records that decision by commenting /approve-spec 6c08e005cc6ac6102d2a33aba151779fa96a843a on this pull request, or by ruling otherwise in a comment; the author may do so as an engineering owner. Whatever that decision, three changes are needed:
- Land or retarget the dependency pull requests, so this diff carries its own 596 lines instead of 54,299.
- In the
swipeActionsJSDoc and doc, say a swipe-only verb is unreachable “by keyboard and by a mouse drag” and that an open row closes on a press elsewhere (Item.tsxlines 280 and 283,Item.doc.mjslines 202 and 257). - Add the physical-device check
spec:AST-057's verification asks for: a real trackpad (a slow swipe and a quick flick) and a real pen on the swipe-actions story.
[Automated review]
| rootRef, | ||
| () => | ||
| new TapGestureRecognizer({ | ||
| name: 'item-swipe-dismiss', |
There was a problem hiding this comment.
Spec hold: with no pointer gate, a mouse or pen press on an open row's own content now closes it and its click is swallowed. spec:AST-057 on main states no content-press dismissal for any pointer (FR7 lists a pointer outside, Escape, firing an entry and unmount; AR3 keeps the primary's activation), so this waits on an engineering owner's decision.
| * the side's panel; a mouse never starts the drag, and a mostly vertical | ||
| * drag stays the scroller's. The gesture is | ||
| * a touch accelerator and never a row's only path to a verb: a verb | ||
| * reachable only by swipe is unreachable by keyboard and by mouse, so every |
There was a problem hiding this comment.
A mouse's horizontal wheel now reaches a swipe-only verb (spec:AST-057 DEC-7), and the record asks the docs to say “unreachable by keyboard and by a mouse drag”; the same phrase is in Item.doc.mjs (the swipeActions description and the guidance).
| * entry must also be reachable through the row's own content (a menu or a | ||
| * button in `endContent`). | ||
| * button in `endContent`). An open row closes on a tap on its own content, | ||
| * which activates nothing and paints no press, on a touch elsewhere, when |
There was a problem hiding this comment.
FR7 closes an open row on “a pointer landing anywhere outside it”, which this head does for every pointer; “on a press elsewhere” would match here and in Item.doc.mjs.
| - while a modal sheet enters under a finger, the layer's input hold adds a | ||
| shield inside the dialog: a transparent cover of the viewport that takes | ||
| a tap or a drag landing during the entrance (`spec:AST-094` FR35). It is | ||
| removed when the sheet lands. A non-modal sheet adds no shield: it holds | ||
| only input on itself, and the page beside it stays live. |
There was a problem hiding this comment.
Spec hold: this names an element job the record on main does not have, and its verification row fails an element in the open sheet's frame with no named job. An engineering owner approves or rejects it, ideally with the pull request that adds the shield; the bullet also leaves the shield's accessibility exposure unstated.
| (`spec:AST-017` FR59; DEC-9). A mouse or pen drag that a recognizer claims | ||
| takes no new text selection for that drag only. A claim made after the | ||
| drag has moved clears the page's selection; a press claimed at its own | ||
| pointerdown, before it moves, keeps it. An unclaimed mouse drag keeps its | ||
| released behavior. |
There was a problem hiding this comment.
Spec hold: FR41 on main says a mouse “otherwise keeps its released behavior”; this adds a rule for drags a recognizer claims (no new selection, a later claim clearing the page's selection, a claim at pointerdown keeping it) and a verification row for it, which waits on an engineering owner's decision, ideally in the pull request that adds the every-pointer host.
6c08e00 to
6cac398
Compare
…ast wheel The detach path and the taker-left path ended the wheel gesture with no time, so its release was stamped at the last wheel plus the end window (180 or 220 ms) and moved the arena's clock there: after a wheel at 0 ms and a detach, or a disable followed by the fingers' next wheel, a long press landing at 20 ms fired at 680 ms instead of 520 ms (spec:AST-094 FR16). Both paths now stamp the release at the last wheel. A detached and a disabled case failed before.
…stream, and a scroller's ends read as a finger's
The latch was a document capture listener outside subscribeInput, so an
app's window-capture stopPropagation of a wheel that left the moved element
hid it from the host and the page scrolled. It now subscribes to the
window's wheel stream with {cancels: true}, as the page hold does, and
leaves it when the gesture ends (architecture:react-component-runtime
INV13).
The wheel path also restated nativeScroll.ts's edge test with other numbers
(1 px at the ends, any room above 0), so a scroller 0.5 to 1 px short of its
end read as at its end to a trackpad and as scrollable to a finger. The
wheel now calls nativeScroll's canElementScroll, the probe's own per-element
test. The right-to-left case uses the measured end instead: WebKit scrolls
a fractional scroller a pixel past its reported overflow, which both tests
read as its end; Chromium's end is the reported overflow (neither stops a
pixel short of it). The latch, its window listener at a detach, and the
0.75 px edge cases failed before.
…them The comments on the three wheel end windows still called them provisional, pending an owner question; spec:AST-094 FR43 and DEC-24 now state them (100 ms for a pinch or a mouse wheel's notches, 180 ms vertical and 220 ms horizontal for a trackpad scroll, whoever keeps it). No code changes.
The public animator (spec:AST-107 FR3, FR6): createAnimator(element) over the internal animator, with animate, decay, set, get, getVelocity, stop and destroy, from the package root; and useAnimator(ref), which takes the element's ref like the gesture hooks, so a gesture hook can share the element, and returns the animator. After every commit the hook follows the element behind the ref. The internal factory is renamed createMotionAnimator, so no internal factory shares a public factory's name. Docs, a changeset, and playing-cards templates in React and plain JavaScript, with their registrations.
…s (spec:AST-107 FR1, FR6)
The public pan: `createPanGesture(element, {axis?, onPan}) → {destroy}` and
`usePanGesture(ref, {axis?, onPan})` over it, reporting
`{state, translation, velocity}` as a drag begins, changes, ends or is
cancelled, under a finger, a mouse and a pen; the translation leaves out the
travel the drag needed to begin, so an element that follows it does not
jump. Docs pages for both, with a playing-card example in plain JavaScript
and in React, the route lock and the sandbox's routes.
…element on snap points
`createDragToSnapPoints(element, {axis, snapPoints, flickThreshold?,
onSettle}) → {settleTo, destroy}` and `useDragToSnapPoints(ref, …)` over it
(spec:AST-107 FR2, FR6): a drag moves the element along its axis under every
pointer, and the release settles it where Core's one release policy says
(the nearest point; the next one past the flick speed, 1008 px/s by default;
back to where the drag started when the release moves back toward it), on
the settle spring from the release velocity. Under Reduce Motion a touch
release cross-fades to its point and any other lands at once. Docs pages and
playing-card examples for both, the route lock and the sandbox's routes.
…n an input event's own task A caller handling an input event (a key, a click, a pointer, a touch) runs in that event's own task, never in a frame callback, so the first frame whose callbacks run after its call is the one that commits the call's animations, however long the task ran. The animator cannot tell that from a call a page's own frame callback makes before a busy task, whose browser start is right, so the caller says so with the internal option `start: 'next-frame'`; a call from rest made with it starts at that frame, and a later listener of the same event that holds the main thread delays the motion instead of letting it skip into its curve. - Chromium, which tells the page the start a frame or more after the compositor takes it: such a call starts at its first frame, as a call whose first frame comes within LATE_START_MS already does, and also when an unrelated task delayed that frame. - WebKit, which dates the start at its last frame time before the task: the task's end is read at the call's first read, not at its first microtask, which runs before a later listener's block, so the dated start moves past the block. The first read stands for the task's end, so a first frame that merely comes late (more than LATE_START_MS after the call, with no block) moves the dated start to that frame too: nothing was painted before it, so the motion is delayed by that slow frame instead of skipping into its curve. A call without the option, a call with an explicit start, and a call that continues motion keep the rule they had. Internal only: the public animator does not take it.
…al options) The public pan and snap-point drag keep their behavior (spec:AST-107 FR1, FR2) and now run on internal factories a Core component can use for a job the public layer does not expose (spec:AST-107 FR7): - createPan, under createPanGesture: the recognizer's own options (which pointers and scrolls start the drag, its claim distance, a begin gate, a selection lock), and what drove the drag and its travel from where the touch began in each report. - createSnapDrag, under createDragToSnapPoints: a model of where a drag draws its element (one for one, a spring, or nothing yet inside a dead zone, the first target catching a settle in flight) and where a release or a cancel lands; a custom property to draw into; settleTo with a spring, a velocity and a touch release, returning the settle's motion. - The animator draws x into a registered custom property (a length or a number), on the main thread, for a component whose parts all read one value. architecture:react-component-runtime INV13 names these jobs among the known public-API gaps.
…set, and a companion follows its travel (internal)
Two more internal options on the shared drag path, for a component whose
own work is timed by its drag (spec:AST-107 FR7):
- settleTo({onPass: {offset, call}}) calls back once, on the first
animation frame whose presented value, read on the settle's own curve at
that frame's time, has passed the offset in the settle's direction (on
the next frame when it is there already). No timer: a settle that lands
short, that another settle or a drag replaces, or that a destroy ends
never calls back; a crossing during a drag is the caller's to read from
the drag.
- The animator's custom-property channel takes a companion: a second
custom property drawn from the travel by a pure mapping, written in the
same keyframes and writes, so it has one start and one curve with the
travel, follows every retarget, and takes whatever Reduce Motion does to
the travel.
…llows the fingers (spec:AST-094 FR43, DEC-24; spec:AST-057) A row's swipe runs the one shared path every followed pan runs: the trackpad splitter, the pan, the snap-point drag and the animator. The row declares its stops (rest, the open width, and out past its edge for a full swipe) and its actions; the drag draws the row's travel into one custom property that every part of the panel reads (spec:AST-057 FR2). - A trackpad's swipe moves the row one for one with the fingers from its first event past the pan's 8 px start, and its release settles to the nearest stop by position and velocity (Core's one release policy) on the shared settle spring, from the fingers' speed at their last move, so the row never stops before it settles. Within one gesture an open row only closes, never passing rest to its other side. - A finger's or a pen's drag stays on spec:AST-057's UIKit model: UIKit's 25 px dead zone, its tracking spring, its release rule and settle spring from the row's own speed. - A full swipe confirms by position only and fires the outermost action at any speed; speed alone never fires an entry (spec:AST-057 FR5), so a fast flick from an open row settles open. - A mouse drag and a mouse wheel's notches never swipe a row; a tap or a click on an open row's content closes it and reaches nothing. Interaction proofs: trackpad cases in WebKit (Chromium's synthetic wheels read as a mouse wheel's), a mouse wheel's notch in Chromium, fingers by real touch in both engines; the swipe actions' UIKit constants and their measurements (UIKit lab, iOS 26.2 simulator, Oct 23 2026).
…s at the frame after it The row's settles asked for in an input event's own task pass the animator's start: 'next-frame' through the shared drag (settleTo's start): Escape, a press outside the row, the dismissing tap on its content, and an entry's activation. Each then starts at the first frame after that task, so a later listener of the same event that holds the main thread delays the close instead of letting it skip into its curve. A scroll's close and every settle a drag's release starts keep the default.
…ke one swipe prop (spec:AST-057 DEC-8)
`Item` and `ListItem` take their swipe actions as one prop, `swipe`, whose
value `createSwipe({leading?, trailing?, swipeBehavior?})` makes (FR1). The
constructor's module, `@astryxdesign/core/Swipe` (also re-exported from the
root), carries every module the swipe runs on: the panels
(SwipeActionsLayer, moved from Item), the row's gesture code
(useSwipeAction and swipeRelease, moved from Item), and through them the
shared drag path and the animator (FR10). `Swipe` is opaque: a frozen
brand with nothing to read, whose host only createSwipe records.
Importing or calling createSwipe does no DOM work.
For a row given `swipe`, Item holds no swipe code: it reads the value
through swipeHost.ts, a reader with no imports, hands the runtime the value
carries its root and the facts it already has (role, disabled, delegation,
link root), and renders the root style, root handlers and layer it gets
back. The runtime decides which rows it serves (DEC-3); routes the root's
keys to the row's swipe through a registry keyed on the root element, with
the same handlers for every row and value, so a new value from an inline
call keeps the gesture and an open row as they are (FR11); and holds the
root's press while the row is open with a private attribute
(privateAttributes/pressHeld.stylex.ts) that the press controller reads as
a disabled surface: it takes the press and paints nothing, a pressable
inside keeps its own, and the touch still counts for the brake (FR7).
`swipeBehavior: 'commit'` with several entries on a side warns in
development (FR3).
The released `swipeActions` and `swipeBehavior` props and the four
`ItemSwipe*` types are deprecated (DEP-0017): static `@deprecated`
metadata, no runtime warning, and `swipe` wins when both are given, with a
development note. They keep their released behavior through DEP-0017's
deprecated path, the only swipe code in Item and on that path only: the
released loader (the one `import()` of swipe code, whose layer brings the
released commit warning), the released row style from the first render
(`spec:AST-017` DEC-17) and role gate, the bridge the loaded layer fills
with the key forwarder into it, and the coexistence diagnostic. It leaves
with the props in 0.8.0 (CLN-0025). The touch-support bundle guard names
the loader and counts its chunk out of Item's import (without it, the 13
row importers would carry the gesture core); `Swipe` joins the imports
that carry the gesture core.
An `astryx upgrade` codemod staged in transforms/next rewrites static
`swipeActions` and `swipeBehavior` on Item and ListItem to
`swipe={createSwipe(…)}`, adds the import, renames the four types, drops a
merged `swipeBehavior="reveal"`, and reports computed values, every row
that spreads props, and code that mutates the leading or trailing array of
a value it retypes as `CreateSwipeOptions` (`spec:AST-017` FR29c, FR29d).
Docs, the story and the changesets teach only `swipe`.
Size (the PR size report's options, gzipped, base 0a00169, whose Item
and List match main's at d96d470): `Item` alone 13,471 B (base 13,482),
`ListItem` alone 58,856 B (base 58,894), `./Swipe` 46,852 B; `Item.tsx`
935 lines (base 964), `ListItem.tsx` 474 (base 475).
…e layer's load, not a clock
…module (spec:AST-057 FR10) FR10's module attribution in the touch-support bundle guard: each import is built with the size report's options and code splitting, and every byte of its entry output and the chunks it reaches by static import is attributed to its source module from the metafile. Item alone and ListItem alone hold no byte of a swipe module (the gesture core, the animator, the snap-point drag, the panels' rendering, the row's gesture code and its UIKit table); the reader of a swipe value and DEP-0017's row style are not swipe modules, and the deprecated loader's chunk is outside what loads at once. Item with createSwipe holds them, so the check finds what it looks for.
…ask's freeze AST-057's current state and contract table no longer list swipe and createSwipe as an implementation gap, and ListItem passes swipe to its Item; ListItem's record drops its swipe gap (two remain). AST-057's current state records that a long main-thread task during a drag or settle freezes the row, then it jumps, as the released swipe does.
…the row, as createSwipe's rows have it
…w, not a count at 60 Hz "the settle after a held release is UIKit's spring, frame by frame" required more than 10 frames with a new travel during the settle, a count that holds at 60 Hz; on a loaded runner the page drew 5, and the test failed with every frame it drew on the curve. The frame recorder now logs every frame the page draws (its animation-frame times, the same time for a frame with or without a new travel), and the test needs at least 4 frames drawn during the settle to judge, and a new travel on each of them, each on the curve as before.
… as a digitizer stamps a finger's The proof that a fling short of the commit point opens the row sent its touches with Input.dispatchTouchEvent and no timestamp. Each dispatch returns only once the page has handled the touch, so on a loaded page the quick last moves and the lift went out late: in a run with each touchmove costing the page 60 ms, the last two moves and the lift were stamped 108 ms and 100 ms apart instead of together, the lift fell past the velocity filter's 40 ms stillness, and the release read as still and closed the row. The gesture reads the touches' own times; the touches now carry the script's times (each also waits out its gap in real time, so no stamp is ahead of the page), and the same busy run opens the row.
eb34463 to
2f7567f
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's spec decision, and some changes are needed either way. An engineering owner records that decision by commenting /approve-spec 2f7567fe6c17fa37ea31db67dd5e7a76e4d7fcf3 (the author may do so as an engineering owner) or rules otherwise. Three points need it: whether a mouse, pen or trackpad press on an open row's content should close it (spec:AST-057 FR7 and AR3 name a touch tap), the two new INV13 gap entries in architecture:react-component-runtime, and whether spec:AST-094 FR43 or spec:AST-057 FR4 and DEC-7 governs a mouse's horizontal wheel.
- The diff is 20,249 authored lines, including five open dependency pull requests; land or retarget them, then split the rest.
- The row's settle now animates a custom property on the main thread, which
spec:AST-094FR5 rules out, and the newspec:AST-057line saysmainbehaves the same althoughmainsettled with a compositortransformtransition. - The open row adds its own document
pointerdownandscrolllisteners besidesubscribeInput; subscribe through it when the row opens instead. spec:AST-057asks for a physical-device check of FR5 and FR6,item-swipe-gesture-system.mdclaims a finger fix that already worked onmain, and the docs say an open row closes "on a touch elsewhere" where any pointer closes it.
[Automated review]
| // UIKit's dismissal does: the tap recognizes at the lift, and the host | ||
| // keeps the click it would have made from the row (it cancels the touch, | ||
| // or where the primary pointer is fine its click guard swallows the | ||
| // click). Any pointer's press dismisses, since a trackpad opens a row too |
There was a problem hiding this comment.
Spec hold: any pointer's press on an open row's content now closes it and its click is swallowed. spec:AST-057 FR7 and AR3 name only a touch tap, so this waits on an engineering owner's decision.
| close(); | ||
| } | ||
| }; | ||
| document.addEventListener('pointerdown', onOutside, true); |
There was a problem hiding this comment.
These document listeners sit beside subscribeInput (utils/inputStream.ts), the one entry for document-wide input in core (architecture:react-component-runtime INV16). The work is caused by the open transition, which setSwipeState already handles for the press hold. Subscribe there through subscribeInput and drop the Effect.
| * element whose parts all read one value (a row's swipe travel, | ||
| * `spec:AST-057` FR2): a `<length>` in px, or a plain `<number>`. The | ||
| * property is registered as inherited so it interpolates, and it animates | ||
| * on the main thread, not the compositor; every other channel renders as |
There was a problem hiding this comment.
A row's settle drawn into this custom property no longer advances through a main-thread block, which spec:AST-094 FR5 requires of every release. On main the root settled with a transform transition that the compositor runs.
| `swipe` prop is not built, on `Item` or on `ListItem`. | ||
| - `ListItem` passes `swipe` to its `Item`, and the released `swipeActions` | ||
| and `swipeBehavior`, deprecated by DEP-0017, through the same passthrough. | ||
| - A long main-thread task during a drag or settle freezes the row, then it |
There was a problem hiding this comment.
On main the row's root settles with a transform transition, which keeps moving through a long task, so "the released swipe behaves the same" does not hold for a settle.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] Swipe actions on `Item` and `ListItem` run on the gesture system. A row claims a sideways touch at 8 px, before a browser commits to scrolling it. Its swipe begins at UIKit's 25 px, from 0, so a short wobble no longer moves the row. A finger or a pen swipes the row whatever the primary pointer, a touchscreen laptop's included, and a mouse drag never does. A second finger that lands before the claim leaves the touch to the browser's pinch; once the row has claimed its finger, a second finger no longer cancels the swipe. The click a browser synthesizes after a swipe is cancelled at its source instead of being swallowed by a 400 ms timer. |
There was a problem hiding this comment.
On main a finger already swiped the row whatever the primary pointer: the gate was pointerType !== 'touch' alone and the layer passed isEnabled: true. Suggested wording: "A pen now swipes the row as a finger does."
| * unreachable by keyboard and by mouse, so every entry must also be | ||
| * reachable through the row's own content (a menu or a button in | ||
| * `endContent`). An open row closes on a tap on its own content, which | ||
| * activates nothing and paints no press, on a touch elsewhere, when the |
There was a problem hiding this comment.
Any pointer landing outside closes an open row (onOutside in Swipe/useSwipeAction.ts), as spec:AST-057 FR7 says. "on a press elsewhere" would match, here and in Item.doc.mjs.
createSwipe's doc is a new public hook page, so the docsite's generated ShadCN registry gains one item, hook-create-swipe at hooks/create-swipe, and the docsite build refuses a registry that differs from the reviewed route lock. The lock gains that one entry; no existing name or path moves.
The docsite's MCP route indexes doc keywords in a plain object, so the
keyword "constructor" read Object.prototype.constructor as an existing
entry and the route threw at load ("includes is not a function"),
failing the docsite build. The keyword added nothing "swipe" does not
find; it goes.
There was a problem hiding this comment.
Approval waits on an engineering owner's spec decision, and some changes are needed either way. An engineering owner records that decision by commenting /approve-spec e870248ecc40ef36a28bd41753c59feb2457a527 (the author may do so as an engineering owner) or rules otherwise. Three points need it: whether a mouse, pen or trackpad press on an open row's content should close it (spec:AST-057 FR7 and AR3 name a touch tap), the two new INV13 gap entries in architecture:react-component-runtime, and whether spec:AST-094 FR43 or spec:AST-057 FR4 and DEC-7 governs a mouse's horizontal wheel.
- The diff is 20,253 authored lines, including five open dependency pull requests; land or retarget them, then split the rest.
- The row's settle now animates a custom property on the main thread, which
spec:AST-094FR5 rules out, and the newspec:AST-057line saysmainbehaves the same althoughmainsettled with a compositortransformtransition. - The open row adds its own document
pointerdownandscrolllisteners besidesubscribeInput; subscribe through it when the row opens instead. spec:AST-057asks for a physical-device check of FR5 and FR6,item-swipe-gesture-system.mdclaims a finger fix that already worked onmain, and the docs say an open row closes "on a touch elsewhere" where any pointer closes it.
[Automated review]
| useGestureRecognizer( | ||
| rootRef, | ||
| () => | ||
| new TapGestureRecognizer({ | ||
| name: 'item-swipe-dismiss', | ||
| shouldReceiveTouch: (_tap, touch) => | ||
| isOpen() && | ||
| !( | ||
| touch.target instanceof Element && | ||
| touch.target.closest('[data-swipe-panel]') != null | ||
| ), | ||
| }), | ||
| tap => { | ||
| // Recognized at the lift, in its pointer event's own task. | ||
| if (tap.state === 'ended') { | ||
| close('next-frame'); | ||
| } | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Spec hold: any pointer's press on an open row's content now closes it and its click is swallowed. spec:AST-057 FR7 and AR3 name only a touch tap, so this waits on an engineering owner's decision.
| document.addEventListener('pointerdown', onOutside, true); | ||
| document.addEventListener('scroll', onScroll, { | ||
| capture: true, | ||
| passive: true, | ||
| }); |
There was a problem hiding this comment.
These document listeners sit beside subscribeInput (utils/inputStream.ts), the one entry for document-wide input in core (architecture:react-component-runtime INV16). The work is caused by the open transition, which setSwipeState already handles for the press hold. Subscribe there through subscribeInput when the row opens, unsubscribe when it closes, and drop this Effect.
| * property is registered as inherited so it interpolates, and it animates | ||
| * on the main thread, not the compositor; every other channel renders as |
There was a problem hiding this comment.
A row's settle drawn into this custom property no longer advances through a main-thread block, which spec:AST-094 FR5 requires of every release. On main the root settled with a transform transition that the compositor runs.
| - A long main-thread task during a drag or settle freezes the row, then it | ||
| jumps; the released swipe behaves the same. |
There was a problem hiding this comment.
On main the row's root settles with a transform transition, which keeps moving through a long task, so "the released swipe behaves the same" does not hold for a settle.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] Swipe actions on `Item` and `ListItem` run on the gesture system. A row claims a sideways touch at 8 px, before a browser commits to scrolling it. Its swipe begins at UIKit's 25 px, from 0, so a short wobble no longer moves the row. A finger or a pen swipes the row whatever the primary pointer, a touchscreen laptop's included, and a mouse drag never does. A second finger that lands before the claim leaves the touch to the browser's pinch; once the row has claimed its finger, a second finger no longer cancels the swipe. The click a browser synthesizes after a swipe is cancelled at its source instead of being swallowed by a 400 ms timer. |
There was a problem hiding this comment.
On main a finger already swiped the row whatever the primary pointer: the gate was pointerType !== 'touch' alone and the layer passed isEnabled: true. Suggested wording: "A pen now swipes the row as a finger does."
| * unreachable by keyboard and by mouse, so every entry must also be | ||
| * reachable through the row's own content (a menu or a button in | ||
| * `endContent`). An open row closes on a tap on its own content, which | ||
| * activates nothing and paints no press, on a touch elsewhere, when the |
There was a problem hiding this comment.
Any pointer landing outside closes an open row (onOutside in Swipe/useSwipeAction.ts), as spec:AST-057 FR7 says. "on a press elsewhere" would match, here and in Item.doc.mjs.
Depends on #9261 (on #8658, #8655, #8653 and #8676). Builds the swipe constructor that
spec:AST-057records (DEC-8). This pull request's own commits: vjeux/astryx@0aa861f...e870248Status: draft. CI is running at this head. Already passed locally at this head: unit tests, lint, typecheck,
check:repo,check:knowledge, the CLI and codemod suites (but for two release-cache cases that fail the same way at the base), Storybook's build, the docsite's preview build, the accessibility contracts (3 repeats), and the interaction proofs in Chromium (pinned to two cores at their default of one worker, every case; at 4 workers, every case but the animator's busy-main-thread proof, which starves there as at #9261's head) and in WebKit (four CPUs: the swipe proofs 10 times, and the animator, snap-point drag and gesture proofs).User need
A list row with swipe actions is a touch accelerator: a finger, a pen or a trackpad's two-finger swipe drags the row aside to uncover its actions.
Itemrow throughItemitself, and the gesture goes live only once a lazily loaded chunk arrives.Sheets and pagers already run one shared drag path. A row that runs its own copy of that machinery drifts from them, so the row should run the same path and declare only what is its own: its stops and its actions.
Problem and solution fit
spec:AST-057DEC-8):swipe={createSwipe({leading, trailing, swipeBehavior})}. The constructor's module,@astryxdesign/core/Swipe, carries every module the swipe runs on, so an app that never importscreateSwipeships none of it, and a row givenswipeis swipeable at its first paint (FR10).spec:AST-094FR43 gives every followed pan (follow from the first event past 8 px, settle by position and velocity, keep the fingers' speed) never reached it.spec:AST-094DEC-24). It declares its stops (rest, the open width, and out past its edge for a full swipe) and its actions, and the drag draws its travel into the one custom property every part of the panel reads (spec:AST-057FR2).Current specification
spec:AST-057:swipeprop, whose valuecreateSwipemakes;Swipeis opaque.swipeBehavior, with a development warning for several entries on acommitside.createSwipe's module. FR11: an inlinecreateSwipecall is fine.swipeActionsandswipeBehaviorare deprecated and keep working.spec:AST-094FR43 and DEC-24: a followed trackpad pan follows the fingers one for one from its first event past 8 px and settles by position and velocity from their last speed; within one gesture an open row only closes; one shared path, and a component declares only its detents and actions.spec:AST-017FR28, FR29 and DEC-17: a deprecation in a patch adds no runtime warning and changes no caller's behavior.spec:AST-107FR7: Core uses the public layer except for the known public-API gaps thatarchitecture:react-component-runtimeINV13 lists. This pull request names the row's jobs there.Public delta and caller burden
packages/core/api-report.mdshows each line):createSwipe(options: CreateSwipeOptions): Swipe, from@astryxdesign/core/Swipeand the package root, withCreateSwipeOptions(leading?,trailing?,swipeBehavior?), the opaqueSwipe,SwipeAction,SwipeActionVariantandSwipeBehavior;swipe?: SwipeonItemandListItem;swipeActions,swipeBehaviorand the typesItemSwipeAction,ItemSwipeActions,ItemSwipeActionVariantandItemSwipeBehavior(DEP-0017). They keep working as released, with static@deprecatedmetadata and no runtime warning, until 0.8.0 removes them (CLN-0025). Whenswipeis set too,swipewins, and a development-only note says the deprecated props are ignored.item-swipe-panel,item-swipe-action) are unchanged.swipeis swipeable at its first paint; a row given the deprecated props loads the swipe the first time it has them, as released;spec:AST-057FR5), and FR43's long fast swipe fires only wherespec:AST-057allows a full swipe.astryx upgraderewrites staticswipeActionsandswipeBehavioronItemandListItemtoswipe={createSwipe(…)}, with a codemod staged intransforms/next/. It's an opt-in early migration from this release, and 0.8.0's upgrade runs it again with the removal. It adds the import, renames the four types, and drops a mergedswipeBehavior="reveal"(the default). It reports computed values, every row that spreads props, and code that mutates theleadingortrailingarray of a value it retypes asCreateSwipeOptions, since those arrays are readonly.Design and alternatives
Where the swipe's code lives (
spec:AST-057DEC-8, FR10).packages/core/src/Swipe/holdscreateSwipe, the panels (SwipeActionsLayer.tsx), the row's gesture code (useSwipeAction.ts,swipeRelease.ts) and, through them, the shared drag path and the animator.swipe.Itemreads the value throughswipeHost.ts, a module with no imports, and hands the runtime the value carries its root and the facts it already has: its role, whether it is disabled, whether it is the enlarged target of a control inside it, and whether its root is a link. The runtime decides whether it serves the row (DEC-3). If it does, it returns the root's style, the root's handlers and the layer, andItemrenders them.Itemholds no swipe code for this path:privateAttributes/pressHeld.stylex.ts). The press controller treats a held surface like a disabled one: it takes the press and paints nothing, a pressable inside it keeps its own press, and the touch still counts for the brake. So a tap on an open row's content only closes it (FR7);Swipeis opaque: a frozen brand with nothing to read. OnlycreateSwiperecords what a value carries, so a value built by hand swipes nothing.Item, for the deprecated props only, and leaves with them (CLN-0025):import()the first time a row has the deprecated props; that layer brings the releasedcommitwarning;spec:AST-017DEC-17), and the released role gate;import()of swipe code and counts it out ofItem's import; without that exception, the 13 imports that render anItemrow would carry the gesture core.Swipejoins the imports that carry it.Internal options the shared primitives gain (no public API; they land with this, their first user):
createPan, under the publiccreatePanGesture: which pointers and scrolls start a drag, its claim distance, a begin gate and the selection lock; each report says what drove the drag.createSnapDrag, under the publiccreateDragToSnapPoints, which keeps its behavior and tests:settleTowith a spring, a velocity, a touch release and the animator'sstart: 'next-frame'(chore(core): the animator takes start: 'next-frame' for a call made in an input event's own task #9261).settleTo({onPass})calls back on the first animation frame whose presented value has passed the offset, read on the settle's own curve. It uses no timer, and never fires for a settle that lands short, is replaced, or is destroyed.privateAttributes/pressHeld.stylex.ts) as a disabled one.What the row's own code holds, and why.
useSwipeAction.tsis 675 code lines (comments and blank lines excluded):The row builds no pan recognizer, runs no frame loop and solves no spring. What remains:
swipeRelease.ts);spec:AST-057FR5);spec:AST-057FR7);SwipeActionsLayer.tsx).Known limitation, inherited from main's released design
A long main-thread task during a drag or settle freezes the row, then it jumps; the released swipe behaves the same.
spec:AST-057's current-state impact records it in this pull request.spec:AST-057FR2 draws the travel into one registered custom property that every part reads, and Chromium animates a custom property on the main thread. The drag's tracking spring draws into the same property. Main's released swipe has the same design.spec:AST-057keeps FR2 as approved, so each part's transform does not move on the compositor (fix(Item): swipe actions move on the compositor with UIKit springs #8097's approach, superseded by this pull request).Evidence
The constructor:
createSwipemakes a frozen value with nothing to read, carries the entries per side andswipeBehavior(revealby default), keeps the entries it was given, and warns in development for several entries on acommitside (createSwipe.test.ts);Item.test.tsx);createSwipe.server.test.ts);listitem, and nooptionormenuitem*row, disabled row, delegating row, link root or row without entries; it gives every row and every value the same root handlers and layer type.The press hold (red first,
pressFeedback.test.ts): a touch on a held surface paints nothing and lights nothing around it; a pressable inside it keeps its own press; a mouse press on it paints nothing; it presses again once the hold is gone. On the row, the hold is written from the release that opens it and freed when it closes, or when the row loses its swipe while open.An inline
createSwipecall (spec:AST-057FR11): a new value keeps a closed row closed and an open or settling row open, fires the newest handler, repaints a changed entry list, creates no second drag and starts no second settle.The deprecated props: a row still rests open and fires a tapped entry, and still slides out under
commit; it has the same root style as a row givenswipefrom its first render, before its chunk loads; the released role gate serves alistitemand not anoption;swipewins, with a development note; no runtime warning on their own; the releasedcommitwarning stays. Their tests wait for the deprecated layer's own load, never a clock: a test helper awaits the module the loader brings, insideact, and one test's mock loads it 1.5 s late.The import graph:
Itemstatically imports onlyswipeHost.tsand the released row style from the Swipe module, and reaches the layer only through DEP-0017's loader;createSwipebrings the layer, and the layer the gesture, statically (Item.source-build.test.mjs, and the touch-support bundle guard).Module attribution (
spec:AST-057FR10). Each public import is built with the size report's esbuild options (minified ESM, code splitting, metafile). Every byte of the JavaScript it loads eagerly, its entry output and the chunks it reaches by static import, is attributed to its source module from the metafile's bytes in output. A module's gzipped share is its part of the eager bytes times the eager output's gzip size. The swipe modules are the gesture core, the animator, the snap-point drag, the panels' rendering and the row's gesture code. The touch-support bundle guard runs this attribution forItemalone andListItemalone and fails on any byte of a swipe module;ItemwithcreateSwipeis its positive control, and a build whereItemusescreateSwipefails it. Base (feat(core): createDragToSnapPoints and useDragToSnapPoints settle an element on snap points #8658's head) against head:swipehandoff and row style, head (min / gzip share)Item's own modules, base → head (min)ItemaloneListItemaloneItemwithcreateSwipe./List./CheckboxList./RadioList./DropdownMenu./ContextMenu./MoreMenu./Selector./Table./Breadcrumbs./Pagination./PowerSearch./MarkdowncreateSwipeholds a byte of a swipe module. From the Swipe module, each import that renders a row keeps only theswipehandoff (swipeHost.js, 57 B) and the deprecated path's row style (swipeHost.stylex.js, 77 B).Item's own modules shrink by about 470 B.privateAttributes/pressHeld.stylex.js, 73 B in every import that renders a row (FR7).import()target is its own chunk. Bundling moves the parts of a module that this chunk shares with the eager code into the eager output, so the row importers carry 57 B more ofutils/touchQueries.jsand 36 B more of the press hold, andTableandPowerSearchalso carry about 1,260 B more ofmotion/uikit/springs.jsand 79 B more ofmotion/uikit/timing.js. Built without thatimport(), those modules match the base byte for byte. None of them is a swipe module, and the bytes leave with the loader (CLN-0025).ListItemalone is 345 B smaller minified and 6 B larger gzipped.Item.tsxis 944 lines (base 973), andListItem.tsx474 (base 475).The codemod: an inline object merged with
swipeBehavior; a defaultswipeBehavior="reveal"dropped; a name guarded, and spread beside aswipeBehaviorexpression; aliases and namespace imports followed, other libraries left alone; an existing import reused, and a free alias taken; the deprecated props removed from a row that setsswipe; computed values and spreads reported once; mutations of a retyped value'sleadingortrailingarray reported once; the four types renamed; idempotent.Trackpad, WebKit:
wheelDelta, which FR43 reads as a mouse wheel's, so the trackpad cases skip there.A mouse wheel's notch never swipes a row (Chromium).
Fingers, by real touch in both engines:
A close held back by a later listener's block starts after the block, and every frame drawn lies on its curve: the row's input-driven settles pass
start: 'next-frame'.On a loaded page:
Unit:
Item.sharedDrag.test.tsx);createSnapDrag.test.ts);spec:AST-085FR1, FR2).Supersedes #8069, #8096, #8097, #8132 and #8134
They were closed in its favor. Each one's tests, changeset and doc changes were checked against this branch:
chore(core): swipe actions' UIKit values and release rule #8069 (UIKit values and release rule):
swipeActions.ts,swipeRelease.ts, itsMEASUREMENTS.mdrows and itsmotion/README.mdrow are here.fix(Item): swipe actions on the gesture system #8096 (the gesture system):
spec:AST-094FR43: gestures attach for every pointer.swiperenders its layer at its first render, and a row given the deprecated props renders it once the loader has brought it.createSwipebrings the layer andItemimports none of it.fix(Item): an open swipe row closes on a tap on its content or a scroll #8132 (an open row's closes): 11 of its 12 test titles are here unchanged; one is renamed for the focus rule ("inside its open panel"). Its changeset is here, reworded the same way.
fix(Item): a swipe row's full swipe confirms, runs ahead and fires like UIKit's #8134 (the full swipe):
spec:AST-057FR5 now records the measured rule.swipeCurvesunit tests (one describe, four cases) covered per-part curves. Underspec:AST-057FR2 every part reads one custom property, so those per-part curves no longer exist; the browser proofs check every part against the travel on every frame.fix(Item): swipe actions move on the compositor with UIKit springs #8097 (swipe actions on the compositor with UIKit springs):
spec:AST-057FR2's single travel property ("moves only by transforms that read one travel", "gives the root back its own styles when the swipe actions go", "fires on the next frame when the release is already past the row's edge").createSnapDrag.test.ts: it calls back once, on the first frame whose presented value has passed the offset, and never for a settle that lands short, is replaced, or is destroyed.swipeParts, one describe and three cases, and the one-clock case) covered per-part transforms, whichspec:AST-057FR2 replaces with one custom property; the browser proofs check every part against the travel on every frame.Scope
Testing
pnpm exec vitest run packages/core, plus the CLI, registry, codemod and touch-bundle guards.pnpm check:repoandpnpm check:knowledge.packages/core/api-report.mdchanges by exactly the linespnpm api:reportchanges between the base's build and this head's.packages/core/src/Item/__tests__/ItemSwipeActions.interaction.browser.spec.ts, with the animator, snap-point drag and gesture specs, in Chromium and WebKit.ItemSwipeActions.a11y.chromium.spec.tsandItemSwipePinchZoom.a11y.chromium.spec.ts.packages/core, base against head.apps/docsite/vercel.json's build command).createSwipe's doc is a new hook page, so the ShadCN route lock (internal/shadcn-registry/routes.lock.json) gains one entry,hook-create-swipeathooks/create-swipe; no existing name or path moves.Writing rubric results
Records edited:
architecture:react-component-runtimeINV13: the known public-API gaps gain the row's jobs, for spec-owner review;spec:AST-057: the contract table's readiness cell and the current-state impact drop the implementation gap this pull request closes; the current-state impact saysListItempassesswipeto itsItem, and records the long-task line above;component:ListItem(ListItem.spec.md): itsswipegap goes, and the readiness cell counts the two that remain.R1 narration: none. Each edit states what the system does, in the present tense; the removed lines described what was not yet built.
R2 research: none. The long-task measurements stay in this description; the record keeps one sentence.
R3 evidence: INV13's addition cites
spec:AST-057AR2, FR2 and FR5; no evidence cell names a pull request.R4 rejected alternatives: none added.
R5 change references: none added; the records name no pull request or issue.
R6 open questions: none.