Repository navigation
feat(core): the animator's springs follow the theme's tempo - #8677
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 of this pull request waits on a spec decision. On main, spec:AST-094 FR4 and DEC-11 keep the touch motion roles internal, while the spec:AST-108 requirements (with the spec:AST-094 DEC-15 and related amendments) that let a theme's tempo and character retune them appear here only as proposed text, in an earlier version than the one approved on #8613. An engineering owner records that decision by commenting /approve-spec 5e235e66b2a97e2e81d798820889f71d03bd8d29 on this pull request (the author may do so if they are an engineering owner); otherwise the pull request waits for #8613 to land on main and rebases onto it. Whatever the decision, two changes are needed:
- Rebase onto
mainonce #8076 and #8661 land, so the diff carries only this change; today it counts 17,872 authored lines against a 499-line own change. - Make the springs follow the knobs the CSS follows (an adaptation rule's
tempoorcharacter, and an enclosing theme's knobs, whichthemeMotionKnobsnever reads), or state in the spec that they do not.
[Automated review]
| export function themeMotionKnobs( | ||
| element: Element | null | undefined, | ||
| ): ThemeMotionKnobsAt { | ||
| const scope = element?.closest?.(THEME_SCOPE); | ||
| if (scope == null) { | ||
| return DEFAULT_KNOBS; | ||
| } | ||
| const tempoValue = Number(scope.getAttribute('data-astryx-motion-tempo')); | ||
| const tempoIsSet = | ||
| scope.hasAttribute('data-astryx-motion-tempo') && | ||
| Number.isFinite(tempoValue) && | ||
| tempoValue > 0; | ||
| const characterValue = scope.getAttribute('data-astryx-motion-character'); | ||
| const character: ThemeMotionCharacter = | ||
| characterValue === 'crisp' || characterValue === 'expressive' | ||
| ? characterValue | ||
| : 'standard'; | ||
| if (!tempoIsSet && character === 'standard') { | ||
| return DEFAULT_KNOBS; | ||
| } | ||
| return {tempo: tempoIsSet ? tempoValue : 1, tempoIsSet, character}; |
There was a problem hiding this comment.
The springs read only this scope's attributes, which Theme.tsx writes from the theme's root motion. The CSS also follows an adaptation rule's tempo or character, and inherits an enclosing theme's scaled --duration-* when the nearest theme sets no motion. In both cases the springs and the CSS diverge. For example, a when: {pointer: 'coarse'} rule with tempo: 1.3 makes the CSS 1.3× while the springs keep the root tempo. Either resolve the knobs the way the CSS does, or state the limit in AST-108 FR3 and FR5, with a test either way.
| return null; | ||
| } | ||
| const curve = solveSpring(toSpring(spring), {from, to, velocity}); | ||
| // The motion role it plays: its table value while DEC-11 holds. |
There was a problem hiding this comment.
After this change a role follows the theme's knobs (resolveMotion.ts), so "its table value while DEC-11 holds" no longer describes it. The same goes for line 137 and for README.md lines 49-50 and 72-73.
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| Slider | 92.9 kB (+416 B) · 8.3 kB CSS (0) | — | — · — · — |
| TreeList | 41.2 kB (+412 B) · 5.4 kB CSS (0) | — | — · — · — |
| ProgressBar · Storybook | 11.4 kB (+399 B) · 6.2 kB CSS (0) | 568 (0) | — · — · — |
33 more components
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| SideNav | 76.7 kB (+394 B) · 10.1 kB CSS (0) | — | — · — · — |
| AppShell | 76.7 kB (+392 B) · 12.2 kB CSS (0) | — | — · — · — |
| DateTimeInput | 187.0 kB (+391 B) · 17.2 kB CSS (0) | — | — · — · — |
| Switch · Storybook | 90.5 kB (+386 B) · 9.5 kB CSS (0) | 1,723 (+6) | — · — · — |
| TimeInput | 141.2 kB (+386 B) · 14.5 kB CSS (0) | — | — · — · — |
| DateInput | 164.3 kB (+370 B) · 15.5 kB CSS (0) | — | — · — · — |
| SegmentedControl | 84.8 kB (+362 B) · 6.7 kB CSS (0) | — | — · — · — |
| PowerSearch | 260.7 kB (+360 B) · 22.6 kB CSS (0) | — | — · — · — |
| charts/Chart | 169.2 kB (+346 B) · 494 B CSS (0) | — | — · — · — |
| Collapsible | 25.0 kB (+339 B) · 4.5 kB CSS (0) | — | — · — · — |
| BottomSheet | 56.9 kB (+308 B) · 7.5 kB CSS (0) | — | — · — · — |
| lab/ListInput | 85.6 kB (+79 B) · 1.6 kB CSS (0) | — | — · — · — |
| MultiSelector | 80.4 kB (+77 B) · 15.6 kB CSS (0) | — | — · — · — |
| TableCell | 84.3 kB (+73 B) · 16.7 kB CSS (0) | — | — · — · — |
| lab/ChatReactionBar | 69.7 kB (+70 B) · 1.1 kB CSS (0) | — | — · — · — |
| Pagination | 93.3 kB (+69 B) · 16.6 kB CSS (0) | — | — · — · — |
| lab/ChatEmojiPicker | 69.2 kB (+69 B) · 773 B CSS (0) | — | — · — · — |
| lab/Comments | 273.9 kB (+69 B) · 2.5 kB CSS (0) | — | — · — · — |
| TableHeaderCell | 83.9 kB (+67 B) · 16.5 kB CSS (0) | — | — · — · — |
| Selector | 83.3 kB (+65 B) · 16.2 kB CSS (0) | — | — · — · — |
| lab/TourStep | 61.4 kB (+64 B) · 590 B CSS (0) | — | — · — · — |
| richtext/RichTextEditorToolbar | 234.1 kB (+63 B) · 454 B CSS (0) | — | — · — · — |
| Table | 92.5 kB (+61 B) · 16.9 kB CSS (0) | — | — · — · — |
| DropdownMenu | 78.0 kB (+60 B) · 16.4 kB CSS (0) | — | — · — · — |
| Markdown | 153.8 kB (+60 B) · 19.2 kB CSS (0) | — | — · — · — |
| ContextMenu | 83.1 kB (+59 B) · 16.3 kB CSS (0) | — | — · — · — |
| MoreMenu | 78.2 kB (+57 B) · 16.4 kB CSS (0) | — | — · — · — |
| lab/CommentThread | 268.4 kB (+57 B) · 2.4 kB CSS (0) | — | — · — · — |
| Popover | 55.4 kB (+55 B) · 12.8 kB CSS (0) | — | — · — · — |
| lab/CommentComposer | 109.1 kB (+49 B) · 1.9 kB CSS (0) | — | — · — · — |
| Tokenizer | 74.9 kB (+41 B) · 17.1 kB CSS (0) | — | — · — · — |
| Overlay | 21.6 kB (0) · 3.9 kB CSS (0) | — | — · — · — |
| Toast | 60.7 kB (0) · 9.0 kB CSS (0) | — | — · — · — |
Package size:
- @astryxdesign/cli 3.37 MB packed (+12 B); no new dependencies
- @astryxdesign/core 6.58 MB packed (+3.0 kB); no new dependencies
- @astryxdesign/theme-butter 45.6 kB packed (+18 B); no new dependencies
6 more packages
- @astryxdesign/theme-chocolate 13.6 kB packed (+16 B); no new dependencies
- @astryxdesign/theme-gothic 32.6 kB packed (+17 B); no new dependencies
- @astryxdesign/theme-matcha 28.0 kB packed (+19 B); no new dependencies
- @astryxdesign/theme-neutral 35.5 kB packed (+18 B); no new dependencies
- @astryxdesign/theme-stone 37.6 kB packed (+14 B); no new dependencies
- @astryxdesign/theme-y2k 34.7 kB packed (+14 B); no new dependencies
API changes: none in this pull request
Committed API report behind this build: cli 9 rows, core 8 rows; run pnpm api:report.
Wide change (shared styles: packages/core/src/theme/defineTheme.ts, packages/core/src/theme/resolveThemeValues.ts, packages/core/src/theme/themeMotion.test.ts, and 1 more): add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.
Generated by PR Enrichment workflow | View full report
…SS part's settle clock
There was a problem hiding this comment.
Approval of this pull request waits on a spec decision: on main, spec:AST-094 FR4 and DEC-11 keep the touch motion roles internal, and the spec:AST-108 records (with architecture:theme-authoring-contract INV15) that let a theme's tempo, character and reducedMotion change them are proposed here without approval for this head. An engineering owner records that decision by commenting /approve-spec f2546dbdc1b0830d730cdae6793c51a5d6c3028f on this pull request (the author may do so as an engineering owner), or the records land on main first. Whatever the decision, these changes are needed:
- Make
reducedMotion: 'always'reach the components' own reduced-motion styles and script readers before the docs and changeset promise that it reduces motion for everyone, or say what it reaches today. - Write
--pre-tempo-duration-*wherever a theme writes a duration, so a nested theme's scale or explicit duration still times Spinner and Skeleton under a theme that setstempo. - Make the springs follow the knobs the CSS follows (an adaptation rule's
tempoorcharacter, and an enclosing theme's knobs), or state that limit in the spec, with a test either way. - Rebase once #8076 and #8661 land, so the diff carries only this change (17,911 authored lines today).
[Automated review]
| items: [ | ||
| "`tempo` scales every motion duration: 1 is Astryx's own, 0.75 the quickest and 1.5 the slowest (values outside are clamped).", | ||
| "`character` sets how lively motion is: `'crisp'` decelerates without overshoot, `'standard'` is Astryx's own, and `'expressive'` overshoots slightly on entrances and moves. Exits and fades never overshoot.", | ||
| "`reducedMotion: 'always'` reduces motion for everyone, as if each person had turned on Reduce Motion; `'system'`, the default, follows each person's setting. No value loosens Reduce Motion.", |
There was a problem hiding this comment.
No core component reads the data-astryx-reduced-motion mark yet: the 50 non-test core files that read prefers-reduced-motion in a style or script (Dialog, BottomSheetPanel, TopNavMegaMenu, Step among them) ignore it, so a person without the setting still gets full motion under a theme on 'always'. Only the theme's own when: {motion} adaptation rules and the internal animator honour it. Either make those styles and script readers key on the mark (as spec:AST-108 FR6 requires) before promising this, or say what 'always' reaches today. The changeset's "so motion is reduced for everyone" has the same problem.
| ): Record<string, string> { | ||
| const knobs = resolveMotionKnobs(config); | ||
| const tokens: Record<string, string> = {}; | ||
| if (knobs.tempoIsSet) { |
There was a problem hiding this comment.
--pre-tempo-duration-* is written only when this theme sets tempo, and resolveThemeValues copies an explicit duration into it only when the name already exists. Custom properties inherit, so a nested theme with its own duration scale or explicit duration but no tempo leaves Spinner and Skeleton reading the outer theme's pre-tempo value. Example: inside a tempo: 1.5 theme, a nested motion: {fast: 100, medium: 250, slow: 600, ratio: 0.75} theme's Spinner turns every 730 ms instead of its own 450 ms --duration-slow-min. Writing the pre-tempo twin wherever a theme writes a duration would fix it; a nested-theme test would pin it.
| * the element is in (`spec:AST-108` FR3): `tempo` scales a spring's duration | ||
| * and a role's duration, and `character` moves a spring's bounce by its | ||
| * role's weight (1 for entrances and presentations, 0.5 for state changes, 0 | ||
| * for exits, which never overshoot). The theme's `MotionScaleConfig` alone |
There was a problem hiding this comment.
"0 for exits, which never overshoot" no longer matches spec:AST-108 FR3 at this head, which says a weight-0 role keeps its measured bounce: springs.menuClose (damping ratio 0.85) overshoots by about 0.6% at every character. The same wording sits in theme/motionKnobs.ts:34, theme/defineTheme.ts:243, cli/assets/theme.template.ts:186 and motion.doc.mjs:159.
…rt's settle clock
There was a problem hiding this comment.
Approval of this pull request waits on one spec decision: architecture:theme-authoring-contract INV15 adds "an explicit token anywhere in the lineage still wins", which no record on main states and which lets a base theme's explicit --duration-* or --ease-* token beat a child's own duration scale. An engineering owner records that decision by commenting /approve-spec eeec68709d2d3c5de4c5138180bcce7b740a4f99 on this pull request (the author may do so as an engineering owner), or the clause can be dropped so a child's own scale keeps winning. Whatever the decision, these changes are needed:
- Make component styles and script readers honour the
reducedMotion: 'always'mark, asspec:AST-108FR6 requires, or keep'always'out of the docs and changeset until they do. - Make the springs follow the knobs the CSS follows: an adaptation rule's
tempoorcharacter, and an enclosing theme's knobs. - Write
--pre-tempo-duration-*wherever a theme writes a duration, so a nested theme still times Spinner and Skeleton under a theme that setstempo. - Rebase once #8076 and #8661 land, so the diff carries only this change (17,803 authored lines today).
[Automated review]
| export function themeMotionKnobs( | ||
| element: Element | null | undefined, | ||
| ): ThemeMotionKnobsAt { | ||
| const scope = element?.closest?.(THEME_SCOPE); |
There was a problem hiding this comment.
The springs read only the nearest theme scope's attributes, which Theme.tsx writes from the theme's root motion. An adaptation rule's tempo or character, and an enclosing theme's scaled durations, reach the CSS but not the springs: a when: {pointer: 'coarse'} rule with tempo: 1.3 makes --duration-medium 533 ms while springs.settle stays 0.5 s. spec:AST-108 FR3 lists an adaptation rule among its verified states, and FR4a needs a CSS fade and its spring to land together.
| ): Record<string, string> { | ||
| const knobs = resolveMotionKnobs(config); | ||
| const tokens: Record<string, string> = {}; | ||
| if (knobs.tempoIsSet) { |
There was a problem hiding this comment.
Only a theme that sets tempo writes --pre-tempo-duration-*, and custom properties inherit. Inside a tempo: 1.5 theme, a nested theme with motion: {fast: 100, medium: 250, slow: 600, ratio: 0.75} leaves its Spinner turning every 730 ms instead of 450 ms and its Skeleton pulsing over 550 ms instead of 335 ms (component:Spinner FR3, component:Skeleton FR4). Writing the pre-tempo twin wherever a theme writes a duration, with a nested-theme test, would fix it.
| items: [ | ||
| "`tempo` scales every motion duration: 1 is Astryx's own, 0.75 the quickest and 1.5 the slowest (values outside are clamped).", | ||
| "`character` sets how lively motion is: `'crisp'` decelerates without overshoot, `'standard'` is Astryx's own, and `'expressive'` overshoots slightly on entrances and moves. Exits and fades never overshoot.", | ||
| "`reducedMotion: 'always'` reduces motion for everyone, as if each person had turned on Reduce Motion; `'system'`, the default, follows each person's setting. No value loosens Reduce Motion.", |
There was a problem hiding this comment.
No core component reads data-astryx-reduced-motion yet: the 48 core files that key reduced motion on prefers-reduced-motion ignore it, so a person without the setting still gets full motion under an 'always' theme. spec:AST-108 FR6 requires those styles and script readers to key on the mark; until they do, this line and the changeset overstate what 'always' does.
| input.motion != null && baseMotion != null | ||
| ? {...baseMotion, ...input.motion} | ||
| : input.motion; | ||
| const lineageMotionTokens = |
There was a problem hiding this comment.
This re-applies the base's explicit --duration-* and --ease-* tokens above whatever the child generates, so a child's own duration scale no longer beats a base's explicit token, as it does today. That is the INV15 clause waiting on an owner decision; if it stays, the record's System model precedence list should say so too.
| * the element is in (`spec:AST-108` FR3): `tempo` scales a spring's duration | ||
| * and a role's duration, and `character` moves a spring's bounce by its | ||
| * role's weight (1 for entrances and presentations, 0.5 for state changes, 0 | ||
| * for exits, which never overshoot). The theme's `MotionScaleConfig` alone |
There was a problem hiding this comment.
"0 for exits, which never overshoot" contradicts spec:AST-108 FR3: a weight-zero role keeps its measured bounce, and springs.menuClose (damping ratio 0.85) overshoots by about 0.6%. The same wording is in motionKnobs.ts:33-34, defineTheme.ts:242-243, theme.template.ts:186 and motion.doc.mjs:159.
…ionAnimator The public createAnimator (spec:AST-107 FR6) takes the name; meta#8677's tests of the internal animator now import it by its own name.
…coasts every channel spec:AST-107 FR3 as merged: - Every animate plays springs.stateChange at the tempo and character of the theme the element is in (spec:AST-108 FR3), which meta#8677 brings; the new case fails with the motion module as it was before it. - decay coasts any of the five channels it is given on the fixed momentum decay (spec:AST-108 FR4): the translate axes as a throw that Reduce Motion lets continue, scale, rotate and opacity as a transition it plans like a move (design:reduced-motion DR2); opacity stops at 0 or 1. It took only x and y before. - destroy halts every channel where it shows before letting go, so nothing reads as moving after it, and every method after it does nothing; destroy removes every listener it added (FR6). The docs and the changeset say the same.
Main's golden counts 367 matches for "icon set"; this branch's public animator pages and meta#8677's motion docs each add one, so the merged count is 369. The listed results and every other field are unchanged, and the diff names no icon set and nothing experimental.
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet, so a maintainer needs to add the ci:run label for it to run.
The springs, role durations and layout glide now follow a theme's motion.tempo as the current theme-motion records describe, and the glide's retiming under a mouse carries the owner's interim decision quoted in the description. Two small follow-ups, neither blocking: the theme authoring contract's known-gap line still says no spring reads the motion knobs, and the new themeTempo.ts header could be trimmed to its reason and constraints.
[Automated review]
| * A theme that sets `tempo` writes it, clamped, to the private custom | ||
| * property `--_astryx-motion-tempo` beside its duration tokens. An | ||
| * adaptation rule writes it where its condition holds, and a nested theme | ||
| * that sets no tempo inherits it. A layer whose durations come from a | ||
| * duration scale writes a tempo of 1, so its springs keep its own pace. One | ||
| * read of a custom property forces no layout. | ||
| * Nothing here imports a theme module, runs on import or runs on the server. |
There was a problem hiding this comment.
Non-blocking: this header runs 18 lines, past the source-comment guide of about 12. Suggest keeping only: "A theme writes its tempo to --_astryx-motion-tempo; a duration-scale layer writes 1, and a nested theme inherits it. Reading a custom property forces no layout. Nothing here runs on import or on the server."
| return null; | ||
| } | ||
| // The motion role it plays: its table value (DEC-15). | ||
| // The motion role it plays, at the tempo of the theme it is in. |
There was a problem hiding this comment.
Non-blocking: this comment restates the resolveMotion call below it; it can go.
Main renamed createMotionAnimator to createMotionEngine.
|
/approve-spec 9e704d9 |
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet. This pull request is a draft from a fork, so once it is marked ready for review, a maintainer adds the ci:run label so CI runs.
A theme's motion.tempo now times the animator's role springs, role durations and the layout glide, as the theme motion and animated-layout specs require, and the glide's retiming under a mouse follows the owner's interim decision quoted in the description. The two tests now calling createMotionEngine pass on this head.
Non-blocking, when convenient:
- The theme authoring contract's known-gaps line "no spring reads the motion knobs yet" is out of date once this lands; a record owner can update it.
- The new
themeTempo.tsheader and the changedresolveMotion.tsheader run past the source-comment guide of about 12 lines, and the comment aboveresolveMotioninanimator.tsrestates the call.
[Automated review]
CI's formatting check reads two lines that Prettier writes differently.
There was a problem hiding this comment.
Changes requested: one formatting fix, and nothing else blocks.
- Miss:
packages/core/src/motion/resolveMotion.tempo.test.ts(lines 86-89) is not formatted with the repository's Prettier, so the required lint check's changed-file formatting step will fail; runpnpm exec prettier --write packages/core/src/motion/resolveMotion.tempo.test.ts.
The springs, role durations and layout glide now follow a theme's motion.tempo as the theme motion and touch presentation specs require, and the glide's retiming under a mouse follows the owner's interim decision quoted in the description. CI has not run on this head yet: once the draft is marked ready for review, a maintainer adds the ci:run label so it runs.
Non-blocking, when convenient:
- The theme authoring contract's known-gap line "no spring reads the motion knobs yet" is out of date for
tempoonce this lands; a record owner can narrow it tocharacter. - The new
themeTempo.tsheader runs past the source-comment guide of about 12 lines, and the comment aboveresolveMotioninanimator.tsrestates the call.
[Automated review]
| expect(slowed.value(seconds * 1.5)).toBeCloseTo( | ||
| base.value(seconds), | ||
| 6, | ||
| ); |
There was a problem hiding this comment.
The repository's Prettier puts this call on one line, so the lint check's changed-file formatting step fails on this file. pnpm exec prettier --write packages/core/src/motion/resolveMotion.tempo.test.ts fixes it.
One call now fits on a line, as Prettier writes it.
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet. The pull request is a draft from a fork, so once it is ready a maintainer adds the ci:run label for CI to run.
The formatting issue from the last review is fixed, and nothing else blocks. The springs, role durations and layout glide now follow a theme's motion.tempo as the theme motion and touch presentation specs require, and the glide's retiming under a mouse follows the owner's interim decision quoted in the description.
Non-blocking, when convenient:
- The theme authoring contract's known-gap line "no spring reads the motion knobs yet" goes out of date for
tempoonce this lands; a record owner can narrow it tocharacter. - The new
themeTempo.tsheader runs past the source-comment guide of about 12 lines, and the comment aboveresolveMotioninanimator.tsrestates the call.
[Automated review]
There was a problem hiding this comment.
Approved pending CI: CI is still running on this head.
Nothing blocks. The springs, role durations and layout glide follow a theme's motion.tempo as the theme motion and touch presentation specs require, and the glide's retiming under a mouse follows the owner's interim decision quoted in the description.
Non-blocking, when convenient:
- Miss: the changeset names the touch springs and the glide, but
createAnimatoranduseAnimator'sanimatenow followtempotoo, under every pointer; one sentence there would tell app authors. - The theme authoring contract's known-gap line "no spring reads the motion knobs yet" goes out of date for
tempoonce this lands; a record owner can narrow it tocharacter. - The new
themeTempo.tsheader runs past the source-comment guide of about 12 lines, and the comment aboveresolveMotioninanimator.tsrestates the call.
[Automated review]
The theme's
characterstays out of this pull request: its spring tables wait on #9704. This one istempoonly.User need
A theme author sets
motion.tempoto slow down or speed up their product's motion. Today only the CSS transitions follow it. The touch springs keep Astryx's own timing: a sheet's settle, a switch's travel, a slider's release and the animated-layout glide. So with a tempo of 1.5, a screen's fades take 1.5 times as long while its sheet still settles at tempo 1.Problem and solution fit
Every touch spring and role duration goes through
resolveMotion(element, role), and that function passed each role through unchanged. It now reads the tempo of the themeelementis in and stretches the role:tempotimes as long.springs.settlethrough it, at the area.Current specification
spec:AST-108FR3 (current): "a spring's duration scales bytempo".spec:AST-094FR45 (current): "The theme'stemposcales the spring's duration".characteroffsets aren't here. The spec owner keeps them parked until spec(AST-108): the fluid character's motion tables #9704, which redefines them, lands.Public delta and caller burden
motion: {tempo: 1.5}now also plays every touch spring and role duration inside it 1.5 times as long, the glide included.--_astryx-motion-tempoonly when it setstempo, and the reader treats a missing value as 1.springs.fingerTrackingand Switch's drag tracking), ProgressBar's reduced-motion pulse, curves, and anything that isn't a role (momentum, the rubber band, paging, press clocks and thresholds).characteror a few explicit durations writes none.tempobut notcharacteryet: the motion doc,defineTheme'smotionJSDoc, the theme template and the author-a-theme doc.Design and alternatives
tempowith a mouse too, asspec:AST-105DEC-2 decides (cixzhang, 2026-11-04): "a theme that setscharacterortemposees its glides change with the rest of its motion, under every pointer." The owner's decision for the interim spring (2026-11-04, 16:24 UTC): "Until the glide moves to the curve tokens (AST-105 DEC-2), its interimsprings.settlefollows tempo under every pointer." At tempo 1 nothing changes on desktop. AST-108 FR1 hastemposcale every motion duration, and FR3 scales every AST-094 FR4 role's spring. The glide plays the FR4 rolesprings.settleunder every pointer (AST-105 FR4, FR5), and AST-107 FR3 saysanimate's spring follows the theme's tempo. The CSS tokens already followtempowith a mouse, so the glide now keeps step with them.--duration-exit, which the tempo already scales, so it needs no reader of its own.Evidence
themeTempo.interaction.browser.spec.ts, Chromium and WebKit on the phone profile:motion.tempo, the glide lasts 0.75, 1 and 1.5 times as long.resolveMotion.tempo.test.ts:themeTempo.test.tsreads the theme's value, and falls back to 1 when there is none or it isn't a positive number.animatedLayout.test.tschecks the glide at the area's tempo.useSwitchTouch.test.ts: a release right after a press waits the minimum lift and settles, all exactly 1.5 and 0.75 times as long at those tempos.themeMotion.test.ts: a theme and an adaptation rule with a duration scale alone write a tempo of 1; a child of a tempo theme keeps its base's tempo.35f25756c9), the Switch case reads a ratio of 1.0 and both theme cases find no tempo.a5b1282f64): 6 unit cases fail andthemeTempo.test.tshas no module to load. The browser scope case is 25 px off; finger tracking passes on both, as it should.Scope
Testing
Gate at
50e47ea46d(rebased ontomainafter the shared press release fade moved to--duration-exit), side by side withmainon one box:pnpm test:ui20,636 vs 20,622, 0 failed;pnpm test:node16,561 passed.main: in a same-moment pair of five runs each,mainfailed 5 and this change failed 4.Earlier, at
6755a6a5a8: the tempo spec three times, Chromium 15/15 and WebKit 15/15; ProgressBar's move and pulse specs 26/26.Feel check: open the
Core/Theme motion tempostory ("Three tempos") on a phone. In each row, toggle the switch, drag the slider, slide the segments, and open "Shipping details". The 1.5 row should feel half again as slow as the middle row, and the 0.75 row quicker. Each finger drag still tracks the finger.