Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
8620b4f to
6347e88
Compare
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewModified ComponentsSlider (@astryxdesign/core) · View in Storybook
Component Size
Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.
|
| Component | JavaScript | CSS |
|---|---|---|
| core/Slider | +32.9KB |
+401B |
Bundle Size Summary
Gzip figures measure each package's entry file, not the code an import pulls in.
| Package | Entry (ESM) | Entry gzip before | Entry gzip after | Delta |
|---|---|---|---|---|
| @astryxdesign/core | 4.9KB | 1.2KB | 1.2KB | 0B |
| Package stylesheet | Gzip before | Gzip after | Delta |
|---|---|---|---|
| @astryxdesign/core | 35.4KB | 35.7KB | +276B |
Accessibility Audit
Status: No accessibility violations detected.
Visual Regression
55 of 504 shot(s) changed.
A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.
| component | story | theme | mode | pixels |
|---|---|---|---|---|
| Selector | Indicator Space Bottom Sheet Narrow End RTL | neutral | light | 753,933 |
| Selector | Indicator Space Bottom Sheet Wide Start | neutral | light | 740,072 |
| RichTextEditor | Markdown parity: side by side | probe | dark | 558,840 |
| RichTextEditor | Markdown parity: side by side | probe | light | 540,756 |
| Chat | Full AI Chat | probe | light | 230,823 |
| Chat | Full AI Chat | probe | dark | 229,805 |
| AppShell | With Mobile Nav | probe | light | 217,010 |
| AppShell | With Mobile Nav | probe | dark | 216,960 |
| Collapsible | Dividers | probe | dark | 66,445 |
| Collapsible | Dividers | probe | light | 66,445 |
| AppShell | Playground | neutral | dark | 65,198 |
| AppShell | Playground | neutral | light | 63,788 |
| Chat | Default | neutral | dark | 36,304 |
| Chat | Default | neutral | light | 35,767 |
| Chat | Empty State | probe | light | 23,531 |
| Chat | Empty State | probe | dark | 22,622 |
| ChatAutoScroll | Scroll Behavior Comparison | neutral | dark | 15,378 |
| ChatAutoScroll | Scroll Behavior Comparison | neutral | light | 14,107 |
| Chat | With Attachments | probe | light | 13,406 |
| Chat | With Attachments | probe | dark | 11,232 |
and 35 more.
Generated by PR Enrichment workflow | Storybook | Sandbox | View full report
There was a problem hiding this comment.
Approval is on hold for an engineering owner's decision: under a coarse pointer the Slider ignores track presses and fires onChangeEnd only after a coast, which changes component:Slider FR5, and the adaptive-menu threshold and the draft spec:AST-094 requirements have no current spec yet, so FR5 needs updating for the touch model and the threshold and the draft's requirements need approving or replacing with a governing spec. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Independently of that decision, please fire onChangeEnd from the handler that ends the touch interaction instead of the layout effect in packages/core/src/Slider/Slider.tsx, and shrink the 27,841-line stack by landing its dependency pull requests first. [Automated review]
| useIsomorphicLayoutEffect(() => { | ||
| if (touchSettle.count === 0) { | ||
| return; | ||
| } | ||
| touchController.handBack(); | ||
| if (touchSettle.report) { | ||
| fireChangeEnd(); | ||
| } | ||
| }, [touchSettle, touchController, fireChangeEnd]); |
There was a problem hiding this comment.
onChangeEnd fires here from a layout effect reacting to touchSettle, which architecture:react-component-runtime INV2 rules out, and it fires again if fireChangeEnd changes identity (a Slider switched between single and range values). Rendering the last value synchronously in the settle callback the controller calls when the interaction ends, and calling fireChangeEnd() there, would leave only handBack() in this effect.
There was a problem hiding this comment.
onChangeEnd has to come from the touch that ends the interaction, exactly once. Fixed in 889f550: the controller now ends an interaction from the handler that ends it (the lift, a cancel, the coast's last frame). flushSync renders the last value, the parts go back to the stylesheet, and onChangeEnd fires there with the rendered value (FR5). The layout effect is gone. A new test switches a single slider to a range after a touch: it fired twice through the old effect, and fires once now.
6347e88 to
889f550
Compare
There was a problem hiding this comment.
Approval is on hold for an engineering owner's decision on the touch model: under a coarse pointer the Slider ignores track presses and reports onChangeEnd only after a coast, which changes component:Slider FR5, and the adaptive-menu threshold and the draft spec:AST-094 requirements have no current spec yet. An engineering owner approves it by recording the Slider and menu-threshold changes in their component records and commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner; otherwise the touch path keeps FR5's track press and its end at release. Independently of that decision, please:
- keep a way to set a value without dragging under touch, since a drag is now the only pointer path (WCAG 2.2 SC 2.5.7);
- end a touch drag that is interrupted by disabling or unmounting the Slider without calling
flushSyncandonChangeEndfrom the effect cleanup inuseSliderTouch.ts; - land the dependency pull requests first so this one carries only the Slider commit.
[Automated review]
| } | ||
| // A finger or a pen under the touch presentation belongs to the thumbs' | ||
| // touch targets; the track ignores it, as UISlider's does. | ||
| if (isTouchPresentation && e.pointerType !== 'mouse') { |
There was a problem hiding this comment.
With this early return a finger or pen can change the value only by dragging the thumb: a tap on the track or a mark does nothing and a tap on the thumb changes nothing. WCAG 2.2 SC 2.5.7 (Dragging Movements, AA) asks for a single-pointer way that does not drag. Letting a tap that never passes the 10 px pan threshold set the value from the track, while a swipe still scrolls, would keep both.
| if (!isEnabled) { | ||
| return undefined; | ||
| } | ||
| return controller.attach(); |
There was a problem hiding this comment.
When this cleanup runs mid-drag (the Slider is disabled or unmounted), detaching the pan cancels it, and the cancel reaches finish: commit calls flushSync during React's commit (React logs "flushSync was called from inside a lifecycle method") and onChangeEnd fires from the cleanup, although dispose() says unmount stops without reporting. Ending live interactions silently before the recognizers detach, and naming the gesture host this Effect synchronizes, would keep the end report in the touch handlers.
889f550 to
79cc0ab
Compare
…fari) Recordings of the iOS 26.2 simulator (iPhone 17 Pro) in Mobile Safari, cropped to the page; the gray dot is the finger, drawn by the test page. main against each pull request: meta#8151 coincident range thumbs, meta#8168 translate travel, meta#8190 the touch presentation and its held-thumb fix.
There was a problem hiding this comment.
Approval is on hold for an engineering owner's decision on the touch model: under a coarse pointer the Slider ignores track presses and reports onChangeEnd only after a coast, which changes component:Slider FR5 and ORD2, while the new adaptive-menu query and the draft spec:AST-094 requirements have no current spec yet. An engineering owner settles it by recording the Slider and menu-query changes in those component records and commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner; otherwise the touch path keeps FR5's track press and its end at release. Independently of that decision, please:
- keep a single-pointer way to set a value without dragging under touch (WCAG 2.2 SC 2.5.7);
- end a touch drag interrupted by disabling or unmounting the Slider without calling
flushSyncandonChangeEndfrom the effect cleanup inuseSliderTouch.ts; - land the dependency pull requests first so this one carries only the Slider commit.
[Automated review]
There was a problem hiding this comment.
Approval is on hold for an engineering owner's decision on the touch model: under a coarse pointer the Slider ignores track presses and reports onChangeEnd only after a coast, which changes component:Slider FR5, while the new adaptive-menu query and the draft spec:AST-094 requirements have no current spec yet. An engineering owner settles it by recording the Slider and menu-query changes in those component records and commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner; otherwise the touch path keeps FR5's track press and its end at release. Independently of that decision, please:
- keep a single-pointer way to set a value without dragging under touch (WCAG 2.2 SC 2.5.7);
- end a touch drag interrupted by disabling or unmounting the Slider without calling
flushSyncandonChangeEndfrom the effect cleanup inuseSliderTouch.ts; - add a pen case to the touch tests, since the track now ignores pens and the thumbs take them;
- land the dependency pull requests first so this one carries only the Slider commits.
[Automated review]
| direction: this.model.isHorizontal ? 'horizontal' : 'vertical', | ||
| maximumNumberOfTouches: 1, | ||
| cancelsTouchesInView: false, | ||
| allowedPointerTypes: ['touch', 'pen'], |
There was a problem hiding this comment.
The thumbs now take pens and the track ignores them (Slider.tsx 888), but neither useSliderTouch.test.tsx nor the interaction spec drives a pen. architecture:interaction-modality asks a change adding pointer-dependent behaviour to verify mouse, touch and pen; one pen case (track ignored, thumb drag tracked) would cover it.
962ba39 to
fbd2d03
Compare
There was a problem hiding this comment.
Approval is on hold for an engineering owner's decision on the touch model: on iOS the Slider ignores track presses and reports onChangeEnd only after a coast, which changes component:Slider FR5, while the adaptive-menu threshold, the theme motion roles and platform condition, the module experimental subpaths and the draft spec:AST-094 requirements (whose FR4 says touch motion is not themable) have no current spec yet. An engineering owner settles it by recording these changes in their records and commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner. Independently of that decision, please:
- keep a single-pointer way to set a value without dragging under touch on iOS (WCAG 2.2 SC 2.5.7);
- end a touch drag interrupted by disabling or unmounting the Slider without calling
flushSyncandonChangeEndfrom the effect cleanup inuseSliderTouch.ts; - add a pen case to the touch tests, since the track ignores pens and the thumbs take them;
- land the dependency pull requests first so this one carries only the Slider commits.
[Automated review]
| } | ||
| // A finger or a pen under the touch presentation belongs to the thumbs' | ||
| // touch targets; the track ignores it, as UISlider's does. | ||
| if (isTouchPresentation && e.pointerType !== 'mouse') { |
There was a problem hiding this comment.
With this early return a finger or pen on iOS can change the value only by dragging the thumb: a tap on the track or a mark does nothing, and a tap on the thumb changes nothing. WCAG 2.2 SC 2.5.7 (Dragging Movements, AA) asks for a single-pointer way that does not drag. Letting a touch on the track that ends before the pan begins set the value, while a swipe still scrolls, would keep both.
| useEffect(() => { | ||
| if (!isEnabled) { | ||
| return undefined; | ||
| } | ||
| return controller.attach(); | ||
| }, [controller, isEnabled, thumbCount, isHorizontal]); |
There was a problem hiding this comment.
When this cleanup runs mid-drag (the Slider is disabled or unmounted), detaching the pan cancels it and the cancel reaches finish: commit calls flushSync inside React's commit (React logs "flushSync was called from inside a lifecycle method") and onChangeEnd fires from the cleanup, although dispose() says unmount stops without reporting. Ending live interactions silently before the recognizers detach, and naming the gesture host this Effect synchronizes, would keep the end report in the touch handlers.
| direction: this.model.isHorizontal ? 'horizontal' : 'vertical', | ||
| maximumNumberOfTouches: 1, | ||
| cancelsTouchesInView: false, | ||
| allowedPointerTypes: ['touch', 'pen'], |
There was a problem hiding this comment.
The thumbs take pens here and the track ignores them (Slider.tsx 905), but neither useSliderTouch.test.tsx nor the interaction spec drives a pen. One pen case (track ignored, thumb drag tracked) would cover the mouse, touch and pen coupling for pointer-dependent behaviour.
| measures (0.55 for a scroll view and a sheet), and a release MUST pick its | ||
| rest point by projecting its velocity the way its UIKit counterpart does: | ||
| with a deceleration rate, or with a measured projection time. | ||
| - **FR4 — Physics are not themable in touch presentations.** A touch |
There was a problem hiding this comment.
FR4 says touch physics are not themable, and DEC-1 rejects tuning touch motion per theme, but this head lets defineTheme({motion}) retune every touch spring, duration and curve by role and resolves slider.releaseSpring under the thumb's theme. One of the two needs to change before an owner can approve the record.
| 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 |
There was a problem hiding this comment.
spec:AST-090 is not in the repository at this head or on main, so the departure FR7 and FR8 propose cannot be checked against it.
| * is everything else. Pair `ios` with `pointer: 'coarse'` for a touch | ||
| * profile, such as touch motion tuned per platform. | ||
| */ | ||
| platform?: 'ios' | 'other'; |
There was a problem hiding this comment.
A platform condition widens the adaptation condition vocabulary, which the current theme authoring contract keeps fixed; it needs that contract amended (or an owner's decision) alongside the motion roles.
…eta#8190 24030a1) iOS 26.2 simulator, Mobile Safari. meta#8190's clips are re-recorded on the build whose lifted thumb grows around its center (the earlier clips showed it 5.5 px off the track); the catch is now side by side with the same flick alone (77 against 63); the past-the-end clip runs through the lift; the tap is held 0.3 s. meta#8151's tooltip clip hides the row label so the tooltips read clearly, and its coincident clip names the off-center press precisely.
24030a1 to
605287f
Compare
… the inline style it reads - The motion registry, the host's engine gate and the README name the presented hit test as an engine workaround (spec:AST-013 FR6) with its review condition (re-measure on each iOS release; remove it once iOS WebKit hit-tests a compositor-moved element where it is drawn) and its regression fixture. - While a curve runs the animator holds the value it starts from inline, so presentation.ts and the README no longer say the inline style holds the end.
…core/internal/touch Core ships the internal entry @astryxdesign/core/internal/touch, which re-exports the gesture primitives for Astryx's own packages, and the Lab-import guard fails a Lab import of core outside core's exports (architecture:react-component-runtime INV13). The README and the barrel's header named internal workspace imports and said nothing here is exported; they now name that entry.
…ill needs the element, and the listeners that cancel The release note, the stream's header, the host's comment and the README said the stream is one passive listener per event type and that a document-capture stopPropagation no longer starves the recognizers. Such a stop never hid a listener on the same node, and some subscriptions cancel. They now say what changed: an app's window-capture stopPropagation, or a document-capture stopImmediatePropagation registered before core, no longer hides a touch; a recognizer that must claim still needs the touch to reach its element; the gesture host's touchend, click and contextmenu cancel while a claimer is attached and the page hold's touchmove while it holds; and the stream stays ahead of an app's window-capture listener only until a type's listener is re-added. The README's list of blocking touchmove listeners gains the claimed touch's target and the document while a claiming element moves.
…default it moves
The {stylexOptions} form of astryxStylex now passes StyleX
treeshakeCompensation: true unless the caller sets it. spec:AST-017 FR3
asks the release note to name the old and new default and the option
that restores the old one; it now does.
…er and for the two stops the input stream fixes - A still finger with sub-pixel jitter over a scroll view keeps its tap and scrolls nothing (spec:AST-094 FR18–FR22). - A tap is still recognized under an app's window-capture stopPropagation, and under a document-capture stopImmediatePropagation registered before core's: the recognizers hear touches on the window's capture phase. Chromium and WebKit pass.
…s once Where nothing that claims is attached, the host's touchend listener is passive and cannot cancel the engine's own click after a retargeted tap clicks what is drawn. The shared click guard swallows that late click whatever is attached; this case pins it, as asked on meta#8293.
…esture host's subscriptions The gesture host subscribes no click: the one cancelling click subscriber is the click guard's, added at a window's first arm and kept. The release note, the stream's header and the gestures README listed click with the host's touchend and contextmenu; they now name the guard's own.
…ads it Slider's touch presentation (meta#8190) reads the table, and a release that ships both would print the old line in its notes. The changeset now says only that the table is not exported.
|
This needs an owner's call on recording Slider's touch presentation in |
…r path Each case fails on this head: - Under Reduce Motion a release's momentum keeps moving and a track tap sets its value at once, both at full opacity (design:reduced-motion DR2: a throw's momentum is direct movement; an in-place value change applies at once). The thumb cross-fades both today. - Range thumbs closer than their touch targets split the gap, so a finger on the lower thumb's exposed side takes the lower thumb (spec:AST-094 FR42). Today the upper thumb's 48 px target covers it. - A valueDisplay change that remounts the thumbs keeps their gestures. Today the new touch targets have none. - A finger's drag moves the thumb even when the owner renders nothing, its position written straight to the thumb (spec:AST-094 FR9). Today the thumb moves only through the owner's render. The pen case now advances the frames before it asserts, so it checks what a pen's tap on the track does: it sets the value on its lift, as a finger's does.
…s gap, remounts, and a drag off the render path - Under Reduce Motion a release's coast plays as momentum (the animator's decay), so it keeps moving, and a tap on the track sets its value at once: design:reduced-motion DR2 calls a throw's momentum direct movement and an in-place value change immediate. Neither cross-fades now. - Each thumb's touch target grows to UISlider's 48 pt, but on the side facing a range's other thumb only to half the gap between them (spec:AST-094 FR42), computed in container units so it needs no layout read. A finger on the lower thumb no longer drags the upper one. - The thumbs' gestures re-attach when a valueDisplay change wraps or unwraps the thumbs in their value tooltips, which mounts new targets. - Each move of a drag writes the thumb's and the fill's positions straight to their translates (spec:AST-094 FR9), with onChange still reported per value; the parts go back to the stylesheet when the interaction ends, and a finished interaction no longer hands back the parts of one still being dragged. - The Effects that hand the controller its model and parts getter, and the one that disposes it, name what they synchronize (architecture:react-component-runtime INV3).
…EC-4) AST-094 FR13 leaves each component's touch presentation to its own record. FR6 records Slider's: under a coarse primary pointer a thumb takes a finger or a pen through UISlider's 48 pt target, clamped to half the gap facing a range's other thumb; a drag moves the value by the finger's travel past the 10 px threshold with no render per move; the release coasts on UISlider's measured spring; a tap on the track sets the value on lift; and Reduce Motion follows DR2. FR5's interaction ends, under a finger, when the coast settles. DEC-4 records the decision, with no prop to restore the released behavior.
…ame window Both 'a dip held before its first frame lands once the hold is …' proofs waited for no running animation, then one frame and a timeout, assuming the next step starts within a frame. The dip lands on a timer due on the call's own clock, though, and the proofs passed only because the evaluate round trips covered the gap: in WebKit on a loaded box the window closed first and the proof read the element mid-dip (opacity 0, translate none; reproduced 1 in 4 with the round trips removed). They now wait on the call's own finished, then on the browser's own animations still running or waiting for their first frame, and read nothing from the animator.
…ment at once and settles then A Reduce Motion dip whose landing runs more than its whole fade back in late (a hold released long after the dip was due) played a 1 ms opacity animation, which WebKit keeps pending until its next frame, and settled the call on a 1 ms timer: the call's finished could resolve while the element still showed opacity 0. The animator's notes say the element appears at once there. Fails now: the landing plays one opacity animation and the call has not settled.
…element at once and settles A Reduce Motion landing that runs at least the whole fade back in late had nothing left of the cross-fade, but still played a 1 ms opacity animation and settled the call 1 ms later. WebKit keeps that animation pending until its next frame, so the call's finished could resolve with the element still at opacity 0. Such a landing now writes the end opacity at once (handing a borrowed one back to the stylesheet), plays nothing, and settles the call then, as the animator's notes say.
|
Pushed 7eb9b4c, answering the Oct 28 review point by point. Each fix came test-first: every new test failed on the previous head.
|
There was a problem hiding this comment.
Approval is on hold for an engineering owner's decision: this revision writes Slider's touch presentation into component:Slider (FR6, an FR5 clause that ends a finger's interaction after the coast, and DEC-4, which rejects a prop restoring today's touch behaviour), and an engineering owner needs to approve or reject those requirements by commenting /approve-spec with the full head SHA, which the author may do as an engineering owner. That decision should also settle DEC-4 against spec:AST-017 DEC-23 and FR3, which ask a touch default for a way back, or state why architecture:public-component-api INV16's exemption for a component's own touch-only behaviour covers Slider. Whatever the decision, these still need changes:
- Register the slider's springs as motion roles (AST-094 FR4) and record the end haptic's iOS Safari gap in FR6 (FR23).
- Carry the touch target without an extra element under touch, or record an exception with element counts, and report counts for the new travel elements (AST-017 FR60, INV7).
- Land or split the four dependency branches behind the 32,667 changed lines, including the gesture host's coarse-only gate (AST-094 FR19, FR43), and add the WebKit busy-thread and real-device evidence AST-094 asks for.
[Automated review]
| upper: (upperValue - min) * pxPerValue, | ||
| // UIKit's release spring, an internal constant (touch motion has no | ||
| // theme surface). | ||
| spring: UIKIT_SLIDER.releaseSpring, |
There was a problem hiding this comment.
This spring, like the lift and drop springs at 948 and 961, is passed inline, so the animator plays it as a literal. spec:AST-094 FR4 says a component's own measured roles "join through the generated motion role table (DEC-5)" and that a component "MUST NOT play a literal"; registering the slider table with registerMotionTable (as motion/uikit/navigation.ts and motion/uikit/scroll.ts do) and naming the roles in FR6 would conform.
| } | ||
| const end = this.endOf(value); | ||
| if (end != null && end !== interaction.atEnd) { | ||
| playHaptic(UIKIT_SLIDER.endHaptic); |
There was a problem hiding this comment.
This end haptic fires mid-drag and mid-coast, where iOS Safari can play nothing. spec:AST-094 FR23 asks that "the component records that gap"; FR6 in component:Slider does not mention it yet.
| undefined, | ||
| positionStyle, | ||
| )}> | ||
| {hasTouchTargets && ( |
There was a problem hiding this comment.
This span exists only under touch, so a touch Slider renders one more element per thumb than a mouse one. spec:AST-017 FR60: "Under touch, a component renders no more elements than under a fine pointer"; a hit area drawn by a pseudo-element on the thumb (with the recognizers on the thumb), or an owner exception with element counts in FR6, would resolve it.
|
|
||
| Under a finger Slider takes iOS's measured slider behavior (FR6), the touch presentation `spec:AST-094/FR13` leaves to this record: the thumb follows the finger's travel rather than jumping to it, and a release coasts. A tap on the track sets the value on lift, where UISlider ignores it, so a drag is not the only pointer path (WCAG 2.2 SC 2.5.7). | ||
|
|
||
| Rejected: a prop that restores the released behavior under a finger. |
There was a problem hiding this comment.
spec:AST-017 DEC-23 rejects "a touch presentation or open-surface touch mode callers cannot pin", and FR3 asks the changeset to name the restoring input. If Slider's tracking is meant to be touch-only behaviour its own record defines (architecture:public-component-api INV16), saying so here would let the approver decide both at once.
…fore and after (a release coasts on; a tap on the track lands at once)
…nothing else does (#8671) ## Why Every pull request that gives a component a gesture fails `test-node` once merged with main, and the failure names no component: `scripts/touch-support-bundle.test.mjs` requires Core's root and hooks entries to carry no gesture code. At least six open pull requests (#8142, #8190, #8337, #8159, #8098, #8658) fail it this way. ## What led to it This comes from the work to make Astryx's touch components behave as native iOS ones do. Since #8465, a component that uses a gesture imports the touch modules directly (`architecture:react-component-runtime` INV13), and the root entry re-exports every component, so it carries gesture code as soon as one component does. ## Options and tradeoffs - Exempt the root entry: unblocks everyone, but stops catching a component that pulls gesture code it does not use. - Check each public import on its own: catches exactly that, at the cost of a list of the imports allowed to carry gesture code. ## Why this choice The per-import check keeps INV13's promise, a component without gestures ships none of their code, while letting gesture users through by name. It was offered on #8562 and aimed at main's test. ## What changes - Each public import of Core is built the way the size report builds it, and may carry gesture code only if `GESTURE_USERS` names it. A failure lists the import, its gesture modules with their sizes, and the chain of imports that brings them. - The root entry may carry only what a named import carries; the utils entry stays gesture-free. - A strict case keeps Button and TextInput free of gesture code whatever the list says. - `GESTURE_USERS` starts empty, so every import stays as strict as on main. The pull request that gives a component a gesture adds that component's import, and each import that reaches it, with a comment naming the gesture, so the new gesture user shows up in review. ## How to check it 1. Run `pnpm exec vitest run --project node scripts/touch-support-bundle.test.mjs` on main with this branch merged: it passes. 2. On a tree whose component uses a gesture, run it without naming that import in `GESTURE_USERS`: the test fails and names that import and the chain that brings the gesture code. Name it, and the test passes. 3. Import a gesture module from Button: the strict case fails. ## Evidence - With the list empty, the test passes on main: no public import carries gesture code today. - On open trees that give a component a gesture (AppShell's scroll-edge bar, Slider, the List reorder), the per-import case fails naming exactly those imports and their chains, and passes once each is named. - No product code changes. ## Scope - [x] No intended public API, product behavior, visual direction, or policy change. - [x] Product changes discovered during the work were removed or split. - [x] Public text and artifacts contain no internal Meta context.
Depends on #8360 (which carries #8076), #8168, #8189 and #8671. Review only this PR's change: vjeux/astryx@e40f7b6...7eb9b4c (since the approval: base merges, the review fixes, and the record change).
User need
Part of Astryx's UIKit-parity work (
spec:AST-094), which makes touch presentations behave as on iOS. Under a finger, a Slider jumped to the touch and blocked scrolling; UISlider drags the thumb by the finger's travel and coasts on release.Problem and solution fit
Under a coarse primary pointer (FR13) each thumb gets a 48 px target; close range thumbs split the gap (FR42). The value follows the finger's travel past 10 px with no render per move (FR9), then coasts; a track tap sets it on lift, and a swipe scrolls the page. Under Reduce Motion nothing grows, a tap applies at once, and the coast keeps moving (
design:reduced-motionDR2). A held finger selects nothing; a mouse keeps today's slider.Current specification
component:SliderFR5 (onChangeEndwaits for the coast), and FR6 and DEC-4, added here as the record's owner decided: no prop restores the old touch behavior.design:user-statesDR1 (the lifted thumb keeps its pressed look).Public delta and caller burden
No API change. Under a finger
onChangekeeps firing about 0.4 s past the lift, and the thumb takes no focus. Size (component-size.js,mainto head): JavaScript 48.5 → 81.3 KB gzip, CSS 8.5 → 8.9 KB.Design and alternatives
Evidence
iOS 26.2 simulator and Android 15 emulator; the gray dot is the finger.
Scope
Testing
In Storybook's Slider stories, on a phone or with touch emulation:
Run on this head, locally (fork pull requests start no CI): each step is a unit or real-touch browser test in Chromium and WebKit, all passing, as are the full core suite, typecheck, lint, the builds and
pnpm check:knowledge. It carries #8671 per-component bundle check: Slider's touch layer puts gesture code in the root entry, which the old check forbids, and the per-component check passes here.Writing rubric results
motion/uikit/slider.