feat(motion): add interactive micro-feedback
This commit is contained in:
@@ -34,7 +34,7 @@ const preview: Preview = {
|
|||||||
motionMode: {
|
motionMode: {
|
||||||
description: "Preview motion mode",
|
description: "Preview motion mode",
|
||||||
toolbar: {
|
toolbar: {
|
||||||
icon: "transfer",
|
icon: "contrast",
|
||||||
dynamicTitle: true,
|
dynamicTitle: true,
|
||||||
items: motionModeNames.map((modeName) => ({
|
items: motionModeNames.map((modeName) => ({
|
||||||
value: modeName,
|
value: modeName,
|
||||||
|
|||||||
@@ -187,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, and motion mode together"
|
"Storybook globals that apply theme, skin, and interactive/static motion mode together"
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item}
|
key={item}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ function StyleMatrixShowcase() {
|
|||||||
|
|
||||||
<section className="grid gap-4">
|
<section className="grid gap-4">
|
||||||
{motionModeNames.map((motionMode) => (
|
{motionModeNames.map((motionMode) => (
|
||||||
<div key={motionMode} className="grid gap-4">
|
<div key={motionMode} className="grid gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-semibold">{motionModeDetails[motionMode].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)]">
|
||||||
@@ -213,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 default and reduced motion modes. The control below
|
inline regression across interactive and static motion modes. The control below
|
||||||
covers the live overlay behavior.
|
covers the live overlay behavior.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -288,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 switches between the default interaction layer
|
directly. The toolbar now switches between the interactive micro-feedback
|
||||||
and the reduced-motion fallback.
|
layer and the static 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">
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ reduced-motion fallback:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- `default`
|
- `interactive`
|
||||||
- `reduced`
|
- `static`
|
||||||
|
|
||||||
### 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="morandi" data-skin="glass" data-motion="default">
|
<html data-theme="morandi" data-skin="glass" data-motion="interactive">
|
||||||
```
|
```
|
||||||
|
|
||||||
`data-skin` should be the runtime contract for component appearance.
|
`data-skin` should be the runtime contract for component appearance.
|
||||||
@@ -422,7 +422,7 @@ 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 MotionModeName = "default" | "reduced";
|
type MotionModeName = "interactive" | "static";
|
||||||
```
|
```
|
||||||
|
|
||||||
Likely helpers:
|
Likely helpers:
|
||||||
@@ -437,7 +437,7 @@ Provider shape if needed:
|
|||||||
<StyleProvider
|
<StyleProvider
|
||||||
theme="morandi"
|
theme="morandi"
|
||||||
skin="glass"
|
skin="glass"
|
||||||
motionMode="default"
|
motionMode="interactive"
|
||||||
>
|
>
|
||||||
<App />
|
<App />
|
||||||
</StyleProvider>
|
</StyleProvider>
|
||||||
@@ -610,8 +610,8 @@ As of 2026-03-20, the project is at this point:
|
|||||||
- pilot recipe extraction completed for `Button`, `Card`, `Input`, `Dialog`, `Switch`,
|
- pilot recipe extraction completed for `Button`, `Card`, `Input`, `Dialog`, `Switch`,
|
||||||
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="static"` now works for nested docs wrappers
|
||||||
- motion now uses a single `default` mode plus a `reduced` override through `data-motion`
|
- motion now uses a single `interactive` mode plus a `static` override through `data-motion`
|
||||||
- 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
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ export const themeDetails = {
|
|||||||
}
|
}
|
||||||
} as const satisfies Record<ThemeName, { label: string; note: string }>;
|
} as const satisfies Record<ThemeName, { label: string; note: string }>;
|
||||||
|
|
||||||
export const motionModeNames = ["default", "reduced"] as const;
|
export const motionModeNames = ["interactive", "static"] as const;
|
||||||
export type MotionModeName = (typeof motionModeNames)[number];
|
export type MotionModeName = (typeof motionModeNames)[number];
|
||||||
|
|
||||||
export const defaultMotionMode: MotionModeName = "default";
|
export const defaultMotionMode: MotionModeName = "interactive";
|
||||||
|
|
||||||
export const motionModeDetails = {
|
export const motionModeDetails = {
|
||||||
default: {
|
interactive: {
|
||||||
label: "Default",
|
label: "Interactive",
|
||||||
note: "Standard Cadence UI motion for hover, press, overlays, and hierarchy"
|
note: "Micro-interactions with hover lift, press feedback, focus transitions, and animated state changes"
|
||||||
},
|
},
|
||||||
reduced: {
|
static: {
|
||||||
label: "Reduced",
|
label: "Static",
|
||||||
note: "Collapse durations, distances, and animated feedback"
|
note: "Keep visual states readable while removing motion-heavy feedback and animation"
|
||||||
}
|
}
|
||||||
} as const satisfies Record<MotionModeName, { label: string; note: string }>;
|
} as const satisfies Record<MotionModeName, { label: string; note: string }>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
:root,
|
:root,
|
||||||
:root[data-motion="default"],
|
:root[data-motion="interactive"],
|
||||||
[data-motion="default"] {
|
[data-motion="interactive"] {
|
||||||
--dur-instant: 1ms;
|
--dur-instant: 1ms;
|
||||||
--dur-fast: 120ms;
|
--dur-fast: 140ms;
|
||||||
--dur-base: 200ms;
|
--dur-base: 200ms;
|
||||||
--dur-slow: 320ms;
|
--dur-slow: 280ms;
|
||||||
--dur-deliberate: 460ms;
|
--dur-deliberate: 300ms;
|
||||||
|
|
||||||
--ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
|
--ease-standard: cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
--ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
|
--ease-emphasized: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
|
--ease-exit: cubic-bezier(0.3, 1, 0.5, 1);
|
||||||
|
|
||||||
--distance-xs: 4px;
|
--distance-xs: 4px;
|
||||||
--distance-sm: 8px;
|
--distance-sm: 8px;
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
--scale-pop: 1.02;
|
--scale-pop: 1.02;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-motion="reduced"],
|
:root[data-motion="static"],
|
||||||
[data-motion="reduced"] {
|
[data-motion="static"] {
|
||||||
--dur-instant: 1ms;
|
--dur-instant: 1ms;
|
||||||
--dur-fast: 1ms;
|
--dur-fast: 1ms;
|
||||||
--dur-base: 1ms;
|
--dur-base: 1ms;
|
||||||
@@ -38,12 +38,12 @@
|
|||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-motion="reduced"] *,
|
:root[data-motion="static"] *,
|
||||||
:root[data-motion="reduced"] *::before,
|
:root[data-motion="static"] *::before,
|
||||||
:root[data-motion="reduced"] *::after,
|
:root[data-motion="static"] *::after,
|
||||||
[data-motion="reduced"] *,
|
[data-motion="static"] *,
|
||||||
[data-motion="reduced"] *::before,
|
[data-motion="static"] *::before,
|
||||||
[data-motion="reduced"] *::after {
|
[data-motion="static"] *::after {
|
||||||
animation-duration: 1ms !important;
|
animation-duration: 1ms !important;
|
||||||
animation-iteration-count: 1 !important;
|
animation-iteration-count: 1 !important;
|
||||||
scroll-behavior: auto !important;
|
scroll-behavior: auto !important;
|
||||||
@@ -113,17 +113,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.motion-pressable {
|
.motion-pressable {
|
||||||
transition-duration: var(--dur-fast);
|
transition-duration: var(--dur-base);
|
||||||
transition-property: color, background-color, border-color, box-shadow, transform;
|
transition-property: color, background-color, border-color, box-shadow, transform;
|
||||||
transition-timing-function: var(--ease-standard);
|
transition-timing-function: var(--ease-emphasized);
|
||||||
|
will-change: transform, box-shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.motion-pressable:hover {
|
@media (hover: hover) {
|
||||||
transform: translateY(calc(var(--distance-xs) * -0.25)) scale(var(--scale-hover));
|
.motion-pressable:hover {
|
||||||
|
transform: translateY(var(--ui-button-hover-translate, -1px))
|
||||||
|
scale(var(--ui-button-hover-scale, 1.02));
|
||||||
|
box-shadow: var(--ui-button-hover-shadow, var(--shadow-sm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-motion="static"] .motion-pressable:hover,
|
||||||
|
[data-motion="static"] .motion-pressable:hover,
|
||||||
|
:root[data-motion="static"] .motion-pressable:active,
|
||||||
|
[data-motion="static"] .motion-pressable:active {
|
||||||
|
box-shadow: inherit;
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.motion-pressable:active {
|
.motion-pressable:active {
|
||||||
transform: scale(var(--scale-press));
|
transform: translateY(0) scale(var(--ui-button-press-scale, var(--scale-press)));
|
||||||
|
box-shadow: var(--ui-button-active-shadow, var(--shadow-xs));
|
||||||
|
transition-duration: var(--dur-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.motion-enter-fade {
|
.motion-enter-fade {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Slot, Slottable } from "@radix-ui/react-slot";
|
import { Slot, Slottable } from "@radix-ui/react-slot";
|
||||||
import { forwardRef, useState } from "react";
|
import { forwardRef, useEffect, useState } from "react";
|
||||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||||
|
|
||||||
import { buttonVariants } from "./button.variants";
|
import { buttonVariants } from "./button.variants";
|
||||||
@@ -24,7 +24,7 @@ function Spinner() {
|
|||||||
exit={{ opacity: 0, rotate: 90, scale: 0.7 }}
|
exit={{ opacity: 0, rotate: 90, scale: 0.7 }}
|
||||||
initial={{ opacity: 0, rotate: -90, scale: 0.7 }}
|
initial={{ opacity: 0, rotate: -90, scale: 0.7 }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.18,
|
duration: 0.16,
|
||||||
ease: [0.22, 1, 0.36, 1]
|
ease: [0.22, 1, 0.36, 1]
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -48,10 +48,32 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const prefersReducedMotion = useReducedMotion();
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
const [isStaticMotion, setIsStaticMotion] = useState(false);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isDisabled = disabled || loading;
|
const isDisabled = disabled || loading;
|
||||||
const Component = asChild ? Slot : "button";
|
const Component = asChild ? Slot : "button";
|
||||||
const baseClassName = cn(buttonVariants({ loading, size, variant }), className);
|
const baseClassName = cn(buttonVariants({ loading, size, variant }), className);
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncMotionMode = () => {
|
||||||
|
setIsStaticMotion(document.documentElement.dataset.motion === "static");
|
||||||
|
};
|
||||||
|
|
||||||
|
syncMotionMode();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(syncMotionMode);
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributeFilter: ["data-motion"]
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const disableMotion = prefersReducedMotion || isStaticMotion;
|
||||||
|
|
||||||
const label = asChild ? (
|
const label = asChild ? (
|
||||||
<Slottable>{children}</Slottable>
|
<Slottable>{children}</Slottable>
|
||||||
) : (
|
) : (
|
||||||
@@ -62,7 +84,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
x: loading ? 1.5 : 0
|
x: loading ? 1.5 : 0
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: prefersReducedMotion ? 0.01 : 0.18,
|
duration: disableMotion ? 0.01 : 0.14,
|
||||||
ease: [0.22, 1, 0.36, 1]
|
ease: [0.22, 1, 0.36, 1]
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -73,7 +95,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
const sheen = !asChild ? (
|
const sheen = !asChild ? (
|
||||||
<motion.span
|
<motion.span
|
||||||
animate={
|
animate={
|
||||||
prefersReducedMotion || isDisabled
|
disableMotion || isDisabled
|
||||||
? { x: "-120%" }
|
? { x: "-120%" }
|
||||||
: isHovered
|
: isHovered
|
||||||
? { x: "115%" }
|
? { x: "115%" }
|
||||||
@@ -83,7 +105,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
className="pointer-events-none absolute inset-y-0 left-0 w-1/2 rounded-[inherit] bg-[var(--ui-button-sheen-gradient)] opacity-[var(--ui-button-sheen-opacity)] [mix-blend-mode:var(--ui-button-sheen-mix)]"
|
className="pointer-events-none absolute inset-y-0 left-0 w-1/2 rounded-[inherit] bg-[var(--ui-button-sheen-gradient)] opacity-[var(--ui-button-sheen-opacity)] [mix-blend-mode:var(--ui-button-sheen-mix)]"
|
||||||
initial={{ x: "-120%" }}
|
initial={{ x: "-120%" }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: prefersReducedMotion ? 0.01 : 0.55,
|
duration: disableMotion ? 0.01 : 0.26,
|
||||||
ease: [0.16, 1, 0.3, 1]
|
ease: [0.16, 1, 0.3, 1]
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export const comboboxTriggerVariants = cva(
|
|||||||
[
|
[
|
||||||
"inline-flex h-11 w-full items-center justify-between gap-3 rounded-[var(--ui-input-radius)] border bg-[var(--ui-input-bg)] px-4 text-left text-sm text-[var(--ui-input-fg)] shadow-[var(--ui-input-shadow)] outline-none",
|
"inline-flex h-11 w-full items-center justify-between gap-3 rounded-[var(--ui-input-radius)] border bg-[var(--ui-input-bg)] px-4 text-left text-sm text-[var(--ui-input-fg)] shadow-[var(--ui-input-shadow)] outline-none",
|
||||||
"[border-width:var(--ui-input-border-width)] border-[var(--ui-input-border)] backdrop-blur-[var(--ui-input-backdrop-blur)]",
|
"[border-width:var(--ui-input-border-width)] border-[var(--ui-input-border)] backdrop-blur-[var(--ui-input-backdrop-blur)]",
|
||||||
|
"transition-[border-color,box-shadow,background-color,transform] duration-[var(--dur-base)] ease-[var(--ease-standard)]",
|
||||||
|
"focus-visible:-translate-y-[var(--ui-input-focus-lift)] focus-visible:border-[var(--ui-input-focus-border)] focus-visible:shadow-[var(--ui-input-focus-shadow)]",
|
||||||
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
||||||
"data-[placeholder]:text-[var(--color-muted-foreground)] data-[disabled]:cursor-not-allowed data-[disabled]:bg-[var(--ui-input-disabled-bg)] data-[disabled]:text-[var(--color-muted-foreground)] data-[disabled]:opacity-100",
|
"data-[placeholder]:text-[var(--color-muted-foreground)] data-[disabled]:cursor-not-allowed data-[disabled]:bg-[var(--ui-input-disabled-bg)] data-[disabled]:text-[var(--color-muted-foreground)] data-[disabled]:opacity-100",
|
||||||
"aria-[invalid=true]:border-[color-mix(in_oklch,var(--color-destructive)_42%,var(--color-border-strong))]",
|
"aria-[invalid=true]:border-[color-mix(in_oklch,var(--color-destructive)_42%,var(--color-border-strong))]",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export const inputVariants = cva(
|
|||||||
"text-[var(--ui-input-fg)] shadow-[var(--ui-input-shadow)] outline-none",
|
"text-[var(--ui-input-fg)] shadow-[var(--ui-input-shadow)] outline-none",
|
||||||
"[border-width:var(--ui-input-border-width)] border-[var(--ui-input-border)] backdrop-blur-[var(--ui-input-backdrop-blur)]",
|
"[border-width:var(--ui-input-border-width)] border-[var(--ui-input-border)] backdrop-blur-[var(--ui-input-backdrop-blur)]",
|
||||||
"placeholder:text-[var(--color-muted-foreground)]",
|
"placeholder:text-[var(--color-muted-foreground)]",
|
||||||
|
"transition-[border-color,box-shadow,background-color,transform] duration-[var(--dur-base)] ease-[var(--ease-standard)]",
|
||||||
|
"focus-visible:-translate-y-[var(--ui-input-focus-lift)] focus-visible:border-[var(--ui-input-focus-border)] focus-visible:shadow-[var(--ui-input-focus-shadow)]",
|
||||||
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
||||||
"disabled:cursor-not-allowed disabled:bg-[var(--ui-input-disabled-bg)] disabled:text-[var(--color-muted-foreground)] disabled:opacity-100",
|
"disabled:cursor-not-allowed disabled:bg-[var(--ui-input-disabled-bg)] disabled:text-[var(--color-muted-foreground)] disabled:opacity-100",
|
||||||
"read-only:bg-[var(--ui-input-readonly-bg)] read-only:text-[var(--color-muted-foreground)]",
|
"read-only:bg-[var(--ui-input-readonly-bg)] read-only:text-[var(--color-muted-foreground)]",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export const selectTriggerVariants = cva(
|
|||||||
"inline-flex h-11 w-full items-center justify-between gap-3 rounded-[var(--ui-input-radius)] border bg-[var(--ui-input-bg)] px-4 text-left text-sm text-[var(--ui-input-fg)] shadow-[var(--ui-input-shadow)] outline-none",
|
"inline-flex h-11 w-full items-center justify-between gap-3 rounded-[var(--ui-input-radius)] border bg-[var(--ui-input-bg)] px-4 text-left text-sm text-[var(--ui-input-fg)] shadow-[var(--ui-input-shadow)] outline-none",
|
||||||
"[border-width:var(--ui-input-border-width)] border-[var(--ui-input-border)] backdrop-blur-[var(--ui-input-backdrop-blur)]",
|
"[border-width:var(--ui-input-border-width)] border-[var(--ui-input-border)] backdrop-blur-[var(--ui-input-backdrop-blur)]",
|
||||||
"placeholder:text-[var(--color-muted-foreground)]",
|
"placeholder:text-[var(--color-muted-foreground)]",
|
||||||
|
"transition-[border-color,box-shadow,background-color,transform] duration-[var(--dur-base)] ease-[var(--ease-standard)]",
|
||||||
|
"focus-visible:-translate-y-[var(--ui-input-focus-lift)] focus-visible:border-[var(--ui-input-focus-border)] focus-visible:shadow-[var(--ui-input-focus-shadow)]",
|
||||||
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
||||||
"data-[placeholder]:text-[var(--color-muted-foreground)] data-[disabled]:cursor-not-allowed data-[disabled]:bg-[var(--ui-input-disabled-bg)] data-[disabled]:text-[var(--color-muted-foreground)] data-[disabled]:opacity-100",
|
"data-[placeholder]:text-[var(--color-muted-foreground)] data-[disabled]:cursor-not-allowed data-[disabled]:bg-[var(--ui-input-disabled-bg)] data-[disabled]:text-[var(--color-muted-foreground)] data-[disabled]:opacity-100",
|
||||||
"aria-[invalid=true]:border-[color-mix(in_oklch,var(--color-destructive)_42%,var(--color-border-strong))]",
|
"aria-[invalid=true]:border-[color-mix(in_oklch,var(--color-destructive)_42%,var(--color-border-strong))]",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const switchVariants = cva(
|
|||||||
[
|
[
|
||||||
"inline-flex h-7 w-12 shrink-0 items-center rounded-[var(--ui-switch-track-radius)] border bg-[var(--ui-switch-track-bg)] shadow-[var(--ui-switch-track-shadow)] outline-none",
|
"inline-flex h-7 w-12 shrink-0 items-center rounded-[var(--ui-switch-track-radius)] border bg-[var(--ui-switch-track-bg)] shadow-[var(--ui-switch-track-shadow)] outline-none",
|
||||||
"[border-width:var(--ui-switch-track-border-width)] border-[var(--ui-switch-track-border)]",
|
"[border-width:var(--ui-switch-track-border-width)] border-[var(--ui-switch-track-border)]",
|
||||||
"transition-[background-color,box-shadow] duration-[var(--dur-fast)] ease-[var(--ease-standard)]",
|
"transition-[background-color,border-color,box-shadow] duration-[var(--ui-switch-transition-duration,var(--dur-base))] ease-[var(--ease-emphasized)]",
|
||||||
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
||||||
"data-[state=checked]:bg-[var(--ui-switch-track-checked-bg)] data-[state=checked]:border-[var(--ui-switch-track-checked-border)] data-[disabled]:cursor-not-allowed data-[disabled]:opacity-45",
|
"data-[state=checked]:bg-[var(--ui-switch-track-checked-bg)] data-[state=checked]:border-[var(--ui-switch-track-checked-border)] data-[disabled]:cursor-not-allowed data-[disabled]:opacity-45",
|
||||||
"aria-[invalid=true]:border-[color-mix(in_oklch,var(--color-destructive)_42%,var(--color-border-strong))]",
|
"aria-[invalid=true]:border-[color-mix(in_oklch,var(--color-destructive)_42%,var(--color-border-strong))]",
|
||||||
@@ -15,6 +15,6 @@ export const switchVariants = cva(
|
|||||||
|
|
||||||
export const switchThumbVariants = cva([
|
export const switchThumbVariants = cva([
|
||||||
"pointer-events-none block size-5 rounded-[var(--ui-switch-thumb-radius)] bg-[var(--ui-switch-thumb-bg)] shadow-[var(--ui-switch-thumb-shadow)]",
|
"pointer-events-none block size-5 rounded-[var(--ui-switch-thumb-radius)] bg-[var(--ui-switch-thumb-bg)] shadow-[var(--ui-switch-thumb-shadow)]",
|
||||||
"translate-x-0.5 transition-transform duration-[var(--dur-fast)] ease-[var(--ease-standard)]",
|
"translate-x-0.5 will-change-transform transition-[transform,box-shadow,background-color] duration-[var(--ui-switch-transition-duration,var(--dur-base))] ease-[var(--ease-emphasized)]",
|
||||||
"data-[state=checked]:translate-x-[1.55rem]"
|
"data-[state=checked]:translate-x-[1.55rem] data-[state=checked]:shadow-[var(--ui-switch-thumb-checked-shadow,var(--ui-switch-thumb-shadow))]"
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ export const tabsTriggerVariants = cva([
|
|||||||
"text-[var(--color-muted-foreground)] transition-[color,background-color,box-shadow,transform] duration-[var(--dur-fast)] ease-[var(--ease-standard)]",
|
"text-[var(--color-muted-foreground)] transition-[color,background-color,box-shadow,transform] duration-[var(--dur-fast)] ease-[var(--ease-standard)]",
|
||||||
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
||||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-45",
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-45",
|
||||||
"data-[state=active]:bg-[var(--ui-panel-bg)] data-[state=active]:text-[var(--color-foreground)] data-[state=active]:shadow-[var(--ui-control-shadow)]",
|
"hover:-translate-y-px hover:bg-[color-mix(in_oklch,var(--ui-control-bg)_76%,white_24%)] hover:text-[var(--color-foreground)]",
|
||||||
|
"data-[state=active]:-translate-y-px data-[state=active]:bg-[var(--ui-panel-bg)] data-[state=active]:text-[var(--color-foreground)] data-[state=active]:shadow-[var(--ui-control-shadow)]",
|
||||||
getMotionRecipeClassNames("ring")
|
getMotionRecipeClassNames("ring")
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const tabsContentVariants = cva([
|
export const tabsContentVariants = cva([
|
||||||
"mt-4 rounded-[var(--ui-card-radius)] border border-[var(--ui-card-default-border)] bg-[var(--ui-card-default-bg)] p-6 text-[var(--color-card-foreground)] shadow-[var(--ui-card-default-shadow)] outline-none [border-width:var(--ui-card-border-width)]",
|
"mt-4 rounded-[var(--ui-card-radius)] border border-[var(--ui-card-default-border)] bg-[var(--ui-card-default-bg)] p-6 text-[var(--color-card-foreground)] shadow-[var(--ui-card-default-shadow)] outline-none [border-width:var(--ui-card-border-width)]",
|
||||||
"data-[state=active]:motion-enter-rise"
|
"data-[state=active]:motion-enter-fade data-[state=active]:motion-enter-rise"
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export const textareaVariants = cva(
|
|||||||
"text-[var(--ui-input-fg)] shadow-[var(--ui-input-shadow)] outline-none",
|
"text-[var(--ui-input-fg)] shadow-[var(--ui-input-shadow)] outline-none",
|
||||||
"[border-width:var(--ui-input-border-width)] border-[var(--ui-input-border)] backdrop-blur-[var(--ui-input-backdrop-blur)]",
|
"[border-width:var(--ui-input-border-width)] border-[var(--ui-input-border)] backdrop-blur-[var(--ui-input-backdrop-blur)]",
|
||||||
"placeholder:text-[var(--color-muted-foreground)]",
|
"placeholder:text-[var(--color-muted-foreground)]",
|
||||||
|
"transition-[border-color,box-shadow,background-color,transform] duration-[var(--dur-base)] ease-[var(--ease-standard)]",
|
||||||
|
"focus-visible:-translate-y-[var(--ui-input-focus-lift)] focus-visible:border-[var(--ui-input-focus-border)] focus-visible:shadow-[var(--ui-input-focus-shadow)]",
|
||||||
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
"focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]",
|
||||||
"disabled:cursor-not-allowed disabled:bg-[var(--ui-input-disabled-bg)] disabled:text-[var(--color-muted-foreground)] disabled:opacity-100",
|
"disabled:cursor-not-allowed disabled:bg-[var(--ui-input-disabled-bg)] disabled:text-[var(--color-muted-foreground)] disabled:opacity-100",
|
||||||
"read-only:bg-[var(--ui-input-readonly-bg)] read-only:text-[var(--color-muted-foreground)]",
|
"read-only:bg-[var(--ui-input-readonly-bg)] read-only:text-[var(--color-muted-foreground)]",
|
||||||
|
|||||||
@@ -13,23 +13,23 @@ describe("motion contract", () => {
|
|||||||
expect(motionModeDetails[defaultMotionMode].label).toBeTruthy();
|
expect(motionModeDetails[defaultMotionMode].label).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets default motion mode on the document root", () => {
|
it("sets interactive motion mode on the document root", () => {
|
||||||
setMotionMode("default");
|
setMotionMode("interactive");
|
||||||
|
|
||||||
expect(document.documentElement.dataset.motion).toBe("default");
|
expect(document.documentElement.dataset.motion).toBe("interactive");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets reduced motion mode on the document root", () => {
|
it("sets static motion mode on the document root", () => {
|
||||||
setMotionMode("reduced");
|
setMotionMode("static");
|
||||||
|
|
||||||
expect(document.documentElement.dataset.motion).toBe("reduced");
|
expect(document.documentElement.dataset.motion).toBe("static");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("supports explicit reduced mode on custom roots", () => {
|
it("supports explicit static mode on custom roots", () => {
|
||||||
const target = document.createElement("div");
|
const target = document.createElement("div");
|
||||||
|
|
||||||
setMotionMode("reduced", target);
|
setMotionMode("static", target);
|
||||||
|
|
||||||
expect(target.dataset.motion).toBe("reduced");
|
expect(target.dataset.motion).toBe("static");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,6 +58,11 @@
|
|||||||
--ui-button-destructive-fg: var(--color-destructive-foreground);
|
--ui-button-destructive-fg: var(--color-destructive-foreground);
|
||||||
--ui-button-destructive-border: transparent;
|
--ui-button-destructive-border: transparent;
|
||||||
--ui-button-destructive-shadow: var(--shadow-xs);
|
--ui-button-destructive-shadow: var(--shadow-xs);
|
||||||
|
--ui-button-hover-scale: 1.02;
|
||||||
|
--ui-button-press-scale: 0.98;
|
||||||
|
--ui-button-hover-translate: -1px;
|
||||||
|
--ui-button-hover-shadow: var(--shadow-sm);
|
||||||
|
--ui-button-active-shadow: var(--shadow-xs);
|
||||||
--ui-spinner-radius: var(--radius-full);
|
--ui-spinner-radius: var(--radius-full);
|
||||||
--ui-spinner-border-width: 2px;
|
--ui-spinner-border-width: 2px;
|
||||||
|
|
||||||
@@ -83,6 +88,11 @@
|
|||||||
--ui-input-border: var(--color-input);
|
--ui-input-border: var(--color-input);
|
||||||
--ui-input-fg: var(--color-foreground);
|
--ui-input-fg: var(--color-foreground);
|
||||||
--ui-input-shadow: var(--shadow-xs);
|
--ui-input-shadow: var(--shadow-xs);
|
||||||
|
--ui-input-focus-border: color-mix(in oklch, var(--color-primary) 32%, var(--color-input));
|
||||||
|
--ui-input-focus-shadow:
|
||||||
|
0 0 0 1px color-mix(in oklch, var(--color-primary) 18%, transparent),
|
||||||
|
var(--shadow-sm);
|
||||||
|
--ui-input-focus-lift: -1px;
|
||||||
--ui-input-disabled-bg: var(--color-surface);
|
--ui-input-disabled-bg: var(--color-surface);
|
||||||
--ui-input-readonly-bg: var(--color-surface);
|
--ui-input-readonly-bg: var(--color-surface);
|
||||||
--ui-input-backdrop-blur: 0px;
|
--ui-input-backdrop-blur: 0px;
|
||||||
@@ -106,6 +116,8 @@
|
|||||||
--ui-switch-thumb-radius: var(--radius-full);
|
--ui-switch-thumb-radius: var(--radius-full);
|
||||||
--ui-switch-thumb-bg: white;
|
--ui-switch-thumb-bg: white;
|
||||||
--ui-switch-thumb-shadow: var(--shadow-xs);
|
--ui-switch-thumb-shadow: var(--shadow-xs);
|
||||||
|
--ui-switch-thumb-checked-shadow: var(--shadow-sm);
|
||||||
|
--ui-switch-transition-duration: var(--dur-base);
|
||||||
|
|
||||||
--ui-skeleton-radius: var(--radius-sm);
|
--ui-skeleton-radius: var(--radius-sm);
|
||||||
--ui-skeleton-block-radius: var(--radius-md);
|
--ui-skeleton-block-radius: var(--radius-md);
|
||||||
@@ -191,6 +203,11 @@
|
|||||||
--ui-button-destructive-fg: var(--color-destructive-foreground);
|
--ui-button-destructive-fg: var(--color-destructive-foreground);
|
||||||
--ui-button-destructive-border: color-mix(in oklch, white 28%, var(--color-destructive));
|
--ui-button-destructive-border: color-mix(in oklch, white 28%, var(--color-destructive));
|
||||||
--ui-button-destructive-shadow: 0 16px 34px oklch(0.32 0.07 18 / 0.18);
|
--ui-button-destructive-shadow: 0 16px 34px oklch(0.32 0.07 18 / 0.18);
|
||||||
|
--ui-button-hover-scale: 1.02;
|
||||||
|
--ui-button-press-scale: 0.985;
|
||||||
|
--ui-button-hover-translate: -2px;
|
||||||
|
--ui-button-hover-shadow: 0 20px 42px oklch(0.2 0.04 250 / 0.18);
|
||||||
|
--ui-button-active-shadow: 0 10px 24px oklch(0.2 0.04 250 / 0.14);
|
||||||
--ui-spinner-radius: var(--radius-full);
|
--ui-spinner-radius: var(--radius-full);
|
||||||
--ui-spinner-border-width: 2px;
|
--ui-spinner-border-width: 2px;
|
||||||
|
|
||||||
@@ -216,6 +233,11 @@
|
|||||||
--ui-input-border: color-mix(in oklch, white 34%, var(--color-border));
|
--ui-input-border: color-mix(in oklch, white 34%, var(--color-border));
|
||||||
--ui-input-fg: var(--color-foreground);
|
--ui-input-fg: var(--color-foreground);
|
||||||
--ui-input-shadow: 0 14px 34px oklch(0.2 0.03 255 / 0.12);
|
--ui-input-shadow: 0 14px 34px oklch(0.2 0.03 255 / 0.12);
|
||||||
|
--ui-input-focus-border: color-mix(in oklch, white 44%, var(--color-primary));
|
||||||
|
--ui-input-focus-shadow:
|
||||||
|
0 0 0 1px color-mix(in oklch, white 22%, var(--color-primary)),
|
||||||
|
0 18px 40px oklch(0.2 0.03 255 / 0.18);
|
||||||
|
--ui-input-focus-lift: -1px;
|
||||||
--ui-input-disabled-bg: color-mix(in oklch, var(--color-surface) 72%, transparent);
|
--ui-input-disabled-bg: color-mix(in oklch, var(--color-surface) 72%, transparent);
|
||||||
--ui-input-readonly-bg: color-mix(in oklch, var(--color-surface) 68%, transparent);
|
--ui-input-readonly-bg: color-mix(in oklch, var(--color-surface) 68%, transparent);
|
||||||
--ui-input-backdrop-blur: 12px;
|
--ui-input-backdrop-blur: 12px;
|
||||||
@@ -239,6 +261,8 @@
|
|||||||
--ui-switch-thumb-radius: var(--radius-full);
|
--ui-switch-thumb-radius: var(--radius-full);
|
||||||
--ui-switch-thumb-bg: color-mix(in oklch, white 84%, var(--color-card));
|
--ui-switch-thumb-bg: color-mix(in oklch, white 84%, var(--color-card));
|
||||||
--ui-switch-thumb-shadow: 0 8px 18px oklch(0.16 0.02 255 / 0.22);
|
--ui-switch-thumb-shadow: 0 8px 18px oklch(0.16 0.02 255 / 0.22);
|
||||||
|
--ui-switch-thumb-checked-shadow: 0 12px 24px oklch(0.18 0.03 255 / 0.28);
|
||||||
|
--ui-switch-transition-duration: var(--dur-base);
|
||||||
|
|
||||||
--ui-skeleton-radius: var(--radius-md);
|
--ui-skeleton-radius: var(--radius-md);
|
||||||
--ui-skeleton-block-radius: var(--radius-lg);
|
--ui-skeleton-block-radius: var(--radius-lg);
|
||||||
@@ -319,6 +343,11 @@
|
|||||||
--ui-button-destructive-fg: var(--color-destructive-foreground);
|
--ui-button-destructive-fg: var(--color-destructive-foreground);
|
||||||
--ui-button-destructive-border: var(--color-foreground);
|
--ui-button-destructive-border: var(--color-foreground);
|
||||||
--ui-button-destructive-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 34%, transparent);
|
--ui-button-destructive-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 34%, transparent);
|
||||||
|
--ui-button-hover-scale: 1;
|
||||||
|
--ui-button-press-scale: 0.98;
|
||||||
|
--ui-button-hover-translate: -1px;
|
||||||
|
--ui-button-hover-shadow: 5px 5px 0 color-mix(in oklch, var(--color-foreground) 36%, transparent);
|
||||||
|
--ui-button-active-shadow: 1px 1px 0 color-mix(in oklch, var(--color-foreground) 28%, transparent);
|
||||||
--ui-spinner-radius: 0px;
|
--ui-spinner-radius: 0px;
|
||||||
--ui-spinner-border-width: 2px;
|
--ui-spinner-border-width: 2px;
|
||||||
|
|
||||||
@@ -344,6 +373,11 @@
|
|||||||
--ui-input-border: var(--color-foreground);
|
--ui-input-border: var(--color-foreground);
|
||||||
--ui-input-fg: var(--color-foreground);
|
--ui-input-fg: var(--color-foreground);
|
||||||
--ui-input-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 28%, transparent);
|
--ui-input-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 28%, transparent);
|
||||||
|
--ui-input-focus-border: var(--color-primary);
|
||||||
|
--ui-input-focus-shadow:
|
||||||
|
0 0 0 2px color-mix(in oklch, var(--color-primary) 42%, transparent),
|
||||||
|
4px 4px 0 color-mix(in oklch, var(--color-foreground) 32%, transparent);
|
||||||
|
--ui-input-focus-lift: 0px;
|
||||||
--ui-input-disabled-bg: var(--color-surface);
|
--ui-input-disabled-bg: var(--color-surface);
|
||||||
--ui-input-readonly-bg: var(--color-surface);
|
--ui-input-readonly-bg: var(--color-surface);
|
||||||
--ui-input-backdrop-blur: 0px;
|
--ui-input-backdrop-blur: 0px;
|
||||||
@@ -367,6 +401,8 @@
|
|||||||
--ui-switch-thumb-radius: 0px;
|
--ui-switch-thumb-radius: 0px;
|
||||||
--ui-switch-thumb-bg: var(--color-background);
|
--ui-switch-thumb-bg: var(--color-background);
|
||||||
--ui-switch-thumb-shadow: 2px 2px 0 color-mix(in oklch, var(--color-foreground) 24%, transparent);
|
--ui-switch-thumb-shadow: 2px 2px 0 color-mix(in oklch, var(--color-foreground) 24%, transparent);
|
||||||
|
--ui-switch-thumb-checked-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 28%, transparent);
|
||||||
|
--ui-switch-transition-duration: var(--dur-fast);
|
||||||
|
|
||||||
--ui-skeleton-radius: 0px;
|
--ui-skeleton-radius: 0px;
|
||||||
--ui-skeleton-block-radius: 0px;
|
--ui-skeleton-block-radius: 0px;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
test("storybook button, select, and reduced-motion form stories stay interactive", async ({
|
test("storybook button, select, and static-motion form stories stay interactive", async ({
|
||||||
page
|
page
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|||||||
Reference in New Issue
Block a user