Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
Approval needs an engineering owner's decision first: the wheel math rests only on the draft spec AST-094 that this branch adds, so an engineering owner has to approve or amend its FR1–FR3 and FR5, and its FR4, FR11 and FR14 need the current theme-tokens INV3, user-states DR1 and AST-025 FR14 updated or dropped from the draft; an engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which the author, as an engineering owner, may do. Whatever that ruling, three changes are needed:
- Land or rebase past the 17 stacked pull requests so this diff is only its own 1,971-line commit; as issued it merges 26,602 authored lines and an older AST-094 revision than the one in #8052.
- Get
lintgreen:eslint . --cacheaborts with a JavaScript heap out-of-memory error at this head, while it passes on the base. - Project the release in
wheelPhysics.tswithr ÷ (1 − r), aspicker.tsdocuments andmotion/projection.tscomputes; the current constant lands 0.41–0.48 px past UIKit's recorded targets.
[Automated review]
| if (speed <= stopSpeed) { | ||
| return offset; | ||
| } | ||
| return offset + (Math.sign(velocity) * (speed - stopSpeed)) / NORMAL_LAMBDA; |
There was a problem hiding this comment.
This projects with the continuous decay constant (÷ λ, 499.5 ms of release speed), but picker.ts:118-121 records UIKit's target as (v − stopSpeed) × r ÷ (1 − r) (499 ms), which projectDistance in motion/projection.ts already computes. On the three recorded releases in wheelPhysics.test.ts this lands 0.41–0.48 px past UIKit's targets, where the documented formula is within 0.15 px, and the 0.1 % error moves the chosen row on about 1.6 % of 500 px flicks. Suggest return offset + Math.sign(velocity) * projectDistance(speed - stopSpeed);, the matching header change at lines 27-28, and a 0.3 px tolerance in the projected-target test.
There was a problem hiding this comment.
Fixed in 420f6ab: projectRelease now uses projectDistance (r ÷ (1 − r) ms of speed), and the header says so. The recorded-target test is tightened from 1 px to 0.3 px; it fails with the old constant (0.45 and 0.41 px off on two of the three recordings) and passes now.
| /** Release every resource; the rows stay where they are. */ | ||
| dispose(): void { | ||
| const motion = this.motion; | ||
| if (motion != null) { | ||
| const offset = this.offset(); | ||
| this.finish(motion); | ||
| this.restingOffset = offset; | ||
| } | ||
| } |
There was a problem hiding this comment.
After finish cancels the animations, the rows show the inline end state play wrote at lines 221-222 (curve.to), while restingOffset becomes the mid-flight offset, so the rows do not stay where they are and a later attach jumps them back. Calling this.render(offset, true) after finish would make the comment true.
There was a problem hiding this comment.
Fixed in 420f6ab: dispose() renders the offset it captured after finish, so the rows stay where they are shown. New wheelMotion.test.ts disposes a turn 400 ms in and checks the strip shows that offset; it fails before the change (the strip showed the turn's end, -640 px).
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewModified ComponentsDateInput (@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. Bundle Size SummaryGzip figures measure each package's entry file, not the code an import pulls in.
Accessibility AuditStatus: No accessibility violations detected. Visual Regression55 of 502 shot(s) changed. A change here is a question, not a failure: check whether the after is the
and 35 more. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
5fee308 to
29b11c5
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the draft spec AST-094 that this branch adds: the wheel math rests only on its FR1–FR3 and FR5, and its FR4, FR11 and FR14 would change the current theme-tokens INV3, user-states DR1 and AST-025 FR14, so an engineering owner needs to approve or amend those requirements by commenting /approve-spec <full head SHA> on this pull request, which the author, as an engineering owner, may do. Whatever that decision, three changes are still needed:
- Land or rebase past the 17 stacked pull requests so this diff is only its own 1,983-line commit; as opened it merges 26,614 authored lines and an older AST-094 than the one in #8052.
- Get
lintgreen: ESLint runs out of JavaScript heap at this head, while the same job passes onmain. - Project the release in
wheelPhysics.tswithr ÷ (1 − r), aspicker.tsdocuments andprojectDistancecomputes; the current constant lands 0.41–0.48 px past UIKit's recorded targets.
[Automated review]
29b11c5 to
eaac590
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the draft spec AST-094 that this branch adds: it is the only authority for this code, its FR4 forbids the theme-tuned touch motion the same head ships, and its FR11 and FR14 change current press and scroll-chaining rules. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner, or names the claims that must change. Whatever that decision, these changes are still needed:
- Land or rebase past the 21 stacked pull requests so this diff is the wheel's own commit; as issued it merges 30,334 authored lines.
- Make the
adaptivecompact test follow current AST-012 FR9 (width below the theme'smdpoint, 768 px by default, or height below 500 px); at 744–767 px it opens the popover where FR9 asks for the sheet. - Drop the
platformadaptation condition or add it to AST-012 FR2, whose list of condition fields is closed. - Stop
astryx docs motionfrom saying sheets, menus and navigation already play the measured touch motion, since no component reads a motion role yet. - Project the wheel's release with
r ÷ (1 − r), aspicker.tsdocuments; the current constant lands 0.41–0.48 px past UIKit's recorded targets.
[Automated review]
| export const COMPACT_QUERY = '(width < 744px), (height < 500px)'; | ||
|
|
||
| /** Both: presentations such as a sheet in place of a popover. */ | ||
| export const COMPACT_TOUCH_QUERY = | ||
| '(width < 744px) and (pointer: coarse), (height < 500px) and (pointer: coarse)'; |
There was a problem hiding this comment.
Current AST-012 FR9 makes compact a width below the theme's effective md point (768 px by default) or a height below 500, with no resolver keeping size numbers of its own, and its DEC-6 rejects mapping compact to size classes. With 744 here, a coarse screen 744–767 px wide (an iPad mini in portrait) gets the popover where FR9 asks for the sheet, and a theme that moves md changes nothing. Suggest deriving compact from the theme's md and the 500 height edge, then updating the menu docs, the stories and touchPresentation.test.ts:328.
| * 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.
AST-012 FR2 limits a when object to width, height, pointer, contrast and motion, and says future fields need an explicit addition to that closed list. platform is not in it, so either drop it from this stack or bring the FR2 amendment for an engineering owner to approve.
There was a problem hiding this comment.
Not in this pull request's commits: themeAdaptations.ts and its platform field come from #8076's stack. The FR2 amendment or the removal belongs to that change.
| content: [ | ||
| { | ||
| type: 'prose', | ||
| text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.', |
There was a problem hiding this comment.
At this head no component reads a motion role (only the new DateInput/wheel* files import the motion foundation), so this tells theme authors that sheets, menus and navigation already play the measured motion and that a retune takes effect, which it does not yet. Say the roles apply as touch presentations adopt them, or hold the section until one does.
|
Posted by Cindy's agent on her behalf. Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:
This is about where the behavior lives, not about the PR's intent. |
|
Agreed: one implementation.
This lands in the restack onto #8076 (1203525). I'll add the commit here once it's pushed. |
eaac590 to
420f6ab
Compare
There was a problem hiding this comment.
Changes are needed before this can merge. The wheel's table, release math and drum follow spec:AST-094, but compared with main this pull request still carries the whole unmerged stack beneath it (31,519 authored lines, about 29,300 of them from #8053 through #8320), and the wheel's own 2,214 lines still need a split, for example the player (wheelMotion.ts) apart from the table, physics and drum.
WheelMotion.crossFademoves the rows in asetTimeoutat the fade's midpoint (wheelMotion.ts:364-370), so on a busy main thread a reduced-motion wheel fades back in on the old row and then jumps; please bake the landing into compositor keyframes, asplay()already does for its end state.- The wheel modules' headers still name
Wheel.tsx,Wheel.test.tsxandPickerWheel.tsxas consumers or SYNC targets, and list a release-velocity export that no longer exists.
[Automated review]
| motion.timers.push( | ||
| window.setTimeout(() => { | ||
| if (this.motion === motion) { | ||
| onMidpoint?.(); | ||
| this.render(to, true); | ||
| } | ||
| }, durationMs / 2), |
There was a problem hiding this comment.
The rows land here on a main-thread timer while the fade itself runs on the compositor. If the main thread is blocked across the midpoint, the fade comes back in on the old row and the rows jump when the block ends, which is what design:reduced-motion DR2 ("An entering element appears in its final place and fades in") and AST-094 FR5 rule out. Writing the destination transforms inline first and giving each moved row and the strip a transform animation that holds the origin until 50% and then steps would keep the landing off the main thread, as play() does for its end state.
There was a problem hiding this comment.
Fixed in 6c9ac0c, in #8526, where the player now lives: the destination transforms go inline first, and each moved row and the strip hold the origin to the fade's midpoint, then step onto it, on the compositor, as play() bakes its end. Without Web Animations (jsdom) the timer still lands them. The new unit case fails without the fix (with no timer run, the strip still showed the origin). #8162 adds a Chromium case that blocks the main thread for 700 ms across the midpoint: every frame of the fade coming back in has the tapped row at the band. On the build without the fix it fails, the row still 28 px below the band 187 ms in.
| * @output Exports the curves a picker wheel moves on: the release plan (a | ||
| * coast onto a row, a snap, a coast into an end and its bounce, a return | ||
| * from past an end), the tap spin, the rubber band while dragging, the | ||
| * release velocity and the row crossings that tick | ||
| * @position Pure math of the Wheel (Wheel.tsx); the drum (wheelDrum.ts) |
There was a problem hiding this comment.
This header still lists "the release velocity", which the module no longer exports, and names Wheel.tsx as the consumer. The head's Wheel.tsx is the scroll-snap column and imports none of these modules. The same stale consumer appears in wheelDrum.ts:11, wheelMotion.ts:11 and the SYNC lists, along with Wheel.test.tsx and PickerWheel.tsx (picker.ts:32), which do not exist at this head.
There was a problem hiding this comment.
Fixed in 6cf08e9: wheelPhysics.ts no longer lists a release velocity, and wheelPhysics.ts, wheelDrum.ts and picker.ts name no consumer; their SYNC lists hold only files present here. #8159, where PickerWheel.tsx arrives and reads them, names it again (f069d97). wheelMotion.ts moved to #8526 with the player, its header naming the touch picker wheel.
|
On the review's points about this pull request's size:
|
There was a problem hiding this comment.
Changes are needed before this can merge: the wheel's table, release math and drum follow spec:AST-094 and reproduce UIKit's recorded releases, but compared with main this pull request still carries the 15 unmerged pull requests beneath it (28,540 authored lines, 26,944 of them the stack's). Everything the wheel imports beyond its own files is already on main, so rebasing its commits onto main, or landing the stack first, leaves a 1,596-line diff.
- Please also drop the release velocity from the
uikit/picker.tsrow ofmotion/README.mdand from thewheelPhysics.test.tsheader, which no longer describe what this pull request holds.
[Automated review]
| | `uikit/timing.ts` | Non-spring clocks and curves: press highlight, Core Animation's named curves, keyboard, Reduced Motion cross-fade. | | ||
| | `uikit/navigation.ts` | UINavigationController: push velocity, parallax, dimming, transition clock, interactive pop finish rule. | | ||
| | `uikit/gestures.ts` | Gesture recognizer thresholds, the velocity filter and haptic stand-ins, as `{value, unit}` entries (sources in JSDoc). | | ||
| | `uikit/picker.ts` | UIPickerView: the wheel geometry and band, the release velocity and its floor, the coast's stop speed, the snap at rest, the tap spin, the tick. | |
There was a problem hiding this comment.
picker.ts holds the velocity floor (release.minimumSpeed) but no release velocity: the new Picker section of MEASUREMENTS.md says the release weighting stays with the gesture system. Dropping “the release velocity and” from this row keeps it accurate.
There was a problem hiding this comment.
Fixed in e3f1c20: the picker row lists the velocity floor without a release velocity, which stays with the gesture system.
| * @input wheelPhysics.ts, the gesture system's velocity tracker, and touch | ||
| * streams and per-frame offsets recorded from a UIPickerView | ||
| * @output Proves the Wheel's curves reproduce UIKit's measured picker: the | ||
| * release velocity, the projected target, the coast onto the row, the coast | ||
| * into an end, the snap, the rubber band, the tap spin and the ticks | ||
| * @position Unit coverage for DateInput/wheelPhysics.ts; the browser specs | ||
| * prove the same curves on screen |
There was a problem hiding this comment.
This header names the gesture system's velocity tracker as an input and says the file proves the release velocity and that browser specs prove the curves, but the file imports neither, and the recorded stream arrays are never read. Dropping those parts (and the unused stream fields), or naming where the velocity is proven, keeps the header true to what the file checks.
There was a problem hiding this comment.
Fixed in a279dbf: the header names what the file reads (UIKit's recorded frames, with the velocity UIKit handed its delegate) and what it proves, without the velocity tracker or the browser specs, and the recordings' unread stream fields are gone.
motion/uikit/picker.ts holds UIPickerView's measured values, each sourced: 32 pt rows on a 97.2 pt cylinder at perspective 900, a 34 pt band inset 9 pt, the band row magnified 23.5/21, dimmed rows at 0.447, a 291 pt rubber band, the coast's stop speed and velocity floor, the snap at rest, a 0.3 s tap spin and a 40 ms tick (iOS 26.2 simulator, real HID touches, Oct 23 2026). Every value is an internal constant: spec:AST-094 FR4 names none of them, so nothing registers this table (DEC-11). The UIKit index exports it, and MEASUREMENTS.md and the motion README list it.
…IKit's recordings wheelPhysics.ts plans a release as UIPickerView does: it projects the rest at the normal deceleration rate (0.998), snaps it to a row and coasts with the rate re-solved to stop there; a slow release snaps; a release past an end bounces on scroll.bounce, and a bounce spring not settled by the animator's 30 s limit snaps onto its end there. It also draws the rubber band, the tap spin and the row crossings that tick. wheelDrum.ts puts rows on the cylinder, finds the row under a point and bakes a curve into per-row keyframes. Their tests replay UIKit's recorded releases: coasts within 0.8 pt RMS, snaps within 0.4 pt, transforms within 0.3 degrees. Nothing imports them yet; the touch picker wheel does.
aaaa5c1 to
a279dbf
Compare
Moving onto main carried an older header for the UIKit index: it dropped the one constant per spring (SHEET_PRESENT_SPRING, …) that main's header lists. The header is main's again, with `picker` added. The bounce curve's comment no longer names motion/resolveMotion.ts, which is not on main: its caller resolves the spring.
There was a problem hiding this comment.
Recommended for approval: the picker table, release math and drum follow spec:AST-094, and the release plan reproduces UIKit's recorded targets and coasts. Two small header fixes are worth folding in: motion/uikit/index.ts should keep listing the per-spring constants it still re-exports, and wheelPhysics.ts should drop its reference to motion/resolveMotion.ts, which does not exist. When the player and the picker wheel play the 300 ms tap spin and the scroll.bounce spring, AST-094 FR4 expects them to go through motion roles.
[Automated review]
| * @output Re-exports `springs`, `scroll`, `timing`, `navigation`, `picker` and | ||
| * the gesture thresholds (one `UIKIT_*` group per recognizer, their union |
There was a problem hiding this comment.
This header no longer mentions the per-spring constants the file still re-exports (ACTION_SHEET_DISMISS_SPRING … VIEW_CHANGE_SPRING, lines 43–79), which it listed before. Suggest keeping “one constant per spring (SHEET_PRESENT_SPRING, …)” and adding picker beside it.
|
|
||
| /** | ||
| * The bounce spring, from `from` with `velocity`, onto `to`. `bounce` is | ||
| * `scroll.bounce`'s spring as the wheel resolves it (motion/resolveMotion.ts). |
There was a problem hiding this comment.
motion/resolveMotion.ts does not exist on main or in this pull request, and registerScrollTable registers no role for scroll.bounce, so nothing resolves this spring. Suggest describing bounce as a spring the caller may hand, scroll.bounce by default; the same phrase recurs at line 312.
# Conflicts: # packages/core/src/DateInput/wheelPhysics.test.ts # packages/core/src/DateInput/wheelPhysics.ts # packages/core/src/motion/README.md # packages/core/src/motion/uikit/index.ts
There was a problem hiding this comment.
Changes requested for one fix: the picker table and release math follow spec:AST-094 and reproduce UIKit's recorded releases, but rowAtPoint in DateInput/wheelDrum.ts ignores the 900 px perspective the drum is drawn through. Drawn as the module describes, the fourth row above and below the band sits 84.7–89.3 px from its centre, and rowAtPoint answers the third row for almost all of it, so a tap there would turn the wheel to the wrong row. Please make the hit test projection-aware (solve dy = R sin θ · p ÷ (p + R(1 − cos θ)) for θ before converting to a distance, or test each visible row's projected extent) with a test against projected positions, or leave rowAtPoint out until a drum consumer needs it. When the player and picker wheel play the 300 ms tap spin and the scroll.bounce spring, AST-094 FR4 expects them to go through motion roles.
[Automated review]
| if (Math.abs(dy) >= geometry.radius) { | ||
| return null; | ||
| } | ||
| const distance = geometry.radius * Math.asin(dy / geometry.radius); |
There was a problem hiding this comment.
This inverts the cylinder orthographically, but the dimmed copies are drawn through the picker's 900 px perspective (lines 18-19), which moves row centres 0.18, 1.31, 3.76 and 7.04 px toward the band for rows ±1 to ±4, not under a pixel as the comment above says. Drawn with rowOrigin and rowTransform in Chromium, the fourth row from the band spans 84.7–89.3 px and this returns the third row for about 89% of it, including its middle. Solving dy = R·sin θ · p / (p + R·(1 − cos θ)) for θ before converting to a distance (or hit-testing each visible row's projected extent) fixes it.
There was a problem hiding this comment.
Fixed in e5bb4b6: rowExtent gives where CSS draws a row (laid out at the band's centre, turned by rowTransform about rowOrigin's origin, seen through the perspective from the band's centre), and rowAtPoint answers the row drawn under the point: inside the band, the band's flat strip; outside it, the drum row whose drawn extent holds the point, the nearer one in the sliver between two rows, nothing past the outermost row. The tests compose the CSS transforms from the strings the drum hands CSS and sweep every 0.25 px of the column; the fourth row's middle now answers the fourth row (before: the third). In Chromium, painting the rows as the drum tells CSS to, every painted device pixel down the drum answers its row (2,866 of 2,869 at DPR 4, the other three on the seams between two rows), against 392 misses before.
…he perspective rowAtPoint inverted the cylinder orthographically, but the rows above and below the band are drawn through the picker's 900 px perspective, which draws them nearer the band: the fourth row from the band spans 84.7-89.3 px, and rowAtPoint answered the third row for 89% of it, middle included. rowExtent now gives where CSS draws a row (laid out at the band's centre, turned by rowTransform about rowOrigin's origin, seen through the perspective from the band's centre), and rowAtPoint answers the row drawn under the point: inside the band, the band's flat strip; outside it, the drum row whose drawn extent holds the point, the nearer one in the sliver between two rows, and nothing past the outermost row. The tests compose the CSS transforms from the strings the drum hands CSS and sweep every 0.25 px of the column at two offsets; without the fix the fourth row's middle answered row 34 instead of 35. In Chromium, painting the rows as the drum tells CSS to, every painted device pixel down the drum answers its row (2,866 of 2,869 at DPR 4, the other three on the seams between two rows), against 392 misses before.
|
On the motion roles: the merged decision covers both values. Under #8257 the touch motion tables register only |
There was a problem hiding this comment.
Recommended for approval: the picker table, the release math and the drum follow spec:AST-094, the release plan reproduces UIKit's recorded targets and coasts, and rowAtPoint now answers the row drawn under a tap through the drum's perspective. One thing to settle before #8159 plays the plan on its 28 px rows: wheelPhysics.ts scales the measured velocity floor and stop speed with the row height, so the floor drops from 250 to 218.75 px/s there; please keep the speeds at their measured values or label the scaling a guess with a measurement task, as AST-094 FR1 and FR2 ask. Whether the 300 ms tap spin and scroll.bounce must be motion roles under AST-094 FR4 is for the pull request that plays them.
[Automated review]
| curve: bounceCurve(offset, velocity, edge, scale, bounce), | ||
| }; | ||
| } | ||
| const minimumSpeed = picker.release.minimumSpeed * 1000 * scale; |
There was a problem hiding this comment.
This scales UIKit's measured 0.25 px/ms floor by the row height over 32, so #8159's 28 px rows get 218.75 px/s, and lifts between 219 and 250 px/s coast about four rows where the measured floor would snap them. AST-094 FR1 asks for the measured value by default and FR2 for a source or a guess label on any other; keeping the speeds unscaled (scaling only lengths such as the rubber-band dimension), or recording the scaling as a guess with a measurement task, would cover it. The stop speed at lines 199 and 301 scales the same way.
The look's drum takes meta#8156's projection-aware hit test (rowExtent, and rowAtPoint answering the row drawn under a point through the perspective), keeping the look's own semantics: a position along the wheel, a copy at every position of a looping wheel. Its drum tests add meta#8156's. # Conflicts: # packages/core/src/DateInput/wheelDrum.ts
The look's drum takes meta#8156's projection-aware hit test (rowExtent, and rowAtPoint answering the row drawn under a point through the perspective), keeping the look's own semantics: a position along the wheel, a copy at every position of a looping wheel. Its drum tests add meta#8156's. # Conflicts: # packages/core/src/DateInput/__snapshots__/PickerWheel.test.tsx.snap
# Conflicts: # packages/core/src/motion/README.md
There was a problem hiding this comment.
Recommended for approval: the picker table, the release math and the drum follow spec:AST-094, and the release plan reproduces UIKit's recorded targets and coasts. One thing to settle in this file before #8159 plays the plan on its 28 px rows: wheelPhysics.ts scales the measured velocity floor and stop speed with the row height, so the floor drops from 250 to 218.75 px/s there; keep the speeds at their measured values, or label the scaling a guess with a measurement task, as AST-094 FR1 and FR2 ask. Whether the 300 ms tap spin and scroll.bounce must be motion roles under FR4 is for the pull request that plays them.
[Automated review]
| curve: bounceCurve(offset, velocity, edge, scale, bounce), | ||
| }; | ||
| } | ||
| const minimumSpeed = picker.release.minimumSpeed * 1000 * scale; |
There was a problem hiding this comment.
This scales the measured 0.25 px/ms floor by the row height over 32, so a 28 px wheel (#8159's WHEEL_ROW_PITCH) coasts lifts from 218.75 px/s that UIKit's measured floor snaps; stopSpeed at lines 199 and 301 scales the same way. AST-094 FR1 asks for the measured value (and FR2 for a labelled guess with a measurement task otherwise), so consider keeping both speeds unscaled and scaling only the lengths that follow the drawn size.
# Conflicts: # packages/core/src/motion/README.md
The look's drum takes meta#8156's projection-aware hit test (rowExtent, and rowAtPoint answering the row drawn under a point through the perspective), keeping the look's own semantics: a position along the wheel, a copy at every position of a looping wheel. Its drum tests add meta#8156's. # Conflicts: # packages/core/src/DateInput/wheelMotion.test.ts
Base:
main. #8526 (the player) and #8159 (the touch wheel) build on it.User need
On a phone, people expect the date and time sheets' wheels to behave like the iOS picker. Today's wheel is a scroller with CSS snapping: a flick travels about a fifth as far as on iOS and can stop on a different row. This adds UIPickerView's measured values and the math behind its motion, for #8159's touch wheel; nothing changes on screen by itself.
Problem and solution fit
What led to it: Astryx's UIKit-parity work (
spec:AST-094) asks the touch wheel to move like UIPickerView, and frame-by-frame recordings of UIKit's picker (iOS 26.2 simulator) showed three things a snapping scroller can't match: how fast a coast slows, where it settles, and the cylinder the rows sit on.motion/uikit/picker.ts: UIPickerView's values (row and cylinder size, band, rubber band, stop speeds, tap spin), each with its source.DateInput/wheelPhysics.ts: where a release comes to rest: UIKit's projected coast, snapped to a row, with the rate re-solved to stop exactly there; a slow release snaps, one past an end bounces back.DateInput/wheelDrum.ts: where each row is drawn on the cylinder, and which row a tap lands on.Current specification
spec:AST-094FR1, FR3, FR4;design:reduced-motionDR2.Public delta and caller burden
None: internal modules, unused until #8159.
Design and alternatives
Evidence
The tests replay UIKit's recorded flicks, drags, end bounces and taps: coasts land within 0.8 pt of iOS, snaps within 0.4 pt.
Scope
Testing
pnpm exec vitest run packages/core/src/DateInput/wheelPhysics.test.ts packages/core/src/DateInput/wheelDrum.test.ts: all pass, each flick, slow drag and bounce compared with iOS's recorded frames.Merge order: this PR → #8526 → #8159 (with #8320 and #8131) → #8162 → #8361 → #8366 → #8527 → #8480 → #8698.