Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .sync/log/f95152949ac6762aa61610482892bd6551a21593.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Port: fix(components): respect `prefers-reduced-motion` in animations

**Upstream:** `f95152949ac6762aa61610482892bd6551a21593` (nuxt/ui, #6723)
**Decision:** port — theme + docs (adapted to b24ui fork variants)

## Upstream change
Wraps every looping `animate-*` utility in the `motion-safe:` variant and adds
`motion-reduce:` fallbacks so animations honor the user's
`prefers-reduced-motion` setting. Touches five theme files (ChatShimmer,
ChatMessages, Progress, Table, Marquee) plus a `::tip` callout in each of their
docs pages.

## b24ui port
- **`src/theme/chat-messages.ts`** — the three bounce indicator dots
(`[&>*:nth-child(n)]:animate-[bounce…]`) prefixed with `motion-safe:`.
- **`src/theme/chat-shimmer.ts`** — `animate-[shimmer…]` and
`rtl:animate-[shimmer-rtl…]` prefixed with `motion-safe:`; base gains
`motion-reduce:bg-none motion-reduce:text-muted` (static muted text fallback).
- **`src/theme/progress.ts`** — all 10 `compoundVariants` indicator animations
prefixed with `motion-safe:` (both the `data-[state=indeterminate]:animate-*`
and the `…:rtl:animate-*` halves), **including the fork-extra
`progressbar-loading` / `progressbar-loading-vertical` variants** that don't
exist upstream; base `indicator` slot gains
`motion-reduce:data-[state=indeterminate]:animate-pulse`.
- **`src/theme/table.ts`** — all 5 loading `thead` `after:animate-*` (and
`rtl:after:animate-*`) compoundVariants prefixed with `motion-safe:`,
**including the fork-extra `progressbar-loading` variant**; base loading
`thead` slot gains `motion-reduce:after:inset-x-0 motion-reduce:after:animate-pulse`.
- **Marquee — N/A.** b24ui has no Marquee component (`src/theme/marquee.ts`
absent), so the upstream Marquee theme/docs hunks were skipped.
- **Docs** — added the upstream `::tip` callout to `chat-shimmer.md`,
`progress.md`, `table.md` (each explaining the reduced-motion fallback).
`marquee.md` skipped (no such page).

## Tests
Theme-class changes only — snapshots regenerated for the directly-edited
components (ChatMessages, ChatShimmer, Progress, Table) **and the components
that embed them**: ChatTool / ChatReasoning render ChatShimmer, and Toast
renders a Progress bar. Total **118 snapshots updated** across 14 spec files
(nuxt + vue variants for each of the 7 components). All diffs are pure
`motion-safe:` / `motion-reduce:` class additions. Suite unchanged:
5485 passed / 6 skipped.

## Verify (CI=true)
`dev:prepare` · `lint` · `typecheck` · `test` · `build` — all green.
10 changes: 8 additions & 2 deletions .sync/nuxt-ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"upstream": "nuxt/ui",
"branch": "v4",
"sync_enabled": false,
"cursor": "17196ecc3057a18d540f8b9a77f857fbbe8969c0",
"cursor": "f95152949ac6762aa61610482892bd6551a21593",
"_cursor_note": "cursor = last upstream commit ported into b24ui (oldest-first, manual cadence). sync_enabled stays false until Phase 2 (porter workflow #67 + CLAUDE_CODE_OAUTH_TOKEN) is wired and trusted. `processed` is maintained per port from now on (backfilled #68-#72 on 2026-06-09).",
"stats": {
"queue_depth": 0,
Expand Down Expand Up @@ -1067,10 +1067,16 @@
"summary": "fix(BlogPost/ChangelogVersion): format date in UTC to prevent hydration mismatch — adds timeZone: 'UTC' to date formatter in BlogPost.vue + ChangelogVersion.vue. NOT APPLICABLE: b24ui has neither component (no blog/changelog files in src or docs). No-op."
},
"17196ecc3057a18d540f8b9a77f857fbbe8969c0": {
"pr": 278,
"b24ui_sha": "97e7e3def23c253127aee55fb1ea58095a0923b5",
"decision": "port",
"summary": "docs(select-menu/input-menu): use grouped items in items type example — restructure flat items into array-of-arrays (one sub-array per label group) in input-menu.md/select-menu.md + add ::note to all three (input-menu/listbox/select-menu). b24ui: grouped Fruits/Vegetables, preserved fork intra-Fruits separator inside sub-array; listbox already grouped so note-only. Docs-content only. Tests 5485."
},
"f95152949ac6762aa61610482892bd6551a21593": {
"pr": null,
"b24ui_sha": "pending-merge",
"decision": "port",
"summary": "docs(select-menu/input-menu): use grouped items in items type example — restructure flat items into array-of-arrays (one sub-array per label group) in input-menu.md/select-menu.md + add ::note to all three (input-menu/listbox/select-menu). b24ui: grouped Fruits/Vegetables, preserved fork intra-Fruits separator inside sub-array; listbox already grouped so note-only. Docs-content only. Tests 5485."
"summary": "fix(components): respect prefers-reduced-motion in animations — wrap animate-* utilities in motion-safe: and add motion-reduce: fallbacks across ChatShimmer/ChatMessages/Progress/Table themes. b24ui: chat-messages.ts (3 bounce indicators → motion-safe:), chat-shimmer.ts (shimmer + rtl-shimmer → motion-safe:, base gets motion-reduce:bg-none motion-reduce:text-muted), progress.ts (10 indicator animations incl. fork-extra progressbar-loading/-vertical → motion-safe:, base indicator gets motion-reduce:data-[state=indeterminate]:animate-pulse), table.ts (5 loading thead animations incl. fork-extra progressbar-loading → motion-safe:, base loading thead gets motion-reduce:after:inset-x-0 motion-reduce:after:animate-pulse). Marquee N/A (b24ui has no Marquee). Docs +tip in chat-shimmer/progress/table. 118 snapshots regenerated across 14 spec files (also ChatTool/ChatReasoning embed ChatShimmer, Toast embeds Progress). Tests 5485."
}
}
}
4 changes: 4 additions & 0 deletions docs/content/docs/2.components/chat-shimmer.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ The ChatShimmer component renders an element with an animated shimmer gradient o
This component is automatically used by the [`ChatTool`](/docs/components/chat-tool/) and [`ChatReasoning`](/docs/components/chat-reasoning/) components when streaming.
::

::tip
The animation is automatically disabled when the user prefers reduced motion, the text is displayed as static muted text instead.
::

### Text

Use the `text` prop to set the shimmer text.
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/2.components/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ props:
---
::

::tip
The animation is automatically disabled when the user prefers reduced motion, the indeterminate bar is displayed as a full width pulse instead.
::

### Animation

Use the `animation` prop to change the animation of the Progress to an inverse carousel, a swinging bar or an elastic bar. Defaults to `loading`.
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/2.components/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ props:
---
::

::tip
The loading animation is automatically disabled when the user prefers reduced motion, the bar is displayed as a full width pulse instead.
::

### Sticky

Use the `sticky` prop to make the header or footer sticky.
Expand Down
2 changes: 1 addition & 1 deletion src/theme/chat-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
export default {
slots: {
root: 'w-full flex flex-col gap-1 flex-1 px-2.5 [&>article]:last-of-type:min-h-(--last-message-height)',
indicator: 'h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-(--ui-color-design-tinted-na-content-icon) [&>*:nth-child(1)]:animate-[bounce_1s_infinite] [&>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] [&>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]',
indicator: 'h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-(--ui-color-design-tinted-na-content-icon) motion-safe:[&>*:nth-child(1)]:animate-[bounce_1s_infinite] motion-safe:[&>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] motion-safe:[&>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]',
viewport: 'absolute inset-x-0 top-[78%] data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]',
autoScroll: 'rounded-full absolute right-1/2 translate-x-1/2 bottom-0'
},
Expand Down
7 changes: 4 additions & 3 deletions src/theme/chat-shimmer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export default {
'bg-no-repeat',
'bg-size-[calc(200%+var(--spread)*2+2px)_100%,auto]',
'bg-[image:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--b24ui-typography-label-color),#0000_calc(50%+var(--spread))),linear-gradient(var(--ui-color-design-plain-na-content-secondary),var(--ui-color-design-plain-na-content-secondary))]',
'animate-[shimmer_var(--duration)_linear_infinite]',
'rtl:animate-[shimmer-rtl_var(--duration)_linear_infinite]',
'will-change-[background-position]'
'motion-safe:animate-[shimmer_var(--duration)_linear_infinite]',
'motion-safe:rtl:animate-[shimmer-rtl_var(--duration)_linear_infinite]',
'will-change-[background-position]',
'motion-reduce:bg-none motion-reduce:text-muted'
].join(' ')
}
22 changes: 11 additions & 11 deletions src/theme/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default {
indicator: [
'rounded-(--ui-border-radius-pill)',
'size-full',
'transition-transform duration-200 ease-out',
'transition-transform duration-200 ease-out motion-reduce:data-[state=indeterminate]:animate-pulse',
'bg-(--b24ui-background)'
].join(' '),
status: [
Expand Down Expand Up @@ -175,70 +175,70 @@ export default {
orientation: 'horizontal',
animation: 'carousel',
class: {
indicator: 'data-[state=indeterminate]:animate-[carousel_2s_ease-in-out_infinite] data-[state=indeterminate]:rtl:animate-[carousel-rtl_2s_ease-in-out_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[carousel_2s_ease-in-out_infinite] motion-safe:data-[state=indeterminate]:rtl:animate-[carousel-rtl_2s_ease-in-out_infinite]'
}
},
{
orientation: 'vertical',
animation: 'carousel',
class: {
indicator: 'data-[state=indeterminate]:animate-[carousel-vertical_2s_ease-in-out_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[carousel-vertical_2s_ease-in-out_infinite]'
}
},
{
orientation: 'horizontal',
animation: 'carousel-inverse',
class: {
indicator: 'data-[state=indeterminate]:animate-[carousel-inverse_2s_ease-in-out_infinite] data-[state=indeterminate]:rtl:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[carousel-inverse_2s_ease-in-out_infinite] motion-safe:data-[state=indeterminate]:rtl:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
}
},
{
orientation: 'vertical',
animation: 'carousel-inverse',
class: {
indicator: 'data-[state=indeterminate]:animate-[carousel-inverse-vertical_2s_ease-in-out_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[carousel-inverse-vertical_2s_ease-in-out_infinite]'
}
},
{
orientation: 'horizontal',
animation: 'swing',
class: {
indicator: 'data-[state=indeterminate]:animate-[swing_2s_ease-in-out_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[swing_2s_ease-in-out_infinite]'
}
},
{
orientation: 'vertical',
animation: 'swing',
class: {
indicator: 'data-[state=indeterminate]:animate-[swing-vertical_2s_ease-in-out_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[swing-vertical_2s_ease-in-out_infinite]'
}
},
{
orientation: 'horizontal',
animation: 'elastic',
class: {
indicator: 'data-[state=indeterminate]:animate-[elastic_2s_ease-in-out_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[elastic_2s_ease-in-out_infinite]'
}
},
{
orientation: 'vertical',
animation: 'elastic',
class: {
indicator: 'data-[state=indeterminate]:animate-[elastic-vertical_2s_ease-in-out_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[elastic-vertical_2s_ease-in-out_infinite]'
}
},
{
orientation: 'horizontal',
animation: 'loading',
class: {
indicator: 'data-[state=indeterminate]:animate-[progressbar-loading_2s_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[progressbar-loading_2s_infinite]'
}
},
{
orientation: 'vertical',
animation: 'loading',
class: {
indicator: 'data-[state=indeterminate]:animate-[progressbar-loading-vertical_2s_infinite]'
indicator: 'motion-safe:data-[state=indeterminate]:animate-[progressbar-loading-vertical_2s_infinite]'
}
}
// endregion ////
Expand Down
13 changes: 7 additions & 6 deletions src/theme/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ export default {
'after:absolute',
'after:z-1',
'after:h-px',
'after:bg-(--b24ui-background)'
'after:bg-(--b24ui-background)',
'motion-reduce:after:inset-x-0 motion-reduce:after:animate-pulse'
].join(' ')
}
},
Expand All @@ -153,35 +154,35 @@ export default {
loading: true,
loadingAnimation: 'loading',
class: {
thead: 'after:animate-[progressbar-loading_2s_infinite]'
thead: 'motion-safe:after:animate-[progressbar-loading_2s_infinite]'
}
},
{
loading: true,
loadingAnimation: 'carousel',
class: {
thead: 'after:animate-[carousel_2s_ease-in-out_infinite] rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
thead: 'motion-safe:after:animate-[carousel_2s_ease-in-out_infinite] motion-safe:rtl:after:animate-[carousel-rtl_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'carousel-inverse',
class: {
thead: 'after:animate-[carousel-inverse_2s_ease-in-out_infinite] rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
thead: 'motion-safe:after:animate-[carousel-inverse_2s_ease-in-out_infinite] motion-safe:rtl:after:animate-[carousel-inverse-rtl_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'swing',
class: {
thead: 'after:animate-[swing_2s_ease-in-out_infinite]'
thead: 'motion-safe:after:animate-[swing_2s_ease-in-out_infinite]'
}
},
{
loading: true,
loadingAnimation: 'elastic',
class: {
thead: 'after:animate-[elastic_2s_ease-in-out_infinite]'
thead: 'motion-safe:after:animate-[elastic_2s_ease-in-out_infinite]'
}
}
// endregion ////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ exports[`ChatMessages > renders with status submitted correctly 1`] = `
<!--v-if-->
<div data-slot="body" class="min-w-0">
<div data-slot="content" class="relative text-pretty wrap-break-word *:first:mt-0 *:last:mb-0 bg-[var(--b24ui-background,var(--b24ui-default-background))] border-[color:var(--b24ui-border-color,var(--b24ui-default-border-color))] border-[length:var(--b24ui-border-width,var(--b24ui-default-border-width))] text-[color:var(--b24ui-color,var(--b24ui-default-color))] [--b24ui-default-background:#dff6c1] [--b24ui-default-border-color:#dff6c1] [--b24ui-default-border-width:var(--ui-design-tinted-success-stroke-weight)] [--b24ui-default-color:var(--ui-color-palette-black-base)] dark:[--b24ui-default-background:var(--ui-color-design-tinted-success-bg)] dark:[--b24ui-default-border-color:var(--ui-color-design-tinted-success-stroke)] dark:[--b24ui-default-color:var(--ui-color-design-tinted-success-content)] space-y-4 px-3 py-2 rounded-md min-h-10.5">
<div data-slot="indicator" class="h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-(--ui-color-design-tinted-na-content-icon) [&amp;>*:nth-child(1)]:animate-[bounce_1s_infinite] [&amp;>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] [&amp;>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]"><span></span><span></span><span></span></div>
<div data-slot="indicator" class="h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-(--ui-color-design-tinted-na-content-icon) motion-safe:[&amp;>*:nth-child(1)]:animate-[bounce_1s_infinite] motion-safe:[&amp;>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] motion-safe:[&amp;>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]"><span></span><span></span><span></span></div>
</div>
<!--v-if-->
</div>
Expand Down
2 changes: 1 addition & 1 deletion test/components/__snapshots__/ChatMessages.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ exports[`ChatMessages > renders with status submitted correctly 1`] = `
<!--v-if-->
<div data-slot="body" class="min-w-0">
<div data-slot="content" class="relative text-pretty wrap-break-word *:first:mt-0 *:last:mb-0 bg-[var(--b24ui-background,var(--b24ui-default-background))] border-[color:var(--b24ui-border-color,var(--b24ui-default-border-color))] border-[length:var(--b24ui-border-width,var(--b24ui-default-border-width))] text-[color:var(--b24ui-color,var(--b24ui-default-color))] [--b24ui-default-background:#dff6c1] [--b24ui-default-border-color:#dff6c1] [--b24ui-default-border-width:var(--ui-design-tinted-success-stroke-weight)] [--b24ui-default-color:var(--ui-color-palette-black-base)] dark:[--b24ui-default-background:var(--ui-color-design-tinted-success-bg)] dark:[--b24ui-default-border-color:var(--ui-color-design-tinted-success-stroke)] dark:[--b24ui-default-color:var(--ui-color-design-tinted-success-content)] space-y-4 px-3 py-2 rounded-md min-h-10.5">
<div data-slot="indicator" class="h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-(--ui-color-design-tinted-na-content-icon) [&amp;>*:nth-child(1)]:animate-[bounce_1s_infinite] [&amp;>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] [&amp;>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]"><span></span><span></span><span></span></div>
<div data-slot="indicator" class="h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-(--ui-color-design-tinted-na-content-icon) motion-safe:[&amp;>*:nth-child(1)]:animate-[bounce_1s_infinite] motion-safe:[&amp;>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] motion-safe:[&amp;>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]"><span></span><span></span><span></span></div>
</div>
<!--v-if-->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ exports[`ChatReasoning > renders with icon correctly 1`] = `

exports[`ChatReasoning > renders with streaming correctly 1`] = `
"<div data-state="open" data-slot="root" class=""><button type="button" aria-controls="" aria-expanded="true" data-state="open" data-slot="trigger" class="group w-full min-w-0 flex items-center gap-1.5 text-muted text-sm disabled:cursor-default disabled:hover:text-muted hover:text-default focus-visible:outline-offset-2 focus-visible:outline-primary transition-colors">
<!--v-if--><span style="--spread: 18px; --duration: 2s;" data-slot="label" class="text-transparent bg-clip-text bg-no-repeat bg-size-[calc(200%+var(--spread)*2+2px)_100%,auto] bg-[image:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--b24ui-typography-label-color),#0000_calc(50%+var(--spread))),linear-gradient(var(--ui-color-design-plain-na-content-secondary),var(--ui-color-design-plain-na-content-secondary))] animate-[shimmer_var(--duration)_linear_infinite] rtl:animate-[shimmer-rtl_var(--duration)_linear_infinite] will-change-[background-position] truncate">Thinking…</span><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true" data-slot="icon" class="size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200">
<!--v-if--><span style="--spread: 18px; --duration: 2s;" data-slot="label" class="text-transparent bg-clip-text bg-no-repeat bg-size-[calc(200%+var(--spread)*2+2px)_100%,auto] bg-[image:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--b24ui-typography-label-color),#0000_calc(50%+var(--spread))),linear-gradient(var(--ui-color-design-plain-na-content-secondary),var(--ui-color-design-plain-na-content-secondary))] motion-safe:animate-[shimmer_var(--duration)_linear_infinite] motion-safe:rtl:animate-[shimmer-rtl_var(--duration)_linear_infinite] will-change-[background-position] motion-reduce:bg-none motion-reduce:text-muted truncate">Thinking…</span><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true" data-slot="icon" class="size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200">
<path fill="currentColor" fill-rule="evenodd" d="M5.505 9.505a.7.7 0 0 1 .99 0L12 15.01l5.505-5.505a.7.7 0 0 1 .99.99l-6 6a.7.7 0 0 1-.99 0l-6-6a.7.7 0 0 1 0-.99" clip-rule="evenodd"></path>
</svg>
</button>
Expand Down
Loading