From 36822f05e04a9997e1d4dc809b0aed50b8ccf5a4 Mon Sep 17 00:00:00 2001 From: kurihada Date: Fri, 20 Mar 2026 17:44:20 +0800 Subject: [PATCH] feat(motion): add interactive micro-feedback --- apps/docs/.storybook/preview.ts | 2 +- apps/docs/src/style-contract.stories.tsx | 2 +- apps/docs/src/style-matrix.stories.tsx | 4 +- apps/docs/src/tokens.stories.tsx | 4 +- docs/rfcs/multi-style-architecture.md | 14 ++--- packages/tokens/src/index.ts | 16 +++--- packages/tokens/src/motion.css | 57 ++++++++++++------- packages/ui/src/components/button.tsx | 32 +++++++++-- .../ui/src/components/combobox.variants.ts | 2 + packages/ui/src/components/input.variants.ts | 2 + packages/ui/src/components/select.variants.ts | 2 + packages/ui/src/components/switch.variants.ts | 6 +- packages/ui/src/components/tabs.variants.ts | 5 +- .../ui/src/components/textarea.variants.ts | 2 + packages/ui/src/lib/motion-contract.test.ts | 18 +++--- packages/ui/src/skins.css | 36 ++++++++++++ tests/e2e/storybook-smoke.spec.ts | 2 +- 17 files changed, 144 insertions(+), 62 deletions(-) diff --git a/apps/docs/.storybook/preview.ts b/apps/docs/.storybook/preview.ts index 02a0211..8a49302 100644 --- a/apps/docs/.storybook/preview.ts +++ b/apps/docs/.storybook/preview.ts @@ -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, diff --git a/apps/docs/src/style-contract.stories.tsx b/apps/docs/src/style-contract.stories.tsx index d4fb698..63cecb1 100644 --- a/apps/docs/src/style-contract.stories.tsx +++ b/apps/docs/src/style-contract.stories.tsx @@ -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) => (
{motionModeNames.map((motionMode) => ( -
+

{motionModeDetails[motionMode].label}

@@ -213,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 default and reduced motion modes. The control below + inline regression across interactive and static 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 e62d997..cae9012 100644 --- a/apps/docs/src/tokens.stories.tsx +++ b/apps/docs/src/tokens.stories.tsx @@ -288,8 +288,8 @@ function TokensOverview({

Motion tokens

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.

diff --git a/docs/rfcs/multi-style-architecture.md b/docs/rfcs/multi-style-architecture.md index 35fd425..bc4ac1a 100644 --- a/docs/rfcs/multi-style-architecture.md +++ b/docs/rfcs/multi-style-architecture.md @@ -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 - + ``` `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: @@ -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 diff --git a/packages/tokens/src/index.ts b/packages/tokens/src/index.ts index 1667562..4355de1 100644 --- a/packages/tokens/src/index.ts +++ b/packages/tokens/src/index.ts @@ -18,19 +18,19 @@ export const themeDetails = { } } as const satisfies Record; -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; diff --git a/packages/tokens/src/motion.css b/packages/tokens/src/motion.css index 79f4fba..375ee5a 100644 --- a/packages/tokens/src/motion.css +++ b/packages/tokens/src/motion.css @@ -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 { diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index 7d2f071..271fcf8 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -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(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 ? ( {children} ) : ( @@ -62,7 +84,7 @@ export const Button = forwardRef(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(function Button const sheen = !asChild ? ( (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] }} /> diff --git a/packages/ui/src/components/combobox.variants.ts b/packages/ui/src/components/combobox.variants.ts index db2df25..b3a2d6d 100644 --- a/packages/ui/src/components/combobox.variants.ts +++ b/packages/ui/src/components/combobox.variants.ts @@ -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))]", diff --git a/packages/ui/src/components/input.variants.ts b/packages/ui/src/components/input.variants.ts index f079711..4db0fe1 100644 --- a/packages/ui/src/components/input.variants.ts +++ b/packages/ui/src/components/input.variants.ts @@ -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)]", diff --git a/packages/ui/src/components/select.variants.ts b/packages/ui/src/components/select.variants.ts index a39ff38..2c64917 100644 --- a/packages/ui/src/components/select.variants.ts +++ b/packages/ui/src/components/select.variants.ts @@ -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))]", diff --git a/packages/ui/src/components/switch.variants.ts b/packages/ui/src/components/switch.variants.ts index e1de98e..7062805 100644 --- a/packages/ui/src/components/switch.variants.ts +++ b/packages/ui/src/components/switch.variants.ts @@ -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))]" ]); diff --git a/packages/ui/src/components/tabs.variants.ts b/packages/ui/src/components/tabs.variants.ts index 3ebb2b0..13b4baf 100644 --- a/packages/ui/src/components/tabs.variants.ts +++ b/packages/ui/src/components/tabs.variants.ts @@ -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" ]); diff --git a/packages/ui/src/components/textarea.variants.ts b/packages/ui/src/components/textarea.variants.ts index 19979c7..d796853 100644 --- a/packages/ui/src/components/textarea.variants.ts +++ b/packages/ui/src/components/textarea.variants.ts @@ -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)]", diff --git a/packages/ui/src/lib/motion-contract.test.ts b/packages/ui/src/lib/motion-contract.test.ts index 641909f..304477c 100644 --- a/packages/ui/src/lib/motion-contract.test.ts +++ b/packages/ui/src/lib/motion-contract.test.ts @@ -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"); }); }); diff --git a/packages/ui/src/skins.css b/packages/ui/src/skins.css index 440eca9..53b8e8e 100644 --- a/packages/ui/src/skins.css +++ b/packages/ui/src/skins.css @@ -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; diff --git a/tests/e2e/storybook-smoke.spec.ts b/tests/e2e/storybook-smoke.spec.ts index 3271438..962fdcf 100644 --- a/tests/e2e/storybook-smoke.spec.ts +++ b/tests/e2e/storybook-smoke.spec.ts @@ -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("/");