Repository navigation
Conversation
… useAnimator's browser proofs
…fine pointer); the changeset says the same animator.test.ts keeps this layer's header (no compositor browser spec in this layer). The changeset now says decay sends a spring back from past an edge through the Reduce Motion policy: a cross-fade on a coarse pointer, at once elsewhere.
…side a scroll view The press profiles' README said a cell's highlight always waits the content-touch delay. PressTracker waits it inside a scroll view and lights at once outside one (spec:AST-094 FR11), as its own header says; the README now says so too.
…angeset wording) as a dependency
What one call becomes under Reduce Motion (an opacity-only cross-fade whose travel lands while the element is transparent, or everything at once; a dip on a borrowed stylesheet opacity) moves from createAnimator's closure to reducedMotionPlan.ts, a pure planner that reads the animator through a view and returns the plan; the animator turns the plan into its segments and runs the landings. No behavior change: the animator's tests pass unchanged, and the planner has its own. Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…ed pull requests This pull request keeps the animator's Reduce Motion plan (now its own planner module), the browser-set start of a call from rest, promotion, atRest, rebase and animatorFor. Two features it carried move up the stack, each with its own tests and changeset: - play(curves), baking any closed-form curve like a spring; - driving a pseudo-element (a dialog's ::backdrop). The changeset is split the same way. Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
play(curves) bakes any curve given as value and velocity functions of time (a spring chasing a moving target from solveSpringTowardRamp, a fitted UIKit curve) exactly like a spring: presentation values, velocities, stop, finished and the shared clock work the same, a later animate starts from the curve's value and velocity, and under Reduce Motion a curve that moves is travel the policy replaces. Tests and a changeset of its own. Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
createAnimator(element, {pseudoElement: '::backdrop'}) animates a
pseudo-element: with no inline style, a static filling animation holds
its value after a curve ends and after set; a custom property on its host
promotes it; a Reduce Motion dip borrows and hands back the
pseudo-element's own opacity, and its holds follow a node replacement.
animatorFor keeps one animator per element and pseudo-element. Tests and
a changeset of its own.
Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…ot the page's frames around it "a harness read inside a trace is not the page's long task…" also asserted that no long task started anywhere in the read's window. On CI's runner each frame of the fixture's 1,200 running rows took 51-54 ms, the page's own long tasks, and failed it; at 4x CPU locally they take 126-220 ms. The read itself was set apart correctly. The test now asserts the read's own classification, and removes the rows before the timer, click and direct-call windows, so each holds only its own work. It passes at 4x CPU, where the previous assertion failed.
…hed curve waitForAnimation's finished branch read every recorded animation, so a scroll- or view-timeline animation that reached its end (a scroller scrolled to its end) came back as finished with a NaN duration and interval. The recorded set now keeps document-timeline animations only. The scroll-timeline self-test scrolls its scroller to the end first; it returned the NaN entry before (Chromium, 3x repeat pass). The README, JSDoc and file header name watchAnimations where they list what moves the wait window and what the file exports. Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…rves), pseudo-element support) as a dependency # Conflicts: # packages/core/src/motion/README.md # packages/core/src/motion/animator.ts
… motion-role animator # Conflicts: # packages/core/src/motion/animator.ts
The travel list moved to the planner with the Reduce Motion plan; nothing in the animator reads it any more (no-unused-vars).
…deselect clock; the animator changeset drops its no-component clause The press profiles' README gave a control UIButton's 0.25 s release. PRESS_PROFILES releases every surface on the list's deselect clock until a control runs its own release, as its comment says; the README now says so. Its test list still credited VelocityTracker's tests with checking each strategy against the true velocity, though one filter remains: it now names what they check. The animator's changeset said no component uses it, which goes false in the release that carries the components that do.
…removed at its end is still reported - Arming the recorder now records first and stamps strictly after the newest entry: a watch taken right after a step that changed style no longer reports that step's transition (created by the arm's own style read) as started after it. - Each entry keeps its target's ancestors as recorded, so an exit whose element the page removes at its end is still reported for the container it left, instead of a wait rejecting with nothing recorded. A self-test for each; both failed before (Chromium, 3x repeat pass). Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
solveSpringTowardRamp went with play(curves)'s tests, and a coast test destructured an element it never reads (no-unused-vars).
play(curves)'s tests use solveSpringTowardRamp, so this layer keeps its import.
…he frames it credits as skipped
With a compositor record and an on-screen start, the moved criterion counted
from the first moved frame after the on-screen frame, while the gap from the
on-screen frame to it was credited as frames the screencast skipped. When the
cast skipped the frames right after the on-screen start, those credits had
no place in the denominator: four unmoved frames that fail on a full cast
("31 of 35 expected frames moved") passed as "26/30 moved and 5 skipped by
the screencast (103%)", and a clean run read 117%.
The criterion now counts the compositor's frames from the on-screen frame,
so both sides cover the same frames: the skipped cast fails like the full
one, and a clean run reads at most 100%. A busy.test.ts case pins all three.
…ts timing is a test annotation @typescript-eslint/promise-function-async asks a function that returns a promise to be async: knobsOf and the two page.evaluate callbacks. The measured forced style and layout is reported as a test annotation instead of a console line (no-console).
|
Thanks.
|
…ites, which the springs now read meta#8677's head reads tempo and character from the custom properties a theme or adaptation rule writes, and Theme writes no tempo or character attributes, so this case's attributes no longer reach the spring: after the merge it fails, the themed spring equal to the plain one. It now declares --_astryx-motion-tempo and --_astryx-motion-character on the element, as a browser inherits them from the theme's root.
… name here, createMotionAnimator This pull request names the internal motion animator createMotionAnimator, since createAnimator is the public one from the package root, and meta#8677's new check bundled createAnimator from motion/animator. After the merge, core's typecheck failed (TS2305) and the check's spring case failed in both engines. Both files now use createMotionAnimator.
There was a problem hiding this comment.
Changes requested. The springs and the layout glide now follow the theme's tempo and character as spec:AST-108 FR3 describes, and reading the knobs from the theme's custom properties covers nested themes and adaptation rules. Two things block landing:
reducedMotion: 'always'is public, but 47 core files still key reduced motion onprefers-reduced-motionalone (Spinner, Skeleton, Dialog, Drawer, Toast and BottomSheet styles; the Carousel, MobileNav and AppShell script readers). AST-108 FR6 requires them to honour the mark too, so please finish that reach with a test of'always'with the setting off, or keep the value out of the public type until it lands.- The diff still carries #8076 and #8661 (16,858 authored lines around a 941-line change), so please land those first and rebase.
Smaller: the theme authoring contract says the animator reads resolveThemeMotion, and a few motion comments still describe the earlier attribute design.
[Automated review]
| * `'always'` reduces motion for everyone; `'system'` follows each person's | ||
| * Reduce Motion setting. No value loosens Reduce Motion. Default `'system'`. | ||
| */ | ||
| reducedMotion?: ThemeReducedMotion; |
There was a problem hiding this comment.
'always' is public here, but 47 core files still key reduced motion on prefers-reduced-motion alone (Spinner, Skeleton, Dialog, Drawer, Toast, BottomSheet; the Carousel, MobileNav and AppShell script readers). spec:AST-108 FR6 requires them to key on the mark too. Either finish that reach with a test of 'always' with the setting off, or keep this value out of the public type until it lands.
| // The loop reads the duration before a theme's tempo scales it | ||
| // (spec:AST-108 FR4). | ||
| default: `var(--_astryx-pre-tempo-duration-slow-min, ${durationVars['--duration-slow-min']})`, | ||
| '@media (prefers-reduced-motion: reduce)': '3s', |
There was a problem hiding this comment.
Under a theme's 'always' with the person's setting off, this loop keeps its normal turn (0.525 s in the Storybook theme) instead of the 3 s it gets under Reduce Motion: this key has no arm for [data-astryx-reduced-motion] or --_astryx-reduced-motion.
| `expandMotionScale.ts` own their derived token/component values. | ||
| - `motionKnobs.ts` owns the motion knobs' types, defaults and clamping; | ||
| `themeMotion.ts` owns the tokens and pre-tempo durations they generate, and | ||
| `resolveThemeMotion`, the pure read the animator and server code use. |
There was a problem hiding this comment.
Nothing outside tests calls resolveThemeMotion. The animator reads the knobs from CSS custom properties through motion/themeKnobs.ts, as this record's gap paragraph says, so "the pure read the animator and server code use" (and the matching line at 270–272) should name themeKnobs.ts for the animator.
| * reaches CSS curves through its easing tokens. With Astryx's own knobs every | ||
| * role plays exactly its table value. The per-role values stay internal | ||
| * (`spec:AST-094` DEC-11, `spec:AST-108` DEC-1): no CSS variable retunes a | ||
| * role. Nothing here reads style or layout (the theme is found by DOM |
There was a problem hiding this comment.
themeMotionKnobs now reads computed style at every motion start, so "Nothing here reads style or layout (the theme is found by DOM attribute)" no longer holds. The same stale wording is at animator.ts:137 and :1944, and at README.md:24, :50 and :72.
…er code reads resolveThemeMotion The theme authoring contract said the animator reads resolveThemeMotion; it reads the knobs from the custom properties the theme writes (motion/themeKnobs.ts), and resolveThemeMotion is server code's pure read. The motion README's themeKnobs row and resolveMotion's header still described the attribute design; they now say the knobs are inherited custom properties, read in one style read that forces no layout.
|
Thanks.
|
There was a problem hiding this comment.
Changes requested: the touch springs, the layout glide and the animator's Reduce Motion check now follow the theme's tempo, character and reducedMotion as spec:AST-108 FR3 and FR6 describe, but the pull request needs these changes before it can land.
- Land #8076 and #8661 first and rebase, so the diff carries only this change (16,877 authored lines today around a 950-line change).
reducedMotion: 'always'is public while 47 core files still key reduced motion onprefers-reduced-motionalone (Spinner, Skeleton, Dialog, Drawer, Toast and BottomSheet styles; the Carousel, MobileNav and AppShell script readers), which AST-108 FR6 requires to honour the mark, so finish that reach with a test of'always'with the setting off, keep the value out of the public type, or let #8661 land first so the gap leaves this diff.- The motion guide says
'crisp'decelerates without overshoot, but crisp springs still bounce (springs.emphasiskeeps a bounce of 0.2), andthemeMotion.ts,animator.ts, the motion README andthemeKnobs.test.tsxstill describe the earlier design.
[Automated review]
| * @input A theme's motion configuration: the three knobs (`tempo`, | ||
| * `character`, `reducedMotion`), the existing `MotionScaleConfig`, or both | ||
| * @output The resolved knobs, the motion tokens they generate, and | ||
| * `resolveThemeMotion`, the internal resolver the animator and server code |
There was a problem hiding this comment.
This header (and the docstring at line 188) says the animator reads resolveThemeMotion, but the animator reads the knobs from the inherited custom properties through motion/themeKnobs.ts, as this pull request's authoring-contract owning-code line now says; only server code is meant to read the resolver. Line 15 also says the animator reads the knobs "from the theme's root", while it reads the element's inherited properties.
| style: 'unordered', | ||
| 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 keep their measured bounce at every character.", |
There was a problem hiding this comment.
Since line 153 now says the knobs drive the springs too, "'crisp' decelerates without overshoot" overstates: 'crisp' lowers a spring's bounce by 0.1 × its weight (resolveMotion.ts:170-175), so springs.emphasis goes from 0.3 to 0.2 and still overshoots about 1.5%. Saying 'crisp' uses a decelerate-only curve and gives springs less bounce would match the code (the same wording is at theme/motionKnobs.ts:32).
| * through a rendered Theme: a role's spring and duration follow the knobs | ||
| * of the theme the element is in (tempo scales the duration; character | ||
| * moves the bounce by the role's weight, never for an exit), a nested theme | ||
| * that sets no knob plays Astryx's own motion, finger-following motion |
There was a problem hiding this comment.
This header says a nested theme that sets no knob "plays Astryx's own motion", but at this head it plays its enclosing theme's (the browser spec asserts exactly that), and line 17 still refers to "the attributes Theme.tsx writes". themeKnobs.ts:25 also names themeKnobs.test.ts for this .tsx file.
…xts describe the custom-property knobs The motion guide said 'crisp' decelerates without overshoot, which is its entrance and move curves; a crisp spring keeps part of its bounce (springs.emphasis keeps 0.2). The guide and motionKnobs' JSDoc now say crisp gives the curves no overshoot and takes some bounce out of springs, and expressive adds some. themeMotion.ts's header, animator.ts's role note, the motion README and themeKnobs.test.tsx's header still described the attribute design or a role that never follows the theme; they now say a role plays its table value moved by the knobs, read from the custom properties the theme writes, and that no role's own variable retunes it (DEC-11, DEC-15). The README lists the new browser check.
|
Thanks.
|
There was a problem hiding this comment.
Changes requested: the touch springs, the layout glide and the animator's Reduce Motion check follow the theme's tempo, character and reducedMotion as spec:AST-108 FR3 and FR6 describe, and the 'crisp' wording now matches the code, but two things still need to change before this can land.
- Land #8076 and #8661 first and merge
main, so the diff carries only this change (16,887 authored lines today around a 994-line change); that also takes the missing reach ofreducedMotion: 'always'out of this diff (47 core files still key reduced motion onprefers-reduced-motionalone, which AST-108 FR6 requires to honour the mark), unless you finish that reach here with its test. - Three comments are still stale:
themeMotion.ts:188-191says the animator readsresolveThemeMotion,Theme.tsx:275-278leaves the animator, the glide andreducedMotionFade()out of the mark's readers, andthemeKnobs.ts:25namesthemeKnobs.test.tsforthemeKnobs.test.tsx.
[Automated review]
| /** | ||
| * A theme's motion values, for the animator and for server code (FR6): the | ||
| * knobs, and the duration and easing tokens they resolve to with the theme's | ||
| * explicit tokens applied. Pure: it reads only `theme`. |
There was a problem hiding this comment.
This docstring still says resolveThemeMotion is "for the animator and for server code", while the file's header (lines 7-9) and the authoring contract's owning-code lines say server code reads it and the animator reads the knobs' custom properties through motion/themeKnobs.ts. Saying "for server code (FR6)" would match the head.
| // spec:AST-108 FR6: 'always' marks the theme's root. So far motion-keyed | ||
| // adaptation rules and the press release fade key reduced motion on the | ||
| // mark; components' other Reduce Motion styles don't yet (a known gap in | ||
| // `architecture:theme-authoring-contract`). |
There was a problem hiding this comment.
The animator, the layout glide and reducedMotionFade() read the mark too (motion/animator.ts:785, motion/animatedLayout.ts:145, motion/reducedMotion.ts:89), as themeMotion.ts's header now says for the animator; this comment names only adaptation rules and the press release fade.
| * plain Node. | ||
| * | ||
| * SYNC: When modified, update: | ||
| * - /packages/core/src/motion/themeKnobs.test.ts |
There was a problem hiding this comment.
The test file is themeKnobs.test.tsx, not themeKnobs.test.ts.
…ry reader of the mark, and themeKnobs' SYNC names its test file resolveThemeMotion's comment still said the animator reads it; the animator reads the knobs' custom properties (themeKnobs.ts). Theme's FR6 comment listed only adaptation rules and the press release fade as the mark's readers; the animator, the layout glide and reducedMotionFade() read it too. themeKnobs.ts's SYNC list named themeKnobs.test.ts for themeKnobs.test.tsx.
|
Thanks.
|
There was a problem hiding this comment.
Changes requested: the touch springs, the layout glide and the animator's Reduce Motion check follow the theme's tempo, character and reducedMotion as spec:AST-108 FR3 and FR6 describe, and the three comments now match the code, but two things still need to change before this can land.
- Land #8076 and #8661 first and merge
main, so the diff carries only this change (16,889 authored lines today around a 1,006-line change); that also takes the missing reach ofreducedMotion: 'always'out of this diff, where 47 core files still key reduced motion onprefers-reduced-motionalone although AST-108 FR6 requires them to honour the theme's mark too. - The
'always'gap paragraph indocs/architecture/theme-authoring-contract.mdshould also name the layout glide andreducedMotionFade(), which already honour the mark.
[Automated review]
| (`spec:AST-108` FR6, INV15). Motion-keyed adaptation rules, the press release | ||
| fade and the animator key reduced motion on the mark; components' other Reduce | ||
| Motion styles and other script readers of `prefers-reduced-motion` do not yet. Until they do, that part of INV15 and FR6 is current authority but not |
There was a problem hiding this comment.
The animated-layout glide and reducedMotionFade() are script readers of prefers-reduced-motion that already key on the mark (packages/core/src/motion/animatedLayout.ts:143-146, packages/core/src/motion/reducedMotion.ts:89), as Theme.tsx's comment now says. Naming them here would leave the gap listing only what still ignores 'always'.
…or's layout glide and Reduce Motion fade The record's known-gap paragraph, the motion topic, the changeset, the theme template, defineTheme's JSDoc and themeMotion.ts listed motion-keyed rules, the press release fade and the animator as what follows 'always' so far; the layout glide (motion/animatedLayout.ts) and reducedMotionFade() honour the mark too, and they now say so.
|
Thanks.
|
There was a problem hiding this comment.
Changes requested: the touch springs and the layout glide now follow a theme's tempo and character as spec:AST-108 describes (the glide runs 1.5 times longer at tempo: 1.5), and the record's 'always' paragraph now names the glide and reducedMotionFade(). Please land #8076 and #8661 first and merge main, so this pull request carries only its own change (today it carries 16,892 authored lines around 1,007 of its own). Also, reducedMotion: 'always' is public while 47 core components, Spinner and Skeleton among them, still key Reduce Motion on prefers-reduced-motion alone, which AST-108 FR6 rules out: finish that reach with its test, or keep 'always' out of the public type until it lands.
[Automated review]
| // The loop reads the duration before a theme's tempo scales it | ||
| // (spec:AST-108 FR4). | ||
| default: `var(--_astryx-pre-tempo-duration-slow-min, ${durationVars['--duration-slow-min']})`, | ||
| '@media (prefers-reduced-motion: reduce)': '3s', |
There was a problem hiding this comment.
Under a theme with reducedMotion: 'always' and the person's own setting off, this loop keeps its regular duration (525 ms in Storybook, against 3 s under Reduce Motion). spec:AST-108 FR6 asks this style to also key on the theme's mark, for example a [data-astryx-reduced-motion] arm or the --_astryx-reduced-motion property, or 'always' should stay out of the public type until that reach lands.
…ch the SideNav rail moves onto (#8721) Implements the records #8783 put on main: `spec:AST-105` FR2 and FR9, `architecture:react-component-runtime` INV12, `component:SideNav` FR6, `component:Resizable` FR8, `component:LayoutPanel` FR7 and AR2, and `component:Table` FR15. Resizable's and LayoutPanel's slide and LayoutPanel's focus rule (AR2: focus moves to the ResizeHandle only when a collapsing pane holds it) are built on this path in #9197 and #9202. This PR moves no focus anywhere. Behavior-module PR: `packages/core/src/motion/animatedLayout.ts` (FR9's shared edge reveal). ## User need A panel that collapses beside content, a Collapsible closed while it opens, a resizable pane sliding shut: each reveals or conceals an element from one of its edges while the content beside it moves. Before this change Core had one such reveal of its own per component. The SideNav rail clipped and rode its content in its own code. Collapsible's reveal could only open downward, and a close while it opened restarted the conceal from fully shown while the content below kept gliding, so a gap opened. Nothing a resize controller could reuse existed. #8783 made these one shared edge reveal in the animated-layout module. ## Problem and solution fit The animated-layout module now produces `spec:AST-105` FR9's edge reveal, in three forms over one ride: - **One-state** (`playLayoutChange`, `holdLayoutExit`): reveals and conceals from the block-end edge by default, or from the block-start or an inline edge it is given, mirrored right to left. Once the content on that edge's side glides, the moving edge rides that glide frame by frame (its keyframes replaced, its clock the glide's), so no gap opens. It rides the content's offset relative to the element: when the same batch also moves the element itself, such as an item above them closing in a single-open CollapsibleGroup, the edge follows only the difference. A conceal by a start edge frees the element's space on that edge's side, so the element stays where it shows while the content before it moves; a layout where that would move the element itself is not held, and the element goes at once. A reversal starts from what the element shows. Collapsible, TreeList and Table's row expansion already play this block-end form, so they gain the ride, and Collapsible and TreeList, which hold their exits, the reversal too. - **Two-state** (`holdLayoutEdge`, `playLayoutEdge`): holds the old state as FR8's swap copy, clips the larger state on the edge from the extent it shows, rides a gliding element, and raises the element above the copy while it conceals. The SideNav rail now plays through it on its inline-end edge and keeps only its labels' and icons' fades (`component:SideNav` FR6); its frames are unchanged. - **A resized element with no copy** (`reportLayoutEdge`): measured before a size change commits and played after the new size applies. A larger extent reveals from the extent shown. A smaller one holds the element at the extent it was laid out at, freeing the difference by the margin on that edge's side so what lies beyond stays where the new size puts it, then clips down to the new extent. This is the path a resize controller's pane uses (`component:Resizable` FR8 and `component:LayoutPanel` FR7 in #9197 and #9202). Reduce Motion follows `design:reduced-motion` DR2 under a coarse primary pointer and DR3 otherwise. ## Current specification `spec:AST-105` FR9 ("One shared edge reveal") and FR2, `architecture:react-component-runtime` INV12, `component:SideNav` FR6, `component:Resizable` FR8, `component:LayoutPanel` FR7 and AR2, and `component:Table` FR15, all current on main since #8783. A LayoutPanel given a hand-built `ResizableProps` gets no slide, and the hook's own `props` passed through `resizable` slide; both are #9197 and #9202's to implement. ## Public delta and caller burden None. The new functions and types stay internal: no entry point exports them (`animatedLayout.boundary.test.ts` checks this), and `useAnimatedLayout`'s contract is unchanged. The visible changes are Collapsible's and TreeList's: content closed while it opens, or opened while it closes, continues from what it shows, and its edge stays on the content after it (`[fix]` changeset). Table's row expansion reveal rides the rows after it. ## Design and alternatives - **Riding:** the edge's keyframes are sampled on the glide's own sample grid and clock, so the clip and the glide are complementary by construction. The alternative, a separate spring tuned to match, drifts whenever the glide carries a running motion. - **Resized element without a copy:** holding the old extent keeps the pane's content laid out as it was. Clipping a pane already at its new size would need its content to overflow, or a copy, which the runtime record rules out for a resize controller. - **Morph routing** of the glide stays out until the shared rect morph lands (#8329), as INV12's "until then" clause says. ## Evidence Status at this head: rebased onto main 8a03d75 after the component records moved to `docs/specs/components/`; the patch is byte-identical to 70ebcc7's. Here, build, type check, CI-strict lint, `check:knowledge`, the record-path tests (knowledge paths, check-knowledge, specs-for-file), `check:changesets` and the motion, SideNav and AppShell unit suites (1,020) pass, and `specs:for` still returns `component:SideNav` and `spec:AST-105` for the changed files. At 70ebcc7 every gate finished: `test:ui`, `check:repo`, the node suites (only the two `cache.test.mjs` lock cases that fail on main), and the three motion specs ×10 in WebKit under the CI job's command (740, with 100 Chromium-only skips) and in Chromium pinned to 2 cores at 4 workers (840). **Still running:** `test:ui`, the node suites, `check:repo` and the three motion specs ×10 in both engines at this head. Each new proof failed on main first: - `animatedLayout.edge.test.ts`: 17 of the 18 one- and two-state cases fail on main (the block-end default passes there and guards it), and the resized-element cases have no function there. - `SideNavRailMotion.test.tsx`: a new case asserts the rail plays through the module's two-state form with no clip, copy or ride of its own. It fails on main; the rail's existing cases pass unchanged with the rail moved. - `animatedLayout.interaction.browser.spec.ts` (FR9): a block-end reveal reversed into a conceal, an inline-end reveal and conceal (LTR and RTL), the two-state form, and a resized pane (start pane LTR to 48 px, start pane RTL to 0, end pane LTR to 48), each with a reversal. On every frame the moving edge stays within 1.5 px of what it rides. All fail on main: the block-end reversal jumped 40 px, the inline edge was clipped from the bottom, and the other forms don't exist there. - `animatedLayoutAdopters.interaction.browser.spec.ts`: a Collapsible closed while it opens, and a TreeList row collapsed while it expands, keep their edge on the content after them. On main each conceal restarted from fully shown: −16 px and −36 px gaps. Two more cases guard this PR's own paths. A block-start reveal and conceal in a column that stacks from its end keep the edge on the content before it, with the element staying where it shows; before the start-edge fix the conceal freed its space at its end, so the element jumped by its 80 px and an 80 px gap opened. And in a single-open CollapsibleGroup switching from a taller item to a shorter one below it, the opening item's edge stays on the next item on every frame; before the ride was taken relative to the element, the content showed 31 px over the next item. How the proofs read a ride: both sides on the module's clock, the edge from its clip animation's keyframes and what it rides from its layout position plus its glide's keyframes, at the animations' current time. Chromium also asserts that its evaluated clip and rect equal those (under 0.5 px). In WebKit the two run a little apart, and that skew is reported per test, not hidden: a compositor-run `translate` is evaluated a little ahead of the frame and, on a late first frame, a main-thread clip reads one frame behind. Under the CI WebKit job's command (`LP_NUM_THREADS=4`), the summed skew per frame reached 0.7 to 3.4 px depending on the test. The reversals pause and seek every animation to 120 ms, and the adopters' wait for the component to commit the close before playing on, so neither a late first frame nor a component committing in a later task moves the moment they test. ## Scope - [x] One capability is added (FR9's shared edge reveal), with its adopters in this PR's scope (the SideNav rail; Collapsible, TreeList and Table's row expansion keeping their form). - [x] Unrelated fixes, cleanup, and policy changes were removed or split. Resizable's and LayoutPanel's adoption is #9197 and #9202. - [x] Consumer docs, focused tests, and migration evidence ship with the feature. - [x] Public text and artifacts contain no internal Meta context. Overlaps: #8677 also edits `motion/animatedLayout.ts` (the theme's motion knobs), and #3890 edits `SideNav/SideNav.tsx`. This PR leaves `SideNav.tsx` unchanged. ## Testing ``` pnpm exec vitest run --project ui packages/core/src/motion packages/core/src/SideNav packages/core/src/AppShell packages/core/src/Collapsible packages/core/src/TreeList packages/core/src/Table pnpm build && pnpm storybook:build pnpm test:interaction packages/core/src/motion/__tests__/animatedLayout.interaction.browser.spec.ts packages/core/src/motion/__tests__/animatedLayoutAdopters.interaction.browser.spec.ts packages/core/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts --project chromium ``` For WebKit, use the CI WebKit job's container command (`internal/interaction-harness/scripts/webkit-container.mjs`) with podman `--cpus=4`, `CI=1`, `LP_NUM_THREADS=4` and 1 worker.
Depends on #8076 and #8661. Review only this PR's change: the springs and the knob reader.
Why
A theme's
tempoandcharacter(#8661) reach CSS transitions, but not the springs of a sheet, popover or menu presented on touch, nor doesreducedMotion: 'always'.spec:AST-108FR3 derives the springs from the same knobs, wherever a theme or adaptation rule sets them.What led to it
The work to make Astryx's touch components behave as native iOS ones do: the touch presentations play on the animator's motion roles (
spec:AST-094FR4), which knew nothing about their theme.Options and tradeoffs
getComputedStyleread per motion start, and CSS supplies inheritance and rule conditions.charactermoves each role: the weights (1 for entrances, 0.5 for state changes, 0 for exits) and the 0.1 step were asked on spec(AST-108): theme motion is three knobs — tempo, character, reducedMotion #8613.Why this choice
The springs follow what the durations follow, and the read forces no layout. Exits keep their measured bounce, so dismissals never wobble.
What changes
--_astryx-motion-characterbeside--_astryx-motion-tempo; existing themes are unchanged (FR5).resolveMotion(element, role)reads both:temposcales spring and duration roles, never finger-following ones (FR4). The layout glide plays one spring at the area's knobs.reducedMotion: 'always'counts as Reduce Motion for the animator (FR6).How to check it
motion: {tempo: 1.5}on a touch device: it presents 1.5 times slower; a drag still follows the finger.reducedMotion: 'always'on an outer theme: the sheet fades in instead.Evidence
'standard'stops an outer character, a coarse-pointer rule applies only on a phone, and a nested spring plays at the outer tempo, starting on its first frame with a dirty 12,000-row layout. Long Animation Frames time the read at 0 ms of forced layout, against 5 ms for reading a size.Scope