refactor(motion): simplify to default and reduced
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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="reduced"` 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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user