feat(motion): add interactive micro-feedback
This commit is contained in:
@@ -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)]",
|
||||
|
||||
Reference in New Issue
Block a user