Skip to content

spec(AST-094): only a gesture's motion springs; other motion plays the theme's tokens - #9706

Merged
cixzhang merged 18 commits into
mainfrom
spec/gesture-motion-springs
Nov 4, 2026
Merged

cixzhang merged 18 commits into
mainfrom
spec/gesture-motion-springs

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Spec only. Maintainer impact: none on any release. This PR changes no code, and no release gate reads it.

Exact decision recorded

One motion rule for every pointer (touch presentation spec FR45, DEC-26):

  • A gesture's motion springs. A gesture is a continuous pointer movement whose velocity carries into the motion: a drag, a flick, a swipe, a pan, a pinch, and the settle after any of them. That motion plays a spring, or for a coast its measured deceleration rate, that starts at the gesture's velocity, under a mouse, a pen, a trackpad and a finger alike. The theme's tempo scales the spring's duration and character offsets each role's measured bounce by a fixed weight, as the theme motion spec states. A part that follows a pointer follows every pointer.
  • Every other motion plays the theme's motion tokens. Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, scaled by tempo, under every pointer, a finger included. A tap has no velocity, so it is not a gesture. A touch presentation keeps its look (a menu grows from its trigger, a thumb lifts) and plays it on those tokens.
  • Measured motion roles apply only to gesture motion. FR1, FR4 and the generated motion role table now require measured roles only for motion a gesture drives. Tap-started touch exits play the theme's exit tokens, not measured roles (FR7, FR45). Measured input timings (who gets a touch, hold durations, flick speeds) stay internal values, not motion roles.
  • Reduce Motion substitutions and continuous busy loops (such as ProgressBar's indeterminate pulse) keep their named roles; they are neither gesture- nor interaction-started motion.
  • Three approved motions keep their springs until their own records change: the layout glide (animated layout spec FR4, which TreeList's group slide rides), the animator's animate (public animator spec FR3), and the snap-point drag's settleTo (public animator spec).
  • Touch exits keep their look and play the theme's exit timing. A touch exit that a tap, a key, an outside press or the program starts (a menu's shrink, the context menu's return, the alert's fade, the viewer's exit) plays the theme's exit duration and easing, --duration-exit and --ease-exit; an exit a gesture drives keeps its velocity spring. The theme's exit switches still never remove a touch exit: the exit motion spec's FR14 and DEC-5 change only their timing clause.

A second decision (DEC-27): Android Chrome claims are met by emulator evidence (Chrome with Gboard in the Android emulator, at the exact head, with animations at normal scale and removed, plus a slow-device profile), with no human device check. iOS Safari claims keep real-device evidence. The mobile keyboard spec's evidence text and its DEC-3 (now: a physical iPhone gates release) are aligned.

What changes for people

Motion that a tap, a hold or a key starts under a finger moves from a measured spring to the theme's tokens:

Component Motion Today After
Switch Thumb travel and track colour on a tap, a hold or a key Measured switch springs Theme duration and easing
Switch Thumb lift while a finger presses Measured lift and settle springs Same lift look, on theme tokens
Slider Thumb lift and drop Measured lift and drop springs Theme tokens
SegmentedControl Selection slide on a tap or a key Measured selection spring Theme duration and easing
DropdownMenu, Popover Grow from the trigger and shrink back Measured menu open and close springs (not built yet) Theme entrance tokens; theme exit timing
DropdownMenu sub-menu Drill-in between levels on a phone Measured sub-menu springs Theme move tokens
ContextMenu Trigger lift, its return and the menu's grow after a long press Measured lift, dismiss and morph springs (not built yet) Theme tokens; the return on the theme's exit timing
AlertDialog Scale-in present and fade-out dismiss Measured present and dismiss springs (not built yet) Theme entrance tokens; theme exit timing
Lightbox Controls fade, a double tap's zoom, the grow from and return into its source, the touch exit Measured roles Theme tokens; theme exit timing
TreeList Group slide under a coarse pointer Measured motion Theme tokens
ProgressBar Determinate fill on a value change under a coarse pointer Linear, 1000 ms for the whole track (shipped) Theme medium duration and easing, every pointer
DateInput sheet wheel A wheel turning onto a clamped or a tapped row Measured tap spin, 300 ms (shipped) Theme move tokens

Motion that a gesture drives now springs, and follows, under every pointer:

Component Motion Today After
Switch Mouse or pen drag Thumb still until the release Thumb follows; the release settles on a spring from its speed
Slider Mouse or pen flick Ends where it is released Coasts and settles as a finger's does
SegmentedControl Mouse or pen drag release Slides on the theme's duration Settles on a spring from the release speed
Carousel paging Trackpad swipe Coarse pointer only Every pointer
DateInput sheet wheel Trackpad scroll Coarse pointer only Wherever the sheet shows

Unchanged: whether a touch exit plays (no exit switch removes it), press feedback timing, gesture thresholds, the follow and the rubber band themselves (fixed), Toast's swipe, BottomSheet's pull, Lightbox's drag paging, drag to close and pinch, a row's swipe, and reduced motion.

Existing authority searched

The touch presentation spec (Intent, Non-goals, FR1, FR4, FR7, FR13, FR31, FR33, FR34, FR38, FR43, FR44, DEC-1, DEC-3, DEC-15, DEC-25, Platform support), the theme motion spec (FR2–FR4), the exit motion spec (FR14, DEC-5), the mobile keyboard spec (Platform support, DEC-3), the reduced-motion design rules, the generated motion role table, and the Switch, Slider, SegmentedControl, Carousel, ContextMenu, DropdownMenu, Popover, AlertDialog, Lightbox, DateInput, TreeList and ProgressBar records. No open pull request claims FR45, DEC-26 or DEC-27.

Decision

  • Touch presentation spec: FR45 (the rule), DEC-26 (its reasons, the owner's decision in plain words, and three Rejected: lines), and DEC-27 (Android emulator evidence). FR1 and FR4 limit measured roles to gesture motion (tap-started touch exits play the theme's exit tokens), and name measured input timings as internal values, not roles. Intent, Non-goals, FR13, FR31, FR33, FR34, FR38 and FR43 point to FR45. Platform support takes DEC-27's evidence rule. Current-state impact lists every record the rule moves.
  • Exit motion spec: FR14's and DEC-5's timing clause (with its Non-goals, Current-state impact and FR14 verification row) says a touch exit that a tap, key or program starts plays --duration-exit and --ease-exit; the switch clause is unchanged. FR7 in the touch spec says the same.
  • Exit motion spec, already-animating surfaces: they keep their durations and curves in a pointer presentation and for an exit a gesture drives (a swipe or a drag with velocity); a touch exit that a tap, a key or the program starts plays --duration-exit and --ease-exit, BottomSheet's close included, with an implementation gap (a coarse-pointer close started by a tap or key plays --duration-medium today).
  • Row actions spec (swipe rows): an open row closed by a tap, a pointer outside, Escape or a fired entry's tap plays --duration-exit and --ease-exit; a close a swipe drives keeps its measured spring (FR5, FR7). The Non-goals' source-constants line now covers only gesture-driven slides and springs. Implementation gap: the shipped row settles every close on one 200 ms ease-out today.
  • Generated motion role table: its intro says a role plays only for gesture motion; tap-started motion, exits included, plays the theme's tokens.
  • React component runtime architecture: INV15's value mode follows its property channels' timing: theme motion tokens for a program-started change, and measured springs only where FR45 keeps them.
  • Mobile keyboard spec: Android claims rest on emulator evidence; the release gate is a physical iPhone.
  • Verification: Android claims rest on emulator evidence in the touch spec's "all" row and AlertDialog's present and dismiss row; a sub-menu drill-in is checked against the theme's move tokens.
  • Component records: Switch, Slider, SegmentedControl, ContextMenu, DropdownMenu, Popover, AlertDialog, Lightbox, DateInput, TreeList and ProgressBar, each with an implementation gap where code still plays a measured role.

Rejected alternatives

  • Keeping the measured springs for motion a tap starts under a finger.
  • A theme curve for a mouse release, which stops a flick dead.
  • Following a pointer only under a finger.
  • A physical Android phone as a gate or a follow-up check.

Claim scope and non-goals

  • The exit switch rule is out of scope: the theme's exit switches still never remove a touch exit.
  • The theme motion spec's tables and fixed-list wording are a separate pull request; this one adds only FR4's pointer to it.
  • The animated layout glide (and SideNav, AppShell and TreeList, which ride it) the animator's animate, and the snap-point drag's settleTo keep their springs, named as an exception in FR45, FR4 and the role table intro. Their own records decide any change.

Evidence and implementation relationship

Owner decisions of 2026-11-02 (motion) and 2026-11-03 (Android evidence), recorded in plain words in DEC-26 and DEC-27. No implementation in this pull request.

Writing rubric results

  • R1 narration: three "supersedes" or "narrows" notes stated as standing scope (touch spec DEC-26, Slider DEC-4, ProgressBar DEC-2), and one in the mobile keyboard spec's DEC-3 pointing to DEC-27; no other narration added.
  • R2 research in the record: none.
  • R3 evidence naming a change: none.
  • R4 rejected alternatives over one line or outside a decision: none; three one-line Rejected: lines in DEC-26 and one in DEC-27.
  • R5 pull request or issue references remaining: none.
  • R6 ruled open questions still listing options: none; the tap question is settled in FR45, not left open.

Scope

  • This PR contains specification records only, plus the generated motion role table's intro text.
  • It records observable behavior and preserves equivalent internal implementations.
  • Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract.
  • Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
  • It records one intentional durable decision, not a rescue for a separable tagalong.
  • The canonical knowledge owner, affected current claims, and overlapping open work are named; any ownership collision is resolved or called out for human decision.
  • Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
  • Public text and artifacts contain no internal Meta context.

Validation

  • pnpm install --frozen-lockfile: clean
  • pnpm check:knowledge: aligned
  • pnpm check:motion-roles: in sync (34 roles)
  • Motion role registry test: 13 passed
  • Prettier: clean

@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 4, 2026 7:58am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 3, 2026
@astracat-bot

astracat-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 4, 2026, 8:16 AM UTC

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval of this spec change waits on an engineering-owner decision: FR44 and DEC-26 in AST-094, and the matching changes to the ten component records, replace current requirements (FR1 and FR4's measured roles, and FR7's native exit, for motion no gesture drives), and no owner approval is recorded at this head yet. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

These need fixing whatever the decision:

  • FR7, AlertDialog FR6 and Lightbox FR16 now play a tap-closed touch exit on AST-090's exit tokens, but AST-090 FR14, its Non-goals and DEC-5 still say those tokens never change a touch exit, so please amend AST-090 here and make FR31's shrink conditional on a non-zero exit duration, since the default is 0 ms.
  • AST-094 FR1 still requires every duration a touch presentation uses to default to the measured native value, which contradicts FR44 for the grow and the lift, so please scope FR1 to motion a gesture drives, as FR4 now is.
  • FR44, FR4 and FR38 say the theme's character "normalizes" a gesture's spring and cite AST-108 FR3, which says character offsets each role's measured bounce by a fixed weight, so please use AST-108's wording or amend AST-108 FR3 in this pull request.
  • DateInput FR14 now requires the wheel to turn on the theme's move duration and easing, but the shipped wheel turns with the browser's smooth scroll (Wheel.tsx), so please record that implementation gap in FR14 and in AST-094's gap list.

Non-blocking: FR44's requirement has no stated exception for the animated-layout and gesture-hook springs the impact section keeps; stating it in FR44 itself would keep the requirement true.

[Automated review]

exit MUST keep the look of its UIKit counterpart's exit, on every touch
platform: a menu shrinks into its trigger, and an alert fades. An exit that
a tap, a key, an outside press or the program starts plays on the theme's
exit duration and easing tokens (`spec:AST-090`, FR44); an exit that a drag

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes a tap-closed touch exit play AST-090's exit tokens, but AST-090 FR14 ("The exit switches, --ease-exit, and --duration-exit do not change that exit"), its Non-goals and DEC-5 still say the opposite and are not amended in this pull request. Please amend AST-090 here; with its 0 ms default, FR31's shrink also needs to say it plays only when a theme sets an exit duration.

spring's parameters and its `tempo` scales the spring's duration
(`spec:AST-108` FR3, FR4). A part that follows a pointer follows it under
every pointer (FR43). Every other motion, which a tap, a click, a hold, a
key or the program starts, MUST play the theme's default motion, its

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FR1 (unchanged) still says every spring and duration a touch presentation uses MUST default to the measured native value, and FR13 now counts the grow and the lift as touch presentation. Scoping FR1 to motion a gesture drives, as FR4 now is, removes the contradiction.

starts at the gesture's velocity, under every pointer (FR43). A gesture is
a continuous pointer movement whose velocity carries into the motion: a
drag's release, a flick, a swipe, a pan's settle, a pinch, a rubber band's
return, and a follow and its settle. The theme's `character` normalizes the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AST-108 FR3 says character offsets each role's measured bounce by a fixed per-role weight and that a spring keeps its measured physics; "normalizes" reads as a different rule. Please use AST-108's wording here and in FR4 and FR38, or state the new rule and amend AST-108 FR3.

| FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it, on the theme's move duration and easing tokens (`spec:AST-094` FR44). | UIDatePicker on the iOS 26.2 simulator; current source |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shipped wheel turns onto an outside value with scroller.scrollTo({top, behavior: 'smooth'}) (packages/core/src/DateInput/Wheel.tsx), which the browser times, so this new timing is not met today. Please add an implementation gap here and in AST-094's gap sentence.

… are; Android claims rest on emulator evidence
@cixzhang cixzhang changed the title spec(AST-094): gesture-driven motion plays a spring on every pointer spec(AST-094): only a gesture's motion springs; other motion plays the theme's tokens Nov 3, 2026
@cixzhang
cixzhang marked this pull request as ready for review November 3, 2026 16:13
@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec b9decfd

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 7fc3273

Storybook · Sandbox · built from this PR's head

Package size: no published package changed; no new dependencies.

API changes: none in this pull request

Committed API report behind this build: cli 8 rows, theme-butter 1 row, theme-chocolate 1 row, theme-gothic 1 row, theme-matcha 1 row, theme-neutral 1 row, theme-stone 1 row, theme-y2k 1 row; run pnpm api:report.

Not run on this PR: add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.


Generated by PR Enrichment workflow | View full report

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 3, 2026
@cixzhang
cixzhang marked this pull request as draft November 3, 2026 16:17
@cixzhang
cixzhang marked this pull request as ready for review November 3, 2026 16:17
@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. An engineering owner approved the motion rule and the Android evidence decision at this head, so the direction is settled; five record fixes remain:

  • FR45 says every motion a tap, a key or the program starts MUST play the theme's tokens, but spec:AST-105 FR4's glide (which TreeList FR8 rides) and spec:AST-107's settleTo still play springs.settle, so write that exception into FR45, the FR4 paragraph and the role table intro, or amend those records here.
  • AST-094's Verification "all" row and AlertDialog's FR6/PR2 row still require real Android Chrome devices against DEC-27, and the FR34/FR35 row still fails a drill-in that does not play on its roles against FR34's new theme tokens.
  • DateInput FR14 now requires the theme's move tokens while the wheel turns with the browser's smooth scroll (packages/core/src/DateInput/Wheel.tsx), so add an implementation gap there and in AST-094's gap sentence.
  • FR45, FR4, FR38 and DEC-26 say character "normalizes" a spring and cite spec:AST-108 FR3, which says it offsets each role's measured bounce by a fixed weight, so use FR3's wording or amend FR3.
  • SegmentedControl FR10's Reduce Motion cross-fade now sits inside its implementation-gap sentence, so move the gap to the end of the cell.

Two smaller points: ProgressBar's new FR4 gap is not listed under Current-state impact, and AST-094's impact table omits SegmentedControl's mouse and pen release.

[Automated review]

its `tempo` scales the spring's duration (`spec:AST-108` FR3); the follow
and the rubber band themselves stay fixed (`spec:AST-108` FR4). A part that
follows a pointer follows it under every pointer (FR43). Every other
motion, which a tap, a click, a hold, a key or the program starts, MUST

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This MUST has no exception, but spec:AST-105 FR4 (unamended) still plays the glide on springs.settle, spec:AST-107's settleTo settles without a drag, and TreeList FR8 rides that glide for a tap's expand. The Current-state impact (lines 829-832) admits they keep their springs. Please write that exception into FR45, the FR4 paragraph (line 196) and the role table intro, or amend AST-105 FR4 and AST-107 FR2 here.

starts at the gesture's velocity, under every pointer (FR43). A gesture is
a continuous pointer movement whose velocity carries into the motion: a
drag, a flick, a swipe, a pan, a pinch, and the settle and rubber-band
return after any of them. The theme's `character` normalizes the spring and

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-108 FR3 says character offsets each role's measured bounce by a fixed per-role weight and that a spring keeps its measured physics; "normalizes" reads as a different rule. Please use FR3's wording here and at lines 198, 569 and 1226, or state the new rule and amend AST-108 FR3.

opened it (`component:DropdownMenu` FR11 and FR20). The change between
levels plays on the motion roles `springs.subMenuOpen` and
`springs.subMenuBack` (FR4); under reduced motion FR8 applies.
levels plays on the theme's move duration and easing tokens (FR45); under

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FR34/FR35 verification row (line 896) still fails "a change between levels that does not play on its roles", which a drill-in on the theme's move tokens now does. Please change it to "off the theme's move tokens".

(`spec:AST-013` FR10).
recordings for every claim about the reference values, real-device
evidence for every claim about iOS Safari behavior, and, for every claim
about Android Chrome behavior, Chrome with Gboard in the Android emulator

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Verification "all" row (line 903) still requires "real iOS Safari and Android Chrome devices", against this sentence and DEC-27. Please change its Android part to the emulator evidence DEC-27 names.

| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| FR1–FR5 | `AlertDialog.test.tsx` ("calls onOpenChange(false) on Escape", "does not call onOpenChange when action is clicked", "does not call onAction on Escape", "does not disable the cancel button while the action is loading", "exposes a named group instead, with no aria-modal") and `useImperativeAlertDialog.test.tsx` ("opens on show()", "closes on hide()") | open, loading, inline, imperative | Escape not cancelling, the action closing by itself, or the inline path claiming the role fails. | `audit:AlertDialog/behavior` |
| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a spring-curve oracle, a busy-main-thread case, and real-device clips on iOS Safari and Android Chrome | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a curve off its role, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` |
| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a theme-token curve oracle for the present and a spring-curve oracle for the dismissal, a busy-main-thread case, and real-device clips on iOS Safari and Android Chrome | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a curve off its role, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This row is rewritten here but still asks for "real-device clips on iOS Safari and Android Chrome"; spec:AST-094 DEC-27 says Android claims need no human device check. Please name the Android emulator evidence instead.

| FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it, on the theme's move duration and easing tokens (`spec:AST-094` FR45). | UIDatePicker on the iOS 26.2 simulator; current source |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shipped wheel turns onto an outside value with scrollToIndex(selectedIndex, 'smooth') (Wheel.tsx:482), which runs scroller.scrollTo({top, behavior}) (:379): the browser times it, so this new timing is not met today. Please add an implementation gap here and in AST-094's gap sentence (line 826).

| FR9 | Under a finger (`spec:AST-094` FR13), a change of selection slides the selected surface from the old segment to the new one as one surface, the radiogroup's `::before`, animating transform and opacity only; under a fine pointer it slides on the theme's duration and easing. | Owner direction; `spec:AST-094` FR13; current source and tests |
| FR10 | The slide plays on the internal `segmented.*` motion roles with UIKit's measured values (`spec:AST-094` FR4, DEC-11; thresholds internal under `spec:AST-002` FR3); under Reduce Motion the old surface fades out in place as the new one fades in (`design:reduced-motion` DR1–DR3). With a fine pointer under Reduce Motion, the theme's transition shows the change. | UIKit measurements (iOS 26.2); `design:reduced-motion` DR1–DR3; current tests |
| FR9 | Under every pointer, a change of selection slides the selected surface from the old segment to the new one as one surface, the radiogroup's `::before`, animating transform and opacity only. A change that a tap, a click, a key or the program starts slides on the theme's duration and easing tokens, a finger included (`spec:AST-094` FR45); a drag's release settles on a spring from the release velocity (FR10, FR13). | Owner direction; `spec:AST-094` FR13; current source and tests |
| FR10 | A drag's release settles on the internal `segmented.*` motion roles with UIKit's measured values, starting at the release velocity under every pointer, with the theme's `character` and `tempo` shaping the spring (`spec:AST-094` FR4, FR45, DEC-15; thresholds internal under `spec:AST-002` FR3). Implementation gap: under a finger a tap's, a click's or a key's slide still plays `segmented.selectionSpring`, and under a mouse or a pen a drag's release slides on the theme's duration; under Reduce Motion the old surface fades out in place as the new one fades in (`design:reduced-motion` DR1–DR3). With a fine pointer under Reduce Motion, the theme's transition shows the change. | UIKit measurements (iOS 26.2); `design:reduced-motion` DR1–DR3; current tests |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Reduce Motion cross-fade now follows "Implementation gap:" after a semicolon, so a required, shipped behavior reads as part of what is missing. Please end the requirement first and put the gap at the end of the cell.

@cixzhang
cixzhang marked this pull request as draft November 3, 2026 16:32
…e spec's character wording, and align evidence rows and gaps
@cixzhang
cixzhang marked this pull request as ready for review November 3, 2026 16:43
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@cixzhang
cixzhang marked this pull request as draft November 3, 2026 16:49

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. An engineering owner approved the motion rule, the touch-exit and swipe-row timing, and the Android evidence decision at this head, and the last round's blocking swipe-row finding is fixed: spec:AST-057 now carries FR45's split in its Non-goals, FR5 and FR7, with the shipped 200 ms ease-out settle recorded as an implementation gap. The spec:AST-090 Non-goals wording is fixed too.

No blocking findings remain. Two small follow-ups in spec:AST-057, fine to take here or later:

  • The Contract at a glance Behavior row and the reversible-toggle, soft-delete and chooser cases still say the row "springs back" after an action fires, while FR5 now plays a tap-fired return on --duration-exit and --ease-exit; "returns" matches FR5.
  • The FR7 verification row fails no close that plays off the exit tokens and no swipe close that starts from rest, unlike the other records this change moves.

A further push needs a fresh /approve-spec for its head.

[Automated review]

| FR2 | `Item` MUST render each side's panel from its entries into an out-of-flow position on its existing root and MUST add no element to the row for it. The gesture MUST drive the row's travel by writing a custom property on the root; the root's transform MUST read it and each panel's transform its negation. | DEC-5; `useToastGesture` | Proposed; real-Chromium evidence required |
| FR3 | `swipeBehavior` MUST be one closed union, `SwipeBehavior`, with the values `reveal` and `commit`, default `reveal`, applying to both sides. `commit` with more than one entry on a side MUST warn in development, because the other entries have no touch path. | DEC-2 | Proposed |
| FR4 | A sideways drag by a finger or a pen, or a horizontal wheel gesture, MUST move the row and uncover the side's panel, showing each entry's `label` and `icon` as a filled surface in its `variant`'s colour; while a full swipe is confirmed (FR5) the outermost entry alone MUST cover the uncovered space; the panel MUST NOT be visible at rest, by any pixel. | Real-browser finding: a zero-width panel still paints its padding | Proposed; real-Chromium evidence required |
| FR5 | Under `reveal`, a release goes by its speed first: at the release speed or faster it opens the row while the row is opening and closes it while it is closing; a slower one opens the row past half the panel's width and closes it short of that. Speed alone MUST NOT fire an entry. Dragged past the confirm point, the row MUST run ahead toward its far edge with the outermost entry covering its place while the finger stays down; a lift still past the confirm point MUST fire the **outermost** entry and no other, at any speed; a swipe that confirmed and came back short of the confirm point MUST close the row and fire nothing; a disabled outermost entry MUST NOT confirm. Under `commit`, only a lift past the confirm point MUST slide the row out and fire the outermost entry; every other release closes; nothing MUST rest. After any entry fires, under either model, the row MUST return unless the entry has `hasRemoval`, in which case it MUST hold out at the edge it travelled toward. When a swipe fired the entry, the return is gesture-driven and springs from the release velocity; when a tap on the entry fired it, the return is tap-started and plays the theme's exit duration and easing, `--duration-exit` and `--ease-exit` (`spec:AST-094` FR45). That bit is the only thing an entry declares about the row; the component MUST NOT wait on, observe, or act on what the caller does next, and MUST NOT warn if a held-out row is never removed. A release short of the threshold MUST spring back under both models. Under reduced motion the slide out, the spring back, and the hold-out follow `design:reduced-motion`: the row reaches its resting place with no travel or spring and cross-fades (DR2) when a finger started the swipe, or, when a pen or a trackpad did, cross-fades on a coarse primary pointer (DR2) and arrives at once otherwise (DR3). `onActivate` MUST fire once the row's content has left its box, and may return a Promise. | DEC-1, DEC-2; UIKit `performsFirstActionWithFullSwipe` | Proposed; real-device evidence required |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this now plays a tap-fired return on --duration-exit and --ease-exit, but the Contract at a glance Behavior row (line 57) and the reversible-toggle, soft-delete and chooser cases (lines 209, 211, 213) still say the row "springs back" after an action fires. Under reveal those actions usually fire by a tap on the resting panel. Suggest "returns" there, as here.

| FR4 | A sideways drag by a finger or a pen, or a horizontal wheel gesture, MUST move the row and uncover the side's panel, showing each entry's `label` and `icon` as a filled surface in its `variant`'s colour; while a full swipe is confirmed (FR5) the outermost entry alone MUST cover the uncovered space; the panel MUST NOT be visible at rest, by any pixel. | Real-browser finding: a zero-width panel still paints its padding | Proposed; real-Chromium evidence required |
| FR5 | Under `reveal`, a release goes by its speed first: at the release speed or faster it opens the row while the row is opening and closes it while it is closing; a slower one opens the row past half the panel's width and closes it short of that. Speed alone MUST NOT fire an entry. Dragged past the confirm point, the row MUST run ahead toward its far edge with the outermost entry covering its place while the finger stays down; a lift still past the confirm point MUST fire the **outermost** entry and no other, at any speed; a swipe that confirmed and came back short of the confirm point MUST close the row and fire nothing; a disabled outermost entry MUST NOT confirm. Under `commit`, only a lift past the confirm point MUST slide the row out and fire the outermost entry; every other release closes; nothing MUST rest. After any entry fires, under either model, the row MUST return unless the entry has `hasRemoval`, in which case it MUST hold out at the edge it travelled toward. When a swipe fired the entry, the return is gesture-driven and springs from the release velocity; when a tap on the entry fired it, the return is tap-started and plays the theme's exit duration and easing, `--duration-exit` and `--ease-exit` (`spec:AST-094` FR45). That bit is the only thing an entry declares about the row; the component MUST NOT wait on, observe, or act on what the caller does next, and MUST NOT warn if a held-out row is never removed. A release short of the threshold MUST spring back under both models. Under reduced motion the slide out, the spring back, and the hold-out follow `design:reduced-motion`: the row reaches its resting place with no travel or spring and cross-fades (DR2) when a finger started the swipe, or, when a pen or a trackpad did, cross-fades on a coarse primary pointer (DR2) and arrives at once otherwise (DR3). `onActivate` MUST fire once the row's content has left its box, and may return a Promise. | DEC-1, DEC-2; UIKit `performsFirstActionWithFullSwipe` | Proposed; real-device evidence required |
| FR6 | The drag MUST decide its axis once, early, and a mostly vertical drag MUST stay the scroller's. A mouse MUST NOT start the drag. The click the browser synthesizes after a drag MUST NOT fire the row's primary action. | Press-model ORD1 | Proposed; real-device evidence required |
| FR7 | An open row (resting open, or settling open from the release that opened it) MUST close on a pointer landing anywhere outside it, on a touch tap on its own content, on a scroll of anything that holds it while focus is outside its open panel, on Escape with focus inside it, on firing an entry, and on unmount. That tap MUST activate nothing and paint no press, an exception to the cell press profile of `spec:AST-094` FR11. Because a drag on a neighbour is a pointer outside it, at most one row is open at a time without any shared state. A close that a tap, a pointer outside, a scroll, Escape, a fired entry's tap or the program starts plays the theme's exit duration and easing, `--duration-exit` and `--ease-exit`; a close a swipe drives keeps its measured spring from the release velocity (`spec:AST-094` FR45). Implementation gap: the shipped row settles every close on one 200 ms ease-out today. | DEC-3; SwiftUI `swipeActionsContainer()` for the outcome | Proposed |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: the FR7 verification row (line 352) fails no close that plays off --duration-exit and --ease-exit, and no swipe close that starts from rest. The Switch, SegmentedControl, AlertDialog, ContextMenu and ProgressBar rows gained that failure signal in this change; adding both cases here keeps AST-057 in step.

@cixzhang
cixzhang marked this pull request as draft November 4, 2026 02:21
@cixzhang
cixzhang marked this pull request as ready for review November 4, 2026 02:21
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 4, 2026
…prings

# Conflicts:
#	docs/architecture/react-component-runtime.md
#	docs/specs/AST-099-mobile-keyboard/spec.md
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 6ea73c7

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: the heavy test, build and docsite-test checks are deferred on this head and run before merge; Lint and typecheck pass.

An engineering owner approved the spec change at this exact head, and the merge from main keeps the mobile keyboard spec's new FR9 rows and the runtime architecture's INV11 text intact beside your edits. Two optional follow-ups in spec:AST-057:

  • The Behavior row (line 57) and the reversible-toggle, soft-delete and chooser cases (lines 209, 211, 213) still say the row "springs back" after an action fires, while FR5 now plays a tap-fired return on --duration-exit and --ease-exit; "returns" would match FR5.
  • The FR7 verification row (line 352) could fail a tap or Escape close that plays off --duration-exit and --ease-exit, and a swipe close that starts from rest, as the other records this change moves now do.

[Automated review]

…prings

# Conflicts:
#	docs/specs/components/DateInput/DateInput.spec.md
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 3eb7fad

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: the heavy test, build and docsite-test checks are deferred on this head and run before merge, and Lint did not complete on this head.

An engineering owner approved the spec change at this exact head, and the merge from main keeps the DateInput FR14 rule that a tapped wheel row commits when its turn ends. Optional record follow-ups:

  • spec:AST-057: the Behavior row (line 57) and the reversible-toggle, soft-delete and chooser cases (lines 209, 211, 213) still say the row "springs back" after an action fires, while FR5 now plays a tap-fired return on --duration-exit and --ease-exit; say "returns".
  • spec:AST-057: the FR7 verification row (line 352) fails no close that plays off the exit tokens and no swipe close that starts from rest.
  • Miss: the DateInput FR14 verification row (line 341) fails no turn that plays a measured role instead of the theme's move duration and easing, which FR14 now requires.
  • Miss: Lightbox FR12 (line 147) times every return into the source on the exit tokens without saying whether a drag-down or pinch-in close returns into it; if one does, spec:AST-094 FR7 and FR45 give that return a spring from the release velocity.

[Automated review]

| FR9 | With `hasZoom`, a pinch in from the fitted size is a release decision: below the measured fraction of the fitted size the release closes the viewer; opening the fingers again by the measured amount before the lift is moving back and cancels. A pinch that starts zoomed in never closes the viewer. | `spec:AST-094` FR36, FR37 | settled; implementation gap |
| FR10 | With `hasZoom`, under a coarse pointer only, a tap on the photo fades the controls and caption and darkens the backdrop. While they are hidden, the next tap on the photo, a tap beside the photo, or a cancelled drag brings them back, and that tap beside the photo does not close the viewer (ORD3). AR5 governs keyboard and assistive-technology access while they are hidden. | `architecture:public-component-api` INV16 | settled; implementation gap |
| FR11 | A mouse, a pen, and a trackpad also use FR7's drag paging, FR8's drag to close, a ctrl-wheel or trackpad pinch over the photo for FR6's zoom and FR9's close, and a wheel to pan a zoomed photo; over the photo these replace the browser's page zoom and scroll. A double-click zooms 2× about the clicked point. A click on the photo keeps the controls, and the viewer opens and closes at once. | `spec:AST-094` FR43; `spec:AST-017` FR59 | settled; implementation gap |
| FR12 | Inside `experimental` only, under a coarse pointer, the photo grows out of its source when the viewer opens, on the theme's duration and easing tokens (`spec:AST-094` FR45), and returns into it when it closes, on the theme's exit duration and easing (`spec:AST-094` FR7): the element `experimental.sourceFor(index)` returns, else the opener when it shows a picture. A close reports `onOpenChange(false)` at the request; the viewer stays only until the photo lands. Pointer input during the entrance is held; keys and assistive technology stay live. Without `experimental`, opening is unchanged and closing plays FR16. | `spec:AST-017` FR21; `spec:AST-094` FR35 | settled; implementation gap |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Miss (non-blocking): this times every return into the source on the theme's exit duration and easing, but does not say whether a drag-down (FR8) or pinch-in (FR9) close returns into the source. If one does, that return is gesture-driven, and spec:AST-094 FR7 and FR45 give it a spring from the release velocity. Suggested wording: a return that a tap, a key or the program starts plays the exit tokens; one a drag or a pinch drives springs from the release.

vjeux added a commit to vjeux/astryx that referenced this pull request Nov 4, 2026
…prings

# Conflicts:
#	docs/specs/components/DateInput/DateInput.spec.md
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 7fc3273

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: the heavy build, test and docsite-test checks are deferred on this head and run before merge; lint and typecheck pass.

An engineering owner approved the spec change at this exact head, and the merge from main keeps the DateInput FR14 rule that a tapped wheel row commits when its turn ends. Optional record follow-ups:

  • Regression: spec:AST-094's DateInput row (line 892) and its gap list (lines 846-850) still give the wheel's turn today as the tap spin only, but since main scoped that to a finger, DateInput FR14 records that the mouse column turns by the browser's smooth scroll, so add "under a finger" and a mouse line.
  • Miss: DateInput's Compatibility/readiness row (line 64), Compatibility class (lines 79-81) and Current-state impact name neither FR14's theme-timed turn under every pointer nor FR15's trackpad wherever the sheet shows, as Switch, Slider and ProgressBar name theirs.
  • spec:AST-057: the Behavior row (line 57) and the reversible-toggle, soft-delete and chooser cases (lines 209, 211, 213) still say the row "springs back" after an action fires, while FR5 now plays a tap-fired return on --duration-exit and --ease-exit; say "returns".
  • spec:AST-057: the FR7 verification row (line 352) fails no close that plays off the exit tokens and no swipe close that starts from rest.
  • DateInput's FR14 verification row (line 343) fails no turn that plays a measured role or the browser's smooth scroll instead of the theme's move duration and easing, which FR14 now requires.
  • Lightbox FR12 (line 147) times every return into the source on the exit tokens without saying whether a drag-down or pinch-in close returns into it; if one does, spec:AST-094 FR7 and FR45 give that return a spring from the release velocity.

[Automated review]

| Lightbox | Controls fade on a tap, a double tap's zoom, the grow from its source | Named measured roles | Theme tokens |
| TreeList | Group slide under a coarse pointer | Measured motion | Theme tokens |
| ProgressBar | Determinate fill on a value change under a coarse pointer | Linear, `progress.fillTrackMs` times the share moved | `--duration-medium` and the theme's easing, every pointer |
| DateInput | Sheet wheel turning onto a clamped or a tapped row | `picker.tapSpin.durationMs` on `timing.curves.move` | Theme move tokens |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression (non-blocking): this row gives the turn today as picker.tapSpin.durationMs only, but since main scoped that to a finger, DateInput FR14 records that the mouse column turns onto a clamped value by the browser's smooth scroll, which FR45 also moves. Say "under a finger" here and add a mouse line ("Browser smooth scroll | Theme move tokens"); the gap list at lines 846-850 needs the same.

| FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. Under a finger, the turn plays the internal motion role `picker.tapSpin.durationMs`, UIKit's measured 300 ms, on `timing.curves.move`, as a tap on a row does. Under a finger, a tapped row commits its value when its turn ends; a keyboard or assistive-technology activation commits at once. | UIDatePicker on the iOS 26.2 simulator; current source |
| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). | `spec:AST-094` FR43; current source |
| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it, on the theme's move duration and easing tokens, as a tap on a row does (`spec:AST-094` FR45). The internal motion role `picker.tapSpin.durationMs`, UIKit's measured 300 ms, stays as the measured reference. Under a finger, a tapped row commits its value when its turn ends; a keyboard or assistive-technology activation commits at once. Implementation gap: under a finger, the turn and a tapped row's turn still play `picker.tapSpin.durationMs` on `timing.curves.move`; under a mouse, the turn plays the browser's smooth scroll. | UIDatePicker on the iOS 26.2 simulator; current source |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Miss (non-blocking): this row now turns the wheel on the theme's move tokens under every pointer, and FR15 below lets a trackpad drive it wherever the sheet shows, but the Compatibility/readiness row (line 64), the Compatibility class (lines 79-81) and Current-state impact name neither change; Switch, Slider and ProgressBar name theirs. The FR14 verification row (line 343) also still lists the tap-spin proof and fails no turn off the theme's move tokens.

This branch was successfully deployed

1 active deployment
Preview — 7fc3273d Deployed Nov 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant