Skip to content

spec(AST-094): touch presentations follow UIKit - #8052

Closed
vjeux wants to merge 7 commits into
meta:mainfrom
vjeux:wip/uikit-pr-spec
Closed

vjeux wants to merge 7 commits into
meta:mainfrom
vjeux:wip/uikit-pr-spec

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Exact decision needed

Whose physics, timings and gesture rules the phone presentations of core
components follow, and what proves it.

Existing authority searched

Records: architecture:interaction-modality, architecture:theme-tokens,
architecture:theme-authoring-contract, family:overlay-dismissal,
design:user-states, design:motion (draft), spec:AST-013, spec:AST-025 FR14, spec:AST-090 (draft, open; overlaps
FR7). AST-094 was the next free id.

Decision

spec:AST-094 (draft, owner vjeux): touch presentations follow UIKit on a
named reference release (today iOS 26.2). One sourced table of UIKit values
(FR1–FR2); UIKit's spring, decay, rubber-band and projection math (FR3);
touch motion configured through the theme, the table supplying every default,
an iOS adaptation condition (FR4); compositor-only, interruptible motion
(FR5–FR6);
UIKit's exits and Reduce Motion substitutions (FR7–FR8); recognizer tracking,
the second-finger rule, two press profiles, per-use long press, one
presentation resolver, nested scrolling decided at drag begin (FR9–FR14);
UIKit's arbitration, clock, readings and browser negotiation (FR15–FR22);
haptics within iOS Safari's limits (FR23).

Requirements changing another owner's record (FR4, FR7, FR8, FR11, FR13,
FR14) are "Proposed; pending owner review", naming record and owners. This
pull request decides none.

Rejected alternatives

  • Native scroll snapping for finger tracking (DEC-2): it ends on WebKit's
    physics after a blocked release.
  • Fixed touch physics (DEC-1): no product could retune a spring or give
    another platform its own profile without forking components.

Claim scope and non-goals

Pointer presentations, scroll physics and per-component adoption stay out.

Evidence and implementation relationship

UIKit read on the simulator, Apple documentation, WWDC sessions, WebKit's
source. Implementation stacks on this record.

Writing rubric results

  • R1 narration: none.
  • R2 research in the record: none.
  • R3 evidence naming a change: none.
  • R4 rejected alternatives over one line or outside a decision: none.
  • R5 pull request or issue references remaining: none.
  • R6 ruled open questions still listing options: none; OQ1–OQ2 are open.
  • FR8, FR19 and FR4/DEC-1 revisions (later commits): rubric re-run over each;
    no findings.
  • Native-by-default revision (iOS and Android, themeable motion, current-record
    rules), rubric re-run over the whole record:
    • R1 narration: "Proposed; pending owner review" markers inside FR4, FR7, FR8,
      FR11, FR13 and FR14 described the change, not the system. Rewritten as
      present-tense amendment statements; pending approval stays in the decision
      log and approved_by.
    • R2 research: none.
    • R3 evidence naming a change: none.
    • R4 rejected alternatives: FR8's "the alternative is UIKit's Reduce Motion
      alone" removed; FR8 states the rule only.
    • R5 change references: FR7 cited spec:AST-090, a record not in the
      repository. The citation is removed and FR7 states the rule itself.
    • R6 open questions: OQ1, OQ2 and OQ3 are unanswered. OQ4 is unanswered and
      keeps its two options. (OQ5, the compact size, is now answered by
      spec:AST-012 FR9 and removed.)
  • Reduce Motion, compact and review-findings revision: rubric re-run.
    • R1 narration: none.
    • R2 research: none.
    • R3 evidence naming a change: none.
    • R4 rejected alternatives: none.
    • R5 change references: none; FR8 cites design:reduced-motion and FR13 cites
      spec:AST-012 FR9 by record id.
    • R6 open questions: OQ5 answered and removed; OQ4 keeps its two options.
  • FR8 source and FR19 lift-listener revision: rubric re-run; no findings.

Scope

  • This PR contains specification records only.
  • 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 check:knowledge: aligned.
  • Prettier and git diff --check: clean.

Stack (merge in this order): #8052 spec → #8053 constants table → #8058 → #8059 → #8060 → #8061 gesture recognizers → #8054 math → #8257 theme motion roles → #8055 animator → #8056 animator curves → #8067 browser proofs → #8258 theme motion in the animator → #8071 → #8072 → #8073 → #8074 gesture system → #8076 experimental subpaths → #8266 start in frame (#8067 also follows the interaction harness, #8043 → #8044 → #8045; #8062 merges any time after #8052).

@vercel

vercel Bot commented Oct 24, 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 Oct 24, 2026 9:30pm UTC

Request Review

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

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 24, 2026, 9:58 PM UTC

@github-actions github-actions Bot added the spec-auto-merge Auto-merge was enabled by the spec owner gate label Oct 24, 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 draft waits on an engineering owner's decision, because DEC-1 and FR1–FR23 add requirements that no current spec states: an engineering owner records it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Whatever that decision, these points need fixing in the record before it can be approved:

  • FR10 contradicts the current useMenuPress FR4 (a second pointer ends a menu's finger slide) and FR13 contradicts the current AST-043 FR1 (date and time sheets on any coarse pointer, no width condition), and neither is named as a proposed amendment.
  • FR1, FR9 and FR13 prescribe internal mechanisms (one shared table, shared recognizers, one resolver), which knowledge-contracts INV18 keeps out of product requirements.
  • FR7 cites AST-090 FR1 and FR10, which are not in the repository yet, and the current-state impact names the overlay-dismissal family, which owns no animation.
  • FR11 defines no press profile for a control inside a scroll view and does not name user-states DR4 among the claims it changes.

[Automated review]


- **FR1 — One table of UIKit values.** Every spring, duration, deceleration
rate, rubber-band coefficient, gesture threshold and haptic moment a touch
presentation uses MUST come from one shared table of UIKit values. A

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

architecture:knowledge-contracts INV18 says a new requirement "MUST NOT prescribe internal modules, files, function names, algorithms, data structures ... unless that exact mechanism is itself an intentional public contract". "One shared table" and "A component MUST NOT carry a number of its own" (like FR9's shared recognizers and FR13's one resolver) are internal seams with no named dependent; the Non-goal at line 45 already sends them to an architecture record. Consider stating the observable outcome here instead.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

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-090 is not on main or at this head; it exists only in an open pull request, so these FR numbers can dangle if it renumbers or does not land.

scroll snapping only with simulator evidence that it tracks through a
main-thread block, ends on UIKit's release curve, ends correctly when the
finger lifts during a block, and lets a finger catch the content mid-flight.
- **FR10 — A second finger never breaks the first.** A second finger MUST NOT

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 contradicts the current module:DropdownMenu/useMenuPress FR4, which requires a menu's finger slide to "treat pointercancel and a second pointer as the end of the gesture with nothing acting". Name it as a proposed amendment pending its owners, or exempt press-tracking menus from FR10.

the browser's whenever no pinch recognizer claims the fingers and no drag is
claimed (WCAG 1.4.4).
- **FR11 — Two press profiles.** Press feedback MUST follow one of two UIKit
profiles. A control (UIControl) highlights on touch-down outside scroll

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No profile is given for a control inside a scroll view, although line 41 treats the page itself as a scroll view on iOS. The drag-out rule also contradicts design:user-states DR4 (Slider keeps the overlay on the thumb through its drag), which the note at line 137 does not name beside DR1 and DR3.

- **FR13 — One touch-presentation resolver.** Whether a component shows its
touch presentation MUST come from one shared resolver: touch behaviors key
on a coarse pointer, layout on the available size, and a presentation that
depends on both (a sheet instead of a popover) asks for both. A component

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 current spec:AST-043 FR1 decides the date and time inputs' sheet on the pointer alone: "Coarse uses the inputs' existing pointer test, no width condition." A sheet-instead-of-popover that asks for both signals changes that on wide touch screens; name AST-043 FR1 as a proposed amendment pending its owner, or scope FR13.

(FR13; pending owner review).
- `family:buttons`: the control and cell press profiles (FR11; pending owner
review).
- `family:overlay-dismissal`: exits and their Reduce Motion substitutions in

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

family:overlay-dismissal does not own exit motion: its AV6 says "Members own animation, visual treatment, and theming anatomy", and its content boundary excludes animation. FR7 and FR8 change the members' own records instead, so this row (and affects_families at line 15) points the ruling at the wrong owners.

…ame the records touch rules amend

- FR8: under reduced motion a touch presentation cross-fades (opacity only,
  at most 200 ms, no travel, scale or spring) and a fine-pointer
  presentation changes at once; no reducedMotion keep in core; the rule is
  design:reduced-motion's.
- FR13: size keys on the one compact definition the theme exports
  (spec:AST-012 FR9), the same on every platform; a presentation asks only
  for the conditions its record names, so the date and time sheet keeps
  AST-043 FR1's pointer-only condition. OQ5 is resolved and removed.
- FR1 gives the table the defaults a theme may override (FR4); FR4 names its
  AST-057 amendment for Item's swipe.
- FR10 keeps useMenuPress FR4 for a menu's press-tracking slide; FR11 gives
  a control inside a scroll view the 150 ms delay, says when a cell
  activates, and keeps user-states DR4 for Slider's thumb.
- FR24 names the shared touch-listener singleton's lifetime under INV5.
- Exits and their cross-fades belong to each overlay member's record, not
  family:overlay-dismissal (its AV6). The Intent states the delivered outcome.

@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 draft waits on an engineering owner's decision, because DEC-1, DEC-2 and FR1–FR25 add requirements that no current spec states: an engineering owner records it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Whatever that decision, these points need fixing in the record before it can be approved:

  • FR13 and FR25 take the compact size from an AST-012 FR9, and FR8 names a design:reduced-motion owner, but both exist only in open pull requests (#8274, #8272), so land those first and name them in the merge order, or state the rules here.
  • FR8 also sets reduced-motion rules for fine-pointer presentations and every core component, which the record's Non-goals put out of scope, and caps touch cross-fades at a 200 ms that names no source although FR1–FR2 require one.
  • FR19 keeps lift-deciding listeners passive although FR21 needs them to cancel touchend, and FR1, FR9, FR13 and FR24 still prescribe internal mechanisms that knowledge-contracts INV18 keeps out of product requirements.

[Automated review]

Comment on lines +181 to +182
there; layout keys on the available size through the one compact definition the
theme exports, the same on every platform (`spec:AST-012` FR9,

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-012 ends at FR8 on main and never defines compact; the definition exists only in open #8274, and knowledge-contracts INV13 keeps open pull requests non-authoritative. Until it lands, which screens count as compact here, and so the Selector default FR25 moves, is not stated. Land #8274 first and name it in the merge order, or state the rule here (or keep OQ5) until it is current.

Comment on lines +136 to +137
it. This is iOS's Reduce Motion with Prefer Cross-Fade Transitions on, and
`design:reduced-motion` owns the rule for every presentation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

design:reduced-motion is not on main; it exists only in open #8272, so this sentence names an owner that does not exist yet. Cite it once it is current, or drop the ownership claim.

Comment on lines +130 to +134
fades out in place, and any other move fades through. A fine-pointer
presentation MUST change state at once. A drag still follows the finger
while it is down; after the lift the presentation reaches its committed
state by the cross-fade. A core component MUST NOT keep its motion under
reduced motion (no `reducedMotion: 'keep'`). The cross-fade's duration is a

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These sentences rule on fine-pointer presentations and on every core component, but the Non-goals (lines 45-46) leave pointer presentations' timing and easing with the theme's tokens. They belong to the record that owns reduced motion, and reducedMotion: 'keep' names an option no current API has.

exit, since members own their animation (`family:overlay-dismissal` AV6).
- **FR8 — Reduce Motion cross-fades.** Under `prefers-reduced-motion:
reduce`, a touch presentation MUST reach each change of state by a
cross-fade: it animates `opacity` only, over at most 200 ms, with no travel,

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 200 ms ceiling names no source, although FR1–FR2 require every duration to take its default from the sourced table, and line 136 presents it as iOS's own behavior (the previous revision stated UIKit's push and pop cross-fade at 0.5 s). Put the value in the table with its source, or state the ceiling as an Astryx accessibility choice.

Comment on lines +229 to +230
element whose recognizers decide only at the lift keeps its listeners
passive. Known limit: a native scroll that starts on an element with a

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A passive listener cannot cancel the touchend that FR21 (line 240) requires when a gesture takes the touch at the lift. Saying "keeps its touchmove listener passive" keeps both rules.

cixzhang added a commit that referenced this pull request Oct 24, 2026
… have a budget (#8271)

## Exact decision needed

How much a component change may add to what every product downloads, and how a pull request shows that cost.

## Existing authority searched

- `spec:AST-080` FR1, FR2, DEC-3: each package ships one whole stylesheet, so CSS any component adds reaches every product.
- `architecture:component-style-authoring` INV1–INV5: no size outcome.
- `architecture:react-component-runtime` INV5 (acquire optional resources only while enabled) and INV8 (reuse shared owners): no byte budget.
- `spec:AST-036` FR22–FR23: the only numeric budgets, for the Markdown parser only.
- No current record requires a size report or sets a per-component JavaScript or CSS budget.
- Search terms: size, bundle, budget, gzip, stylesheet, CSS delta, lazy, value table, constants table, touch-only. Overlapping open pull requests: #8277 (the size-report fix this record governs), #8052 (touch presentation spec citing a shared value table), #8053 (the touch constants table, which the value-table rule covers), #7358 (per-component English catalog slices; catalogs stay with `architecture:internationalization` INV15), #8286 (a component-local tokenizer byte budget, unaffected).

## Decision

- `spec:AST-080` FR8 / DEC-4: every pull request that changes styled modules reports, base against head, each changed package stylesheet's gzip size and each changed component's gzip CSS delta. A feature that runs only on a coarse pointer and adds more than 1 KB gzip of CSS to one component is flagged and justified; the CSS stays in the package stylesheet.
- `architecture:component-style-authoring` INV6: every pull request that changes a component reports the gzip JavaScript its import pulls in, transitive modules included, and its CSS delta, base against head. Touch-only work over 2 KB gzip JavaScript or 1 KB gzip CSS per component is flagged; the JavaScript is loaded lazily, only when a coarse pointer needs it, or the cost is justified. A shared JavaScript tuning table ships to a component only the rows that component uses; StyleX variable groups and message catalogs keep their own owners.

## Rejected alternatives

- A separate stylesheet for touch-only CSS: breaks the one-import-per-package setup of `spec:AST-080` DEC-3.

## Claim scope and non-goals

Size reporting and the touch-only budget only. Budgets for other kinds of work, render or listener counts, and the report's CI implementation are outside this PR.

## Evidence and implementation relationship

The pull-request size report measures no stylesheet, reports a component's re-export file instead of what its import pulls in, and never fills its before and delta columns. A report fix and the budget flags follow as separate implementation PRs. No implementation is included.

## Writing rubric results

- R1 narration: none.
- R2 research in the record: none.
- R3 evidence naming a change: none; verification names report states.
- R4 rejected alternatives over one line or outside a decision: one, inside DEC-4, one line.
- R5 pull request or issue references remaining: none.
- R6 ruled open questions still listing options: none.

## Scope

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

## Validation

- `pnpm check:knowledge` (CI lint)
- Prettier
- `git diff --check`
cixzhang added a commit that referenced this pull request Oct 24, 2026
…presentation (#8273)

## Exact decision needed

Whether callers can keep a stable component's existing surface when the component gains touch behavior, and through what API.

## Existing authority searched

- `architecture:public-component-api` INV4 (keep derivable decisions internal), INV9 (released behavior changes under `spec:AST-017`), INV3 (one prop, one concept).
- `spec:AST-002` FR2–FR3: derivable differences, thresholds, and tuning stay internal.
- `spec:AST-017` FR3 and DEC-17: a functional default change names the prop that restores the old default; FR21 and FR27: an omitted experimental opt-in leaves stable behavior unchanged, which covers experimental surfaces only.
- No current record requires a caller opt-out for touch behavior on a stable component.
- Search terms: presentation, opt-out, touch default, restoring prop, adaptive, touch mode. Overlapping open pull requests: #8229 (Popover `presentation`), #8195 (DropdownMenu touch tracking), #8052 (AST-094 resolver), #8062 (shared touch resolver), #7977 and #8001 (touch sheets behind `experimental.presentation`).

## Decision

- `architecture:public-component-api` INV16: a stable component that, under a coarse pointer, presents a different surface or switches how an open surface follows input takes one `presentation` prop, and a value such as `presentation="popover"` keeps its fine-pointer surface on every device. Touch behavior is the default where it serves touch users; the caller's explicit choice always wins. Every such touch mode is reachable by callers; thresholds and tuning stay internal. Touch styling and touch-only behavior other records define (press paint, the iOS text-size floor, a row swipe, a tooltip's touch trigger) stay with those records.
- `spec:AST-017` FR3 and DEC-23: for a touch default, that `presentation` value is the restoring input the Changeset names.

## Rejected alternatives

- Touch behavior that callers cannot turn off or pin.

## Claim scope and non-goals

Caller control over touch presentations and modes on stable components. Which components adopt touch defaults, the touch behavior itself, and whether a given touch default is breaking stay with their owners.

## Evidence and implementation relationship

Selector, MultiSelector, DropdownMenu, MoreMenu, ContextMenu, and the date and time inputs already take `presentation`. Related open work, not edited here: #8229 adds `presentation` to Popover (its `'popover'` default conforms); #8195 adds a touch-tracking mode to DropdownMenu, which INV16 requires callers to be able to reach; the `spec:AST-094` revision in #8052 carries the touch presentation resolver. No implementation is included.

## Writing rubric results

- R1 narration: none.
- R2 research in the record: none.
- R3 evidence naming a change: none; verification names test states.
- R4 rejected alternatives over one line or outside a decision: one, inside DEC-23, one line.
- R5 pull request or issue references remaining: none.
- R6 ruled open questions still listing options: none.

## Scope

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

## Validation

- `pnpm check:knowledge` (CI lint)
- Prettier
- `git diff --check`
cixzhang added a commit that referenced this pull request Oct 24, 2026
… platform (#8274)

## Exact decision needed

What "compact" means when a component picks a presentation by screen size, and whether it differs by platform.

## Existing authority searched

- `spec:AST-012` FR1–FR2: width points (`md` is 768) and a closed `when` vocabulary with no height field.
- `architecture:public-component-api` INV14 and change coupling: components keep no parallel hardcoded breakpoint table.
- `component:Selector` FR4: `adaptive` uses the sheet on "compact coarse-pointer screens", with no number.
- `design:mobile-typography` DR3 uses `(width < 1024px) and (pointer: coarse)` for text sizing, a separate concern this PR leaves alone.
- No current record defines compact. Code uses `(max-width: 768px) and (pointer: coarse)`.
- Search terms: compact, 744, 768, height 500, size class, adaptive, coarse pointer, breakpoint. Overlapping open pull requests: #8062 (shared touch resolver and its compact query), #8052 (AST-094 resolver), #8015 (NumberInput steppers on coarse pointers; not a size test), #6817 (compact density, a different meaning), #8089, #8104, #8095 (phone presentations that consume the resolver).

## Decision

- `spec:AST-012` FR9 / DEC-6: a screen is compact while its width is below the effective `md` point or its height is below 500, with the same numbers on every platform (CSS px on the web, pt on iOS, dp on Android); native does not map compact to platform size classes. The theme exports this one definition; the web touch presentation resolver, Selector's `adaptive` presentation, and `@astryxdesign/native` all use it.
- `spec:AST-012` FR2: `when` gains `height`, accepting only `{below: 500}`.
- `architecture:public-component-api` INV17: a component that chooses a presentation by screen size uses that definition and keeps no numbers of its own.
- `spec:AST-082` FR18: `PortableTheme` carries the compact definition, so native reads the same numbers; FR11 and `spec:AST-093` FR3: a native choice between presentations by screen size uses it, not size classes, while the platform's own control still presents the surface.
- Compact governs presentation choice only; a layout width a caller picks, such as AppShell's `mobileNav.breakpoint`, is not compact.
- `component:Selector` records the `adaptive` edge change as pending and compatible under `spec:AST-017` FR1.

## Rejected alternatives

- Mapping compact to each platform's size classes: the same screen would be compact on one platform and not another.

## Claim scope and non-goals

The compact definition and the height condition only. Text sizing under `design:mobile-typography`, which surfaces adopt a touch presentation, and resolver code are outside this PR.

## Evidence and implementation relationship

The touch presentation resolver contract lives only in the open `spec:AST-094` revision (#8052), which this PR does not edit; it must use FR9. Native adopts FR9 when it gains a compact resolver. Behavior delta when the code conforms: a coarse-pointer screen exactly 768 wide and at least 500 tall opens the popover, and a landscape phone wider than 768 but under 500 tall opens the bottom sheet. Code follow-up belongs to separate implementation PRs. INV16 is the caller-control invariant from #8273 (merged); this PR adds INV17. No implementation is included.

## Writing rubric results

- R1 narration: none.
- R2 research in the record: none.
- R3 evidence naming a change: none; verification names edge states.
- R4 rejected alternatives over one line or outside a decision: one, inside DEC-6, one line.
- R5 pull request or issue references remaining: none added.
- R6 ruled open questions still listing options: none.

## Scope

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

## Validation

- `pnpm check:knowledge` (CI lint)
- Prettier
- `git diff --check`
cixzhang added a commit that referenced this pull request Oct 24, 2026
…ers change at once (#8272)

## Exact decision needed

What a person who prefers reduced motion sees when a core component animates a change, on touch and on other pointers, and whether a component may keep its motion.

## Existing authority searched

- `design:motion` DR5 (draft): every animated transition has an immediate or minimally moving form. Draft, so not authority.
- `spec:AST-013` FR13: reduced-motion behavior is unchanged by server-rendered entrances; it defines none.
- `spec:AST-012` FR2: `motion: reduce` and `pointer: coarse/fine` are the adaptation conditions.
- No current web record defines component behavior under reduced motion.
- Search terms: reduced motion, prefers-reduced-motion, cross-fade, Reduce Motion, motion DR5, spring, travel. Overlapping open pull requests: #7367 (draft `design:motion` DEC-2, hover entrances; no reduced-motion rule), #8214 (layer fade under a finger with Reduce Motion; conforms once its fade is 200 ms or less), #8052 (AST-094 FR8 carries the matching touch rule), #8211, #8239, #8253 (touch indicator and placeholder work; continuous indicators are outside this record).

## Decision

New current record `design:reduced-motion`, matching iOS Reduce Motion with Prefer Cross-Fade:

- DR1: under `prefers-reduced-motion: reduce`, a core component's entrances, exits, presentation switches, and state changes never move, scale, resize, or spring.
- DR2: on a coarse pointer, they cross-fade: opacity only, 200 ms or less.
- DR3: on any other pointer, they complete at once.
- DR4: no core component, prop, or theme value keeps motion under reduced motion.

`spec:AST-057` FR5 names that a swiped row's slide out, spring back, and hold-out follow DR2 under reduced motion. The draft `design:motion` keeps its other rules unapproved; its DR5 points to the new record. A separate record keeps the promotion to this one rule, as `docs/design/README.md` advises for a different approval lifecycle.

## Rejected alternatives

- A per-component setting that keeps full motion under reduced motion.

## Claim scope and non-goals

Only transitions under reduced motion. Motion without the preference, durations and easing, tokens, continuous indicators such as a spinner, and content the person drags with their own finger are outside this record.

## Evidence and implementation relationship

Layers that open under a finger with Reduce Motion appear in one frame today, where iOS cross-fades. The open fix #8214 makes them fade under a coarse pointer and stay immediate under a mouse, which conforms to DR2 and DR3 once its fade is 200 ms or less. The open draft #7367 (`design:motion` DEC-2, hover-summoned layers) does not touch reduced motion. The AST-094 revision in #8052 carries the matching touch rule. No implementation is included.

## Writing rubric results

- R1 narration: none.
- R2 research in the record: none.
- R3 evidence naming a change: none; verification names browser states.
- R4 rejected alternatives over one line or outside a decision: one, inside DEC-1, one line.
- R5 pull request or issue references remaining: none.
- R6 ruled open questions still listing options: none.

## Scope

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

## Validation

- `pnpm check:knowledge` (CI lint)
- Prettier
- `git diff --check`
…lable touchend

- FR8 sources its 200 ms cap to design:reduced-motion and leaves the
  pointer presentations' instant change to that record, which owns the rule
  for every presentation; this record rules only on touch presentations.
- FR19: an element whose recognizers decide at the lift keeps its touchmove
  listener passive and its touchend listener cancelable, so a gesture can
  cancel the click FR21 requires.

@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 draft waits on an engineering owner's decision, because DEC-1, DEC-2 and FR1–FR25 add requirements that no current spec states: an engineering owner records it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Whatever that decision, FR25 needs to follow the current way back for touch defaults, the component's presentation value (architecture:public-component-api INV16, spec:AST-017 FR3 and DEC-23), instead of a separate restoring prop, and should drop the long-press example, which FR4 keeps internal. FR1, FR9, FR13 and FR24 also name internal modules that architecture:knowledge-contracts INV18 keeps in architecture records, and the review notes a few smaller wording fixes.

[Automated review]

Comment on lines +273 to +281
- **FR25 — A moved default keeps a way back.** Turning a touch presentation on
by default MUST NOT silently change a released default. Where a released
default moves, such as Selector's `adaptive` presentation policy or a menu's
long-press delay, the component MUST offer a prop that restores the released
behavior and MUST document the change as a behavior change, not as a
visual-only change (`spec:AST-017` FR3, FR25 and DEC-17). This replaces
`component:Selector` DEC-1's width-and-pointer query with the FR13 touch rule
for `adaptive`, shipped as a documented behavior change with a restoring
option.

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 current architecture:public-component-api INV16 and spec:AST-017 FR3/DEC-23 already make the component's presentation value the way back for a touch default, and Selector's own record already moves adaptive to the spec:AST-012 FR9 compact test with popover/bottom-sheet as pins. A prop that "restores the released behavior" (and the Verification row that fails any prop differing from it) would need Selector to reproduce the released (max-width: 768px) query, which AST-012 FR9 forbids, and the long-press example asks for a public threshold that FR4 keeps internal. Citing INV16 and DEC-23 and dropping the long-press and Selector sentences would align it.

press, the measured values for context menus and menus. A rubber band MUST
use the coefficient its use measures.
- **FR13 — One touch-presentation resolver.** Whether a component shows its
touch presentation MUST come from one shared resolver, beside the shared

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With the current architecture:public-component-api INV16 ("a caller's explicit choice always wins over that default"), this reads as letting the resolver override a caller's presentation. Saying the resolver decides the default and a caller's presentation value wins would keep both.

extends. Touch behaviors key on a coarse pointer and are on by default
there; layout keys on the available size through the one compact definition the
theme exports, the same on every platform (`spec:AST-012` FR9,
`architecture:public-component-api` INV14), and a component never restates

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

INV14 is the breakpoint-name rule; on main the compact definition is architecture:public-component-api INV17.

follows.
- **FR7 — Exits animate as the native counterpart's do.** A touch
presentation MUST play the exit its UIKit counterpart plays, on every touch
platform. A rule that turns exits off by default does not apply to touch presentations;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No current record states a rule that turns exits off by default, so this sets aside an unnamed target. Naming the record, or deleting the sentence, would make FR7 self-contained.

the finger while it is down; after the lift the presentation reaches its
committed state by the cross-fade. A core component MUST NOT keep its motion
under reduced motion (no `reducedMotion: 'keep'`). The cross-fade's duration
is a theme motion role whose default is at most 200 ms (FR4); a theme MAY

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR4 makes this role's default its table value, and FR2 admits only Apple documentation, WWDC, WebKit source or a simulator measurement as a table source, so a 200 ms ceiling from the reduced-motion design record fails FR2's own table check. FR2 could admit a current Astryx record as the source of a ceiling.

with a deceleration rate, or with a measured projection time.
- **FR4 — Touch motion is themeable.** Every spring, duration and curve a
touch presentation plays MUST be a theme motion role, named by its table path
(`sheet.present`, `timing.curves.easeInOut`). The role's default value is

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

timing.curves.easeInOut names a curve, while architecture:theme-tokens INV3 asks for purpose-led motion role names (and line 305 says INV3 is unchanged). FR4 also leaves deceleration rates and rubber-band coefficients, which FR1 lists, in neither the role list nor the constant list.


- **FR1 — One table of measured values.** Every spring, duration,
deceleration rate, rubber-band coefficient, gesture threshold and haptic
moment a touch presentation uses MUST take its default from one shared table

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"One shared table" here, "the shared gesture recognizers" (FR9), "one shared resolver" (FR13) and "one shared document-owned owner" (FR24) prescribe internal modules, which architecture:knowledge-contracts INV18 and this record's own Non-goal at lines 55-57 leave to architecture records. Stating the observable outcomes would keep equivalent implementations valid.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #8316, which carries this record and the owner rulings since.

@vjeux vjeux closed this Oct 24, 2026
@vjeux
vjeux deleted the wip/uikit-pr-spec branch October 24, 2026 23:43
cixzhang pushed a commit that referenced this pull request Oct 26, 2026
…rena (#8058)

Depends on #8053 (and its predecessors); review the last commit.

<!-- Template: other.md (internal infrastructure: nothing is exported, so new-feature.md does not fit). -->

## Primary intent

Add the core of an internal gesture-recognizer system for touch presentations: UIKit's recognizer state machine and the arena that settles which recognizer owns a touch sequence. Recognizers, the DOM host and component migrations follow in their own pull requests; nothing is exported from the package.

## User or maintainer impact

Touch components each carry their own pointer handling (sheet drags, swipe actions, toast dismissal, long press, press feedback), with their own answer to "who owns this touch". Two can act on one touch, and a second finger can break a drag. With one arena every component gets UIKit's answers: exclusive recognition by default, `requireToFail`, simultaneous recognition only on request, a deterministic resolution order, and the view's touches cancelled (no click, no pressed state) once a gesture takes them.

## Problem and solution fit

- Why does it happen? Ownership of a touch is decided separately in each component.
- Why does that hurt? Ghost clicks, double actions, and rules that differ from component to component.
- Why does it matter? Touch presentations aim at UIKit parity, and UIKit settles this in one place.

Deltas:

- `GestureRecognizer`:
  - UIKit's states (possible, began, changed, ended, cancelled, failed) and allowed transitions;
  - discrete and continuous recognizers;
  - `requireToFail`, `recognizeSimultaneouslyWith`, `shouldBegin`, `shouldReceiveTouch`;
  - `cancelsTouchesInView`, `delaysTouchesBegan` and `delaysTouchesEnded`, `allowedPointerTypes`.
- `GestureArena`, with no DOM:
  - participants per touch sequence, resolution and claims;
  - delivery to the view;
  - click dispositions: allow, suppress, or defer and replay if the recognizer fails;
  - timers as deadlines on the event clock.

## Authority

`spec:AST-094` FR15 (arbitration) and FR16 (the event clock), proposed; pending owner review.

## Change and evidence

The arena's rules are tested with scripted recognizers, test doubles that begin, fail or wait on command. The rules hold independently of any real recognizer:

- exclusivity and simultaneity;
- require-to-fail: waiting, failing, delegate requirements, idle recognizers;
- delegate hooks and innermost resolution;
- independent touches;
- delivery to the view, deferred and replayed clicks, the browser taking a touch;
- cancellation;
- a deadline that runs at its own time before a late input.

Size: 2,198 lines, about 400 of them tests. This is one indivisible intent: the recognizer base and the arena call each other, so neither builds or tests alone, and the doubles-only tests are the smallest proof of the rules.

## Scope

- [x] This PR has one primary intent.
- [x] Independently removable deltas were removed or split.
- [x] Public text and artifacts contain no internal Meta context.

## Testing

- `pnpm exec vitest run --project ui packages/core/src/gestures packages/core/src/motion`: 159 tests passed.
- `tsc --project packages/core/tsconfig.json --noEmit`: clean.
- `eslint packages/core/src/gestures`: clean.
- `pnpm check:changesets`, `node scripts/release/changeset-coverage.mjs pr --base origin/main`: clean.
- Size: 6 files, 2,205 lines added.


Stack (merge in this order): #8052 spec → #8053 constants table → #8058 gesture core → #8059 tap and long press → #8060 pan → #8061 other recognizers → #8054 math → #8055 animator → #8056 animator curves; the gesture DOM host, press tracking and browser proof follow the animator.

This branch was successfully deployed

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

Labels

CLA Signed This label is managed by the Meta Open Source bot. spec-auto-merge Auto-merge was enabled by the spec owner gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants