feat(motion): add interactive micro-feedback
This commit is contained in:
@@ -34,7 +34,7 @@ const preview: Preview = {
|
||||
motionMode: {
|
||||
description: "Preview motion mode",
|
||||
toolbar: {
|
||||
icon: "transfer",
|
||||
icon: "contrast",
|
||||
dynamicTitle: true,
|
||||
items: motionModeNames.map((modeName) => ({
|
||||
value: modeName,
|
||||
|
||||
@@ -187,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, and motion mode together"
|
||||
"Storybook globals that apply theme, skin, and interactive/static motion mode together"
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
|
||||
@@ -187,7 +187,7 @@ function StyleMatrixShowcase() {
|
||||
|
||||
<section className="grid gap-4">
|
||||
{motionModeNames.map((motionMode) => (
|
||||
<div key={motionMode} className="grid gap-4">
|
||||
<div key={motionMode} className="grid gap-4">
|
||||
<div>
|
||||
<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)]">
|
||||
@@ -213,7 +213,7 @@ 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 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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -288,8 +288,8 @@ function TokensOverview({
|
||||
<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 now switches between the default interaction layer
|
||||
and the reduced-motion fallback.
|
||||
directly. The toolbar now switches between the interactive micro-feedback
|
||||
layer and the static fallback.
|
||||
</p>
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -175,8 +175,8 @@ reduced-motion fallback:
|
||||
|
||||
Examples:
|
||||
|
||||
- `default`
|
||||
- `reduced`
|
||||
- `interactive`
|
||||
- `static`
|
||||
|
||||
### Layout Pattern
|
||||
|
||||
@@ -218,7 +218,7 @@ The existing token groups remain the baseline:
|
||||
Add a new root attribute:
|
||||
|
||||
```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.
|
||||
@@ -422,7 +422,7 @@ Minimum contract:
|
||||
```ts
|
||||
type ThemeName = "morandi" | "earth" | "brand";
|
||||
type SkinName = "minimal" | "glass" | "pixel";
|
||||
type MotionModeName = "default" | "reduced";
|
||||
type MotionModeName = "interactive" | "static";
|
||||
```
|
||||
|
||||
Likely helpers:
|
||||
@@ -437,7 +437,7 @@ Provider shape if needed:
|
||||
<StyleProvider
|
||||
theme="morandi"
|
||||
skin="glass"
|
||||
motionMode="default"
|
||||
motionMode="interactive"
|
||||
>
|
||||
<App />
|
||||
</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`,
|
||||
and `Skeleton`
|
||||
- screenshot-friendly validation surface added in `Foundation/Style Matrix`
|
||||
- scoped `data-motion="reduced"` now works for nested docs wrappers
|
||||
- motion now uses a single `default` mode plus a `reduced` override through `data-motion`
|
||||
- scoped `data-motion="static"` now works for nested docs wrappers
|
||||
- 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,
|
||||
including controls, menus, overlays, feedback, and data-heavy patterns
|
||||
- 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 }>;
|
||||
|
||||
export const motionModeNames = ["default", "reduced"] as const;
|
||||
export const motionModeNames = ["interactive", "static"] as const;
|
||||
export type MotionModeName = (typeof motionModeNames)[number];
|
||||
|
||||
export const defaultMotionMode: MotionModeName = "default";
|
||||
export const defaultMotionMode: MotionModeName = "interactive";
|
||||
|
||||
export const motionModeDetails = {
|
||||
default: {
|
||||
label: "Default",
|
||||
note: "Standard Cadence UI motion for hover, press, overlays, and hierarchy"
|
||||
interactive: {
|
||||
label: "Interactive",
|
||||
note: "Micro-interactions with hover lift, press feedback, focus transitions, and animated state changes"
|
||||
},
|
||||
reduced: {
|
||||
label: "Reduced",
|
||||
note: "Collapse durations, distances, and animated feedback"
|
||||
static: {
|
||||
label: "Static",
|
||||
note: "Keep visual states readable while removing motion-heavy feedback and animation"
|
||||
}
|
||||
} as const satisfies Record<MotionModeName, { label: string; note: string }>;
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
:root,
|
||||
:root[data-motion="default"],
|
||||
[data-motion="default"] {
|
||||
:root[data-motion="interactive"],
|
||||
[data-motion="interactive"] {
|
||||
--dur-instant: 1ms;
|
||||
--dur-fast: 120ms;
|
||||
--dur-fast: 140ms;
|
||||
--dur-base: 200ms;
|
||||
--dur-slow: 320ms;
|
||||
--dur-deliberate: 460ms;
|
||||
--dur-slow: 280ms;
|
||||
--dur-deliberate: 300ms;
|
||||
|
||||
--ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-standard: cubic-bezier(0.25, 1, 0.5, 1);
|
||||
--ease-emphasized: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--ease-exit: cubic-bezier(0.3, 1, 0.5, 1);
|
||||
|
||||
--distance-xs: 4px;
|
||||
--distance-sm: 8px;
|
||||
@@ -21,8 +21,8 @@
|
||||
--scale-pop: 1.02;
|
||||
}
|
||||
|
||||
:root[data-motion="reduced"],
|
||||
[data-motion="reduced"] {
|
||||
:root[data-motion="static"],
|
||||
[data-motion="static"] {
|
||||
--dur-instant: 1ms;
|
||||
--dur-fast: 1ms;
|
||||
--dur-base: 1ms;
|
||||
@@ -38,12 +38,12 @@
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
:root[data-motion="reduced"] *,
|
||||
:root[data-motion="reduced"] *::before,
|
||||
:root[data-motion="reduced"] *::after,
|
||||
[data-motion="reduced"] *,
|
||||
[data-motion="reduced"] *::before,
|
||||
[data-motion="reduced"] *::after {
|
||||
:root[data-motion="static"] *,
|
||||
:root[data-motion="static"] *::before,
|
||||
:root[data-motion="static"] *::after,
|
||||
[data-motion="static"] *,
|
||||
[data-motion="static"] *::before,
|
||||
[data-motion="static"] *::after {
|
||||
animation-duration: 1ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
@@ -113,17 +113,32 @@
|
||||
}
|
||||
|
||||
.motion-pressable {
|
||||
transition-duration: var(--dur-fast);
|
||||
transition-duration: var(--dur-base);
|
||||
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 {
|
||||
transform: translateY(calc(var(--distance-xs) * -0.25)) scale(var(--scale-hover));
|
||||
@media (hover: 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 {
|
||||
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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { buttonVariants } from "./button.variants";
|
||||
@@ -24,7 +24,7 @@ function Spinner() {
|
||||
exit={{ opacity: 0, rotate: 90, scale: 0.7 }}
|
||||
initial={{ opacity: 0, rotate: -90, scale: 0.7 }}
|
||||
transition={{
|
||||
duration: 0.18,
|
||||
duration: 0.16,
|
||||
ease: [0.22, 1, 0.36, 1]
|
||||
}}
|
||||
/>
|
||||
@@ -48,10 +48,32 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
ref
|
||||
) {
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
const [isStaticMotion, setIsStaticMotion] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isDisabled = disabled || loading;
|
||||
const Component = asChild ? Slot : "button";
|
||||
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 ? (
|
||||
<Slottable>{children}</Slottable>
|
||||
) : (
|
||||
@@ -62,7 +84,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
x: loading ? 1.5 : 0
|
||||
}}
|
||||
transition={{
|
||||
duration: prefersReducedMotion ? 0.01 : 0.18,
|
||||
duration: disableMotion ? 0.01 : 0.14,
|
||||
ease: [0.22, 1, 0.36, 1]
|
||||
}}
|
||||
>
|
||||
@@ -73,7 +95,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
const sheen = !asChild ? (
|
||||
<motion.span
|
||||
animate={
|
||||
prefersReducedMotion || isDisabled
|
||||
disableMotion || isDisabled
|
||||
? { x: "-120%" }
|
||||
: isHovered
|
||||
? { 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)]"
|
||||
initial={{ x: "-120%" }}
|
||||
transition={{
|
||||
duration: prefersReducedMotion ? 0.01 : 0.55,
|
||||
duration: disableMotion ? 0.01 : 0.26,
|
||||
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",
|
||||
"[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)]",
|
||||
"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))]",
|
||||
|
||||
@@ -7,6 +7,8 @@ export const inputVariants = cva(
|
||||
"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)]",
|
||||
"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)]",
|
||||
"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)]",
|
||||
|
||||
@@ -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",
|
||||
"[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)]",
|
||||
"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)]",
|
||||
"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))]",
|
||||
|
||||
@@ -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",
|
||||
"[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)]",
|
||||
"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))]",
|
||||
@@ -15,6 +15,6 @@ export const switchVariants = 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)]",
|
||||
"translate-x-0.5 transition-transform duration-[var(--dur-fast)] ease-[var(--ease-standard)]",
|
||||
"data-[state=checked]:translate-x-[1.55rem]"
|
||||
"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]: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)]",
|
||||
"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-[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")
|
||||
]);
|
||||
|
||||
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)]",
|
||||
"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",
|
||||
"[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)]",
|
||||
"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)]",
|
||||
"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)]",
|
||||
|
||||
@@ -13,23 +13,23 @@ describe("motion contract", () => {
|
||||
expect(motionModeDetails[defaultMotionMode].label).toBeTruthy();
|
||||
});
|
||||
|
||||
it("sets default motion mode on the document root", () => {
|
||||
setMotionMode("default");
|
||||
it("sets interactive motion mode on the document root", () => {
|
||||
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", () => {
|
||||
setMotionMode("reduced");
|
||||
it("sets static motion mode on the document root", () => {
|
||||
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");
|
||||
|
||||
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-border: transparent;
|
||||
--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-border-width: 2px;
|
||||
|
||||
@@ -83,6 +88,11 @@
|
||||
--ui-input-border: var(--color-input);
|
||||
--ui-input-fg: var(--color-foreground);
|
||||
--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-readonly-bg: var(--color-surface);
|
||||
--ui-input-backdrop-blur: 0px;
|
||||
@@ -106,6 +116,8 @@
|
||||
--ui-switch-thumb-radius: var(--radius-full);
|
||||
--ui-switch-thumb-bg: white;
|
||||
--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-block-radius: var(--radius-md);
|
||||
@@ -191,6 +203,11 @@
|
||||
--ui-button-destructive-fg: var(--color-destructive-foreground);
|
||||
--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-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-border-width: 2px;
|
||||
|
||||
@@ -216,6 +233,11 @@
|
||||
--ui-input-border: color-mix(in oklch, white 34%, var(--color-border));
|
||||
--ui-input-fg: var(--color-foreground);
|
||||
--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-readonly-bg: color-mix(in oklch, var(--color-surface) 68%, transparent);
|
||||
--ui-input-backdrop-blur: 12px;
|
||||
@@ -239,6 +261,8 @@
|
||||
--ui-switch-thumb-radius: var(--radius-full);
|
||||
--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-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-block-radius: var(--radius-lg);
|
||||
@@ -319,6 +343,11 @@
|
||||
--ui-button-destructive-fg: var(--color-destructive-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-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-border-width: 2px;
|
||||
|
||||
@@ -344,6 +373,11 @@
|
||||
--ui-input-border: 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-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-readonly-bg: var(--color-surface);
|
||||
--ui-input-backdrop-blur: 0px;
|
||||
@@ -367,6 +401,8 @@
|
||||
--ui-switch-thumb-radius: 0px;
|
||||
--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-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-block-radius: 0px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
Reference in New Issue
Block a user