diff --git a/apps/docs/.storybook/preview.ts b/apps/docs/.storybook/preview.ts index f2a3819..02a0211 100644 --- a/apps/docs/.storybook/preview.ts +++ b/apps/docs/.storybook/preview.ts @@ -8,15 +8,11 @@ import { skinNames } from "@ai-ui/ui"; import { - defaultMotionAccessibility, - defaultMotionPack, + defaultMotionMode, defaultTheme, - motionAccessibilityDetails, - motionAccessibilityNames, - motionPackDetails, - motionPackNames, - setMotionAccessibility, - setMotionPack, + motionModeDetails, + motionModeNames, + setMotionMode, setTheme, themeDetails, themeNames @@ -35,25 +31,14 @@ const preview: Preview = { })) } }, - motionPack: { - description: "Preview motion pack", + motionMode: { + description: "Preview motion mode", toolbar: { icon: "transfer", dynamicTitle: true, - 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) => ({ + items: motionModeNames.map((modeName) => ({ value: modeName, - title: motionAccessibilityDetails[modeName].label + title: motionModeDetails[modeName].label })) } }, @@ -70,8 +55,7 @@ const preview: Preview = { } }, initialGlobals: { - motionAccessibility: defaultMotionAccessibility, - motionPack: defaultMotionPack, + motionMode: defaultMotionMode, skin: defaultSkin, theme: defaultTheme }, @@ -97,13 +81,11 @@ const preview: Preview = { (Story, context) => { if (typeof document !== "undefined") { setTheme(context.globals.theme ?? defaultTheme); - setMotionPack(context.globals.motionPack ?? defaultMotionPack); - setMotionAccessibility( - context.globals.motionAccessibility ?? defaultMotionAccessibility - ); + setMotionMode(context.globals.motionMode ?? defaultMotionMode); setSkin(context.globals.skin ?? defaultSkin); document.body.dataset.theme = context.globals.theme ?? defaultTheme; + document.body.dataset.motion = context.globals.motionMode ?? defaultMotionMode; document.body.dataset.skin = context.globals.skin ?? defaultSkin; } diff --git a/apps/docs/src/style-contract.stories.tsx b/apps/docs/src/style-contract.stories.tsx index f24dc27..d4fb698 100644 --- a/apps/docs/src/style-contract.stories.tsx +++ b/apps/docs/src/style-contract.stories.tsx @@ -11,18 +11,15 @@ import { type SkinName } from "@ai-ui/ui"; import { - defaultMotionAccessibility, - defaultMotionPack, + defaultMotionMode, defaultTheme, - type MotionAccessibilityName, - type MotionPackName, + type MotionModeName, type ThemeName } from "@ai-ui/tokens"; import type { Meta, StoryObj } from "@storybook/react"; type StyleContractShowcaseProps = { - motionAccessibility: MotionAccessibilityName; - motionPack: MotionPackName; + motionMode: MotionModeName; skin: SkinName; theme: ThemeName; }; @@ -147,8 +144,7 @@ function SkinPanel({ } function StyleContractShowcase({ - motionAccessibility, - motionPack, + motionMode, skin, theme }: StyleContractShowcaseProps) { @@ -180,8 +176,7 @@ function StyleContractShowcase({
- - +
@@ -192,7 +187,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, motion pack, and accessibility override together" + "Storybook globals that apply theme, skin, and motion mode together" ].map((item) => (
(
@@ -196,52 +179,30 @@ function StyleMatrixShowcase() {

This page is the screenshot-friendly regression target for the pilot skin work. - The grid uses nested `data-skin`, `data-motion-pack`, and - `data-motion="reduced"` scopes so the same building blocks can be + The grid uses nested `data-skin` and `data-motion` scopes so the same + building blocks can be reviewed side by side.

- {motionPackNames.map((motionPack) => ( -
+ {motionModeNames.map((motionMode) => ( +
-

{motionPackDetails[motionPack].label}

+

{motionModeDetails[motionMode].label}

- {motionPackDetails[motionPack].note} + {motionModeDetails[motionMode].note}

- {motionAccessibilityModes.map((motionAccessibilityMode) => ( -
-
-
-

- {motionAccessibilityMode.label} -

-

- {motionAccessibilityMode.value === "reduced" - ? '`data-motion="reduced"`' - : "System preference"} - {" "}with{" "} - - {`data-motion-pack="${motionPack}"`} - - . -

-
-
-
- {skinNames.map((skin) => ( - - ))} -
-
- ))} +
+ {skinNames.map((skin) => ( + + ))} +
))}
@@ -252,7 +213,7 @@ function StyleMatrixShowcase() {

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 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.

diff --git a/apps/docs/src/tokens.stories.tsx b/apps/docs/src/tokens.stories.tsx index 6a2ea61..e62d997 100644 --- a/apps/docs/src/tokens.stories.tsx +++ b/apps/docs/src/tokens.stories.tsx @@ -1,25 +1,21 @@ import { colorTokens, defaultTheme, - defaultMotionAccessibility, - defaultMotionPack, + defaultMotionMode, motionTokens, - motionAccessibilityDetails, - motionPackDetails, + motionModeDetails, radiusTokens, shadowTokens, themeDetails, themeNames, typographyTokens, - type MotionAccessibilityName, - type MotionPackName, + type MotionModeName, type ThemeName } from "@ai-ui/tokens"; import type { Meta, StoryObj } from "@storybook/react"; type TokensOverviewProps = { - motionAccessibility: MotionAccessibilityName; - motionPack: MotionPackName; + motionMode: MotionModeName; theme: ThemeName; }; @@ -127,8 +123,7 @@ function ThemeCard({ themeName }: { themeName: ThemeName }) { } function TokensOverview({ - motionAccessibility, - motionPack, + motionMode, theme }: TokensOverviewProps) { return ( @@ -168,18 +163,10 @@ function TokensOverview({

- Motion Pack + Motion

- {motionPackDetails[motionPack].label} -

-
-
-

- Accessibility Override -

-

- {motionAccessibilityDetails[motionAccessibility].label} + {motionModeDetails[motionMode].label}

@@ -301,8 +288,8 @@ function TokensOverview({

Motion tokens

Timing and motion scale now live in variables that components can consume - directly. The toolbar now separates the active motion pack from the - accessibility override. + directly. The toolbar now switches between the default interaction layer + and the reduced-motion fallback.

@@ -390,18 +377,13 @@ type Story = StoryObj; export const Overview: Story = { args: { - motionAccessibility: defaultMotionAccessibility, - motionPack: defaultMotionPack, + motionMode: defaultMotionMode, theme: defaultTheme }, render: (_args, context) => ( diff --git a/docs/rfcs/multi-style-architecture.md b/docs/rfcs/multi-style-architecture.md index ee3d8a2..35fd425 100644 --- a/docs/rfcs/multi-style-architecture.md +++ b/docs/rfcs/multi-style-architecture.md @@ -162,9 +162,10 @@ Examples: - `glass` - `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 - easing @@ -174,9 +175,8 @@ Motion pack defines interaction feel: Examples: -- `calm` -- `micro` -- `spring` +- `default` +- `reduced` ### Layout Pattern @@ -218,7 +218,7 @@ The existing token groups remain the baseline: Add a new root attribute: ```html - + ``` `data-skin` should be the runtime contract for component appearance. @@ -422,17 +422,14 @@ Minimum contract: ```ts type ThemeName = "morandi" | "earth" | "brand"; type SkinName = "minimal" | "glass" | "pixel"; -type MotionPackName = "calm" | "snappy" | "spring"; -type MotionAccessibilityName = "system" | "full" | "reduced"; +type MotionModeName = "default" | "reduced"; ``` Likely helpers: - `setTheme(theme, root?)` - `setSkin(skin, root?)` -- `setMotionPack(pack, root?)` -- `setMotionAccessibility(mode, root?)` -- `setMotionMode(mode, root?)` as a backward-compatible alias for accessibility mode +- `setMotionMode(mode, root?)` Provider shape if needed: @@ -440,8 +437,7 @@ Provider shape if needed: @@ -615,13 +611,12 @@ 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 +- motion now uses a single `default` mode plus a `reduced` override through `data-motion` - 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 `@ai-ui/ui/styles.css` 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. diff --git a/packages/tokens/src/index.ts b/packages/tokens/src/index.ts index 93cc78c..1667562 100644 --- a/packages/tokens/src/index.ts +++ b/packages/tokens/src/index.ts @@ -18,50 +18,21 @@ export const themeDetails = { } } as const satisfies Record; -export const motionPackNames = ["calm", "snappy", "spring"] as const; -export type MotionPackName = (typeof motionPackNames)[number]; +export const motionModeNames = ["default", "reduced"] as const; +export type MotionModeName = (typeof motionModeNames)[number]; -export const defaultMotionPack: MotionPackName = "calm"; +export const defaultMotionMode: MotionModeName = "default"; -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; - -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" +export const motionModeDetails = { + default: { + label: "Default", + note: "Standard Cadence UI motion for hover, press, overlays, and hierarchy" }, reduced: { label: "Reduced", - note: "Always collapse durations, distances, and animated feedback" + note: "Collapse durations, distances, and animated feedback" } -} as const satisfies Record; - -export const motionModeNames = motionAccessibilityNames; -export type MotionModeName = MotionAccessibilityName; - -export const defaultMotionMode: MotionModeName = defaultMotionAccessibility; +} as const satisfies Record; export const motionScale = { instant: "var(--dur-instant)", @@ -190,16 +161,6 @@ 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); @@ -210,24 +171,12 @@ export function setTheme(theme: ThemeName, root?: HTMLElement) { target.dataset.theme = theme; } -export function setMotionAccessibility( - mode: MotionAccessibilityName, - root?: HTMLElement -) { +export function setMotionMode(mode: MotionModeName, root?: HTMLElement) { const target = getTargetElement(root); if (!target) { return; } - if (mode === "system") { - delete target.dataset.motion; - return; - } - target.dataset.motion = mode; } - -export function setMotionMode(mode: MotionModeName, root?: HTMLElement) { - setMotionAccessibility(mode, root); -} diff --git a/packages/tokens/src/motion.css b/packages/tokens/src/motion.css index b822867..79f4fba 100644 --- a/packages/tokens/src/motion.css +++ b/packages/tokens/src/motion.css @@ -1,5 +1,6 @@ :root, -[data-motion-pack="calm"] { +:root[data-motion="default"], +[data-motion="default"] { --dur-instant: 1ms; --dur-fast: 120ms; --dur-base: 200ms; @@ -20,50 +21,6 @@ --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; @@ -93,35 +50,6 @@ 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 { from { diff --git a/packages/ui/src/lib/motion-contract.test.ts b/packages/ui/src/lib/motion-contract.test.ts index 4159a69..641909f 100644 --- a/packages/ui/src/lib/motion-contract.test.ts +++ b/packages/ui/src/lib/motion-contract.test.ts @@ -1,52 +1,35 @@ import { describe, expect, it } from "vitest"; import { - defaultMotionAccessibility, defaultMotionMode, - defaultMotionPack, - motionAccessibilityDetails, - motionAccessibilityNames, motionModeNames, - motionPackDetails, - motionPackNames, - setMotionAccessibility, - setMotionMode, - setMotionPack + motionModeDetails, + setMotionMode } 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(); + expect(motionModeDetails[defaultMotionMode].label).toBeTruthy(); }); - it("sets the active motion pack on the document root", () => { - setMotionPack("spring"); + it("sets default motion mode on the document root", () => { + 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", () => { - setMotionAccessibility("reduced"); + it("sets reduced motion mode on the document root", () => { + setMotionMode("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", () => { + it("supports explicit reduced mode on custom roots", () => { const target = document.createElement("div"); - setMotionAccessibility("full", target); + setMotionMode("reduced", target); - expect(target.dataset.motion).toBe("full"); + expect(target.dataset.motion).toBe("reduced"); }); });