feat(motion): add interactive micro-feedback

This commit is contained in:
2026-03-20 17:44:20 +08:00
parent 142f4a399a
commit 36822f05e0
17 changed files with 144 additions and 62 deletions
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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}
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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">
+7 -7
View File
@@ -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
+8 -8
View File
@@ -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 }>;
+36 -21
View File
@@ -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 {
+27 -5
View File
@@ -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))]"
]);
+3 -2
View File
@@ -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)]",
+9 -9
View File
@@ -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");
});
});
+36
View File
@@ -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 -1
View File
@@ -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("/");