Skip to content

refactor(Wheel): one listbox hook for both wheel presentations - #8361

Draft
vjeux wants to merge 1803 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-listbox
Draft

vjeux wants to merge 1803 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-listbox

Conversation

@vjeux

@vjeux vjeux commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8162. Review only this PR's change: vjeux/astryx@276a6e9...eeeea52

Maintainer impact

The wheel has two presentations: today's scrolling column under a mouse and the touch column under a finger. Each wired the same listbox by hand (an id, the ARIA attributes, the Arrow, Page, Home and End keys, and row clicks), so a fix to one had to be repeated in the other, and the copies could drift.

Intended invariant

What led to it: Astryx's UIKit-parity work added the touch column (#8159) beside today's, and with it the second copy. Both keep their behavior and markup exactly:

  • Today's column steps from the row under its band, and a disabled row stops a key.
  • The touch column steps from the row it is heading to, past disabled rows; Home and End go to the first and last enabled rows.
  • Both stay listboxes: one tab stop, aria-activedescendant on the active row, every row reachable by a click.

Change

Options: keep two copies (no churn, but they drift); render both presentations from one component (their markup and motion differ too much); or share only the listbox wiring (chosen). A new internal hook, useWheelListbox, owns the id, the listbox and option attributes and the key map. Each presentation passes its own three moves (step, go to an end, activate a row), because those are where the two differ. Wheel.tsx loses 18 lines and PickerWheel.tsx 33.

Evidence

Two markup snapshots, recorded on #8162 before the move, cover both presentations' attributes, classes and order; they pass unchanged. The existing key, click and drag tests pass unchanged.

Scope

  • No intended public API, product behavior, visual direction, or policy change.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  1. In Storybook, open TimeInput › Bottom Sheet With Min Max with a mouse, then with a finger. Tab onto a wheel and press Arrow, Page, Home and End: each moves as on test(Wheel): the picker wheel's browser proofs, storyboard and activation #8162, and a screen reader announces a listbox with the active row.
  2. pnpm exec vitest run packages/core/src/DateInput passes with both snapshots unchanged; the browser specs for both presentations pass in Chromium and WebKit.
  3. Not this change: CI's root-entry bundle check fails on main's test, which test(core): a touch component's import carries its gesture code, and nothing else does #8671 fixes.

@vercel

vercel Bot commented Oct 25, 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 29, 2026 9:09pm UTC

Request Review

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 25, 2026
@astracat-bot

astracat-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 31, 2026, 5:41 PM UTC

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…21f16b / meta#8361 7785b28): iOS 26.2 sim drag, block, rubber band, downward drag, catch, reduced-motion release, group flick, sim block, 200-year fling and drag; Android Chromium 158 flick, reduced-motion flick, 200-year fling
@vjeux
vjeux force-pushed the uikit/c21-wheel-listbox branch from 7785b28 to 5e4e7a3 Compare October 25, 2026 05:09
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 25, 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 waits on an engineering owner's decision: this pull request adds the draft record spec:AST-094, and measured against main it ships the whole touch-presentation stack it depends on (the touch picker wheel, two experimental subpaths, a stable motion/reducedMotion.stylex export and theme motion roles), which no current record admits yet. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, or by amending the records; the author may record it as an engineering owner. Independently, please cut this pull request down to its own commit once #8289 and its predecessors land (against main it is about 41,000 authored lines), and address these points:

  • spec:AST-094 FR4/DEC-1 forbid themable touch motion while the stack adds theme motion roles, FR8's 0.5 s cross-fade exceeds the 200 ms limit of the current reduced-motion design record, and FR7 cites a spec:AST-090 that does not exist.
  • The mouse column's new rest row and Reduce Motion landing need the before/after fine-pointer evidence that spec:AST-017 FR59 asks for.
  • packages/cli/assets/theme.template.ts documents a platform adaptation condition that defineTheme rejects, and the touch-experimental-subpaths changeset still lists the removed VelocityStrategy.

[Automated review]

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

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 says touch physics are not themable (and DEC-1 rejects "tuning touch motion per theme"), but this stack adds theme motion roles in defineTheme({motion: {springs, durations, curves}}) and PickerWheel reads picker.tapSpin.durationMs, timing.curves.easeInOut and scroll.bounce through resolveMotion (PickerWheel.tsx:292-293, :662). One of the two has to change before the record can be approved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Outdated: this pull request now carries main's version of this file unchanged, so it no longer shows in the diff.

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 exists neither on main nor at this head, so the departure from its FR1 and FR10 cannot be checked. Please cite the record that holds those requirements, or drop the reference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Outdated: this pull request now carries main's version of this file unchanged, so it no longer shows in the diff.

UIKit counterpart does with Reduce Motion and Prefer Cross-Fade Transitions
on: a transition that travels becomes a cross-fade carrying the same change
of state. An entrance lands in place and fades in, an exit fades out in
place, and any other move fades through. A navigation push or pop

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 0.5 s cross-fade (and a sheet fading on its own presentation spring) exceeds the current reduced-motion design record's DR2, which requires a touch transition under Reduce Motion to change opacity only and finish in 200 ms or less. The stack's animator already uses reducedMotionCrossFadeMs: 200 (motion/uikit/timing.ts:151), and the Current-state impact section does not list the reduced-motion record as amended.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Outdated: this pull request now carries main's version of this file unchanged, so it no longer shows in the diff.

const restIndex =
index === selectedIndex
? index
: nearestEnabledIndex(options, index, selectedIndex);

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 changes where today's mouse column comes to rest (and the Reduce Motion landing at lines 357-372 changes how it moves). spec:AST-017 FR59 lets touch work change fine-pointer behavior only to fix a defect those users hit, or for a named cross-device consistency, and asks the pull request to show fine-pointer evidence before and after. Please name the reason and add that evidence, such as a mouse-drag test that fails on the base.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Named and evidenced at the rule's origin, #8131: component:DateInput DEC-1 (cixzhang, 2026-10-25) makes the rest the same for every pointer, and #8131's description shows a trackpad on desktop Chromium before (back to 10) and after (settles on 9). The reduced-motion landing for a fine pointer is DR3's "at once".

Comment thread packages/cli/assets/theme.template.ts Outdated
* `from` includes its point; `below` excludes it. The other supported axes
* are primary-pointer precision, contrast preference, and motion preference.
* are primary-pointer precision, contrast preference, motion preference, and
* platform (`ios` for the browsers of iPhone and iPad, `other` for the rest).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Adaptation rules have no platform axis: CONDITION_AXES is width, pointer, contrast and motion (themeAdaptations.ts:248-253), and normalizeCondition throws "… is not supported" for any other key. A theme author following this template would write when: {platform: 'ios'} and get an error. The base sentence without the platform clause is still accurate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file comes from #8076, which this pull request carries because it is stacked on it; the change belongs there and reaches this pull request when its base does.

'@astryxdesign/core': patch
---

[experimental] Two experimental subpaths for building touch presentations, outside the stable compatibility promise (they may change or be removed in a patch release). `@astryxdesign/core/motion/experimental` exports the compositor animator (`createAnimator`, `useAnimator`), UIKit's spring, rubber-band and cubic-bézier math, `resolveMotion` (a motion role's value under the element's theme), and the UIKit constants table as the read-only `uikit` namespace. `@astryxdesign/core/gestures/experimental` exports UIKit-style gesture recognizers (tap, long press, pan, screen-edge pan, swipe, pinch, rotation, hover), `useGestureRecognizer`, `usePressTracker`, `useTouchPresentation`, `readTouchPresentation`, `playHaptic` and `attachTapHaptic`, with the `GestureArena` and `VelocityStrategy` types their options reach. Nothing is added to the package root or to any other subpath.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

VelocityStrategy was removed later in this stack (see gesture-recognizer-fixes.md), and packages/core/package.json also adds the stable ./motion/reducedMotion.stylex subpath, so "Nothing is added to the package root or to any other subpath" is no longer true. The release notes would currently describe VelocityStrategy as both exported and removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Outdated: this pull request now carries main's version of this file unchanged, so it no longer shows in the diff.

Comment thread scripts/sync-exports.js Outdated
@@ -177,6 +190,13 @@ const STATIC_EXPORTS = {
types: './dist/utils/interactionOverlay.stylex.d.ts',
default: './dist/utils/interactionOverlay.stylex.js',
},
// Proposed; pending owner review (spec:AST-094 OQ4): the Reduce Motion

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-094 has only OQ1 and OQ2; there is no OQ4 recording this export. This stable subpath also has no @experimental marker and no changeset that announces it as an import path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file comes from #8076, which this pull request carries because it is stacked on it; the change belongs there and reaches this pull request when its base does.

@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 waits on an engineering owner's decision: measured against main, this pull request ships its whole chain of open predecessors, and no current spec governs the UIPickerView-style touch wheel, the defineTheme({motion: {springs, durations, curves}}) theme option, the press-feedback text-selection lock or the new astryxStylex undefinedVariables option. An engineering owner (the author may, as one) needs to identify the governing record for each, or approve one such as the draft spec:AST-094 in #8316, and record that decision on this pull request by commenting /approve-spec with the full head SHA. These changes are needed whatever that decision is:

  • Rebase once the predecessors land, so the diff is this pull request's own commit instead of 40,558 authored lines.
  • Load PickerWheel only when a coarse pointer needs it, or report and justify the roughly 28 KB gzip it adds to the DateInput, TimeInput and DateTimeInput imports (architecture:component-style-authoring INV6).
  • State an INV7 exception with element counts, or stop repeating the wheel's labels through ::before content.
  • Name the reason and show mouse before-and-after evidence for the new rest row (spec:AST-017 FR59), remove Lab's path to the velocity tracker (architecture:interaction-modality INV10), and fix the spec:AST-094 citations and the changeset that says no component reads the motion roles.

[Automated review]

import {focusOutlineStyles} from '../utils';
import {reducedMotionFade} from '../motion/reducedMotion';
import {useTouchPresentation} from '../utils/touchPresentation';
import {PickerWheel} from './PickerWheel';

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 static import puts the touch-only wheel, and through it the gesture recognizers, host and arena and the wheel's motion player and UIKit tables, into every DateInput, TimeInput and DateTimeInput import, desktop included (an esbuild estimate: about +28 KB gzip each). architecture:component-style-authoring INV6 asks a coarse-pointer-only feature over 2 KB gzip to load lazily, only when a coarse pointer needs it, or to justify the cost with base-against-head figures. PickerWheel.tsx:11-12 also says Wheel "loads it lazily", which it does not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not lazy: the touch column stays a static import behind isTouch, because an extra load on mobile is extremely costly. #8159's description gives the cost: DateInput grows by 24.2 KB gzip, about 16.7 KB of it gesture code every touch layer shares and a page pays once.

Comment on lines +203 to +207
rowLabel: {
'::before': {
content: 'attr(data-label)',
},
},

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:component-style-authoring INV7: "A pseudo-element carries decoration that has no semantic role and never repeats content." The cylinder rows here and the band copy's stripLabel (:251-256) repeat each visible option's label through content: attr(data-label) (22 repeats in the 24-row hour snapshot) beside the transparent options. Inside an open sheet that needs an exception stated with measured element counts for an owner decision, or a drawing that does not repeat the text.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In this push the band copy and the drum's label copies left with the UIKit look (#8480). Under today's look each row is its option, its label rendered once.

Comment thread packages/core/src/DateInput/Wheel.tsx Outdated
Comment on lines +406 to +411
const restIndex =
index === selectedIndex
? index
: nearestEnabledIndex(options, index, selectedIndex);
if (restIndex !== index) {
scrollToIndex(restIndex, 'smooth');

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 a mouse, a stop on a disabled row now travels on to the nearest enabled row instead of bouncing back. spec:AST-017 FR59 allows touch work to change fine-pointer behavior only to fix a defect those people hit or for a named cross-device consistency, with fine-pointer evidence before and after in the pull request. Please name the reason and show base and head mouse evidence, or keep this touch-only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Named and evidenced at the rule's origin, #8131: component:DateInput DEC-1 (cixzhang, 2026-10-25), with a trackpad before and after on desktop Chromium in #8131's description.

Comment thread packages/lab/tsconfig.json Outdated
Comment on lines +8 to +9
"@astryxdesign/core/gestures": ["../core/dist/gestures/index"],
"@astryxdesign/core/gestures/*": ["../core/dist/gestures/*"],

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 paths give Lab core's gesture barrel, which re-exports VelocityTracker (packages/core/src/gestures/index.ts:94) and says "Lab imports it". architecture:interaction-modality INV10: "Lab cannot import the tracker; a Lab gesture keeps one local tracker with the same blend and stillness interval." No Lab file imports it yet; dropping the gestures mappings (here and in tsconfig.build.json) keeps it that way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file comes from #8076, which this pull request carries because it is stacked on it; the change belongs there and reaches this pull request when its base does.

Comment thread .changeset/touch-motion-theme-roles.md Outdated
'@astryxdesign/cli': patch
---

[feat] Themes can retune touch motion roles. `defineTheme({motion})` takes `springs`, `durations` and `curves` keyed by motion role, the table path of a measured touch motion value (`springs.sheetPresent`, `navigation.transitionMs`), at the root or in an adaptation rule, and writes one `--motion-*` variable per role it sets. A role the theme leaves out keeps its measured default, an unknown role id is refused, and the duration scale (`fast`, `medium`, `ratio`) is optional when a theme sets only roles. No shipped component reads the roles yet. `astryx docs motion` lists every motion role with its CSS variable and default, and the theme authoring guide and template describe the new fields.

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 shipped component reads the roles yet" is untrue in the release this ships in: .changeset/wheel-uikit-picker.md says a theme retunes the wheel's picker.tapSpin.durationMs, timing.curves.easeInOut and scroll.bounce, which PickerWheel.tsx:287-288 and :661 read. Please drop the sentence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file comes from #8076, which this pull request carries because it is stacked on it; the change belongs there and reaches this pull request when its base does.

### Multiple fingers

A drag a recognizer has claimed keeps its finger: a second finger never
cancels or redirects it (`spec:AST-094` FR10), as in UIKit, where a pan

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-094 is not on main and not in this pull request (it is proposed in the open #8316), so citing its FR10 here, at :474, in GestureHost.ts:1573 and in two GestureHost.test.ts test names points readers at a rule they cannot find. Cite a current record, or state the behavior without a record id until one governs it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file comes from #8320, which this pull request carries because it is stacked on it; the change belongs there and reaches this pull request when its base does.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

DateInput (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 266B 266B 0B
Lines of Code 3532 5498 -
Complexity Very High Very High (485) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 56.9KB 57.2KB +294B 12.4KB 12.4KB 0B
core/AppShell 57.9KB 58.1KB +242B 11.6KB 11.6KB 0B
core/AvatarGroupOverflow 39.9KB 40.2KB +303B 6.4KB 6.4KB 0B
core/Banner 47.8KB 48.1KB +289B 9.9KB 9.9KB 0B
core/BaseTypeahead 54.3KB 54.6KB +251B 9.9KB 9.9KB 0B
core/BreadcrumbItem 66.4KB 66.6KB +297B 11.0KB 11.0KB 0B
core/Button 43.3KB 43.5KB +273B 9.2KB 9.2KB 0B
core/Calendar 54.9KB 55.2KB +290B 10.0KB 10.0KB 0B
core/Carousel 50.2KB 50.5KB +316B 10.2KB 10.2KB 0B
core/ChatComposer 60.6KB 60.9KB +270B 10.8KB 10.8KB 0B
core/ChatComposerInput 55.7KB 56.0KB +264B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.7KB 56.0KB +264B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.5KB 46.8KB +264B 9.5KB 9.5KB 0B
core/ChatLayout 48.8KB 49.1KB +273B 9.9KB 9.9KB 0B
core/ChatLayoutScrollButton 46.4KB 46.6KB +255B 9.6KB 9.6KB 0B
core/ChatSendButton 45.8KB 46.1KB +265B 9.2KB 9.2KB 0B
core/CheckboxInput 49.9KB 50.2KB +273B 9.3KB 9.3KB 0B
core/CheckboxListItem 54.0KB 54.3KB +244B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.1KB +289B 6.9KB 6.9KB 0B
core/CodeBlock 60.5KB 60.8KB +278B 10.7KB 10.7KB 0B
core/Collapsible 11.7KB 12.0KB +291B 4.2KB 4.2KB 0B
core/CommandPalette 50.9KB 51.2KB +281B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 8.7KB +280B 3.5KB 3.5KB 0B
core/ComplexSelector 55.8KB 56.1KB +318B 10.6KB 10.6KB 0B
core/ContextMenu 77.7KB 78.0KB +275B 14.6KB 14.6KB 0B
core/ContextMenuItem 16.6KB 16.8KB +280B 5.2KB 5.2KB 0B
core/DateInput 99.9KB 128.3KB +28.4KB ⚠️ 14.8KB 14.8KB +14B
core/DateRangeInput 69.4KB 69.6KB +266B 11.2KB 11.2KB 0B
core/DateTimeInput 110.1KB 137.9KB +27.8KB ⚠️ 15.2KB 15.2KB +15B
core/DialogHeader 48.8KB 49.1KB +280B 9.8KB 9.8KB 0B
core/DrawerHeader 48.9KB 49.2KB +249B 11.2KB 11.2KB 0B
core/DropdownMenu 72.7KB 73.0KB +278B 14.7KB 14.7KB 0B
core/DropdownMenuCheckboxItem 16.1KB 16.4KB +284B 5.6KB 5.6KB 0B
core/DropdownMenuItem 15.4KB 15.7KB +287B 5.2KB 5.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.5KB +275B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 58.1KB 58.3KB +270B 9.5KB 9.5KB 0B
core/FileInput 54.1KB 54.4KB +262B 10.3KB 10.3KB 0B
core/IconButton 43.3KB 43.6KB +273B 9.2KB 9.2KB 0B
core/Item 11.4KB 11.6KB +273B 4.7KB 4.7KB 0B
core/LayerProvider 63.3KB 63.6KB +288B 10.1KB 10.1KB 0B
core/Lightbox 51.2KB 51.4KB +249B 9.8KB 9.8KB 0B
core/Link 43.5KB 43.8KB +288B 7.6KB 7.6KB 0B
core/ListItem 53.5KB 53.7KB +268B 10.5KB 10.5KB 0B
core/Markdown 144.5KB 144.7KB +247B 17.4KB 17.4KB 0B
core/MobileNav 51.0KB 51.3KB +264B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.2KB 46.5KB +259B 9.4KB 9.4KB 0B
core/MoreMenu 72.9KB 73.2KB +278B 14.7KB 14.7KB 0B
core/MultiSelector 76.2KB 76.4KB +270B 14.6KB 14.6KB 0B
core/NumberInput 55.7KB 56.0KB +278B 10.7KB 10.7KB 0B
core/Outline 28.6KB 28.9KB +290B 4.0KB 4.0KB 0B
core/Pagination 87.7KB 88.0KB +263B 15.3KB 15.3KB 0B
core/Popover 49.8KB 50.1KB +238B 12.1KB 12.1KB 0B
core/PowerSearch 155.1KB 183.2KB +28.1KB ⚠️ 18.6KB 18.6KB +14B
core/RadioListItem 52.1KB 52.4KB +286B 9.7KB 9.7KB 0B
core/SegmentedControlItem 8.6KB 8.9KB +285B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 13.1KB +294B 7.1KB 7.1KB 0B
core/Selector 79.1KB 79.4KB +268B 15.0KB 15.0KB 0B
core/SelectorOption 14.5KB 14.8KB +251B 5.1KB 5.1KB 0B
core/SideNav 58.0KB 58.3KB +272B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.6KB 46.9KB +242B 9.5KB 9.5KB 0B
core/SideNavHeading 54.7KB 55.0KB +248B 10.1KB 10.1KB 0B
core/SideNavItem 54.5KB 54.8KB +266B 10.1KB 10.1KB 0B
core/Step 32.4KB 32.7KB +279B 5.3KB 5.3KB 0B
core/Stepper 47.4KB 47.7KB +265B 9.6KB 9.6KB 0B
core/Switch 48.4KB 48.7KB +285B 9.3KB 9.3KB 0B
core/Tab 9.7KB 10.0KB +294B 4.1KB 4.1KB 0B
core/TabMenu 51.6KB 51.9KB +276B 10.0KB 10.0KB 0B
core/Table 84.0KB 84.2KB +253B 15.1KB 15.1KB 0B
core/TableCell 78.8KB 79.1KB +267B 14.8KB 14.8KB 0B
core/TableHeaderCell 78.6KB 78.8KB +272B 14.8KB 14.8KB 0B
core/TableSelectionToolbar 52.4KB 52.6KB +278B 11.2KB 11.2KB 0B
core/TextInput 53.5KB 53.8KB +281B 10.5KB 10.5KB 0B
core/Thumbnail 50.7KB 50.9KB +274B 11.0KB 11.0KB 0B
core/TimeInput 86.8KB 114.1KB +27.2KB ⚠️ 13.8KB 13.8KB +30B
core/Timestamp 27.5KB 27.5KB -1B 9.5KB 9.5KB 0B
core/Toast 59.8KB 60.1KB +258B 9.7KB 9.7KB 0B
core/ToggleButton 44.1KB 44.4KB +243B 9.3KB 9.3KB 0B
core/Token 29.6KB 29.9KB +289B 4.5KB 4.5KB 0B
core/Tokenizer 64.8KB 65.1KB +306B 11.7KB 11.7KB 0B
core/TopNav 51.9KB 52.1KB +277B 10.4KB 10.4KB 0B
core/TopNavHeading 54.2KB 54.4KB +231B 9.9KB 9.9KB 0B
core/TopNavItem 9.0KB 9.3KB +283B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.2KB 54.4KB +268B 10.8KB 10.8KB 0B
core/TopNavMegaMenuItem 9.1KB 9.4KB +281B 4.1KB 4.1KB 0B
core/TopNavMenu 53.6KB 53.8KB +258B 10.1KB 10.1KB 0B
core/TreeList 32.5KB 32.8KB +274B 4.7KB 4.7KB 0B
core/Typeahead 61.5KB 61.7KB +268B 11.3KB 11.3KB 0B

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.

⚠️ Touch-only size budget

These components grew past the touch-only budget (2 KB gzip JavaScript or 1 KB gzip CSS per component). If the growth runs only on a coarse pointer, load its JavaScript lazily so the import does not carry it, or justify the cost in the pull request description; CSS stays in the package stylesheet, so justify it.

Component JavaScript CSS
core/DateInput +28.4KB ⚠️ +14B
core/DateTimeInput +27.8KB ⚠️ +15B
core/PowerSearch +28.1KB ⚠️ +14B
core/TimeInput +27.2KB ⚠️ +30B

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 34.6KB 34.6KB +21B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 502 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

@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 waits on an engineering owner's decision: measured against main, this pull request ships its whole chain of open predecessors, and no current spec governs the UIPickerView-style touch wheel, the defineTheme({motion: {springs, durations, curves}}) theme option, the press-feedback text-selection lock or the new astryxStylex undefinedVariables option. An engineering owner needs to identify the governing record for each, or approve one such as the draft spec:AST-094 in #8316, and record that decision on this pull request by commenting /approve-spec with the full head SHA; the author may do so as an engineering owner. These changes are needed whatever that decision is:

  • Rebase once the predecessors land, so the diff is this pull request's own commit instead of about 43,000 authored lines.
  • Load PickerWheel only when a coarse pointer needs it, or justify the roughly 28 KB gzip it adds to the DateInput, TimeInput, DateTimeInput and PowerSearch imports (architecture:component-style-authoring INV6).
  • Record an INV7 exception with element counts, or stop repeating the wheel's labels through ::before content.
  • Show base-and-head mouse evidence for the new rest row (spec:AST-017 FR59), keep the velocity tracker out of Lab's import route (architecture:interaction-modality INV10), and make the motion-role docs and examples use roles a shipped component reads.

[Automated review]

Comment thread packages/core/src/theme/defineTheme.ts Outdated
Comment on lines +259 to +264
* // A softer sheet, and a longer navigation transition (it paces
* // completion callbacks):
* motion: {
* springs: { 'springs.sheetPresent': { duration: 0.6, bounce: 0.1 } },
* durations: { 'navigation.transitionMs': 300 },
* }

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 shipped component reads springs.sheetPresent or navigation.transitionMs: only the picker wheel resolves motion roles (picker.tapSpin.durationMs, timing.curves.easeInOut, scroll.bounce), so a theme author who copies this example to soften a sheet sees nothing change. Please use a role a component reads, or say which component reads each role; the same example is in packages/cli/assets/theme.template.ts:191-192.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file comes from #8258, which this pull request carries because it is stacked on it; the change belongs there and reaches this pull request when its base does.

@vjeux
vjeux force-pushed the uikit/c21-wheel-listbox branch from 58307a4 to f45f58e Compare October 26, 2026 02:52
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
… listbox hook) into uikit/c21-wheel-short-loop

The touch column's files take meta#8361's side: the short loop below was drawn
as one row per position on the cylinder, which today's look has no place
for. The next commit repeats a short looping list on today's strip.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 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 waits on an engineering owner: measured against main, this pull request ships its open predecessors, and two parts of that stack have no current spec, the rest rule in the draft component:DateInput FR13/DEC-1 and component:DateTimeInput FR8 (which also changes where a wheel driven by a mouse or trackpad comes to rest) and 29 theme motion roles that spec:AST-094 FR4 does not list. An engineering owner records the decision on the rest rule by commenting /approve-spec with the full head SHA (the author may do so as an engineering owner) and names the record that admits those roles. Whatever that decision, please:

  • Remove the packages/build/README.md paragraph about an undeclared-variable warning the build no longer emits.
  • Use motion roles a shipped component reads in the defineTheme and theme-template examples, and drop "no component uses it yet" from the gesture changesets the wheel now uses.
  • Rebase once #8162 and its predecessors land, so the diff is this pull request's own 299 lines rather than about 41,000.

[Automated review]

Comment thread packages/build/README.md Outdated

Unsupported declarations fail the build by default with StyleX's replacement guidance. Set `propertyValidationMode` explicitly in `stylexOverrides` (modern API) or `stylexOptions` (legacy API) only when a migration needs the upstream `warn` or `silent` behavior.

A production build also checks the StyleX stylesheet for generated custom properties (`--<prefix><hash>`) that it reads but no stylesheet in the build declares, which browsers drop: a `.stylex.ts` export read in a style that is not a `defineVars`/`defineConsts` value, or one whose module never reached the build. It warns, naming the variable and the rule that reads it. Only stylesheets count as declarations, so a property the app sets at runtime (an inline style) is reported too: give it a name outside the StyleX class-name prefixes.

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 paragraph describes the undeclared-variable warning that the revert "astryxStylex keeps no undeclared-variable warning" removed; vite.ts no longer checks for it, so readers are promised a warning they never get. Please delete it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Gone in this head: the merge of #8320 2cac769 brings #8076's 6cee78a, which removes the paragraph.

Comment thread packages/core/src/theme/defineTheme.ts Outdated
Comment on lines +259 to +264
* // A softer sheet, and a longer navigation transition (it paces
* // completion callbacks):
* motion: {
* springs: { 'springs.sheetPresent': { duration: 0.6, bounce: 0.1 } },
* durations: { 'navigation.transitionMs': 450 },
* }

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 shipped component reads springs.sheetPresent or navigation.transitionMs yet; only the touch wheel resolves roles (picker.tapSpin.durationMs, timing.curves.move, scroll.bounce). A theme author who copies this sees nothing change. Please use a role a component reads, or say which component reads each; the same example is in packages/cli/assets/theme.template.ts:189-192.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Gone in this head: the base keeps touch motion internal (#8320 2cac769), so defineTheme({motion}) takes only the duration scale and this example is removed. The wheel's own values are internal constants too (024b4ef, 89b190a).

Comment thread .changeset/gesture-pan.md Outdated
'@astryxdesign/core': patch
---

[chore] Internal: the pan gesture recognizer, with UIKit's measured hysteresis and velocity filter. Not exported from the package entry point; no component uses it yet.

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 touch wheel in this stack is built on the pan recognizer (PickerWheel.tsx:81), so "no component uses it yet" would be wrong in the release notes. The same clause is in gesture-tap-long-press.md, gesture-host.md, gesture-host-fixes.md, gesture-hook-fixes.md and touch-selection-lock-gestures.md.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The base rewrote these notes (#8320 2cac769, with #8076's 8d9fb69): gesture-pan.md now reads "Internal: the pan gesture recognizer, with UIKit's measured hysteresis and velocity filter. Not exported from the package entry point.", and none of the notes you list says that no component uses it.

| FR10 | Standalone mode MUST compose Field; admitted InputGroup mode MUST remove competing Field chrome while preserving name, description, value, keyboard, focus, and editing semantics. | `family:input-fields/FR3`; current source and InputGroup tests | Settled shared rule and verified adoption |
| FR11 | Clear MUST emit `undefined` through the ordinary change pipeline. The pointer and bottom-sheet surfaces restore field focus using their shipped timing; the native surface MUST NOT reclaim focus because doing so reopens the OS picker. Implementation gap: when the field becomes disabled or busy while its clear control holds focus, the control unmounts and focus falls to the document body; `spec:AST-085/FR4` requires focus to stay or move to a destination this record names. | Current source and focused clear tests; `spec:AST-085/FR4` | Verified shipped behavior |
| FR12 | No surface renders native `disabled`. A disabled field MUST stay focusable with `aria-disabled`, block typing, picking, clearing, and opening, describe its disabled reason when one is given, and take disabled paint. While busy (`isLoading` or a pending Action) the pointer and bottom-sheet surfaces expose `aria-disabled` and `aria-busy`, stay focusable, and block new commits; the native surface does the same for `isLoading`, and during a pending Action stays editable so the OS picker remains attached but rejects additional commits until the Action settles. A busy field that is not disabled keeps its ordinary paint and Spinner, shows no hover ring while commits are blocked, and keeps its keyboard focus ring. | `spec:AST-085/FR1–FR6`; `family:input-fields/FR4–FR6`; `family:buttons/FR4`; current source and focused tests | Settled shared rule and verified adoption |
| FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. | DEC-1; UIDatePicker on the iOS 26.2 simulator; wheel unit and real-browser tests | Settled by DEC-1 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR13 and DEC-1 are new requirements in a draft record, and they also change where a wheel driven by a mouse or trackpad comes to rest. They govern once an engineering owner approves this head with /approve-spec <full head SHA>, or amends them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Named and evidenced at the rule's origin, #8131: component:DateInput FR13 and DEC-1 (cixzhang, 2026-10-25) make the rest the same for every pointer, and #8131's description shows a trackpad on desktop Chromium before (back to 10) and after (settles on 9), the fine-pointer evidence spec:AST-017 FR59 asks for. This pull request carries #8131 because it is stacked on it.

vjeux added 24 commits October 29, 2026 10:16
…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.
Takes meta#8360's head, which removes a document's or shadow root's
touch-action rules with its last marked element and names the build
changeset's treeshakeCompensation default, in place of 1e1431f.
…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.
…e turn

- The wheel reads the pointer type on the way down. usePointerDragScroll
  stops a primary mouse press from bubbling, so after a touch a mouse
  drag's travel still cross-faded on a fine pointer under reduced motion.
- With meta#8483 merged in, the travel waits for a mouse drag's release instead
  of running whenever the hand pauses; a test covers it here.
- DateInput's Owns line claims the shared wheel's mechanics; the TimeInput
  and DateTimeInput records own their sheets' time wheels.
- FR13 and DateTimeInput FR8 say a shown wheel turns onto a value changed
  from outside, which the code and its tests now cite.
…ps its mark after a nudge

- A value changed from outside while the wheel is shown (a bound clamping
  the time after another wheel turned) turns the wheel onto its row, as
  UIDatePicker does and FR13 now says. The first park when the wheel is
  shown stays at once.
- A motion that ends on the row it began on (a nudge that eases back, a
  drag off a row and back) gives the band's mark back to that row: the
  player forgets its last report when a motion starts, so the row it ends
  on is reported again.
- The ctrl-wheel note cites FR43 (a ctrl-wheel is a pinch) and FR10 (with
  no pinch of its own, the wheel leaves it to the page's zoom).
…sheet on the desktop viewport

This file's mouse cases run on a desktop viewport, where the field shows
its popover unless it asks for the sheet; the case meta#8483 added now asks
for it, as its neighbours do.
…e new foundation) into uikit/c21-wheel-proofs
… FR5 names

The coast's proof now blocks the main thread for 1 s, the length spec:AST-094
FR5's verification names, ending while the coast is still over a row from
rest so every frame moves a pixel or more. It follows the row that crosses
the band halfway through, which stays in sight the whole second. The WebKit
skip says what covers Mobile Safari instead: an iOS simulator capture through
a 530 ms block.
The four Effects without a note now say what they synchronize: the latest
props for callbacks attached once, the band's mark with the row the motion
reports, the motion's end with the wheel's, and the latest touch-down
handler.
The touch column's options lie under its drawn rows, so the proof taps the
PM row's place on the screen, where the column's tap recognizer takes it,
rather than asking for a tap on the option element.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 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 waits on an engineering owner's decision: through its open predecessors this pull request ships the draft component:DateInput FR13/DEC-1 and component:DateTimeInput FR8, under which a bounded sheet wheel that stops on a disabled value moves on to the nearest allowed one, for a mouse or trackpad in a forced bottom sheet as well as a finger. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner; otherwise an owner asks for the records to be amended. The useWheelListbox refactor itself keeps both columns' markup, and these changes are needed whatever the decision:

  • Rebase once #8162 and the pull requests below it land, so the diff shrinks to the hook's 306 lines; against main it is 35,318 authored lines.
  • spec:AST-094 FR4 asks every duration a touch presentation plays to be a motion role, but the tap spin's 300 ms still comes from an unregistered table value (PickerWheel.tsx:226).
  • Drop “no shipped component reads the roles yet” from motion/README.md and motion/roles.ts, and name what the unmount Effect at Wheel.tsx:317 releases.

[Automated review]

// The running dip, so a step during its fade-in starts the next one where
// the column is.
const dipRef = useRef<Animation | null>(null);
useEffect(

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:react-component-runtime INV3: “A new or changed Effect MUST name the external system it synchronizes.” One line saying this releases the pending reduced-motion dip's timer when the wheel unmounts would do; every Effect in PickerWheel.tsx now has one.

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:35
@vjeux

vjeux commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Mapped to 811a02f:

Open: the tap spin's duration as a role (FR4 names no picker role, and roles.test.ts holds the roles to its list), and approval of the draft FR13, DEC-1 and DateTimeInput FR8, which stays with an engineering owner. The compare carries #8162 and the stack below until they land.

This branch was successfully deployed

1 active deployment
Preview — 811a02fd Deployed Oct 29, 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. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants