Split motion packs from accessibility overrides
This commit is contained in:
@@ -8,10 +8,15 @@ import {
|
||||
skinNames
|
||||
} from "@ai-ui/ui";
|
||||
import {
|
||||
defaultMotionMode,
|
||||
defaultMotionAccessibility,
|
||||
defaultMotionPack,
|
||||
defaultTheme,
|
||||
motionModeNames,
|
||||
setMotionMode,
|
||||
motionAccessibilityDetails,
|
||||
motionAccessibilityNames,
|
||||
motionPackDetails,
|
||||
motionPackNames,
|
||||
setMotionAccessibility,
|
||||
setMotionPack,
|
||||
setTheme,
|
||||
themeDetails,
|
||||
themeNames
|
||||
@@ -30,14 +35,25 @@ const preview: Preview = {
|
||||
}))
|
||||
}
|
||||
},
|
||||
motion: {
|
||||
description: "Preview motion mode",
|
||||
motionPack: {
|
||||
description: "Preview motion pack",
|
||||
toolbar: {
|
||||
icon: "transfer",
|
||||
dynamicTitle: true,
|
||||
items: motionModeNames.map((modeName) => ({
|
||||
items: motionPackNames.map((packName) => ({
|
||||
value: packName,
|
||||
title: motionPackDetails[packName].label
|
||||
}))
|
||||
}
|
||||
},
|
||||
motionAccessibility: {
|
||||
description: "Preview motion accessibility override",
|
||||
toolbar: {
|
||||
icon: "accessibility",
|
||||
dynamicTitle: true,
|
||||
items: motionAccessibilityNames.map((modeName) => ({
|
||||
value: modeName,
|
||||
title: modeName === "system" ? "Motion / System" : "Motion / Reduced"
|
||||
title: motionAccessibilityDetails[modeName].label
|
||||
}))
|
||||
}
|
||||
},
|
||||
@@ -54,7 +70,8 @@ const preview: Preview = {
|
||||
}
|
||||
},
|
||||
initialGlobals: {
|
||||
motion: defaultMotionMode,
|
||||
motionAccessibility: defaultMotionAccessibility,
|
||||
motionPack: defaultMotionPack,
|
||||
skin: defaultSkin,
|
||||
theme: defaultTheme
|
||||
},
|
||||
@@ -80,7 +97,10 @@ const preview: Preview = {
|
||||
(Story, context) => {
|
||||
if (typeof document !== "undefined") {
|
||||
setTheme(context.globals.theme ?? defaultTheme);
|
||||
setMotionMode(context.globals.motion ?? defaultMotionMode);
|
||||
setMotionPack(context.globals.motionPack ?? defaultMotionPack);
|
||||
setMotionAccessibility(
|
||||
context.globals.motionAccessibility ?? defaultMotionAccessibility
|
||||
);
|
||||
setSkin(context.globals.skin ?? defaultSkin);
|
||||
|
||||
document.body.dataset.theme = context.globals.theme ?? defaultTheme;
|
||||
|
||||
@@ -11,15 +11,18 @@ import {
|
||||
type SkinName
|
||||
} from "@ai-ui/ui";
|
||||
import {
|
||||
defaultMotionMode,
|
||||
defaultMotionAccessibility,
|
||||
defaultMotionPack,
|
||||
defaultTheme,
|
||||
type MotionModeName,
|
||||
type MotionAccessibilityName,
|
||||
type MotionPackName,
|
||||
type ThemeName
|
||||
} from "@ai-ui/tokens";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
type StyleContractShowcaseProps = {
|
||||
motion: MotionModeName;
|
||||
motionAccessibility: MotionAccessibilityName;
|
||||
motionPack: MotionPackName;
|
||||
skin: SkinName;
|
||||
theme: ThemeName;
|
||||
};
|
||||
@@ -140,7 +143,8 @@ function SkinPanel({
|
||||
}
|
||||
|
||||
function StyleContractShowcase({
|
||||
motion,
|
||||
motionAccessibility,
|
||||
motionPack,
|
||||
skin,
|
||||
theme
|
||||
}: StyleContractShowcaseProps) {
|
||||
@@ -172,7 +176,8 @@ function StyleContractShowcase({
|
||||
<section className="flex flex-wrap gap-3">
|
||||
<RuntimeBadge label="theme" value={theme} />
|
||||
<RuntimeBadge label="skin" value={skin} />
|
||||
<RuntimeBadge label="motion" value={motion} />
|
||||
<RuntimeBadge label="motion pack" value={motionPack} />
|
||||
<RuntimeBadge label="accessibility" value={motionAccessibility} />
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||
@@ -183,7 +188,7 @@ function StyleContractShowcase({
|
||||
"A new runtime attribute: `data-skin`",
|
||||
"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",
|
||||
"Storybook globals that apply theme, skin, and motion together"
|
||||
"Storybook globals that apply theme, skin, motion pack, and accessibility override together"
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
@@ -201,7 +206,8 @@ function StyleContractShowcase({
|
||||
{[
|
||||
"Button, card, input, dialog, switch, and skeleton recipe extraction",
|
||||
"Skin-specific component semantic variables such as `--button-*` and `--panel-*`",
|
||||
"A docs comparison page where existing components fully restyle under each skin"
|
||||
"A docs comparison page where existing components fully restyle under each skin",
|
||||
"Consumer-facing polish after the runtime contract and docs surface are stable"
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
@@ -232,7 +238,8 @@ const meta = {
|
||||
title: "Foundation/Style Contract",
|
||||
component: StyleContractShowcase,
|
||||
args: {
|
||||
motion: defaultMotionMode,
|
||||
motionAccessibility: defaultMotionAccessibility,
|
||||
motionPack: defaultMotionPack,
|
||||
skin: defaultSkin,
|
||||
theme: defaultTheme
|
||||
},
|
||||
@@ -240,13 +247,19 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Phase 1 adds the runtime style contract. Use the Storybook toolbar to switch the active `theme`, `skin`, and `motion` values 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`, `motion pack`, and accessibility override globally, or inspect the side-by-side nested `data-skin` panels below."
|
||||
}
|
||||
}
|
||||
},
|
||||
render: (_args, context) => (
|
||||
<StyleContractShowcase
|
||||
motion={(context.globals.motion as MotionModeName | undefined) ?? defaultMotionMode}
|
||||
motionAccessibility={
|
||||
(context.globals.motionAccessibility as MotionAccessibilityName | undefined) ??
|
||||
defaultMotionAccessibility
|
||||
}
|
||||
motionPack={
|
||||
(context.globals.motionPack as MotionPackName | undefined) ?? defaultMotionPack
|
||||
}
|
||||
skin={(context.globals.skin as SkinName | undefined) ?? defaultSkin}
|
||||
theme={(context.globals.theme as ThemeName | undefined) ?? defaultTheme}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
motionPackDetails,
|
||||
motionPackNames,
|
||||
type MotionPackName
|
||||
} from "@ai-ui/tokens";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -21,18 +26,18 @@ import {
|
||||
} from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const motionModes = [
|
||||
const motionAccessibilityModes = [
|
||||
{
|
||||
label: "System motion",
|
||||
label: "System accessibility",
|
||||
value: "system"
|
||||
},
|
||||
{
|
||||
label: "Reduced motion",
|
||||
label: "Reduced accessibility",
|
||||
value: "reduced"
|
||||
}
|
||||
] as const;
|
||||
|
||||
type MotionMode = (typeof motionModes)[number]["value"];
|
||||
type MotionAccessibilityMode = (typeof motionAccessibilityModes)[number]["value"];
|
||||
|
||||
function RuntimePill({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -78,21 +83,25 @@ function PanelPreview() {
|
||||
}
|
||||
|
||||
function ComparisonCell({
|
||||
motion,
|
||||
motionAccessibility,
|
||||
motionPack,
|
||||
skin
|
||||
}: {
|
||||
motion: MotionMode;
|
||||
motionAccessibility: MotionAccessibilityMode;
|
||||
motionPack: MotionPackName;
|
||||
skin: SkinName;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
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={motion === "reduced" ? "reduced" : undefined}
|
||||
data-motion={motionAccessibility === "reduced" ? "reduced" : undefined}
|
||||
data-motion-pack={motionPack}
|
||||
data-skin={skin}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<RuntimePill>{motionPackDetails[motionPack].label}</RuntimePill>
|
||||
<RuntimePill>{skinDetails[skin].label}</RuntimePill>
|
||||
<RuntimePill>{motion}</RuntimePill>
|
||||
<RuntimePill>{motionAccessibility}</RuntimePill>
|
||||
</div>
|
||||
|
||||
<Card interactive tone="default">
|
||||
@@ -166,29 +175,52 @@ function StyleMatrixShowcase() {
|
||||
</h1>
|
||||
<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.
|
||||
The grid uses nested `data-skin` and `data-motion` scopes so the same building
|
||||
blocks can be reviewed side by side.
|
||||
The grid uses nested `data-skin`, `data-motion-pack`, and
|
||||
`data-motion="reduced"` scopes so the same building blocks can be
|
||||
reviewed side by side.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="grid gap-4">
|
||||
{motionModes.map((motionMode) => (
|
||||
<div key={motionMode.value} className="grid gap-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">{motionMode.label}</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||||
`{motionMode.value === "reduced" ? 'data-motion="reduced"' : "default"}`
|
||||
{" "}
|
||||
on the wrapper scope.
|
||||
</p>
|
||||
{motionPackNames.map((motionPack) => (
|
||||
<div key={motionPack} className="grid gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">{motionPackDetails[motionPack].label}</h2>
|
||||
<p className="mt-1 max-w-3xl text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
{motionPackDetails[motionPack].note}
|
||||
</p>
|
||||
</div>
|
||||
{motionAccessibilityModes.map((motionAccessibilityMode) => (
|
||||
<div key={`${motionPack}-${motionAccessibilityMode.value}`} className="grid gap-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">
|
||||
{motionAccessibilityMode.label}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||||
{motionAccessibilityMode.value === "reduced"
|
||||
? '`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 className="grid gap-4 xl:grid-cols-3">
|
||||
{skinNames.map((skin) => (
|
||||
<ComparisonCell key={`${motionMode.value}-${skin}`} motion={motionMode.value} skin={skin} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
@@ -199,7 +231,8 @@ function StyleMatrixShowcase() {
|
||||
<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
|
||||
panel treatment with the Storybook toolbar. The matrix above covers scoped
|
||||
inline regression. The control below covers the live overlay behavior.
|
||||
inline regression across packs and reduced-motion overlay. The control below
|
||||
covers the live overlay behavior.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-start lg:justify-end">
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import {
|
||||
colorTokens,
|
||||
defaultMotionAccessibility,
|
||||
defaultMotionPack,
|
||||
motionTokens,
|
||||
motionAccessibilityDetails,
|
||||
motionPackDetails,
|
||||
radiusTokens,
|
||||
shadowTokens,
|
||||
themeDetails,
|
||||
themeNames,
|
||||
typographyTokens,
|
||||
type MotionModeName,
|
||||
type MotionAccessibilityName,
|
||||
type MotionPackName,
|
||||
type ThemeName
|
||||
} from "@ai-ui/tokens";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
type TokensOverviewProps = {
|
||||
motionMode: MotionModeName;
|
||||
motionAccessibility: MotionAccessibilityName;
|
||||
motionPack: MotionPackName;
|
||||
theme: ThemeName;
|
||||
};
|
||||
|
||||
@@ -119,7 +125,11 @@ function ThemeCard({ themeName }: { themeName: ThemeName }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TokensOverview({ motionMode, theme }: TokensOverviewProps) {
|
||||
function TokensOverview({
|
||||
motionAccessibility,
|
||||
motionPack,
|
||||
theme
|
||||
}: TokensOverviewProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-background)] px-6 py-10 text-[var(--color-foreground)] sm:px-10">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8">
|
||||
@@ -157,10 +167,18 @@ function TokensOverview({ motionMode, theme }: TokensOverviewProps) {
|
||||
</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)]">
|
||||
Motion Mode
|
||||
Motion Pack
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
|
||||
{motionMode === "system" ? "System preference" : "Reduced motion"}
|
||||
{motionPackDetails[motionPack].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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,7 +300,8 @@ function TokensOverview({ motionMode, theme }: TokensOverviewProps) {
|
||||
<h2 className="text-2xl font-semibold">Motion tokens</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-muted-foreground)]">
|
||||
Timing and motion scale now live in variables that components can consume
|
||||
directly. The toolbar can force reduced motion for preview validation.
|
||||
directly. The toolbar now separates the active motion pack from the
|
||||
accessibility override.
|
||||
</p>
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-3">
|
||||
@@ -370,12 +389,19 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Overview: Story = {
|
||||
args: {
|
||||
motionMode: "system",
|
||||
motionAccessibility: defaultMotionAccessibility,
|
||||
motionPack: defaultMotionPack,
|
||||
theme: "light"
|
||||
},
|
||||
render: (_args, context) => (
|
||||
<TokensOverview
|
||||
motionMode={context.globals.motion as MotionModeName}
|
||||
motionAccessibility={
|
||||
(context.globals.motionAccessibility as MotionAccessibilityName | undefined) ??
|
||||
defaultMotionAccessibility
|
||||
}
|
||||
motionPack={
|
||||
(context.globals.motionPack as MotionPackName | undefined) ?? defaultMotionPack
|
||||
}
|
||||
theme={context.globals.theme as ThemeName}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -422,19 +422,27 @@ Minimum contract:
|
||||
```ts
|
||||
type ThemeName = "light" | "dark" | "brand" | "minimal";
|
||||
type SkinName = "minimal" | "glass" | "pixel";
|
||||
type MotionName = "system" | "reduced" | "micro" | "spring";
|
||||
type MotionPackName = "calm" | "snappy" | "spring";
|
||||
type MotionAccessibilityName = "system" | "full" | "reduced";
|
||||
```
|
||||
|
||||
Likely helpers:
|
||||
|
||||
- `setTheme(theme, root?)`
|
||||
- `setSkin(skin, root?)`
|
||||
- `setMotionMode(mode, root?)`
|
||||
- `setMotionPack(pack, root?)`
|
||||
- `setMotionAccessibility(mode, root?)`
|
||||
- `setMotionMode(mode, root?)` as a backward-compatible alias for accessibility mode
|
||||
|
||||
Provider shape if needed:
|
||||
|
||||
```tsx
|
||||
<StyleProvider theme="minimal" skin="glass" motion="micro">
|
||||
<StyleProvider
|
||||
theme="minimal"
|
||||
skin="glass"
|
||||
motionPack="spring"
|
||||
motionAccessibility="system"
|
||||
>
|
||||
<App />
|
||||
</StyleProvider>
|
||||
```
|
||||
@@ -587,8 +595,8 @@ proven inside this repo.
|
||||
recommendation is `@ai-ui/ui`.
|
||||
- Should the runtime API expose a `StyleProvider`, or should root attributes remain the
|
||||
only public contract at first?
|
||||
- Should motion packs beyond `system` and `reduced` ship in the first skin milestone, or
|
||||
should additional motion packs wait until after the pilot components land?
|
||||
- Should the current `calm / snappy / spring` set remain the long-term pack list, or
|
||||
should product-specific packs be introduced later?
|
||||
- Should `minimal` become the new default appearance, or should the current warm
|
||||
editorial look remain the default and be renamed explicitly?
|
||||
|
||||
@@ -607,6 +615,8 @@ As of 2026-03-20, the project is at this point:
|
||||
and `Skeleton`
|
||||
- screenshot-friendly validation surface added in `Foundation/Style Matrix`
|
||||
- scoped `data-motion="reduced"` now works for nested docs wrappers
|
||||
- motion now uses real packs through `data-motion-pack`
|
||||
- reduced motion remains available as a separate accessibility override layer
|
||||
- shared skin-aware treatment now extends across the broader component library surface,
|
||||
including controls, menus, overlays, feedback, and data-heavy patterns
|
||||
- package consumers can now import a single combined stylesheet from
|
||||
|
||||
@@ -18,10 +18,50 @@ export const themeDetails = {
|
||||
}
|
||||
} as const satisfies Record<ThemeName, { label: string; note: string }>;
|
||||
|
||||
export const motionModeNames = ["system", "reduced"] as const;
|
||||
export type MotionModeName = (typeof motionModeNames)[number];
|
||||
export const motionPackNames = ["calm", "snappy", "spring"] as const;
|
||||
export type MotionPackName = (typeof motionPackNames)[number];
|
||||
|
||||
export const defaultMotionMode: MotionModeName = "system";
|
||||
export const defaultMotionPack: MotionPackName = "calm";
|
||||
|
||||
export const motionPackDetails = {
|
||||
calm: {
|
||||
label: "Calm",
|
||||
note: "Editorial default with restrained lift and steady transitions"
|
||||
},
|
||||
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: {
|
||||
label: "Reduced",
|
||||
note: "Always collapse durations, distances, and animated feedback"
|
||||
}
|
||||
} as const satisfies Record<MotionAccessibilityName, { label: string; note: string }>;
|
||||
|
||||
export const motionModeNames = motionAccessibilityNames;
|
||||
export type MotionModeName = MotionAccessibilityName;
|
||||
|
||||
export const defaultMotionMode: MotionModeName = defaultMotionAccessibility;
|
||||
|
||||
export const motionScale = {
|
||||
instant: "var(--dur-instant)",
|
||||
@@ -150,6 +190,16 @@ function getTargetElement(root?: HTMLElement) {
|
||||
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) {
|
||||
const target = getTargetElement(root);
|
||||
|
||||
@@ -160,7 +210,10 @@ export function setTheme(theme: ThemeName, root?: HTMLElement) {
|
||||
target.dataset.theme = theme;
|
||||
}
|
||||
|
||||
export function setMotionMode(mode: MotionModeName, root?: HTMLElement) {
|
||||
export function setMotionAccessibility(
|
||||
mode: MotionAccessibilityName,
|
||||
root?: HTMLElement
|
||||
) {
|
||||
const target = getTargetElement(root);
|
||||
|
||||
if (!target) {
|
||||
@@ -174,3 +227,7 @@ export function setMotionMode(mode: MotionModeName, root?: HTMLElement) {
|
||||
|
||||
target.dataset.motion = mode;
|
||||
}
|
||||
|
||||
export function setMotionMode(mode: MotionModeName, root?: HTMLElement) {
|
||||
setMotionAccessibility(mode, root);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
:root {
|
||||
:root,
|
||||
[data-motion-pack="calm"] {
|
||||
--dur-instant: 1ms;
|
||||
--dur-fast: 120ms;
|
||||
--dur-base: 200ms;
|
||||
@@ -19,6 +20,50 @@
|
||||
--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"],
|
||||
[data-motion="reduced"] {
|
||||
--dur-instant: 1ms;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
defaultMotionAccessibility,
|
||||
defaultMotionMode,
|
||||
defaultMotionPack,
|
||||
motionAccessibilityDetails,
|
||||
motionAccessibilityNames,
|
||||
motionModeNames,
|
||||
motionPackDetails,
|
||||
motionPackNames,
|
||||
setMotionAccessibility,
|
||||
setMotionMode,
|
||||
setMotionPack
|
||||
} from "@ai-ui/tokens";
|
||||
|
||||
describe("motion contract", () => {
|
||||
it("exposes default values that exist in the public name sets", () => {
|
||||
expect(motionPackNames).toContain(defaultMotionPack);
|
||||
expect(motionAccessibilityNames).toContain(defaultMotionAccessibility);
|
||||
expect(motionModeNames).toContain(defaultMotionMode);
|
||||
expect(motionPackDetails[defaultMotionPack].label).toBeTruthy();
|
||||
expect(motionAccessibilityDetails[defaultMotionAccessibility].label).toBeTruthy();
|
||||
});
|
||||
|
||||
it("sets the active motion pack on the document root", () => {
|
||||
setMotionPack("spring");
|
||||
|
||||
expect(document.documentElement.dataset.motionPack).toBe("spring");
|
||||
});
|
||||
|
||||
it("sets reduced motion accessibility on the document root", () => {
|
||||
setMotionAccessibility("reduced");
|
||||
|
||||
expect(document.documentElement.dataset.motion).toBe("reduced");
|
||||
});
|
||||
|
||||
it("removes the accessibility override when system mode is restored", () => {
|
||||
setMotionAccessibility("reduced");
|
||||
setMotionMode("system");
|
||||
|
||||
expect(document.documentElement.dataset.motion).toBeUndefined();
|
||||
});
|
||||
|
||||
it("supports explicit full motion override on custom roots", () => {
|
||||
const target = document.createElement("div");
|
||||
|
||||
setMotionAccessibility("full", target);
|
||||
|
||||
expect(target.dataset.motion).toBe("full");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user