Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about Lab PageControl, and four fixes are needed either way.
Owner decision needed. No current record governs (a) a Lab PageControl beside Pagination's variant="dots", with its one-page tap rule and slider semantics; (b) its held look, which design:user-states DR1 does not list; (c) its fades, slide delay and window spring played as table values instead of spec:AST-094 FR4 motion roles; or (d) Lab scrubbing with Core's internal PanGestureRecognizer where spec:AST-107 FR7 points to usePanGesture and architecture:react-component-runtime INV13 lists no pan gap. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request or by naming the record change that settles it; the author may do so as an engineering owner.
Blocking findings:
- The diff against
mainis 10,793 authored lines, over the 8,000-line limit for an owner-directed batch, because it carries the stacked SearchField from #9399; land #9399 and #9669 first and rebase so this pull request holds PageControl alone. SearchFieldTouch.tsswallows a claimed tap's latemousedown,mouseupandclickwith its own document listeners beside Core's click guard (armClickGuard); extend the guard and arm it from the claimed tap instead.SearchFieldTouch.tsbuilds its ownResizeObserverwhere Core'sobserveResizealready owns resize observation; use the shared observer, extending it per window if a same-origin iframe must work.PageControl.tsxclearsrequestRefin a layout effect that synchronizes no external system and relies on the touch layer's effect running first; clear it in that fade effect right after it readskind.
A non-blocking note on the component's JSDoc placement is inline.
[Automated review]
| // lets go (its click, the next press, or its window), the late events are | ||
| // over, and this swallow lets go too. | ||
| const disarmRef = useRef<(() => void) | null>(null); | ||
| const armSwallow = useCallback((doc: Document) => { |
There was a problem hiding this comment.
This swallow installs its own document-capture mousedown/mouseup/click listeners beside Core's click guard (packages/core/src/utils/clickGuard.ts, armClickGuard), which owns the browser's late click after a gesture that already acted. Extending the guard to also swallow the compatibility mousedown/mouseup, and arming it here through internal/touch, would leave one path to fix.
| if (field == null || Observer == null || placement === 'tab') { | ||
| return; | ||
| } | ||
| const observer = new Observer(() => { |
There was a problem hiding this comment.
observeResize from @astryxdesign/core/utils (sharedResizeObserver.ts) already owns resize observation without per-instance observers. If this per-window constructor is for same-origin iframes, extending the shared module to keep one observer per window would serve both.
|
|
||
| // A shown page consumes its request (the touch layer read its kind first). | ||
| const consumedPageRef = useRef(current); | ||
| useLayoutEffect(() => { |
There was a problem hiding this comment.
This layout effect synchronizes no external system and relies on the touch layer's fade effect (PageControlTouch.ts:537-545) running first. Clearing requestRef in that effect, right after it reads kind, removes the cross-file ordering dependency (architecture:react-component-runtime INV3).
There was a problem hiding this comment.
Done in 3cd5389: the touch layer's fade effect clears the request right after it reads its kind, and the second layout effect is gone.
| * <PageControl count={5} value={page} onChange={setPage} /> | ||
| * ``` | ||
| */ | ||
| /** The page count: 0 for a count that is not a finite number. */ |
There was a problem hiding this comment.
Non-blocking: this helper's JSDoc sits between the component's JSDoc (lines 173-199) and PageControl, so the component block documents nothing. Moving pageCountOf above that block restores the hover docs.
There was a problem hiding this comment.
Done in 3cd5389: pageCountOf now sits above the component's JSDoc.
1cbb09e to
3cd5389
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about Lab PageControl, and three fixes are needed either way.
Owner decision needed. No current record governs (a) a Lab PageControl beside Pagination's variant="dots", with its one-page tap rule and slider semantics; (b) its held look, which design:user-states DR1 does not list; (c) its fades, slide delay and window spring played as table values instead of spec:AST-094 FR4 motion roles; or (d) scrubbing with Core's internal PanGestureRecognizer, where spec:AST-107 FR7 points to a public pan hook that Core does not ship yet and architecture:react-component-runtime INV13 lists no pan gap. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request or by naming the record change that settles it; the author may do so as an engineering owner.
Blocking findings:
- The diff against
mainis 10,786 authored lines, over the 8,000-line limit for an owner-directed batch, because it carries the stacked SearchField from #9399; land #9399 and #9669 first and rebase so this pull request holds PageControl alone. SearchFieldTouch.tsswallows a claimed tap's latemousedown,mouseupandclickwith its own document listeners beside Core's click guard (armClickGuard); extend the guard and arm it from the claimed tap instead.SearchFieldTouch.tsbuilds its ownResizeObserverwhere Core'sobserveResizealready owns resize observation; use the shared observer, extending it per window if a same-origin iframe must work.
The request-consumer effect and the JSDoc placement from the last review are fixed.
[Automated review]
3cd5389 to
fde400d
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about Lab PageControl, and one fix is needed either way.
Owner decision needed. No current record governs (a) a Lab PageControl beside Pagination's variant="dots", with its one-page tap rule and single slider semantics; (b) its held look, which design:user-states DR1 does not list; (c) its fades, slide delay and window spring played as table values instead of spec:AST-094 FR4 motion roles; or (d) scrubbing with Core's internal PanGestureRecognizer, where spec:AST-107 FR7 points to a public pan hook that Core does not ship yet and architecture:react-component-runtime INV13 lists no pan gap. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request or by naming the record change that settles it; the author may do so as an engineering owner.
Blocking finding:
- The diff against
mainis 10,911 authored lines, over the 8,000-line limit for an owner-directed batch, because it carries the stacked SearchField from #9399; land #9399 and #9669 first and rebase so this pull request holds PageControl alone.
SearchField now relies on Core's click guard and shared resize observer, as the earlier inline comments asked.
[Automated review]
Internal entries land with their first user, per meta#8562. Lab's NavigationStack, PageControl and TabBar animate with Core's animator, solve UIKit's timing curves with Core's cubic-bézier solver, listen for taps and pans with Core's gesture hook and pan recognizer, stretch past their ends with Core's rubber band and read the writing direction as Core does, so `@astryxdesign/core/internal/touch` re-exports, by name: - the motion engine, `createMotionEngine`, for its jobs beyond the public `createAnimator` (architecture:react-component-runtime INV13), with its types `MotionEngine` and `MotionEngineOptions` (the engine's `Animator` and `AnimatorOptions`), `AnimateOptions`, `MotionCurve` and `MotionHandle`. The engine's factory is renamed from `createMotionAnimator` at its source and in every internal caller, so the engine and the public animator differ by more than one word; - `cubicBezier` and UIKit's `timing` table; - `useGestureRecognizer`, `useTouchPresentation` and `isRtlElement`; - `PanGestureRecognizer`, `rubberBand`, UIKit's `UIKIT_GESTURES` table and `TOUCH_QUERY`, which more than one of those components reads. Named re-exports of code already on main, plus that rename: no behavior changes, no docs, no root re-export, and the `@astryx/no-core-internal-import` lint rule already limits the entry to Core and Lab. The entry's test checks it re-exports the same values Core imports by path, and that it carries no public factory's name. One Lab module that those components share comes with them: `internal/useMotionEngine`, a stable motion engine bound to an element through a callback ref.
…e Motion cross-fade and the hit-area watcher through Core's internal touch entry Internal entries land with their first user. Lab's PageControl animates its strip and dots with each element's one shared animator, where the strip's slide and a dot's size and fade compose. It reads the Reduce Motion cross-fade's timing, and it keeps its coarse-pointer hit area live with the hit-area watcher. So `@astryxdesign/core/internal/touch` also re-exports, by name: - `animatorFor`, an element's shared animator, as Core's components reach it (no public API has the name); - `reducedMotionFade`; - `watchHitArea`. Named re-exports only, of code already on main: no behavior change, no docs, no root re-export, no new entry, and the `@astryx/no-core-internal-import` lint rule already limits the entry to Core and Lab. The entry's test checks it re-exports the same values Core imports by path.
fde400d to
b8e3a66
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about Lab PageControl, and three fixes are needed either way.
Owner decision needed. No current record governs (a) a Lab PageControl beside Pagination's variant="dots", with its one-page-per-tap rule, single slider semantics and other dots at 48 %; (b) its held look (a pressed-overlay platter, other dots at 25 %), which design:user-states DR1 does not list; or (c) its window spring, slide delay and fades played as table values, where spec:AST-094 FR4 asks for motion roles. The owners' answer already covers the internal pan recognizer. An engineering owner records the decision for (a)–(c) by commenting /approve-spec <full head SHA> on this pull request, or names the record change that settles them; the author may do so as an engineering owner.
Required fixes:
- Land #9754 and #9669 first, or drop the
createMotionEnginerename, the uncalleduseMotionEnginehook, the unused re-exports and the changeset that says Lab components use them, because they put this diff at 3,914 authored lines outside the one-component batch exemption. - When a tap or key slides the window, the old indicator's fade runs 0.015625 s instead of 0.125 s or 0.25 s, because the window-slide effect reads computed style (
PageControlTouch.ts:464) before the fade effect writes the transitions; write the transitions first and test the fade that actually runs in a window-slide case. - The layout effect at
PageControlTouch.ts:526-533only marks rendered dots for the window-slide effect; derive the newly mounted dots there from the previous window and remove it.
[Automated review]
| if (strip == null || root == null) { | ||
| return undefined; | ||
| } | ||
| const isRtl = isRtlElement(root); |
There was a problem hiding this comment.
This getComputedStyle read runs before the fade effect below writes the dots' transitions. When the window slides, the old dot's opacity transition starts on its previous 15.625 ms switch: in Chromium the fade that runs is 0.015625 s instead of 0.125 s after a tap or 0.25 s after a key. Writing the transitions first (for example, declaring the fade effect ahead of this one) keeps the measured fade.
There was a problem hiding this comment.
Fixed in 288574e: the fade effect now runs before the window effect, so the transitions are written before its style reads. A new browser case checks the fade that runs when a tap or a key slides the window.
| }, [windowStart, pages, isTouch]); | ||
|
|
||
| // Mark rendered dots, so a later window move knows which ones are new. | ||
| useLayoutEffect(() => { |
There was a problem hiding this comment.
This Effect runs after every commit only to mark dots for the window-slide effect. That effect already keeps the previous window in shownRef, so a dot is new when its page lies outside the previous window's rendered range (windowStart ± RENDER_MARGIN); computing that there removes this Effect.
There was a problem hiding this comment.
Done in 288574e: that layout effect is gone. The window effect finds the dots that mounted from the previous window's rendered range.
| } | ||
| const kind = | ||
| requestRef.current?.page === current ? requestRef.current.kind : 'other'; | ||
| // The shown page consumes its request. |
There was a problem hiding this comment.
Optional: this restates the next line. Drop it, or keep only the reason: "Clear it. A later change from the app then fades as other."
| } | ||
| // Under Reduce Motion every dot takes the policy's fade. | ||
| const reduced = reducedFadeOf(root); | ||
| // The table's measured values: touch motion stays internal. |
There was a problem hiding this comment.
Optional: the next lines already show the values come from the table; dropping this comment also drops a policy statement that the owner question about motion roles still has to settle.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: Core's internal touch entry re-exports the motion engine, curve and gesture helpers Lab's touch components use, and the engine's factory is renamed `createMotionEngine`. Apps import nothing new. |
There was a problem hiding this comment.
No Lab component at this head imports the motion engine, cubicBezier, timing, rubberBand, UIKIT_GESTURES or TOUCH_QUERY; this changeset can move with those re-exports to #9754, or name only the re-exports PageControl uses.
There was a problem hiding this comment.
That changeset belongs to #9754, the base this PR is stacked on; this PR adds no changeset for those re-exports.
b8e3a66 to
288574e
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about Lab PageControl, and three fixes are needed either way.
Owner decision needed. No current record governs (a) a Lab PageControl beside Pagination's variant="dots", with its one-page-per-tap rule, single slider semantics and other dots at 48 % instead of the measured 45 %; (b) its held look, a pressed-overlay platter with the other dots at 25 %, which the design:user-states table does not list; and (c) its window spring, slide delay, fades and switch played as table literals, where spec:AST-094 FR4 says a component must read touch motion through named roles. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request or by naming the record change that settles each point; the author may do so as an engineering owner. The owners' answer already covers the internal pan recognizer.
Required changes:
- Land #9754 first or drop its parts from this branch (the
createMotionEnginerename, the uncalleduseMotionEnginehook, the re-exports nothing here imports and the changeset that says Lab components use them), since they put this owner-directed batch at 4,018 authored lines with unrelated changes mixed in. - Call Core's
writeHitInsetsthrough@astryxdesign/core/internal/touchinstead of the copy atPageControl.tsx:332-339, and read--astryx-hit-inset-*in the hit layer. - Return every dot to the measured 15.625 ms switch once a page change is shown, because a held finger now dims the previously current dot over 0.125 s or 0.25 s, and a lift that keeps the page restores it as slowly, while the others switch at once.
The window-slide fade and the extra layout effect from the last round are fixed.
[Automated review]
| return watchHitArea(root, insets => { | ||
| for (const side of ['top', 'right', 'bottom', 'left'] as const) { | ||
| root.style.setProperty( | ||
| `--_page-control-hit-${side}`, | ||
| `${insets[side]}px`, | ||
| ); | ||
| } | ||
| }); |
There was a problem hiding this comment.
This loop copies Core's writeHitInsets (packages/core/src/utils/hitArea.ts), which InfoTip and Switch call as watchHitArea(el, insets => writeHitInsets(el, insets)) with a hit layer reading --astryx-hit-inset-*. Re-export writeHitInsets from internal/touch beside watchHitArea, call it here, and point the ::after layer at --astryx-hit-inset-*, so a change to the shared writer reaches PageControl too.
There was a problem hiding this comment.
Done in 9bd8205: internal/touch re-exports writeHitInsets beside watchHitArea, PageControl calls it, and the hit layer reads --astryx-hit-inset-*.
| const switchFade = fadeOf(PC.switchMs, PC.switchCurve); | ||
| for (const dot of Array.from( | ||
| stripRef.current?.children ?? [], | ||
| ) as HTMLElement[]) { | ||
| dot.style.transition = | ||
| reduced ?? (Number(dot.dataset.page) === old ? fade : switchFade); | ||
| } |
There was a problem hiding this comment.
The old dot keeps this 125 ms or 250 ms fade after the change. Nothing sets it back to the switch, so the next held finger dims that dot over 0.25 s (0.125 s after a tap) while the others switch in 0.015625 s, and a lift that keeps the page restores it as slowly. Chromium on the Default story, after End: dots 1, 2 and 4 reach 0.25 at 9 ms, dot 3 at 242 ms. The table names the switch for that change (pageControlTable.ts:162-164). Writing reduced ?? SWITCH to every dot when isInteracting changes would fix it.
There was a problem hiding this comment.
Fixed in 9bd8205, with a browser case that failed before (the previous current dot dimmed and returned over 0.25 s). Once the old dot's fade starts (transitionrun), it goes back to the switch; the running fade keeps its duration. Writing the switch on every isInteracting change would also replace a page change's fade, because a lift's step and a scrub's first page commit together with the platter change.
288574e to
9bd8205
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about Lab PageControl, and two fixes are needed either way.
Owner decision needed. No current spec governs (a) a Lab PageControl beside Pagination's variant="dots", with its one-page-per-tap rule, single slider semantics and other dots at 48 % instead of the measured 45 %; (b) its held look, a pressed-overlay platter with the other dots at 25 %, which the design:user-states table does not list; and (c) its window spring, slide delay, fades and switch played as table literals, where spec:AST-094 FR4 says a component must read touch motion through named roles. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, or provides or identifies the record that settles each point; the author may do so as an engineering owner.
Required changes:
- Land #9754 first or drop its parts from this branch (the
createMotionEnginerename, the uncalleduseMotionEnginehook, the re-exports nothing here imports and the changeset that says Lab components use them), since they put this owner-directed batch at 4,123 authored lines with unrelated changes mixed in. - Return the old dot to the switch when its fade cannot run, because after a page change made while the control is hidden (
display: none) the first held finger dims that dot over about 0.22 s while the others switch in one frame.
The hit-inset copy and the previous dot's dimming from the last round are fixed.
[Automated review]
| // Once the fade runs, the dot's next change is a switch again. A running | ||
| // transition keeps the duration it started with. | ||
| const dot = fading; | ||
| const onRun = (event: TransitionEvent) => { | ||
| if (event.target === dot && event.propertyName === 'opacity') { | ||
| dot.style.transition = SWITCH; | ||
| dot.removeEventListener('transitionrun', onRun); | ||
| } | ||
| }; | ||
| dot.addEventListener('transitionrun', onRun); |
There was a problem hiding this comment.
This hands the old dot back to the switch only when its fade starts. Under display: none no transition runs, so transitionrun never fires: after a page change made while the control is hidden, the dot keeps the 250 ms fade, and the next held finger dims it in about 217 ms while the others take one frame (Chromium, Default story, End while hidden). Also writing SWITCH once the fade's duration has passed with no transitionrun (a timer cleared in the cleanup), plus a case that changes the page while hidden, would close it.
There was a problem hiding this comment.
Fixed in 9197699, with a case that hides the control, changes the page, then holds (before: [0.25, 0.015625]). The effect now reads the old dot's style once after writing the fade, which starts the fade with that duration, and then writes the switch at once; a running transition keeps its duration, and a hidden control starts none. No listener or timer.
| // The held look: a held or scrubbing finger's platter paints the system | ||
| // pressed overlay on the root, under `data-interacting`. It has no element | ||
| // of its own. |
There was a problem hiding this comment.
Non-blocking (comment style): platterShown follows isInteracting through StyleX, and data-interacting styles nothing. Suggest: "The held look has no element of its own. The root's ::before paints it."
| // The input (PageControlTouch.ts): the tap and the scrub for every pointer. | ||
| // Under a finger, it adds the touch presentation: the held platter, the | ||
| // window's slide and the fades. |
There was a problem hiding this comment.
Non-blocking (comment style): this restates usePageControlTouch's own JSDoc; it can go.
| // Under a finger the slots centre on the control; the cropped row starts inside the | ||
| // padding. |
There was a problem hiding this comment.
Non-blocking (comment style): two topics in one sentence, and the first restates stripStart. Suggest only: "The cropped row starts inside the padding."
9bd8205 to
9197699
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about Lab PageControl, and three fixes are needed either way.
Owner decision needed. No current spec governs (a) a Lab PageControl beside Pagination's variant="dots", with its one-page-per-tap rule, single slider semantics and other dots at 48 % instead of the measured 45 %; (b) its held look, a pressed-overlay platter with the other dots at 25 %, which the design:user-states table does not list; and (c) its window spring, slide delay, fades and switch played as table literals, where spec:AST-094 FR4 says a component must read touch motion through named roles. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, or provides or identifies the record that settles each point; the author may do so as an engineering owner.
Required changes:
- Land #9754 first or drop its parts from this branch (the
createMotionEnginerename, the uncalleduseMotionEnginehook and the re-exports only that hook uses), since they put this owner-directed batch at 4,206 authored lines with unrelated changes mixed in. - Drop or correct
.changeset/core-internal-touch-entry-lab-animator.md, which says Lab's touch components use helpers that no Lab component imports. - Move the held look's Reduce Motion fade out of the layout effect at
PageControlTouch.ts:571-589and intoshowHeld()andfinish(), which setisInteractingand can write that fade first.
The previous dot's dimming after a change made while the control is hidden, and the three comment suggestions, are fixed.
[Automated review]
|
|
||
| // Under Reduce Motion the platter and the dots' dimming, which show and | ||
| // hide with a finger, take the policy's fade too. | ||
| useLayoutEffect(() => { |
There was a problem hiding this comment.
This Effect reacts to isInteracting, which only showHeld() and finish() (lines 267-285) change. Both can write the Reduce Motion fade to the platter's custom property and the dots before they call setInteracting, so the fade needs no Effect (architecture:react-component-runtime INV2), and the note above names no external system (INV3). One helper both handlers call, and deleting this Effect, would close it; the page-change fade effect already covers dots that mount on a slide.
There was a problem hiding this comment.
Done in 489ca21: the effect is gone. The hold and the lift write the Reduce Motion fade before they change the held look.
…e, a finger scrubs, every pointer pages
PageControl is a row of page indicators, the current one highlighted, that
pages like UIPageControl on iOS 26 (measured on the iOS 26.2 simulator):
- What it draws: 9.67 px indicators 8 px apart, at most ten slots; with
more pages the row is a window whose edge slots shrink and slide on
UIKit's spring; the old indicator fades out after a change.
- How it pages: the app owns the page (`value`, `onChange`). A tap on
either side of the current indicator asks for the next or the previous
page (mirrored in RTL); a finger that travels 16 px scrubs to the
indicator under it and jogs past a window's edge; a press held 155 ms
shows the held platter and dims the others. A vertical swipe stays the
page's scroll.
- Every pointer: with a mouse or a pen a click steps one page
toward its side and a drag scrubs; the platter, the dimming, the window's
slide and the fades stay the touch presentation, on a coarse pointer.
- Reduce Motion: every change fades in 200 ms, nothing travels.
- Accessibility: one adjustable `slider` ("Page 3 of 5"); the arrow keys,
Home and End change the page. Under a coarse pointer an invisible hit area
grows the control toward 44 px, as far as its neighbours allow.
- A mouse scrub on a cropped long row holds the row still until the lift, so
the page stays the dot under the pointer.
Its measured values live in its own table (pageControlTable.ts): only
PageControl reads them, so they stay in Lab. It reaches Core's pan and tap
recognizers, each element's shared animator, the Reduce Motion cross-fade,
the hit-area watcher and the touch presentation through Core's internal
touch entry.
`astryx discover` lists it, and the registry has its route.
Its timing holds on a loaded runner. A jog step commits in the timer's own
task (`flushSync`): a render scheduled after it could wait behind a busy
page's other tasks past the next step, and two steps would show as one. A
jog step counts from when the previous one was due, so a timer that runs
late does not slow the run (after
a stall longer than a step, from the late step: no burst). The window's
slide starts 50 ms after the change at a start time on the document
timeline, for the strip and every dot, which a busy main thread cannot push
back as it would a timer. The held-platter proof dates each read on the
page's clock from the touch, the clock the 155 ms runs on, and the slide
proof dates the change at the lift's first event, the task the change
commits in, not by the next frame or the end of that task. The slide's
storyboard arms the harness's hold before the tap (`armHold`), so the page
holds the motion as it starts, however late the test reads it. The RTL
scrub holds the finger 6 px past the content's edge, where the jog steps
about once a second, so exactly one step comes before the lift with
hundreds of milliseconds to spare.
The unit tests' media-query stub answers from the case's pointer at every
read: Core's gesture host keeps the `(pointer: coarse)` list it asked for
when the document's host was created, so a stub that froze its answer let
an earlier mouse case's pointer reach a later touch case (that case failed
after any mouse case, and passed alone).
Core's internal touch entry also re-exports `writeHitInsets`, Core's hit-area
inset writer, beside `watchHitArea`: PageControl is its first Lab user, and its
hit layer reads the same `--astryx-hit-inset-*` properties as InfoTip and
Switch.
9197699 to
489ca21
Compare
There was a problem hiding this comment.
Approval waits on engineering owners' decisions about Lab PageControl, and two fixes are needed either way.
Owner decisions needed. No current spec governs (a) a Lab PageControl beside Pagination's variant="dots", with its one-page-per-tap rule, single slider semantics and other dots at 48 % instead of the measured 45 %; (b) its held look, a pressed-overlay platter with the other dots at 25 %, which the design:user-states table does not list; and (c) its window spring, slide delay, fades and switch played as table literals, where spec:AST-094 FR4 says a component must read touch motion through named roles. Separately, (d) its count and 0-based value props name concepts Pagination calls totalPages and a 1-based page (architecture:public-component-api INV2 and DEC-1). An engineering owner records each decision by commenting /approve-spec <full head SHA> on this pull request, or provides or identifies the record that settles it; the author may do so as an engineering owner.
Required changes:
- Land #9754 first or drop its parts from this branch (the
createMotionEnginerename, the uncalleduseMotionEnginehook and the re-exports no Lab component imports), or put one identicalStack: #9754 → #9669 → #9684line in all three pull request bodies, since this owner-directed batch now reaches 4,254 authored lines with unrelated changes mixed in. - Drop or correct
.changeset/core-internal-touch-entry-lab-animator.md, which says Lab's touch components use the motion engine and curve helpers, though no Lab component imports them.
The held look's Reduce Motion fade now lives in the hold and lift handlers, as asked.
[Automated review]
Stacked on #9669 (on #9754). Review only this PR's change: vjeux/astryx@1ea5d3e...489ca21
Batch: L1 · Lab: iOS chrome (TabBar, SearchField, NavigationStack, PageControl)
User need
A phone's paging view shows its page with UIPageControl's dots; Pagination's are desktop-sized.
Evidence
The iOS 26.2 simulator; P7: the Android 15 emulator. Red ring: the finger.
Problem and solution fit
Measured on iOS 26.2 (
pageControltable):slider, "Page indicators": "Page 3 of 5".Current specification
spec:AST-094FR1, FR2, FR19, FR42 and FR43;design:reduced-motionDR2 and DR3. The scrub may use the internal pan recognizer, listed as a gap until the public pan hook lands (architecture:react-component-runtimeINV13).For the owner: (a) a Lab PageControl beside Pagination's
variant="dots", with its one-page tap rule andslidersemantics; (b) the held look, which has nodesign:user-statesrow; (c) the fades, window spring and slide delay as table constants, not motion roles (FR4). Open until promotion: tap- and key-started motion will play theme motion tokens; only the scrub's will spring from velocity. Real-phone clips of P1–P7 are not captured yet.Public delta and caller burden
New Lab
PageControl(count,value,onChange,hidesForSinglePage), with a story, discover entry and registry route; Core's internal entry also re-exportswriteHitInsets.Design and alternatives
Supersedes #8164, #8174 and #8705.
Scope
Testing
[0.25, 0.015625]);[0.25, 0.25]).pnpm exec vitest run packages/lab/src/PageControl: 47 passed.PageControl.interaction.browser.spec.ts, real touch) at 489ca21: Chromium on a pinned pair of 2 cores (shared with another run at the time), 4 workers: 31 passed; WebKit in the CI job's profile (podman--cpus=4,CI=1,LP_NUM_THREADS=4, 1 worker) ×10: 300 passed, 10 skipped.pnpm linton the changed files have no warnings; Prettier passes (the registry'sroutes.lock.jsonkeeps its generator's format, as on main); check:knowledge, check:changesets, check:i18n-runtime, check:i18n-catalog, check:use-client, check:sync, sync:exports:check and lab:readiness:check pass; the touch entry test 2 passed; the touch-support bundle, CLI discovery and registry tests 57 passed; the Storybook build passes.spec:AST-094FR1, FR2, FR4, FR19, FR42, FR43;design:reduced-motionDR2, DR3;design:user-statesDR1;architecture:react-component-runtimeINV13;architecture:knowledge-contractsINV29; and the global records thatpnpm specs:for --difflists.Stack: #9754 → #9669 → #9684