Skip to content

feat(core): a theme's motion takes tempo and character - #8661

Merged
cixzhang merged 7 commits into
meta:mainfrom
vjeux:uikit/f1-theme-motion-knobs
Oct 31, 2026
Merged

cixzhang merged 7 commits into
meta:mainfrom
vjeux:uikit/f1-theme-motion-knobs

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Why

A theme author cannot say how motion should feel, quicker, crisper or reduced, without overriding durations and curves per component. spec:AST-108 (#8613) gives defineTheme's motion three knobs: tempo, character and reducedMotion. This pull request ships tempo and character, and exports their types (ThemeMotionConfig, ThemeMotionKnobs, MotionCharacter). reducedMotion stays out of the public type until 'always' works: component Reduce Motion styles, script readers and the animator must follow it first. Every theme follows each person's setting, as 'system' does.

What led to it

This comes from the work to make Astryx's touch components behave as native iOS ones do, where a theme's motion must reach springs and press feedback, not only CSS. Themes could scale their duration tokens, but easing was one curve for everything, and Reduce Motion only followed the system setting.

Options and tradeoffs

  • CSS-only knobs compose through the cascade but cost the animator a style read per motion; attribute-only knobs are free for scripts but invisible to CSS.
  • Busy loops (Spinner, Skeleton) could be exempted from tempo by name, or read a copy of each duration taken before tempo.
  • An extending theme could resolve base first, then generated (the theme-authoring contract), or let a base's explicit motion tokens win over its children's knobs.

Why this choice

  • CSS gets tokens now; the attributes the animator reads come with the animator's own pull request.
  • The copies: every layer that writes a duration writes one, so later components need nothing special.
  • The contract's precedence, as the maintainer ruled; a child with no slow keeps its base's slow band (docs(theme): an extending theme's motion inherits field by field #8748).
  • --ease-exit is feat(tokens): add --ease-exit, the curve BottomSheet already closes on #8004's; this defines none, and character never moves it.
  • reducedMotion: 'always' would have reached only motion-keyed adaptation rules and the press release fade, so a theme setting it would reduce a little of the page and leave Spinner, Skeleton and most components moving. With 'system' as the only value the field would do nothing, so the public type leaves it out until every component follows 'always'.

How to check it

  1. Wrap a Dialog story in <Theme theme={defineTheme({name: 'Slow', motion: {tempo: 1.5}})}>: it opens 1.5 times slower; a Spinner beside it keeps its speed.
  2. Add character: 'expressive': --ease-entrance and --ease-move take the overshooting curve, so a style that eases on them overshoots on entrance; --ease-standard and exits do not change. Dialog still eases on --ease-standard, so it does not overshoot yet.
  3. Under tempo 1.5, tap or click a Button: its pressed highlight fades out over 300 ms instead of 200 ms.
  4. Extend a theme that sets --duration-medium: 300ms with tempo: 1.2: the child's is 492 ms.
  5. defineTheme({name: 'Partial', motion: {fast: 100, tempo: 1.2}}) throws: a duration scale needs fast, medium and ratio, from the theme or its base.

Evidence

  • Tests: packages/core/src/theme/themeMotion.test.ts and packages/core/src/utils/pressFeedback.test.ts. Each new case fails without its part of the change.
  • The theme, press feedback, Spinner and Skeleton suites, the theme private-variable and CSS build checks, the CLI doc goldens, type check and lint pass on this head.

Writing rubric results

R1 narration, R2 research, R4 rejected alternatives, R5 change references and R6 open questions: none. R3 evidence: INV15's row cites its test file.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 31, 2026 4:29am UTC

Request Review

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

astracat-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 31, 2026, 4:42 AM UTC

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 27, 2026
@vjeux
vjeux force-pushed the uikit/f1-theme-motion-knobs branch from aeb37c1 to 50323d9 Compare October 27, 2026 07:55
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This needs an engineering owner's decision before it can be approved. The motion knobs rest on spec:AST-108 and the spec:AST-094, spec:AST-090, design:reduced-motion DR2a, design:motion DR3 and architecture:theme-authoring-contract INV15 text this pull request adds, which is not on main and not approved for this head. An engineering owner can approve it by commenting /approve-spec <full head SHA> here (the author can, as an engineering owner); the approval should cover the build's 'crisp' and 'expressive' curves and INV15's rule that a lineage's explicit tokens win, and without it this waits for #8613 to land. Either way, these need fixing:

  • reducedMotion: 'always' adds the mark, but nothing reads it, so motion is not reduced, although the docs, template and changeset say it is.
  • A child theme with its own duration scale now loses to its base's explicit --duration-* and --ease-* tokens, which changes existing themes.
  • A theme nested inside a tempo theme times Spinner and Skeleton from the outer theme's pre-tempo values.
  • The docs say the knobs drive touch springs; resolveThemeMotion disagrees with the CSS for an explicit --ease-standard; and Spinner's and Skeleton's records still say the loops take the scaled tokens.

[Automated review]

Comment thread packages/core/src/theme/defineTheme.ts Outdated
Comment on lines +576 to +584
const lineageMotionTokens =
input.motion != null && base?.__inputTokens != null
? Object.fromEntries(
Object.entries(base.__inputTokens).filter(
([name]) =>
name.startsWith('--duration-') || name.startsWith('--ease-'),
),
)
: undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-applying every explicit --duration-*/--ease-* token in the lineage whenever the child sets motion changes existing themes. A child with its own scale and easing ({fast: 100, medium: 250, ratio: 0.75, easing: 'ease-in'}) over a base with explicit --duration-medium: 300ms and --ease-standard: linear now resolves to 300 ms and linear instead of its own 250 ms and ease-in. Limiting the re-application to tokens the knobs regenerate keeps today's order: base, then the child's generated values, then explicit tokens.

Comment on lines +114 to +119
if (knobs.tempoIsSet) {
Object.assign(tokens, durationTokensAt(knobs.tempo));
for (const [name, value] of Object.entries(durationDefaults)) {
tokens[preTempoName(name)] = value;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only a theme that sets tempo writes the --pre-tempo-* copies. A theme nested inside a tempo theme that sets its own durations (a scale or explicit tokens) therefore inherits the outer copies, and its Spinner and Skeleton loops run at 0.73 s and 0.55 s instead of its own 0.6 s and 0.335 s. Writing the copy beside every --duration-* token a theme writes stops the leak.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
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.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing reads data-astryx-reduced-motion at this head; only Theme.tsx writes it. With 'always', Spinner keeps turning at its normal speed and Skeleton keeps pulsing, exactly as without it. Until components key on the mark, this sentence and the matching template, JSDoc and changeset wording promise a reduction the release will not have.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: "A theme sets how motion feels with three knobs in `defineTheme`'s `motion`. They drive the CSS motion tokens and the springs of touch motion from the same values, so both stay in step.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No animator code reads the knobs yet (resolveThemeMotion has no caller), and no component reads --ease-entrance or --ease-move. In this change the knobs reach neither touch springs nor Astryx's own entrances.

Comment thread packages/core/src/theme/themeMotion.ts Outdated
Comment on lines +177 to +179
'--ease-standard': standard,
'--ease-entrance': value('--ease-entrance', standard),
'--ease-move': value('--ease-move', standard),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For a theme that sets only an explicit --ease-standard, this reports --ease-entrance and --ease-move as that value. expandThemeMotion writes neither token, though, so the CSS keeps the default cubic-bezier(0.24, 1, 0.4, 1). Writing both tokens whenever the theme sets --ease-standard, or returning the CSS default here, keeps the resolver and the CSS in step.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This needs an engineering owner's decision before it can be approved: the motion knobs rest on spec:AST-108 and the spec:AST-094, spec:AST-090, design:reduced-motion DR2a, design:motion DR3 and architecture:theme-authoring-contract INV15 text this pull request adds, which is not on main and not approved for this head. An engineering owner can approve it by commenting /approve-spec 619b3a5c54cb1b1001bf4257a28fb105fe261592 here (the author can, as an engineering owner), and the approval should cover the build's 'crisp' and 'expressive' curves, the new tempo and character attributes and INV15's inheritance rules; without it this waits for #8613 to land. Either way, these need fixing:

  • reducedMotion: 'always' adds the mark, but nothing reads it, so motion is not reduced, although the docs, template, JSDoc and changeset say it is.
  • A child theme that sets its own duration scale now resolves differently: its base's explicit --duration-* and --ease-* tokens win, and its ratio re-derives an inherited slow band.
  • A theme nested inside a tempo theme times Spinner and Skeleton from the outer theme's pre-tempo values.
  • The docs say the knobs drive touch springs, --ease-entrance and --ease-move ignore an explicit --ease-standard (where resolveThemeMotion disagrees with the CSS), and Spinner's and Skeleton's records still say the loops take the scaled tokens.

[Automated review]

Comment thread packages/core/src/theme/Theme.tsx Outdated
useRootThemeSync(isNested, mode, theme.name);
// spec:AST-108 FR6: 'always' marks the theme's root; every component style
// keyed on prefers-reduced-motion also keys on the mark. FR3: the tempo and
// character a theme sets ride on the same elements, where the animator

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing reads data-astryx-motion-tempo or data-astryx-motion-character at this head: no animator or motion code looks for them, and the proposed AST-108 FR6 names resolveThemeMotion as the animator's read. Until FR3 lands they are public DOM output with no effect, and this comment and the JSDoc above useRootThemeSync describe a reader that does not exist yet.

const baseMotion = base?.__axes?.motion;
const motion: ThemeMotionConfig | undefined =
input.motion != null && baseMotion != null
? {...baseMotion, ...input.motion}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Merging the base's scale fields into a child that sets its own scale changes existing themes too: {fast: 100, medium: 250, ratio: 0.8} over a base with slow: 700, ratio: 0.75 now re-derives the slow band with 0.8, so --duration-slow-min moves from 525 ms to 560 ms (Spinner's cycle) and --duration-slow-max from 935 ms to 875 ms. Merging only the knob fields keeps a child's own scale replacing the base's, as it does on main.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This needs an engineering owner's decision before it can be approved: the motion knobs rest on spec:AST-108 and the spec:AST-094, spec:AST-090, spec:AST-012, architecture:theme-application INV1, design:reduced-motion DR2a, design:motion DR3 and architecture:theme-authoring-contract INV15 text this pull request adds, which is not on main and not approved for this head. An engineering owner can approve it by commenting /approve-spec d409b49d20b034a517443e7395bf70fdaab4de9e here (the author can, as an engineering owner), and the approval should cover the build's 'crisp' and 'expressive' curves, the tempo and character attributes nothing reads yet and INV15's inheritance rules; without it this waits for #8613 to land. Either way, these need fixing:

  • reducedMotion: 'always' reaches only adaptation rules, so every component keeps full motion, although the docs, template, JSDoc and changeset say it reduces motion for everyone.
  • A child theme that sets its own duration scale now resolves differently, since its base's explicit --duration-* and --ease-* tokens win and its ratio re-derives an inherited slow band, and it resolves differently again over a built base.
  • A theme nested inside a tempo theme times Spinner and Skeleton from the outer theme's pre-tempo values.
  • The docs say the knobs drive touch springs, --ease-entrance and --ease-move ignore an explicit --ease-standard (where resolveThemeMotion also disagrees with the CSS), and Spinner's and Skeleton's records still say the loops take the scaled tokens.

[Automated review]

Comment thread packages/core/src/theme/defineTheme.ts Outdated
input.motion != null && baseMotion != null
? {...baseMotion, ...input.motion}
: input.motion;
const lineageMotionTokens =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-applying every explicit --duration-*/--ease-* input token of the base whenever the child sets motion changes existing child themes that set only a duration scale: {fast: 100, medium: 250, ratio: 0.75, easing: 'ease-in'} over a base with --duration-medium: 300ms and --ease-standard: linear resolves to 300 ms and linear here and to 250 ms and ease-in on main. Built theme modules carry no __inputTokens, so the same child resolves differently again over a built base. Limiting the re-application to tokens the knobs regenerate, and applying it the same way over built bases, keeps today's order: base, then the child's generated values, then explicit tokens.

const baseMotion = base?.__axes?.motion;
const motion: ThemeMotionConfig | undefined =
input.motion != null && baseMotion != null
? {...baseMotion, ...input.motion}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Merging the base's scale fields into a child that sets its own scale re-derives inherited bands with the child's ratio: {fast: 100, medium: 250, ratio: 0.8} over a base with slow: 700, ratio: 0.75 moves --duration-slow-min from 525 ms to 560 ms (Spinner's cycle). Merging only the knob fields keeps a child's own scale replacing the base's, as it does on main.

): Record<string, string> {
const knobs = resolveMotionKnobs(config);
const tokens: Record<string, string> = {};
if (knobs.tempoIsSet) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only a theme that sets tempo writes the --pre-tempo-* copies, so a theme nested inside a tempo theme that sets its own durations inherits the outer copies: in Chromium its Spinner and Skeleton loops run at 0.73 s and 0.55 s instead of its own 0.6 s and 0.335 s. Writing the copy beside every --duration-* token a theme writes, generated or explicit, stops the leak.

Comment thread packages/core/src/theme/themeMotion.ts Outdated
durations,
easings: {
'--ease-standard': standard,
'--ease-entrance': value('--ease-entrance', standard),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For a theme with only an explicit --ease-standard, this reports --ease-entrance and --ease-move as that value, but expandThemeMotion writes neither token, so the CSS keeps cubic-bezier(0.24, 1, 0.4, 1). Given a defined theme, the knobs also come back as tempo 1 and 'standard', because a defined theme keeps them in __axes.motion, not motion.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
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.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

At this head the mark changes only adaptation rules keyed on when: {motion}; no component style or script reader of prefers-reduced-motion reads it, so under 'always' Spinner keeps turning at its normal speed and Skeleton keeps pulsing for anyone whose own setting is off. Until components key on the mark, this sentence, the template, the JSDoc and the changeset promise a reduction the release does not have.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: "A theme sets how motion feels with three knobs in `defineTheme`'s `motion`. They drive the CSS motion tokens and the springs of touch motion from the same values, so both stay in step.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No motion-foundation code reads the knobs yet (resolveThemeMotion has no caller and nothing reads the tempo and character attributes), so in this change the knobs reach neither touch springs nor any Astryx component's entrances.

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

Skeleton (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 387B 387B 0B
Lines of Code 130 130 -
Complexity Low Low (3) -
Spinner (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 405B 405B 0B
Lines of Code 296 296 -
Complexity High High (25) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
charts/Chart N/A 65.5KB N/A N/A 20B N/A
charts/ChartAxis N/A 6.7KB N/A N/A 112B N/A
charts/ChartLegend N/A 54.7KB N/A N/A 20B N/A
charts/ChartTooltip N/A 54.7KB N/A N/A 20B N/A
core/AlertDialog 58.4KB 58.6KB +190B 12.6KB 12.6KB +78B
core/AppShell 67.5KB 67.7KB +162B 11.8KB 11.9KB +76B
core/AspectRatio 3.7KB 3.8KB +48B 3.5KB 3.6KB +80B
core/Avatar 38.0KB 38.0KB +57B 6.3KB 6.4KB +62B
core/AvatarGroup 40.3KB 40.3KB +61B 6.5KB 6.5KB +61B
core/AvatarGroupOverflow 41.2KB 41.4KB +196B 6.4KB 6.4KB +63B
core/AvatarStatusDot 7.1KB 7.1KB +41B 3.4KB 3.5KB +79B
core/Badge 3.9KB 3.9KB +42B 3.8KB 3.9KB +81B
core/Banner 49.4KB 49.6KB +198B 10.1KB 10.2KB +75B
core/BaseTypeahead 55.9KB 56.1KB +219B 10.1KB 10.1KB +77B
core/Blockquote 3.5KB 3.6KB +48B 3.4KB 3.5KB +79B
core/BottomSheet 34.4KB 34.4KB +48B 7.9KB 8.0KB +63B
core/BottomSheetSwitcher 14.8KB 14.9KB +48B 3.3KB 3.4KB +81B
core/BreadcrumbItem 68.3KB 68.5KB +203B 11.3KB 11.3KB +79B
core/Breadcrumbs 22.8KB 22.9KB +40B 3.3KB 3.4KB +79B
core/Button 44.6KB 44.8KB +194B 9.3KB 9.4KB +77B
core/ButtonGroup 8.4KB 8.4KB +46B 3.3KB 3.4KB +79B
core/Calendar 56.5KB 56.7KB +189B 10.2KB 10.2KB +75B
core/Card 6.4KB 6.5KB +45B 6.3KB 6.4KB +80B
core/Carousel 51.9KB 52.0KB +161B 10.4KB 10.4KB +77B
core/Center 4.2KB 4.2KB +43B 4.6KB 4.7KB +81B
core/ChatComposer 62.3KB 62.5KB +195B 11.0KB 11.1KB +74B
core/ChatComposerDrawer 24.3KB 24.3KB +45B 4.6KB 4.6KB +79B
core/ChatComposerInput 57.1KB 57.3KB +211B 10.2KB 10.3KB +74B
core/ChatComposerTokenElement 57.1KB 57.3KB +210B 10.2KB 10.3KB +74B
core/ChatDictationButton 48.1KB 48.3KB +196B 9.6KB 9.7KB +74B
core/ChatLayout 50.5KB 50.7KB +201B 10.1KB 10.2KB +76B
core/ChatLayoutScrollButton 48.0KB 48.2KB +198B 9.8KB 9.8KB +76B
core/ChatMessage 23.4KB 23.5KB +47B 3.6KB 3.6KB +79B
core/ChatMessageBubble 7.0KB 7.0KB +47B 3.6KB 3.7KB +79B
core/ChatMessageList 29.3KB 29.4KB +62B 7.8KB 7.8KB +76B
core/ChatMessageMetadata 26.4KB 26.4KB +41B 4.0KB 4.1KB +61B
core/ChatSendButton 47.3KB 47.5KB +204B 9.3KB 9.4KB +75B
core/ChatSystemMessage 6.7KB 6.8KB +51B 3.7KB 3.7KB +79B
core/ChatTokenizedText 6.9KB 7.0KB +44B 3.9KB 4.0KB +80B
core/ChatToolCalls 32.5KB 32.5KB +47B 9.1KB 9.2KB +68B
core/CheckIndicator 9.2KB 9.2KB +42B 3.8KB 3.8KB +77B
core/CheckboxIndicator 4.1KB 4.1KB +46B 3.8KB 3.9KB +64B
core/CheckboxInput 51.5KB 51.7KB +195B 9.5KB 9.5KB +72B
core/CheckboxList 46.8KB 46.8KB +59B 8.4KB 8.4KB +59B
core/CheckboxListItem 55.9KB 56.1KB +182B 10.8KB 10.9KB +73B
core/Citation 26.5KB 26.6KB +48B 4.3KB 4.4KB +77B
core/ClickableCard 13.6KB 13.8KB +186B 6.9KB 7.0KB +79B
core/Code 3.5KB 3.5KB +41B 3.4KB 3.4KB +78B
core/CodeBlock 62.4KB 62.6KB +194B 10.9KB 10.9KB +73B
core/Collapsible 19.5KB 19.7KB +152B 4.4KB 4.5KB +74B
core/CollapsibleGroup 6.4KB 6.4KB +47B 3.2KB 3.3KB +81B
core/CommandPalette 53.0KB 53.2KB +209B 12.0KB 12.1KB +78B
core/CommandPaletteEmpty 6.1KB 6.1KB +40B 3.4KB 3.4KB +80B
core/CommandPaletteFooter 23.6KB 23.7KB +47B 3.5KB 3.6KB +81B
core/CommandPaletteGroup 3.5KB 3.5KB +41B 3.3KB 3.4KB +80B
core/CommandPaletteInput 32.9KB 33.0KB +60B 8.3KB 8.4KB +74B
core/CommandPaletteItem 9.2KB 9.4KB +165B 3.5KB 3.6KB +80B
core/CommandPaletteList 22.8KB 22.8KB +39B 3.3KB 3.4KB +80B
core/ComplexSelector 57.5KB 57.6KB +179B 10.8KB 10.9KB +74B
core/ContextMenu 76.3KB 76.5KB +198B 15.1KB 15.2KB +77B
core/ContextMenuItem 54.5KB 54.7KB +188B 9.8KB 9.9KB +74B
core/DateInput 101.7KB 101.8KB +181B 15.0KB 15.0KB +80B
core/DateRangeInput 71.4KB 71.6KB +211B 11.4KB 11.4KB +73B
core/DateTimeInput 111.9KB 112.1KB +210B 15.4KB 15.5KB +81B
core/Dialog 16.4KB 16.4KB +47B 7.1KB 7.2KB +70B
core/DialogHeader 50.4KB 50.6KB +201B 10.0KB 10.1KB +76B
core/Divider 6.5KB 6.5KB +40B 3.6KB 3.7KB +78B
core/Drawer 32.0KB 32.0KB +74B 8.1KB 8.1KB +62B
core/DrawerHeader 50.5KB 50.7KB +199B 11.4KB 11.5KB +73B
core/DropdownMenu 74.1KB 74.3KB +213B 15.2KB 15.3KB +76B
core/DropdownMenuCheckboxItem 17.3KB 17.5KB +186B 5.8KB 5.9KB +60B
core/DropdownMenuDivider 6.6KB 6.6KB +39B 3.6KB 3.7KB +81B
core/DropdownMenuGroup 6.1KB 6.1KB +46B 3.3KB 3.4KB +80B
core/DropdownMenuItem 16.6KB 16.8KB +189B 5.4KB 5.5KB +78B
core/DropdownMenuRadioGroup 6.0KB 6.1KB +38B 3.2KB 3.3KB +82B
core/DropdownMenuRadioItem 17.4KB 17.6KB +175B 5.8KB 5.9KB +60B
core/DropdownMenuSubMenu 59.8KB 59.9KB +187B 9.8KB 9.8KB +72B
core/EmptyState 7.7KB 7.8KB +41B 3.5KB 3.6KB +81B
core/Field 45.0KB 45.0KB +52B 8.1KB 8.2KB +61B
core/FieldLabel 40.5KB 40.5KB +52B 6.7KB 6.8KB +62B
core/FieldStatus 10.6KB 10.6KB +47B 4.4KB 4.5KB +58B
core/FileInput 55.7KB 55.9KB +194B 10.5KB 10.6KB +77B
core/FormLayout 6.3KB 6.3KB +42B 3.4KB 3.5KB +81B
core/Grid 4.3KB 4.4KB +40B 3.7KB 3.7KB +80B
core/GridSpan 3.4KB 3.5KB +42B 3.2KB 3.3KB +79B
core/HStack 7.3KB 7.3KB +40B 4.9KB 5.0KB +83B
core/Heading 9.1KB 9.2KB +58B 6.8KB 6.9KB +65B
core/HoverCard 17.3KB 17.3KB +17B 4.9KB 5.0KB +12B
core/Icon 9.0KB 9.0KB +37B 3.8KB 3.8KB +77B
core/IconButton 44.6KB 44.8KB +183B 9.3KB 9.4KB +77B
core/InputGroup 45.5KB 45.6KB +53B 8.2KB 8.3KB +61B
core/InputGroupText 6.3KB 6.4KB +46B 3.7KB 3.8KB +79B
core/Item 12.4KB 12.6KB +174B 4.7KB 4.8KB +79B
core/Kbd 23.4KB 23.4KB +52B 3.5KB 3.5KB +80B
core/LayerProvider 64.9KB 65.1KB +194B 10.3KB 10.4KB +78B
core/Layout 8.1KB 8.2KB +47B 5.5KB 5.6KB +81B
core/LayoutContent 7.4KB 7.4KB +43B 4.9KB 5.0KB +82B
core/LayoutFooter 7.3KB 7.4KB +48B 4.8KB 4.9KB +81B
core/LayoutHeader 7.4KB 7.4KB +46B 4.8KB 4.9KB +81B
core/LayoutPanel 7.6KB 7.6KB +43B 4.8KB 4.9KB +79B
core/Lightbox 52.8KB 53.0KB +188B 10.0KB 10.1KB +81B
core/Link 45.0KB 45.2KB +188B 7.8KB 7.8KB +62B
core/List 6.8KB 6.8KB +45B 3.5KB 3.6KB +77B
core/ListItem 55.3KB 55.5KB +190B 10.8KB 10.9KB +73B
core/Markdown 146.5KB 146.7KB +222B 18.0KB 18.1KB +72B
core/MetadataList 23.5KB 23.6KB +41B 3.6KB 3.7KB +81B
core/MetadataListItem 6.5KB 6.5KB +40B 3.4KB 3.5KB +80B
core/MobileNav 52.6KB 52.8KB +159B 10.2KB 10.3KB +78B
core/MobileNavToggle 47.8KB 48.0KB +198B 9.6KB 9.6KB +75B
core/MoreMenu 74.4KB 74.6KB +211B 15.2KB 15.3KB +76B
core/MultiSelector 74.3KB 74.5KB +209B 14.8KB 14.9KB +100B
core/NavHeadingMenu 13.0KB 13.0KB +46B 3.3KB 3.4KB +80B
core/NavIcon 3.4KB 3.5KB +42B 3.3KB 3.4KB +81B
core/NumberInput 57.4KB 57.6KB +178B 10.9KB 11.0KB +76B
core/Outline 29.8KB 30.0KB +164B 4.0KB 4.1KB +79B
core/OverflowList 8.5KB 8.5KB +41B 3.5KB 3.6KB +81B
core/Overlay 19.6KB 19.7KB +61B 3.8KB 3.9KB +60B
core/Pagination 86.7KB 86.9KB +220B 15.6KB 15.7KB +72B
core/Popover 51.0KB 51.2KB +209B 12.2KB 12.3KB +77B
core/PowerSearch 168.4KB 168.7KB +237B 20.1KB 20.2KB +78B
core/ProgressBar 7.8KB 7.8KB +70B 6.6KB 6.7KB +62B
core/RadioIndicator 3.8KB 3.9KB +42B 3.7KB 3.8KB +63B
core/RadioList 46.3KB 46.4KB +61B 8.2KB 8.2KB +61B
core/RadioListItem 53.8KB 54.1KB +211B 9.9KB 10.0KB +60B
core/ResizeHandle 25.5KB 25.5KB +42B 3.9KB 4.0KB +80B
core/ScrollableArea 10.7KB 10.7KB 0B 4.3KB 4.3KB +13B
core/Section 7.5KB 7.5KB +45B 6.2KB 6.3KB +80B
core/SegmentedControl 39.3KB 39.3KB +64B 6.3KB 6.4KB +63B
core/SegmentedControlItem 9.4KB 9.6KB +176B 3.9KB 4.0KB +79B
core/SelectableCard 13.6KB 13.7KB +177B 7.1KB 7.2KB +79B
core/Selector 78.3KB 78.5KB +188B 15.3KB 15.3KB +72B
core/SelectorOption 15.6KB 15.8KB +167B 5.3KB 5.4KB +78B
core/SideNav 65.6KB 65.7KB +141B 10.7KB 10.8KB +78B
core/SideNavCollapseButton 48.2KB 48.4KB +198B 9.6KB 9.7KB +75B
core/SideNavHeading 60.1KB 60.3KB +179B 10.4KB 10.5KB +76B
core/SideNavItem 56.2KB 56.4KB +191B 10.3KB 10.4KB +75B
core/SideNavSection 6.5KB 6.5KB +41B 3.6KB 3.7KB +79B
core/Skeleton 3.8KB 3.8KB +43B 3.6KB 3.7KB +72B
core/Slider 49.2KB 49.3KB +67B 8.7KB 8.8KB +60B
core/Spinner 26.7KB 26.8KB +64B 7.5KB 7.6KB +75B
core/Stack 7.1KB 7.1KB +39B 4.9KB 5.0KB +83B
core/StackItem 6.0KB 6.1KB +37B 3.3KB 3.4KB +80B
core/StatusDot 19.1KB 19.2KB +54B 6.2KB 6.3KB +62B
core/Step 33.8KB 34.0KB +187B 5.5KB 5.6KB +60B
core/Stepper 49.0KB 49.2KB +202B 9.8KB 9.8KB +74B
core/Switch 49.9KB 50.1KB +205B 9.5KB 9.6KB +72B
core/Tab 10.5KB 10.7KB +162B 4.1KB 4.2KB +82B
core/TabList 40.8KB 40.8KB +41B 7.3KB 7.4KB +60B
core/TabMenu 56.9KB 57.1KB +192B 10.3KB 10.3KB +76B
core/Table 85.9KB 86.1KB +208B 15.8KB 15.9KB +73B
core/TableBody 3.3KB 3.3KB +44B 3.2KB 3.2KB +80B
core/TableCell 77.4KB 77.7KB +220B 15.5KB 15.6KB +74B
core/TableFooter 3.3KB 3.3KB +44B 3.2KB 3.2KB +80B
core/TableHeader 3.3KB 3.3KB +42B 3.2KB 3.2KB +80B
core/TableHeaderCell 77.1KB 77.3KB +220B 15.3KB 15.4KB +82B
core/TableRow 6.4KB 6.5KB +39B 3.8KB 3.9KB +81B
core/TableSelectionToolbar 53.8KB 53.9KB +189B 11.3KB 11.4KB +71B
core/Text 8.6KB 8.6KB +62B 6.8KB 6.9KB +65B
core/TextArea 50.1KB 50.2KB +61B 9.6KB 9.6KB +74B
core/TextInput 55.2KB 55.4KB +191B 10.6KB 10.7KB +75B
core/Thumbnail 52.3KB 52.5KB +197B 11.2KB 11.2KB +73B
core/TimeInput 88.8KB 89.0KB +158B 13.9KB 14.0KB +76B
core/Timer 9.2KB 9.3KB +64B 6.8KB 6.9KB +65B
core/Timestamp 28.0KB 28.0KB +64B 9.7KB 9.8KB +74B
core/Toast 61.5KB 61.7KB +191B 9.9KB 10.0KB +75B
core/ToggleButton 45.4KB 45.6KB +207B 9.4KB 9.5KB +74B
core/ToggleButtonGroup 6.3KB 6.4KB +43B 3.3KB 3.3KB +80B
core/Token 31.0KB 31.2KB +169B 4.6KB 4.7KB +76B
core/Tokenizer 70.1KB 70.3KB +210B 16.7KB 16.8KB +85B
core/Toolbar 40.2KB 40.3KB +42B 7.7KB 7.8KB +80B
core/Tooltip 17.0KB 17.0KB +17B 4.9KB 5.0KB +12B
core/TopNav 53.4KB 53.6KB +171B 10.5KB 10.6KB +75B
core/TopNavHeading 59.5KB 59.7KB +173B 10.2KB 10.3KB +78B
core/TopNavItem 9.9KB 10.0KB +172B 3.9KB 3.9KB +79B
core/TopNavMegaMenu 55.9KB 56.1KB +191B 11.0KB 11.1KB +74B
core/TopNavMegaMenuFeaturedCard 6.7KB 6.8KB +47B 3.4KB 3.5KB +78B
core/TopNavMegaMenuItem 9.9KB 10.1KB +171B 4.1KB 4.2KB +79B
core/TopNavMenu 59.2KB 59.4KB +192B 10.3KB 10.4KB +75B
core/TreeList 41.2KB 41.3KB +159B 4.8KB 4.9KB +77B
core/Typeahead 63.1KB 63.3KB +205B 11.5KB 11.5KB +76B
core/TypeaheadItem 6.2KB 6.2KB +38B 3.4KB 3.5KB +80B
core/VStack 7.3KB 7.3KB +40B 4.9KB 5.0KB +83B
lab/ChartDotGL N/A 7.3KB N/A N/A 20B N/A
lab/ChartDotGLInteractive N/A 8.3KB N/A N/A 20B N/A
lab/ChartHeatmapGL N/A 8.4KB N/A N/A 20B N/A
lab/ChartStreamGL N/A 7.4KB N/A N/A 20B N/A
lab/ChatEmojiPicker N/A 67.1KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 67.6KB N/A N/A 1.1KB N/A
lab/ChatReasoning N/A 6.9KB N/A N/A 967B N/A
lab/ChatTypingIndicator N/A 24.0KB N/A N/A 612B N/A
lab/ChatUnreadDivider N/A 3.6KB N/A N/A 348B N/A
lab/CircularProgress N/A 7.2KB N/A N/A 1.0KB N/A
lab/CodeEditor N/A 14.6KB N/A N/A 665B N/A
lab/CommentComposer N/A 96.0KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 49.0KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 218.5KB N/A N/A 2.4KB N/A
lab/Comments N/A 223.1KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 47.5KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 65.1KB N/A N/A 722B N/A
lab/InfoTip N/A 22.1KB N/A N/A 458B N/A
lab/ListInput N/A 77.0KB N/A N/A 1.6KB N/A
lab/LogStream N/A 7.9KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 155.6KB N/A N/A 1.2KB N/A
lab/MobileTokenizer N/A 94.7KB N/A N/A 880B N/A
lab/SVGIcon N/A 7.6KB N/A N/A 764B N/A
lab/Schedule N/A 61.8KB N/A N/A 3.9KB N/A
lab/Stat N/A 4.1KB N/A N/A 646B N/A
lab/ThreeDScatterGL N/A 7.6KB N/A N/A 20B N/A
lab/TourStep N/A 56.6KB N/A N/A 590B N/A
lab/TransferList N/A 69.0KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 74.6KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 272.6KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 214.7KB N/A N/A 454B N/A
richtext/RichTextView N/A 253.0KB N/A N/A 2.3KB N/A

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 Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 35.6KB 35.7KB +92B

Accessibility Audit

Status: The accessibility audit did not run on this pull request. It runs after merges to main and before each release; pull requests that touch components run it with the ci:heavy-lanes label.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This needs an engineering owner's decision before it can be approved. The motion knobs rest on spec:AST-108 and on new AST-094, AST-090, AST-012, theme-application INV1, theme-authoring-contract INV15, reduced-motion DR2a, motion DR3, Spinner and Skeleton text in this pull request; none of it is on main or approved for this head. An engineering owner can approve it by commenting /approve-spec 2a849e7419276c52f7c205ccf49f8fbec0fa28e0 here (the author can, as an engineering owner), and that approval should cover the build's 'crisp' and 'expressive' curves, the tempo and character attributes that nothing reads yet, and INV15's inheritance rule; without it, this waits for #8613 to land. Either way, these need fixing:

  • reducedMotion: 'always' reaches only when: {motion} adaptation rules, so every component keeps full motion, although the docs, template, JSDoc and changeset say it reduces motion for everyone.
  • A child theme that sets its own duration scale now resolves differently from main: its base's explicit --duration-*/--ease-* tokens win, and its ratio re-derives an inherited slow band. It resolves differently again over a built base.
  • A theme nested inside a tempo theme times Spinner and Skeleton from the outer theme's pre-tempo values.
  • --ease-entrance and --ease-move ignore an explicit --ease-standard, resolveThemeMotion disagrees with the CSS, and the docs say the knobs drive touch springs, which nothing reads yet.

[Automated review]

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
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.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only Theme.tsx writes data-astryx-reduced-motion at this head, and only when: {motion} adaptation rules read it. With 'always' and the person's own setting off, Spinner keeps its 0.525 s cycle and Skeleton keeps pulsing every 0.4 s, exactly as without the mark. Until component styles and script readers key on the mark, this sentence and the matching template, JSDoc and changeset wording promise a reduction this release does not have.

Comment on lines +571 to +584
const baseMotion = base?.__axes?.motion;
const motion: ThemeMotionConfig | undefined =
input.motion != null && baseMotion != null
? {...baseMotion, ...input.motion}
: input.motion;
const lineageMotionTokens =
input.motion != null && base?.__inputTokens != null
? Object.fromEntries(
Object.entries(base.__inputTokens).filter(
([name]) =>
name.startsWith('--duration-') || name.startsWith('--ease-'),
),
)
: undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This changes existing child themes that set their own duration scale:

  • {fast: 100, medium: 250, ratio: 0.75, easing: 'ease-in'} over a base with explicit --duration-medium: 300ms and --ease-standard: linear resolves to 300 ms and linear here, but to 250 ms and ease-in on main.
  • {fast: 100, medium: 250, ratio: 0.8} over a base with slow: 700, ratio: 0.75 moves --duration-slow-min from 525 ms to 560 ms.
  • base.__inputTokens is missing from built theme modules, so the first child still gets 250 ms and ease-in over a built base.

Inheriting only the knob fields, and re-applying lineage tokens only where the knobs regenerate them, keeps the current order: base, then the child's generated values, then explicit tokens.

): Record<string, string> {
const knobs = resolveMotionKnobs(config);
const tokens: Record<string, string> = {};
if (knobs.tempoIsSet) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only a theme that sets tempo writes the --pre-tempo-* copies. A theme nested inside a tempo theme that sets its own durations, through a scale or explicit tokens, inherits the outer copies instead. In Chromium its Spinner and Skeleton run at 0.73 s and 0.55 s rather than its own 0.6 s and 0.335 s. Writing the copy beside every --duration-* token a theme writes stops the leak.

Comment thread packages/core/src/theme/themeMotion.ts Outdated
const knobs = resolveMotionKnobs(theme.motion);
const generated = theme.motion ? expandThemeMotion(theme.motion) : {};
const value = (name: string, fallback: string): string =>
theme.tokens?.[name] ?? generated[name] ?? fallback;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For a theme whose only motion setting is an explicit --ease-standard: linear, this reports --ease-entrance and --ease-move as linear. expandThemeMotion writes neither token, though, so the CSS keeps cubic-bezier(0.24, 1, 0.4, 1). Given a defined theme, theme.motion is also undefined, because the knobs live in __axes.motion, so the knobs come back as tempo 1 and 'standard'.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: "A theme sets how motion feels with three knobs in `defineTheme`'s `motion`. They drive the CSS motion tokens and the springs of touch motion from the same values, so both stay in step.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No animator code reads the knobs yet: resolveThemeMotion has no caller, and nothing reads the tempo and character attributes. In this release the knobs reach neither touch springs nor any Astryx component's entrances.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This needs an engineering owner's decision before it can be approved: the motion knobs rest on the new spec:AST-108 and on AST-094, AST-090, AST-012, theme-application INV1, theme-authoring-contract INV15, reduced-motion DR2a, motion DR3, Spinner, Skeleton and Button text in this pull request, none of which is on main or approved for this head. An engineering owner can approve it by commenting /approve-spec 5b7ac475ac54f6338d3efa067d7b1139be8670cd here (the author can, as an engineering owner), and that approval should cover the build's 'crisp' and 'expressive' curves, the tempo and character attributes, and INV15's inheritance rule; without it, this waits for #8613 to land. Either way, these need fixing:

  • reducedMotion: 'always' reaches only when: {motion} adaptation rules, so Spinner and Skeleton keep full motion although the docs, template, JSDoc and changeset say motion is reduced for everyone.
  • A child theme that sets its own duration scale now resolves differently from main, since its base's explicit motion tokens win and its ratio re-derives an inherited slow band, and it resolves differently again over a built base.
  • Under Reduce Motion on a touch screen, a tempo above 1 stretches the press release past 200 ms (300 ms at 1.5).
  • A theme nested in a tempo theme takes the outer theme's Spinner and Skeleton timing and press tempo, and resolveThemeMotion and the docs about springs and press clocks disagree with what ships.

[Automated review]

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
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.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only Theme.tsx writes data-astryx-reduced-motion at this head, and only when: {motion} adaptation rules read it. With 'always' and the person's own setting off, Spinner keeps its 0.525 s cycle and both Skeletons keep pulsing every 0.4 s in Chromium, exactly like a 'system' theme. Until component styles and script readers key on the mark, this sentence and the matching template, JSDoc and changeset wording promise a reduction this release does not have.

const baseMotion = base?.__axes?.motion;
const motion: ThemeMotionConfig | undefined =
input.motion != null && baseMotion != null
? {...baseMotion, ...input.motion}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This changes existing child themes that set their own duration scale:

  • {fast: 100, medium: 250, ratio: 0.75, easing: 'ease-in'} over a base with explicit --duration-medium: 300ms and --ease-standard: linear resolves to 300 ms and linear here, but to 250 ms and ease-in on main.
  • {fast: 100, medium: 250, ratio: 0.8} over a base with slow: 700, ratio: 0.75 moves --duration-slow-min, Spinner's cycle, from 525 ms to 560 ms.
  • Built theme modules carry no __inputTokens, so the first child keeps main's result over a built base.

Inheriting only the knob fields (and the base's scale fields only when the child sets none), and re-applying lineage tokens only to names the knobs regenerate, keeps the current order: base, then the child's generated values, then explicit tokens.

* its duration tokens (`spec:AST-108` FR4, `component:Button` FR15). The
* controller's timer scales by the same tempo (pressFeedback.ts).
*/
const PRESS_RELEASE_DURATION = `calc(${pressConsts.releaseMs}ms * var(--_astryx-motion-tempo, 1))`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing caps this under reduced motion, and the controller's timer (pressFeedback.ts:190) scales the same way. With prefers-reduced-motion: reduce on a coarse pointer, a tempo: 1.5 theme fades the press over 0.3 s in Chromium, against design:reduced-motion DR4 and DR2's 200 ms cap, and against AST-108 FR4's "tempo may shorten a Reduce Motion cross-fade but never lengthens it". Capping the scaled release at 200 ms under reduced motion, in the stylesheet and the timer, keeps both.

): Record<string, string> {
const knobs = resolveMotionKnobs(config);
const tokens: Record<string, string> = {};
if (knobs.tempoIsSet) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only a theme that sets tempo writes the --pre-tempo-* copies and --_astryx-motion-tempo. A theme nested inside a tempo: 1.5 theme that sets its own durations inherits the outer values: in Chromium its Spinner turns in 0.73 s instead of its own 0.45 s, and its press release runs at 1.5. Writing the copy beside every --duration-* token a theme writes, generated or explicit, and the tempo property at 1 for a theme without one, stops the leak.

Comment thread packages/core/src/theme/themeMotion.ts Outdated
durations,
easings: {
'--ease-standard': standard,
'--ease-entrance': value('--ease-entrance', standard),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For a theme whose only motion setting is an explicit --ease-standard: linear, this reports --ease-entrance and --ease-move as linear, but nothing writes either token, so the CSS keeps cubic-bezier(0.24, 1, 0.4, 1) (architecture:theme-tokens INV6). Given a defined theme, the knobs also come back as tempo 1 and 'standard', because a defined theme keeps them in __axes.motion, not motion.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: "A theme sets how motion feels with three knobs in `defineTheme`'s `motion`. They drive the CSS motion tokens and the springs of touch motion from the same values, so both stay in step.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No animator code reads the knobs yet: resolveThemeMotion has no caller, and nothing reads data-astryx-motion-character. In this release the knobs reach no touch spring. The template and the defineTheme JSDoc also say press clocks never follow the knobs, while the press release fade now does.

}),
);
},
TIMER_MS[kind] * (kind === 'fade' ? themeTempo(live.element) : 1),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The timer reads the root's data-astryx-motion-tempo, but an adaptation rule that sets tempo changes only --_astryx-motion-tempo inside its media query. For a root tempo: 1.2 with a coarse-pointer rule at 0.8, the stylesheet fades over 160 ms while this clears the state at 240 ms; a rule that raises tempo ends the fade early. Timing the release from the element's resolved --_astryx-motion-tempo, or ending it on animationend, keeps one clock.

@cixzhang

Copy link
Copy Markdown
Contributor

/approve-spec 5b7ac47

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

One test before this merges, for spec:AST-108 FR6's verification row (an ancestor's 'always' reaches every nested theme, and a nested theme can't remove it):

  • an ancestor theme with reducedMotion: 'always';
  • inside it, a nested theme with reducedMotion: 'system', and another nested theme with no motion config;
  • children under each nested theme still reduced, both through component CSS and through a script reader (for example the animator or a Carousel/BottomSheet reader).

Everything else in this delta matches AST-108. Thanks!

vjeux and others added 2 commits October 29, 2026 20:04
Rebased onto main as one change from the theme motion knobs branch (spec:AST-108 FR1-FR7): tempo scales every motion duration, character sets the --ease-entrance and --ease-move curves, and reducedMotion marks the theme's root. Spinner's and Skeleton's loops keep their timing under every tempo, the press release fade follows tempo up to the Reduce Motion cap, adaptation rules take tempo and character, an extending theme inherits motion field by field, and theme build checks a defined theme's authored tokens and rules. The motion, token and theme-authoring docs, the theme template and the authoring contract describe the knobs.
…'always'

A theme's reducedMotion: 'always' reached only its motion-keyed adaptation
rules and the press release fade; components' own Reduce Motion styles,
Spinner and Skeleton among them, did not follow it. A theme that set it got
a promise most of the page did not keep. The public type now offers only
'system', the default, and the root mark, its custom property, the marked
adaptation blocks and the press release's mark arm go with it. The docs,
template, changeset and the authoring contract's unshipped note say so.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: the motion knobs follow the theme motion spec and existing themes resolve to the same tokens, but two places contradict current spec.

  • resolveThemeMotion returns var() strings for motion tokens written as references (themeMotion.ts:181-182); resolve them through the token graph as resolveThemeTokens does, with a test (architecture:theme-tokens INV6, spec:AST-108 FR6).
  • spec:AST-108 FR7 asks every listed doc for the knobs, the fixed list and the precedence; the duration and easing token descriptions, the theme-authoring guide's motion row and INV15 still lack the fixed list.

[Automated review]

Comment thread packages/core/src/theme/themeMotion.ts Outdated
Comment on lines +181 to +182
const value = (name: string, fallback: string): string =>
theme.tokens?.[name] ?? generated[name] ?? fallback;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This reads theme.tokens raw, so a motion token written as a reference comes back unresolved: with tokens: {'--duration-medium': 'var(--duration-slow)'} and tempo: 1.2, this returns var(--duration-slow) while the CSS and resolveThemeTokens(theme, {mode}) give 1170ms. architecture:theme-tokens INV6 asks JavaScript helpers to follow token references, and spec:AST-108 FR6 asks the server and client to resolve the same values. Reading through resolveThemeTokens and keeping expandThemeMotion as the fallback would fix it; a referenced-token case in the FR6 test would pin it.

Comment thread scripts/generate-token-docs.mjs Outdated
title: 'Duration Tokens',
description:
'Motion duration primitives. Three bands: fast (micro-interactions), medium (entrance/exit), slow (continuous). Min/max variants derive from base × ratio.',
"Motion duration primitives. Three bands: fast (micro-interactions), medium (entrance/exit), slow (continuous). Min/max variants derive from base × ratio. A theme's `motion.tempo` (0.75–1.5) scales every one of them; a duration scale or an explicit token wins over it.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

spec:AST-108 FR7's verification row fails "a doc without the knobs, the fixed list, or the precedence". This description names only tempo and no fixed list, so a reader expects every component that reads these tokens, Spinner and Skeleton included, to follow tempo. One sentence naming what never follows the knobs, as the motion topic lists it, would cover it; the easing description at line 271, the guide's motion row and INV15 need the same.

…oc lists what the knobs never move

resolveThemeMotion returned a motion token written as a reference as its
var() string, where resolveThemeTokens resolves it (architecture:theme-tokens
INV6). The var() resolver moves out of tokens.ts into tokenReferences.ts,
unchanged, and resolveThemeMotion resolves through the defaults, the knobs'
tokens and the theme's own; tokens.ts keeps evaluating color functions on
it. The duration and easing token descriptions, the theme-authoring guide's
motion row and INV15 now name the fixed list (spec:AST-108 FR4, FR7).

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: the motion knobs follow the theme motion spec, existing themes resolve to the same tokens, and resolveThemeMotion now resolves referenced tokens, but three of the docs spec:AST-108 FR7 lists still miss part of what it requires.

  • The easing-token description (scripts/generate-token-docs.mjs:271) names only character and gives no precedence, and the duration-token description (:261) names only tempo; both need all three knobs and the precedence sentence the motion topic uses.
  • The theme template's list of what never follows the knobs (packages/cli/assets/theme.template.ts:192-194) leaves out the rubber band, the press's cancel distance and gesture thresholds.

Regenerate the token docs with node scripts/generate-token-docs.mjs after editing the descriptions.

[Automated review]

Comment thread scripts/generate-token-docs.mjs Outdated
title: 'Easing Tokens',
description: 'Easing curves for animations and transitions.',
description:
"Easing curves for animations and transitions. `--ease-entrance` and `--ease-move` follow a theme's `motion.character`: `crisp` decelerates without overshoot, `expressive` overshoots slightly, and at `standard` both equal `--ease-standard`. Exits keep their own curve at every character. Neither knob moves finger tracking, momentum, the rubber band, the on-screen keyboard, the press's onset delay, cancel distance and tap flash, hover-intent delays, gesture thresholds, the loops of busy indicators such as Spinner and Skeleton, and the 200 ms Reduce Motion cap.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

spec:AST-108 FR7 fails a listed doc "without the knobs, the fixed list, or the precedence". This easing description names only character (while saying "Neither knob") and never says that an explicit --ease-entrance or --ease-move token beats it. Naming tempo, character and reducedMotion in one clause and adding the precedence sentence from the motion topic would cover it; rerun node scripts/generate-token-docs.mjs afterwards.

Comment thread scripts/generate-token-docs.mjs Outdated
title: 'Duration Tokens',
description:
'Motion duration primitives. Three bands: fast (micro-interactions), medium (entrance/exit), slow (continuous). Min/max variants derive from base × ratio.',
"Motion duration primitives. Three bands: fast (micro-interactions), medium (entrance/exit), slow (continuous). Min/max variants derive from base × ratio. A theme's `motion.tempo` (0.75–1.5) scales every one of them; a duration scale or an explicit token wins over it. Never scaled by it: finger tracking, momentum, the rubber band, the on-screen keyboard, the press's onset delay, cancel distance and tap flash, hover-intent delays, gesture thresholds, the loops of busy indicators such as Spinner and Skeleton, and the 200 ms Reduce Motion cap.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This duration description now carries the fixed list and the precedence, but names only tempo; FR7 asks each listed doc for all three knobs, so one clause naming character and reducedMotion would finish it.

Comment thread packages/cli/assets/theme.template.ts Outdated
Comment on lines +192 to +194
* Finger tracking, momentum, the keyboard, hover, the press's onset delay
* and tap flash, and busy loops never follow them; the press release fade
* follows tempo, up to the 200 ms Reduce Motion cap.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FR7 asks the theme template for the fixed list; this one leaves out the rubber band, the press's cancel distance and gesture thresholds, which the motion topic and the authoring guide name.

…and what they never move

The duration and easing token descriptions name all three knobs and the
precedence, each in its own words, and the theme template's fixed list adds
the rubber band, the press's cancel distance and gesture thresholds
(spec:AST-108 FR4, FR7). Token docs and the search goldens are regenerated.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Recommended for approval: the three motion knobs follow the theme motion spec, existing themes resolve to the same public tokens, and the duration-token, easing-token and template docs now carry the knobs, the precedence and the fixed list. Before merge, the repository's spec gate still needs an engineering owner's /approve-spec for this exact head, because the pull request edits the theme authoring contract; the author may record it as an engineering owner. One small fix to the description: check step 2 says a Dialog's entrance overshoots under character: 'expressive', but Dialog still eases on --ease-standard, so for now character reaches only styles that read --ease-entrance or --ease-move.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor

/approve-spec 71d0814

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Oct 30, 2026
@cixzhang
cixzhang marked this pull request as ready for review October 30, 2026 19:34
@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 30, 2026
@cixzhang cixzhang closed this Oct 30, 2026
@cixzhang cixzhang reopened this Oct 30, 2026
Keeps main's --ease-exit beside --ease-entrance and --ease-move; the
motion knobs never write it, and the easing docs name it as the curve
that is the same at every character.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. The motion knobs follow the theme motion spec, and existing themes resolve to the same public tokens, but one docs conflict needs fixing now that #8004 is merged in. The motion topic's Easing section still says there are two easing curves and puts arrivals on --ease-standard, while this pull request's own guidance puts them on --ease-entrance. Please name all four curves and their jobs there, and name --ease-exit beside --ease-entrance and --ease-move in the motion guidance and the theme template, as asked earlier in this thread.

[Automated review]

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
{
type: 'prose',
text: 'Set transition durations and easing from the motion tokens, so a theme that retunes motion retunes your animations too.',
text: "Set transition durations and easing from the motion tokens, so a theme that retunes motion retunes your animations too. Ease something arriving on `--ease-entrance` and something moving on screen on `--ease-move`, so it follows the theme's `character`.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This now conflicts with the Easing section at line 53, which (since #8004 merged in) still reads "Two easing curves. --ease-standard decelerates, for things arriving…" above a table that lists four. Please rewrite line 53 to name --ease-standard, --ease-entrance, --ease-move and --ease-exit with their jobs, and name --ease-exit for something leaving here too.

Comment thread packages/cli/assets/theme.template.ts Outdated
* yet):
* tempo — 1 is Astryx's own; 0.75 the quickest, 1.5 the slowest.
* character — 'crisp' | 'standard' | 'expressive': the entrance and move
* curves. Exits and fades never overshoot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With #8004 landed, please name --ease-exit here as the curve for something leaving, beside the entrance and move curves, as requested earlier in the thread.

…es, and names --ease-exit

- reducedMotion leaves the public motion type until 'always' ships; every
  theme still resolves to 'system'.
- ThemeMotionConfig, ThemeMotionKnobs and MotionCharacter are exported.
- The motion guide's Easing section names all four curves and their jobs,
  and the motion guidance and theme template name --ease-exit for
  something leaving.
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval waits on an engineering owner's spec decision: this head rewrites the reducedMotion known gap in the theme authoring contract, so the earlier approval no longer covers the spec text, and an engineering owner, the author included, records the decision by commenting /approve-spec 72881da62404a7fa2e81abfdfe9ef7311f3f0440. That decision covers dropping reducedMotion from the public motion type, which the theme motion spec's FR1, its example and FR7 still state, and the three newly exported knob types. Separately, please reject a partial duration scale written beside a knob: motion: {fast: 100, tempo: 1.2} compiles but drops fast: 100 and renders 210 ms, while FR5 says explicit scale fields win over tempo. The easing-docs request from the last round is fixed.

[Automated review]

Comment on lines +745 to +748
const motion: ThemeMotionConfig | undefined =
input.motion != null && baseMotion != null
? {...baseMotion, ...input.motion}
: input.motion;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A partial scale beside a knob passes the type check and is dropped here without an error: on a theme with no base, motion: {fast: 100, tempo: 1.2} gives --duration-fast 210 ms, {fast: 100, medium: 250, slow: 700, tempo: 1} (no ratio) gives the defaults, and {easing: 'linear', character: 'standard'} never writes --ease-standard, because expandThemeMotion applies a scale only when fast, medium and ratio are all present. The theme motion spec's FR5 says explicit MotionScaleConfig fields win over tempo. Rules already reject this (completeMotion: "must supply fast, medium, and ratio…"); applying the same check to the merged motion here, with a test, would close it.

…not dropped

defineTheme refuses a motion that sets a duration scale field without
fast, medium and ratio after inheritance, as adaptation rules already do,
so explicit scale fields never silently lose to tempo (AST-108 FR5).
AST-108's current-state impact names the reducedMotion known gap.
@cixzhang

Copy link
Copy Markdown
Contributor

/approve-spec 18b09c4

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Recommended for approval: the motion knobs follow the theme motion spec, existing themes keep their tokens, and a partial duration scale written beside a knob now fails with a clear error instead of being silently dropped. An engineering owner approved the spec text this pull request adds for this exact commit. One optional tidy-up: the description still says reducedMotion ships with its 'system' default, but the public type no longer has that field.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — 18b09c4a Deployed Oct 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants