refactor(motion): simplify to default and reduced

This commit is contained in:
2026-03-20 16:44:24 +08:00
parent 9009ce4853
commit 142f4a399a
8 changed files with 92 additions and 322 deletions
+11 -29
View File
@@ -8,15 +8,11 @@ import {
skinNames skinNames
} from "@ai-ui/ui"; } from "@ai-ui/ui";
import { import {
defaultMotionAccessibility, defaultMotionMode,
defaultMotionPack,
defaultTheme, defaultTheme,
motionAccessibilityDetails, motionModeDetails,
motionAccessibilityNames, motionModeNames,
motionPackDetails, setMotionMode,
motionPackNames,
setMotionAccessibility,
setMotionPack,
setTheme, setTheme,
themeDetails, themeDetails,
themeNames themeNames
@@ -35,25 +31,14 @@ const preview: Preview = {
})) }))
} }
}, },
motionPack: { motionMode: {
description: "Preview motion pack", description: "Preview motion mode",
toolbar: { toolbar: {
icon: "transfer", icon: "transfer",
dynamicTitle: true, dynamicTitle: true,
items: motionPackNames.map((packName) => ({ items: motionModeNames.map((modeName) => ({
value: packName,
title: motionPackDetails[packName].label
}))
}
},
motionAccessibility: {
description: "Preview motion accessibility override",
toolbar: {
icon: "accessibility",
dynamicTitle: true,
items: motionAccessibilityNames.map((modeName) => ({
value: modeName, value: modeName,
title: motionAccessibilityDetails[modeName].label title: motionModeDetails[modeName].label
})) }))
} }
}, },
@@ -70,8 +55,7 @@ const preview: Preview = {
} }
}, },
initialGlobals: { initialGlobals: {
motionAccessibility: defaultMotionAccessibility, motionMode: defaultMotionMode,
motionPack: defaultMotionPack,
skin: defaultSkin, skin: defaultSkin,
theme: defaultTheme theme: defaultTheme
}, },
@@ -97,13 +81,11 @@ const preview: Preview = {
(Story, context) => { (Story, context) => {
if (typeof document !== "undefined") { if (typeof document !== "undefined") {
setTheme(context.globals.theme ?? defaultTheme); setTheme(context.globals.theme ?? defaultTheme);
setMotionPack(context.globals.motionPack ?? defaultMotionPack); setMotionMode(context.globals.motionMode ?? defaultMotionMode);
setMotionAccessibility(
context.globals.motionAccessibility ?? defaultMotionAccessibility
);
setSkin(context.globals.skin ?? defaultSkin); setSkin(context.globals.skin ?? defaultSkin);
document.body.dataset.theme = context.globals.theme ?? defaultTheme; document.body.dataset.theme = context.globals.theme ?? defaultTheme;
document.body.dataset.motion = context.globals.motionMode ?? defaultMotionMode;
document.body.dataset.skin = context.globals.skin ?? defaultSkin; document.body.dataset.skin = context.globals.skin ?? defaultSkin;
} }
+10 -20
View File
@@ -11,18 +11,15 @@ import {
type SkinName type SkinName
} from "@ai-ui/ui"; } from "@ai-ui/ui";
import { import {
defaultMotionAccessibility, defaultMotionMode,
defaultMotionPack,
defaultTheme, defaultTheme,
type MotionAccessibilityName, type MotionModeName,
type MotionPackName,
type ThemeName type ThemeName
} from "@ai-ui/tokens"; } from "@ai-ui/tokens";
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
type StyleContractShowcaseProps = { type StyleContractShowcaseProps = {
motionAccessibility: MotionAccessibilityName; motionMode: MotionModeName;
motionPack: MotionPackName;
skin: SkinName; skin: SkinName;
theme: ThemeName; theme: ThemeName;
}; };
@@ -147,8 +144,7 @@ function SkinPanel({
} }
function StyleContractShowcase({ function StyleContractShowcase({
motionAccessibility, motionMode,
motionPack,
skin, skin,
theme theme
}: StyleContractShowcaseProps) { }: StyleContractShowcaseProps) {
@@ -180,8 +176,7 @@ function StyleContractShowcase({
<section className="flex flex-wrap gap-3"> <section className="flex flex-wrap gap-3">
<RuntimeBadge label="theme" value={theme} /> <RuntimeBadge label="theme" value={theme} />
<RuntimeBadge label="skin" value={skin} /> <RuntimeBadge label="skin" value={skin} />
<RuntimeBadge label="motion pack" value={motionPack} /> <RuntimeBadge label="motion" value={motionMode} />
<RuntimeBadge label="accessibility" value={motionAccessibility} />
</section> </section>
<section className="grid gap-4 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]"> <section className="grid gap-4 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
@@ -192,7 +187,7 @@ function StyleContractShowcase({
"A new runtime attribute: `data-skin`", "A new runtime attribute: `data-skin`",
"Public helpers from `@ai-ui/ui` for skin names, defaults, and root updates", "Public helpers from `@ai-ui/ui` for skin names, defaults, and root updates",
"A dedicated `@ai-ui/ui/skins.css` entrypoint imported by the docs app", "A dedicated `@ai-ui/ui/skins.css` entrypoint imported by the docs app",
"Storybook globals that apply theme, skin, motion pack, and accessibility override together" "Storybook globals that apply theme, skin, and motion mode together"
].map((item) => ( ].map((item) => (
<div <div
key={item} key={item}
@@ -242,8 +237,7 @@ const meta = {
title: "Foundation/Style Contract", title: "Foundation/Style Contract",
component: StyleContractShowcase, component: StyleContractShowcase,
args: { args: {
motionAccessibility: defaultMotionAccessibility, motionMode: defaultMotionMode,
motionPack: defaultMotionPack,
skin: defaultSkin, skin: defaultSkin,
theme: defaultTheme theme: defaultTheme
}, },
@@ -251,18 +245,14 @@ const meta = {
docs: { docs: {
description: { description: {
component: component:
"Phase 1 adds the runtime style contract. Use the Storybook toolbar to switch the active `theme`, `skin`, `motion pack`, and accessibility override globally, or inspect the side-by-side nested `data-skin` panels below." "Phase 1 adds the runtime style contract. Use the Storybook toolbar to switch the active `theme`, `skin`, and `motion` mode globally, or inspect the side-by-side nested `data-skin` panels below."
} }
} }
}, },
render: (_args, context) => ( render: (_args, context) => (
<StyleContractShowcase <StyleContractShowcase
motionAccessibility={ motionMode={
(context.globals.motionAccessibility as MotionAccessibilityName | undefined) ?? (context.globals.motionMode as MotionModeName | undefined) ?? defaultMotionMode
defaultMotionAccessibility
}
motionPack={
(context.globals.motionPack as MotionPackName | undefined) ?? defaultMotionPack
} }
skin={(context.globals.skin as SkinName | undefined) ?? defaultSkin} skin={(context.globals.skin as SkinName | undefined) ?? defaultSkin}
theme={(context.globals.theme as ThemeName | undefined) ?? defaultTheme} theme={(context.globals.theme as ThemeName | undefined) ?? defaultTheme}
+25 -64
View File
@@ -1,7 +1,7 @@
import { import {
motionPackDetails, motionModeDetails,
motionPackNames, motionModeNames,
type MotionPackName type MotionModeName
} from "@ai-ui/tokens"; } from "@ai-ui/tokens";
import { import {
Button, Button,
@@ -26,19 +26,6 @@ import {
} from "@ai-ui/ui"; } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
const motionAccessibilityModes = [
{
label: "System accessibility",
value: "system"
},
{
label: "Reduced accessibility",
value: "reduced"
}
] as const;
type MotionAccessibilityMode = (typeof motionAccessibilityModes)[number]["value"];
function ClosePreviewIcon() { function ClosePreviewIcon() {
return ( return (
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 16 16"> <svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 16 16">
@@ -97,25 +84,21 @@ function PanelPreview() {
} }
function ComparisonCell({ function ComparisonCell({
motionAccessibility, motionMode,
motionPack,
skin skin
}: { }: {
motionAccessibility: MotionAccessibilityMode; motionMode: MotionModeName;
motionPack: MotionPackName;
skin: SkinName; skin: SkinName;
}) { }) {
return ( return (
<section <section
className="grid gap-4 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4 shadow-[var(--shadow-sm)]" className="grid gap-4 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-4 shadow-[var(--shadow-sm)]"
data-motion={motionAccessibility === "reduced" ? "reduced" : undefined} data-motion={motionMode}
data-motion-pack={motionPack}
data-skin={skin} data-skin={skin}
> >
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<RuntimePill>{motionPackDetails[motionPack].label}</RuntimePill> <RuntimePill>{motionModeDetails[motionMode].label}</RuntimePill>
<RuntimePill>{skinDetails[skin].label}</RuntimePill> <RuntimePill>{skinDetails[skin].label}</RuntimePill>
<RuntimePill>{motionAccessibility}</RuntimePill>
</div> </div>
<Card interactive tone="default"> <Card interactive tone="default">
@@ -127,7 +110,7 @@ function ComparisonCell({
</CardHeader> </CardHeader>
<CardContent className="grid gap-3"> <CardContent className="grid gap-3">
<Input <Input
aria-label={`${motionPack} ${skin} release note status`} aria-label={`${motionMode} ${skin} release note status`}
defaultValue="Launch notes approved" defaultValue="Launch notes approved"
readOnly readOnly
/> />
@@ -136,7 +119,7 @@ function ComparisonCell({
Quiet notifications Quiet notifications
</span> </span>
<Switch <Switch
aria-label={`${motionPack} ${skin} quiet notifications`} aria-label={`${motionMode} ${skin} quiet notifications`}
checked checked
/> />
</div> </div>
@@ -196,52 +179,30 @@ function StyleMatrixShowcase() {
</h1> </h1>
<p className="max-w-3xl text-[var(--text-lg)] leading-[var(--leading-loose)] text-[var(--color-muted-foreground)]"> <p className="max-w-3xl text-[var(--text-lg)] leading-[var(--leading-loose)] text-[var(--color-muted-foreground)]">
This page is the screenshot-friendly regression target for the pilot skin work. This page is the screenshot-friendly regression target for the pilot skin work.
The grid uses nested `data-skin`, `data-motion-pack`, and The grid uses nested `data-skin` and `data-motion` scopes so the same
`data-motion=&quot;reduced&quot;` scopes so the same building blocks can be building blocks can be
reviewed side by side. reviewed side by side.
</p> </p>
</header> </header>
<section className="grid gap-4"> <section className="grid gap-4">
{motionPackNames.map((motionPack) => ( {motionModeNames.map((motionMode) => (
<div key={motionPack} className="grid gap-4"> <div key={motionMode} className="grid gap-4">
<div> <div>
<h2 className="text-2xl font-semibold">{motionPackDetails[motionPack].label}</h2> <h2 className="text-2xl font-semibold">{motionModeDetails[motionMode].label}</h2>
<p className="mt-1 max-w-3xl text-sm leading-6 text-[var(--color-muted-foreground)]"> <p className="mt-1 max-w-3xl text-sm leading-6 text-[var(--color-muted-foreground)]">
{motionPackDetails[motionPack].note} {motionModeDetails[motionMode].note}
</p> </p>
</div> </div>
{motionAccessibilityModes.map((motionAccessibilityMode) => ( <div className="grid gap-4 xl:grid-cols-3">
<div key={`${motionPack}-${motionAccessibilityMode.value}`} className="grid gap-3"> {skinNames.map((skin) => (
<div className="flex items-center justify-between gap-4"> <ComparisonCell
<div> key={`${motionMode}-${skin}`}
<h3 className="text-xl font-semibold"> motionMode={motionMode}
{motionAccessibilityMode.label} skin={skin}
</h3> />
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]"> ))}
{motionAccessibilityMode.value === "reduced" </div>
? '`data-motion="reduced"`'
: "System preference"}
{" "}with{" "}
<code className="text-[var(--color-foreground)]">
{`data-motion-pack="${motionPack}"`}
</code>
.
</p>
</div>
</div>
<div className="grid gap-4 xl:grid-cols-3">
{skinNames.map((skin) => (
<ComparisonCell
key={`${motionPack}-${motionAccessibilityMode.value}-${skin}`}
motionAccessibility={motionAccessibilityMode.value}
motionPack={motionPack}
skin={skin}
/>
))}
</div>
</div>
))}
</div> </div>
))} ))}
</section> </section>
@@ -252,7 +213,7 @@ function StyleMatrixShowcase() {
<p className="mt-2 max-w-3xl text-sm leading-6 text-[var(--color-muted-foreground)]"> <p className="mt-2 max-w-3xl text-sm leading-6 text-[var(--color-muted-foreground)]">
Dialog still portals to the document root, so compare its real overlay and Dialog still portals to the document root, so compare its real overlay and
panel treatment with the Storybook toolbar. The matrix above covers scoped panel treatment with the Storybook toolbar. The matrix above covers scoped
inline regression across packs and reduced-motion overlay. The control below inline regression across default and reduced motion modes. The control below
covers the live overlay behavior. covers the live overlay behavior.
</p> </p>
</div> </div>
+12 -30
View File
@@ -1,25 +1,21 @@
import { import {
colorTokens, colorTokens,
defaultTheme, defaultTheme,
defaultMotionAccessibility, defaultMotionMode,
defaultMotionPack,
motionTokens, motionTokens,
motionAccessibilityDetails, motionModeDetails,
motionPackDetails,
radiusTokens, radiusTokens,
shadowTokens, shadowTokens,
themeDetails, themeDetails,
themeNames, themeNames,
typographyTokens, typographyTokens,
type MotionAccessibilityName, type MotionModeName,
type MotionPackName,
type ThemeName type ThemeName
} from "@ai-ui/tokens"; } from "@ai-ui/tokens";
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
type TokensOverviewProps = { type TokensOverviewProps = {
motionAccessibility: MotionAccessibilityName; motionMode: MotionModeName;
motionPack: MotionPackName;
theme: ThemeName; theme: ThemeName;
}; };
@@ -127,8 +123,7 @@ function ThemeCard({ themeName }: { themeName: ThemeName }) {
} }
function TokensOverview({ function TokensOverview({
motionAccessibility, motionMode,
motionPack,
theme theme
}: TokensOverviewProps) { }: TokensOverviewProps) {
return ( return (
@@ -168,18 +163,10 @@ function TokensOverview({
</div> </div>
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 shadow-[var(--shadow-xs)]"> <div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 shadow-[var(--shadow-xs)]">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]"> <p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Motion Pack Motion
</p> </p>
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]"> <p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
{motionPackDetails[motionPack].label} {motionModeDetails[motionMode].label}
</p>
</div>
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 shadow-[var(--shadow-xs)]">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Accessibility Override
</p>
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
{motionAccessibilityDetails[motionAccessibility].label}
</p> </p>
</div> </div>
</div> </div>
@@ -301,8 +288,8 @@ function TokensOverview({
<h2 className="text-2xl font-semibold">Motion tokens</h2> <h2 className="text-2xl font-semibold">Motion tokens</h2>
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]"> <p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
Timing and motion scale now live in variables that components can consume Timing and motion scale now live in variables that components can consume
directly. The toolbar now separates the active motion pack from the directly. The toolbar now switches between the default interaction layer
accessibility override. and the reduced-motion fallback.
</p> </p>
<div className="mt-6 grid gap-4 md:grid-cols-2"> <div className="mt-6 grid gap-4 md:grid-cols-2">
<div className="space-y-3"> <div className="space-y-3">
@@ -390,18 +377,13 @@ type Story = StoryObj<typeof meta>;
export const Overview: Story = { export const Overview: Story = {
args: { args: {
motionAccessibility: defaultMotionAccessibility, motionMode: defaultMotionMode,
motionPack: defaultMotionPack,
theme: defaultTheme theme: defaultTheme
}, },
render: (_args, context) => ( render: (_args, context) => (
<TokensOverview <TokensOverview
motionAccessibility={ motionMode={
(context.globals.motionAccessibility as MotionAccessibilityName | undefined) ?? (context.globals.motionMode as MotionModeName | undefined) ?? defaultMotionMode
defaultMotionAccessibility
}
motionPack={
(context.globals.motionPack as MotionPackName | undefined) ?? defaultMotionPack
} }
theme={context.globals.theme as ThemeName} theme={context.globals.theme as ThemeName}
/> />
+11 -16
View File
@@ -162,9 +162,10 @@ Examples:
- `glass` - `glass`
- `pixel` - `pixel`
### Motion Pack ### Motion Mode
Motion pack defines interaction feel: Motion mode defines whether the system uses its standard interaction vocabulary or its
reduced-motion fallback:
- durations - durations
- easing - easing
@@ -174,9 +175,8 @@ Motion pack defines interaction feel:
Examples: Examples:
- `calm` - `default`
- `micro` - `reduced`
- `spring`
### Layout Pattern ### Layout Pattern
@@ -218,7 +218,7 @@ The existing token groups remain the baseline:
Add a new root attribute: Add a new root attribute:
```html ```html
<html data-theme="minimal" data-skin="glass" data-motion="micro"> <html data-theme="morandi" data-skin="glass" data-motion="default">
``` ```
`data-skin` should be the runtime contract for component appearance. `data-skin` should be the runtime contract for component appearance.
@@ -422,17 +422,14 @@ Minimum contract:
```ts ```ts
type ThemeName = "morandi" | "earth" | "brand"; type ThemeName = "morandi" | "earth" | "brand";
type SkinName = "minimal" | "glass" | "pixel"; type SkinName = "minimal" | "glass" | "pixel";
type MotionPackName = "calm" | "snappy" | "spring"; type MotionModeName = "default" | "reduced";
type MotionAccessibilityName = "system" | "full" | "reduced";
``` ```
Likely helpers: Likely helpers:
- `setTheme(theme, root?)` - `setTheme(theme, root?)`
- `setSkin(skin, root?)` - `setSkin(skin, root?)`
- `setMotionPack(pack, root?)` - `setMotionMode(mode, root?)`
- `setMotionAccessibility(mode, root?)`
- `setMotionMode(mode, root?)` as a backward-compatible alias for accessibility mode
Provider shape if needed: Provider shape if needed:
@@ -440,8 +437,7 @@ Provider shape if needed:
<StyleProvider <StyleProvider
theme="morandi" theme="morandi"
skin="glass" skin="glass"
motionPack="spring" motionMode="default"
motionAccessibility="system"
> >
<App /> <App />
</StyleProvider> </StyleProvider>
@@ -615,13 +611,12 @@ As of 2026-03-20, the project is at this point:
and `Skeleton` and `Skeleton`
- screenshot-friendly validation surface added in `Foundation/Style Matrix` - screenshot-friendly validation surface added in `Foundation/Style Matrix`
- scoped `data-motion="reduced"` now works for nested docs wrappers - scoped `data-motion="reduced"` now works for nested docs wrappers
- motion now uses real packs through `data-motion-pack` - motion now uses a single `default` mode plus a `reduced` override through `data-motion`
- reduced motion remains available as a separate accessibility override layer
- shared skin-aware treatment now extends across the broader component library surface, - shared skin-aware treatment now extends across the broader component library surface,
including controls, menus, overlays, feedback, and data-heavy patterns including controls, menus, overlays, feedback, and data-heavy patterns
- package consumers can now import a single combined stylesheet from - package consumers can now import a single combined stylesheet from
`@ai-ui/ui/styles.css` `@ai-ui/ui/styles.css`
The original Phase 0-5 implementation sequence is now complete. Further work should be The original Phase 0-5 implementation sequence is now complete. Further work should be
treated as iteration: refining skins, expanding motion packs, or hardening package and treated as iteration: refining skins, shaping the default motion language, or hardening package and
registry distribution. registry distribution.
+10 -61
View File
@@ -18,50 +18,21 @@ export const themeDetails = {
} }
} as const satisfies Record<ThemeName, { label: string; note: string }>; } as const satisfies Record<ThemeName, { label: string; note: string }>;
export const motionPackNames = ["calm", "snappy", "spring"] as const; export const motionModeNames = ["default", "reduced"] as const;
export type MotionPackName = (typeof motionPackNames)[number]; export type MotionModeName = (typeof motionModeNames)[number];
export const defaultMotionPack: MotionPackName = "calm"; export const defaultMotionMode: MotionModeName = "default";
export const motionPackDetails = { export const motionModeDetails = {
calm: { default: {
label: "Calm", label: "Default",
note: "Editorial default with restrained lift and steady transitions" note: "Standard Cadence UI motion for hover, press, overlays, and hierarchy"
},
snappy: {
label: "Snappy",
note: "Shorter durations, tighter distances, and more direct response"
},
spring: {
label: "Spring",
note: "More elastic easing, wider movement, and livelier feedback"
}
} as const satisfies Record<MotionPackName, { label: string; note: string }>;
export const motionAccessibilityNames = ["system", "full", "reduced"] as const;
export type MotionAccessibilityName = (typeof motionAccessibilityNames)[number];
export const defaultMotionAccessibility: MotionAccessibilityName = "system";
export const motionAccessibilityDetails = {
system: {
label: "System",
note: "Follow the operating system reduced-motion preference"
},
full: {
label: "Full",
note: "Always show the selected motion pack, even if the OS prefers reduced motion"
}, },
reduced: { reduced: {
label: "Reduced", label: "Reduced",
note: "Always collapse durations, distances, and animated feedback" note: "Collapse durations, distances, and animated feedback"
} }
} as const satisfies Record<MotionAccessibilityName, { label: string; note: string }>; } as const satisfies Record<MotionModeName, { label: string; note: string }>;
export const motionModeNames = motionAccessibilityNames;
export type MotionModeName = MotionAccessibilityName;
export const defaultMotionMode: MotionModeName = defaultMotionAccessibility;
export const motionScale = { export const motionScale = {
instant: "var(--dur-instant)", instant: "var(--dur-instant)",
@@ -190,16 +161,6 @@ function getTargetElement(root?: HTMLElement) {
return document.documentElement; return document.documentElement;
} }
export function setMotionPack(pack: MotionPackName, root?: HTMLElement) {
const target = getTargetElement(root);
if (!target) {
return;
}
target.dataset.motionPack = pack;
}
export function setTheme(theme: ThemeName, root?: HTMLElement) { export function setTheme(theme: ThemeName, root?: HTMLElement) {
const target = getTargetElement(root); const target = getTargetElement(root);
@@ -210,24 +171,12 @@ export function setTheme(theme: ThemeName, root?: HTMLElement) {
target.dataset.theme = theme; target.dataset.theme = theme;
} }
export function setMotionAccessibility( export function setMotionMode(mode: MotionModeName, root?: HTMLElement) {
mode: MotionAccessibilityName,
root?: HTMLElement
) {
const target = getTargetElement(root); const target = getTargetElement(root);
if (!target) { if (!target) {
return; return;
} }
if (mode === "system") {
delete target.dataset.motion;
return;
}
target.dataset.motion = mode; target.dataset.motion = mode;
} }
export function setMotionMode(mode: MotionModeName, root?: HTMLElement) {
setMotionAccessibility(mode, root);
}
+2 -74
View File
@@ -1,5 +1,6 @@
:root, :root,
[data-motion-pack="calm"] { :root[data-motion="default"],
[data-motion="default"] {
--dur-instant: 1ms; --dur-instant: 1ms;
--dur-fast: 120ms; --dur-fast: 120ms;
--dur-base: 200ms; --dur-base: 200ms;
@@ -20,50 +21,6 @@
--scale-pop: 1.02; --scale-pop: 1.02;
} }
:root[data-motion-pack="snappy"],
[data-motion-pack="snappy"] {
--dur-instant: 1ms;
--dur-fast: 90ms;
--dur-base: 150ms;
--dur-slow: 220ms;
--dur-deliberate: 300ms;
--ease-standard: cubic-bezier(0.18, 1, 0.32, 1);
--ease-emphasized: cubic-bezier(0.2, 1.08, 0.28, 1);
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
--distance-xs: 2px;
--distance-sm: 4px;
--distance-md: 10px;
--distance-lg: 16px;
--scale-press: 0.985;
--scale-hover: 1.006;
--scale-pop: 1.012;
}
:root[data-motion-pack="spring"],
[data-motion-pack="spring"] {
--dur-instant: 1ms;
--dur-fast: 140ms;
--dur-base: 220ms;
--dur-slow: 360ms;
--dur-deliberate: 520ms;
--ease-standard: cubic-bezier(0.2, 1.08, 0.28, 1);
--ease-emphasized: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-exit: cubic-bezier(0.42, 0, 1, 1);
--distance-xs: 6px;
--distance-sm: 12px;
--distance-md: 20px;
--distance-lg: 32px;
--scale-press: 0.96;
--scale-hover: 1.018;
--scale-pop: 1.035;
}
:root[data-motion="reduced"], :root[data-motion="reduced"],
[data-motion="reduced"] { [data-motion="reduced"] {
--dur-instant: 1ms; --dur-instant: 1ms;
@@ -93,35 +50,6 @@
transition-duration: 1ms !important; transition-duration: 1ms !important;
} }
@media (prefers-reduced-motion: reduce) {
:root:not([data-motion="full"]) {
--dur-instant: 1ms;
--dur-fast: 1ms;
--dur-base: 1ms;
--dur-slow: 1ms;
--dur-deliberate: 1ms;
--distance-xs: 0px;
--distance-sm: 0px;
--distance-md: 0px;
--distance-lg: 0px;
--scale-press: 1;
--scale-hover: 1;
--scale-pop: 1;
}
:root {
scroll-behavior: auto;
}
:root:not([data-motion="full"]) *,
:root:not([data-motion="full"]) *::before,
:root:not([data-motion="full"]) *::after {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 1ms !important;
}
}
@keyframes aiui-fade-in { @keyframes aiui-fade-in {
from { from {
+11 -28
View File
@@ -1,52 +1,35 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
defaultMotionAccessibility,
defaultMotionMode, defaultMotionMode,
defaultMotionPack,
motionAccessibilityDetails,
motionAccessibilityNames,
motionModeNames, motionModeNames,
motionPackDetails, motionModeDetails,
motionPackNames, setMotionMode
setMotionAccessibility,
setMotionMode,
setMotionPack
} from "@ai-ui/tokens"; } from "@ai-ui/tokens";
describe("motion contract", () => { describe("motion contract", () => {
it("exposes default values that exist in the public name sets", () => { it("exposes default values that exist in the public name sets", () => {
expect(motionPackNames).toContain(defaultMotionPack);
expect(motionAccessibilityNames).toContain(defaultMotionAccessibility);
expect(motionModeNames).toContain(defaultMotionMode); expect(motionModeNames).toContain(defaultMotionMode);
expect(motionPackDetails[defaultMotionPack].label).toBeTruthy(); expect(motionModeDetails[defaultMotionMode].label).toBeTruthy();
expect(motionAccessibilityDetails[defaultMotionAccessibility].label).toBeTruthy();
}); });
it("sets the active motion pack on the document root", () => { it("sets default motion mode on the document root", () => {
setMotionPack("spring"); setMotionMode("default");
expect(document.documentElement.dataset.motionPack).toBe("spring"); expect(document.documentElement.dataset.motion).toBe("default");
}); });
it("sets reduced motion accessibility on the document root", () => { it("sets reduced motion mode on the document root", () => {
setMotionAccessibility("reduced"); setMotionMode("reduced");
expect(document.documentElement.dataset.motion).toBe("reduced"); expect(document.documentElement.dataset.motion).toBe("reduced");
}); });
it("removes the accessibility override when system mode is restored", () => { it("supports explicit reduced mode on custom roots", () => {
setMotionAccessibility("reduced");
setMotionMode("system");
expect(document.documentElement.dataset.motion).toBeUndefined();
});
it("supports explicit full motion override on custom roots", () => {
const target = document.createElement("div"); const target = document.createElement("div");
setMotionAccessibility("full", target); setMotionMode("reduced", target);
expect(target.dataset.motion).toBe("full"); expect(target.dataset.motion).toBe("reduced");
}); });
}); });