Repository navigation
fix(core): layout glides play the theme's move curve, not a spring - #10236
Conversation
Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
|
/approve-spec 3b7f815 |
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| TableBody | 3.5 kB (0) · 3.3 kB CSS (0) | — | ✓ · ✓ · ✗ |
| TableFooter | 3.5 kB (0) · 3.3 kB CSS (0) | — | ✓ · ✓ · ✗ |
| TableHeader | 3.5 kB (0) · 3.3 kB CSS (0) | — | ✓ · ✓ · ✗ |
17 more components
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| TableRow | 6.7 kB (0) · 3.9 kB CSS (0) | — | ✓ · ✓ · ✗ |
| AppShell · Storybook | 80.2 kB (−1.4 kB) · 12.8 kB CSS (0) | 1,096 (0) | ✗ · ✓ · ✓ |
| PowerSearch | 268.5 kB (+409 B) · 23.1 kB CSS (0) | — | — · — · — |
| SegmentedControl | 89.4 kB (+330 B) · 6.8 kB CSS (0) | — | ✓ · ✓ · ✓ |
| DateTimeInput | 194.0 kB (+213 B) · 18.1 kB CSS (0) | — | — · — · — |
| SegmentedControlItem | 16.0 kB (0) · 4.3 kB CSS (0) | — | ✓ · ✓ · ✓ |
| SideNavCollapseButton | 52.4 kB (0) · 9.4 kB CSS (0) | — | ✓ · ✓ · ✓ |
| SideNavHeading | 66.7 kB (0) · 10.6 kB CSS (0) | — | ✓ · ✓ · ✓ |
| SideNavItem | 61.6 kB (0) · 10.4 kB CSS (0) | — | ✓ · ✓ · ✓ |
| SideNavSection | 6.8 kB (0) · 3.7 kB CSS (0) | — | ✓ · ✓ · ✓ |
| Table | 99.5 kB (0) · 17.3 kB CSS (0) | — | ✓ · ✓ · — |
| TableCell | 91.3 kB (0) · 17.0 kB CSS (0) | — | ✓ · ✓ · ✓ |
| TableHeaderCell | 90.9 kB (0) · 16.8 kB CSS (0) | — | ✓ · ✓ · ✓ |
| TableSelectionToolbar | 61.1 kB (0) · 12.4 kB CSS (0) | — | ✓ · ✓ · ✓ |
| Collapsible | 24.1 kB (−1.2 kB) · 4.5 kB CSS (0) | — | — · — · — |
| TreeList | 40.3 kB (−1.2 kB) · 5.4 kB CSS (0) | — | — · — · — |
| SideNav | 80.1 kB (−1.5 kB) · 10.7 kB CSS (0) | — | ✓ · ✓ · ✓ |
Visual changes: 2 snapshots in 1 component
A change is a question for the reviewer, not a failure.
| Component · story | Before | After | Diff |
|---|---|---|---|
| AppShell · Playground (neutral, dark) | ![]() |
![]() |
![]() |
| AppShell · Playground (neutral, light) | ![]() |
![]() |
![]() |
Package size: @astryxdesign/core 6.71 MB packed (+2.2 kB); no new dependencies.
API changes: none in this pull request
Committed API report behind this build: cli 19 rows, core 21 rows; run pnpm api:report.
Touched components checked; add ci:run-visual or ci:run-rtl to run the full suites.
CI impact: +5.5 job-min per update · pool +4.7% · standard runners · 1 CI action or script file changed (not measured)
| Before | After | Change | |
|---|---|---|---|
| Job-minutes per PR update | 116.8 | 122.3 | +5.5 |
| Runner pool, runner-minutes a day | 53,692 | 56,224 | +2,532 (+4.7%) |
| Average concurrent jobs | 37.3 | 39 | +1.8 |
| Time to green (median → this run) | — | 20.2 min | — |
| Runs a day | 460 | 460 | 0 |
Changed jobs: pr-interaction-webkit changed: standard
Not measured (the jobs that use these keep their recent minutes above): .github/scripts/ci-interaction-webkit.test.mjs.
Assumptions: before is each job's average over the last 20 pull-request runs; after keeps that for unchanged jobs and uses this run's minutes for added or changed ones, weighted by how often each changed job ran in those runs; runs a day averages the seven whole days before today; time to green compares this run with the median of those runs that ran the same jobs.
Generated by PR Enrichment workflow | View full report
There was a problem hiding this comment.
Changes requested: one fix stands between this change and approval.
The glide now plays --ease-move over --duration-medium-min, as spec:AST-105 FR4 and DEC-2 require. The new curve, tempo, character, linear(), steps() and retarget cases fail against main and pass here in Chromium and WebKit. An engineering owner's /approve-spec for this head covers the record edit and the mouse and trackpad retiming.
- Under a theme scope nested between the area and a Collapsible, TreeList or Table row, the chevron's
var(--duration-medium-min)andvar(--ease-move)(packages/core/src/motion/animatedLayout.ts:3078-3082) resolve at the chevron while the glide reads the area's tokens, so the chevron turns on different timing from the content it reveals (620 ms against a 310 ms glide in a probe), whichcomponent:CollapsibleFR10 andcomponent:TreeListFR9 rule out; write the duration and easing read at the area instead, and add a nested-scope case to the indicator tests.
[Automated review]
| 'var(--duration-medium-min)', | ||
| ); | ||
| element.style.setProperty('transition-timing-function', 'var(--ease-move)'); |
There was a problem hiding this comment.
These var() values resolve where the chevron sits, but glideProgress reads --duration-medium-min and --ease-move on the area. Under a theme scope nested between the two, the chevron turns on that scope's timing while the revealed content and the rows after it glide on the area's: with the area at 310 ms and a nested scope at 620 ms on cubic-bezier(0, 0, 0.2, 1), the chevron computes 0.62s cubic-bezier(0, 0, 0.2, 1) against a 310 ms glide. component:Collapsible FR10 and component:TreeList FR9 keep the chevron on the same motion. Writing the duration and the easing text read at the area (as the base wrote the area's sampled values) keeps them together; a nested-scope case in the indicator tests would hold it.
|
/approve-spec 16bca2c |
There was a problem hiding this comment.
Approved pending CI: CI is still running on this head.
The glide now plays --ease-move over --duration-medium-min, as spec:AST-105 FR4 and DEC-2 require, and the chevron takes the duration and easing read at the area, so it keeps the glide's timing under a nested theme scope. The new chevron cases fail against the previous revision and pass here, and the animated-layout browser spec passes in Chromium and WebKit. An engineering owner's /approve-spec for this head covers the record edit and the mouse and trackpad retiming.
[Automated review]
…10229) * fix(core): layout glides play the theme's move curve, not a spring The animated-layout glide, the in-place swap and the edge reveal played the settle spring, about 735 ms, so a theme's `character` never reached them, and on a mouse they kept moving after the motion around them had landed. They now play the theme's move curve, `--ease-move`, over `--duration-medium-min`, read where the area is through the shared theme-timing reader. The tokens carry the theme's `character` and `tempo`, so nothing scales them twice. A zero duration lands the change at once. Reduce Motion is unchanged: a cross-fade under a coarse pointer, an immediate change otherwise. A running glide is now read as the browser draws it, a straight line between two of its keyframes, so a new glide starts exactly where the old one shows. The curve bends more than the spring did where a retarget lands, and reading the true curve there left a jump of up to about 0.3 px on an 80 px move. A disclosure's indicator takes the same curve and duration as CSS values. The motion's kind is now named for what it is, a glide, in AppShell and SideNav's rail too, which share it. The record's line that the glide still plays the spring goes, since FR4 now holds. The glides of AppShell's content area, Collapsible, Table's row expansion and TreeList change feel: 225 ms in the Neutral theme and 310 ms with no theme, instead of about 735 ms. * fix(core): the theme-timing reader runs no StyleX at import The reader took core's declared timing values from the token file, which declares its variables through StyleX. Importing it ran `stylex.defineVars`: DOM work under the test runtime, and an error in a plain Node process. Since the glide reads the theme's timing, the animated-layout module and every factory that imports it took that on. The reader now keeps a typed copy of the timing tokens' declared values, which a unit test holds equal to the token file's, and imports only the token names, as types. A server test imports the reader and the animated-layout module in plain Node. * fix(core): the glide plays a linear() or steps() move easing A theme's move easing can be a linear() or steps() function: the motion scale's easing option writes one. The theme-timing reader read only a cubic Bézier, so such a theme gave no curve and the change landed at once. The reader now plays any other easing as the browser plays it: a paused animation with that easing gives its eased progress. A jump in such an easing carries no speed, so a retarget at a jump inherits none. The segmented control's selection reads its timing the same way, so it plays such an easing too. * test(core): the glide keeps advancing through a blocked main thread The animated-layout glide runs on the compositor (AST-094 FR5), but no proof read it while the main thread is blocked. This one blocks the page for 700 ms across a glide and judges the screencast against the harness's compositor control: the content after a Collapsible, after a TreeList branch and after a Table row keeps moving on the frames the control moves. The judgment reads the block while the glide's own keyframes move the follower by a cast step or more per frame; an easing tail moves under what the cast can show. Chromium only: Linux WebKit starts the glide on its compositor 30 to 100 ms late, while Apple's WebKit (the iOS 26.2 Simulator) moves on every frame from the block's start. * spec(core): AST-094's FR5 verification covers the shared glide FR5 holds that motion runs on the compositor and keeps advancing through a main-thread block. Its verification gains a row for the animated-layout glide's busy-main-thread proof, and the record links the proof's file, so a lookup on that file finds the claim it verifies. * spec(core): AST-094's FR5 glide row names its WebKit evidence gap The row's proof runs in Chromium. WebKit rests on a spot-check on the iOS Simulator, not an automated run, and the row says so, so FR5 does not read as fully covered. * test(core): the busy proof types the glides it reads The page reads the glides into an array declared with no type, which the type check rejects as an implicit any. It now holds GlideRead entries, each keyframe pair typed as a pair. * Merge main into the glide proof branch (#10236 landed) * test(core): the busy glide proof decodes each frame once The busy proof decoded every frame of its screencast twice, once for the marker and once for the control. On a loaded machine that decoding ran past the test's 60 s budget. The harness's trackMarkers finds several colors in one decode of each frame, and the proof decodes only the frames busyWindowProof reads, up to BUSY_PROOF_LOOKAROUND_MS past the judged part. The verdict is unchanged. The glide's length is set through both short medium durations, whichever the module plays. * Merge branch 'main' into uikit/f1-glide-busy-proof Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…tedLayout handle (#8106) * feat(List): animate row inserts, removals and moves with animatedLayout A List that takes the handle useAnimatedLayout returns animates its row changes on one shared motion, for every pointer. An inserted row enters, a removed row collapses in place, and a row that crosses rows moving the other way rides above them. The rows after a change, and the content after the list, glide to their new places. Rows match across renders by their itemKey; a row without one changes at once. The row marks the update module writes (item key, leaving, travelling) live in their own private-attribute owner modules. A list under the handle isolates for every pointer, so a sliding row stays below a page-level sticky header. The List and ListItem records now say the row updates and itemKey are implemented, and their verified_by lists the files that verify them. * Merge main into the branch (#10236 landed) * fix(core): the layout glide takes --duration-medium Owner decision, Nov 6 2026 (Cindy): the layout glide takes --duration-medium. The shared glide played over --duration-medium-min; it now plays over --duration-medium: 300 ms in the Neutral theme and 410 ms with no theme, instead of 225 ms and 310 ms, both scaled by tempo. Collapsible, TreeList, Table's row expansion, AppShell's content area and List share it. AST-105 FR4 names the token, and its DEC-3 records the decision. * Merge uikit/f1-glide-medium into uikit/c11-list-updates * fix(List): a change reads every near row before it settles any Settling a swiped row read the next row's transform after writing the row before it, and read every row's place after all the writes. One pass now reads each near row's translation and place, and then the settles are written. A settled row's place is its box less its own translation. * Merge main into the List updates branch (#10261 landed) * Merge branch 'main' into uikit/c13-list-reorder * docs(AST-105): DEC-3 names List among the glide's adopters This pull request makes List adopt the shared layout glide, so DEC-3 names it with Collapsible, TreeList, Table's row expansion and AppShell's content area. The merge of main took main's side of DEC-3, which lists only the components already on main. * fix(List): measuring a change only reads A row a swipe translated keeps its translation across the commit, so a change with a row swiped open lays the page out as often as one with none. A row that leaves settles after the commit, before any place is read again, so its exit still runs on its own box, in view, at no layout of its own. The glide its old, translated place gives it lands at once. * fix(List): a leaving swiped row keeps its move in the list The module reads a swiped row's old place with its translation on it, so its glide adds that translation to the row's move in the list. A row that leaves now keeps only the vertical part of that glide, so a row removed by an update that also moves it glides with its neighbours instead of jumping. The swipe-away probe and its comment say what they check. * fix(List): a leaving row keeps its place and its glide's record A second change while a swiped row exits restarted it sideways, because the List rewrote the glide's frames while the module's record of the glide kept the swipe's offset. The module's landLayoutGlideSideways now changes both together. A reorder while a row exits carried that row to the top of the list, because React moves only its own rows. Each leaving row now stays before the row that came after it, or last. * fix(List): a leaving row keeps its old index among the staying rows A removed row still exiting travelled with the row that came after it as it left, when that row moved. Each row that leaves, and each row still leaving, now goes after as many staying rows as came before it, so it stays where it showed whichever rows move around it. One pass over the rows replaces the put-back anchor and the later correction. * Merge main into the List updates branch (#10280 landed) List.spec.md's verified_by keeps main's ListDividers spec beside this branch's two List update tests. * fix(List): a crossing row is marked after the module reads the layout markCrossings set the travelling mark between the list's row reads and the module's batched read, so a reorder with a crossing row restyled and laid out the page again in its commit. It now sets the marks in the microtask it already queues, after the module's read and before the frame paints, and marks only a row that has a glide. A mark's end clears only its own mark, so a second reorder while a row rides keeps the new mark until the new glide ends. * fix(List): a travelling row keeps its surface in a list with dividers The press layer's overDividers, composed after the travelling style, also sets box-shadow, so a travelling row in a list with dividers and pressable rows lost its surface, and its label drew over the rows it crossed. The travelling style now also sets the surface as a private custom property, and overDividers keeps it in each of its shadows. Every other row resets the property, so a nested row never inherits it. Co-authored-by: Cindy Zhang <cindyxz@meta.com>






Supersedes #10094 (co-authored by vjeux)
The change of #10094, re-landed whole on current
mainfrom a maintainer branch with the author as co-author. It lands whole because it also addstheme/tokenReferences.tsto the WebKit interaction lane's scope pattern in.github/workflows/ci.yml.It is #10094's head
e69dd39d(which adds the fix that keeps the expand chevron on the area's glide timing under a nested theme), merged withmain. The WebKit scope line in.github/workflows/ci.yml, whichmainalso changed, ismain's line pluspackages/core/src/theme/tokenReferences.ts. The filesmainalso changed since #10094's base merge with no conflict; every other file matchese69dd39dbyte for byte.A follow-up replaces the WebKit lane's hand-kept scope pattern with one derived from where the interaction specs live; this entry moves into its shared-code pattern.
The animated-layout glide now plays the theme's move curve,
--ease-move, over--duration-medium-min, asspec:AST-105FR4 and DEC-2 require. It no longer plays the settle spring. This retires #8677's interim line: "Until the glide moves to the curve tokens (AST-105 DEC-2), its interimsprings.settlefollows tempo under every pointer."User impact
People who open a Collapsible, a TreeList branch or a Table row's panel, or who collapse AppShell's side rail, see the content after it glide. On
mainthat glide plays the settle spring, which lasts about 735 ms, longer than the theme's other medium motion. A theme'scharacternever reaches it, so a crisp or expressive theme glides the same as the standard one. With this change the glide follows the theme: 225 ms in the Neutral theme and 310 ms with no theme, on the theme's move curve.Problem and solution fit
characterhas no effect on it.The fix changes only where the module gets its timing. It reads
--duration-medium-minand--ease-movewhere the area is, through #10068'sreadThemeTiming. Summing a running glide with a new move works on any progress curve, so that code is unchanged.Expected behavior and authority
spec:AST-105FR4 and DEC-2: the glide, the in-place swap and the edge reveal play--ease-moveover--duration-medium-min, never a spring. The record's gap line saying they still playsprings.settlegoes, since FR4 now holds.spec:AST-094FR45: motion that a tap or a click starts plays the theme's motion tokens.spec:AST-108FR2: the duration tokens carry the theme'stempo, so the glide is not scaled twice.Smallest restoration
motion/animatedLayout.tsreads the curve throughreadThemeTiming. A zero duration, or an easing the browser cannot play, lands the change at once. The motion's kind is renamed from'spring'to'glide'. A disclosure's indicator takesvar(--duration-medium-min)andvar(--ease-move)as CSS values instead of a sampledlinear().--ease-movecan be alinear()orsteps()function, which the motion scale's easing option writes, and such a theme got no glide. A jump in such an easing carries no speed, so a retarget at a jump inherits none. The segmented control's selection, the reader's other caller, now plays such an easing too instead of moving at once.theme/tokenReferences.ts: the glide reads its timing through the theme-timing reader, which resolves token references, so the WebKit proofs' bundle of the module carries that file. The lane's routing test requires every bundled source in scope.themeTiming.ts. KeepfirstEasingTextwith its JSDoc, and leavefirstEasing's one-line JSDoc dropped, as fix(core): a calc() duration token times the press release clock #10069 drops it.Evidence
springs.settle), whatever the scope's--duration-medium-minand--ease-move.Core/Animated layout),mainglides the content after the Collapsible and the TreeList in 734.77 ms; this change takes 225 ms, oncubic-bezier(.24, 1, .4, 1). A quarter of the way through the time, the content is 70.5% of the way there onmainand 78.8% with this change.Core/Theme motion tempo). Every keyframe sits on the theme's curve: standardcubic-bezier(0.24, 1, 0.4, 1), crisp and expressive. In Storybook's Neutral theme it lasts 225 ms.Core/Animated layout(Collapsible and TreeList) and in theCore/Theme motion temporows has the same durations and keyframes, byte for byte, as at c36b15d, the head card 80 passed.linear()orsteps()move easing, red without the reader change on a not-for-landing arm (vjeux@52cd5efb04) in Chromium and WebKit: the FR4 cases forlinear(0, 0.8 50%, 1)andsteps(4, jump-end)find no glide, the reader's case for both patterns finds no curve, and a click's slide in the segmented control lands at once (1 ms where the theme gives 125 ms). A mutant that ignores the browser's easing (vjeux@cd83d61530) fails the same cases on the curve: −75.79 px wherelinear()gives −73.26 px at 16.3 ms. All pass with the change.steps(4)over 300 ms, a two-sided difference reads about 125 progress per second. The curve now reads 0 there, and the unit cases for it fail without the change.mainsince test(core): the adopters' edge proofs read the module's clock #10150. They pass against this change in Chromium and WebKit.Scope
Testing
pnpm build,pnpm typecheckandCI=true pnpm linton the changed files.pnpm test:ui: 21,455 passed.ci-interaction-webkit.test.mjs: 21 of 21 (red without the scope line: the animated-layout bundle carriestheme/tokenReferences.ts); every test that readsci.yml, 647 of 647.Core/Animated layout's Collapsible and TreeList, onmain's deploy and on this head's.Writing rubric results
AST-105 is the one record this pull request touches.
springs.settle"today" until FR4 moves them, described a change, not the system. It is deleted. Current-state impact keeps its lines about the present state, which that section is for.