Spaces:
Running
Running
| import { ButtonHTMLAttributes, forwardRef } from "react"; | |
| import { cn } from "@/utils/cn"; | |
| import "./button.css"; | |
| interface Props extends ButtonHTMLAttributes<HTMLButtonElement> { | |
| variant?: "primary" | "secondary" | "tertiary" | "playground" | "destructive"; | |
| size?: "default" | "large"; | |
| disabled?: boolean; | |
| loadingLabel?: string; | |
| isLoading?: boolean; | |
| } | |
| const Button = forwardRef<HTMLButtonElement, Props>( | |
| ( | |
| { | |
| variant = "primary", | |
| size = "default", | |
| disabled, | |
| isLoading = false, | |
| loadingLabel = "Loading…", | |
| ...attrs | |
| }, | |
| ref, | |
| ) => { | |
| const isNonInteractive = Boolean(disabled || isLoading); | |
| // Focus ring adapts to light/dark variants | |
| const focusRing = | |
| variant === "primary" || variant === "destructive" | |
| ? "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white" | |
| : "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-black"; | |
| return ( | |
| <button | |
| {...attrs} | |
| ref={ref} | |
| type={attrs.type ?? "button"} | |
| aria-disabled={isNonInteractive || undefined} | |
| aria-busy={isLoading || undefined} | |
| aria-live={isLoading ? "polite" : undefined} | |
| data-state={ | |
| isLoading ? "loading" : isNonInteractive ? "disabled" : "idle" | |
| } | |
| className={cn( | |
| attrs.className, | |
| "flex items-center justify-center button relative [&>*]:relative", | |
| "text-label-medium lg-max:[&_svg]:size-24", | |
| `button-${variant} group/button`, | |
| focusRing, | |
| // Shared non-interactive styles | |
| "disabled:cursor-not-allowed", | |
| isNonInteractive && "cursor-not-allowed", | |
| // Size | |
| size === "default" && "rounded-8 px-10 py-6 gap-4", | |
| size === "large" && "rounded-10 px-12 py-8 gap-6", | |
| // Variant + interactive nuances | |
| variant === "primary" && [ | |
| "text-accent-white", | |
| // Hover/active only when interactive | |
| !isNonInteractive && | |
| "hover:bg-[color:var(--heat-90)] active:[scale:0.995]", | |
| // Disabled: dim a bit, no hover, dim overlay bg layer if present | |
| "disabled:opacity-80", | |
| "disabled:[&_.button-background]:opacity-70", | |
| ], | |
| ["secondary", "tertiary", "playground"].includes(variant) && [ | |
| "text-accent-black", | |
| !isNonInteractive && "active:[scale:0.99] active:bg-black-alpha-7", | |
| ], | |
| variant === "secondary" && [ | |
| "bg-black-alpha-4", | |
| !isNonInteractive && "hover:bg-black-alpha-6", | |
| // Disabled: lighter fill + muted text, no hover | |
| "disabled:bg-black-alpha-3", | |
| "disabled:text-black-alpha-48", | |
| "disabled:hover:bg-black-alpha-3", | |
| ], | |
| variant === "tertiary" && [ | |
| !isNonInteractive && "hover:bg-black-alpha-4", | |
| // Disabled: no hover background, text muted | |
| "disabled:text-black-alpha-48", | |
| "disabled:hover:bg-transparent", | |
| ], | |
| variant === "destructive" && [ | |
| "bg-red-600 text-accent-white", | |
| !isNonInteractive && "hover:bg-red-700 active:scale-[0.98]", | |
| // Disabled: keep red but softer; soften text slightly | |
| "disabled:bg-red-600/70", | |
| "disabled:text-white-alpha-72", | |
| "disabled:hover:bg-red-600/70", | |
| ], | |
| variant === "playground" && [ | |
| "inside-border before:border-black-alpha-4", | |
| isNonInteractive | |
| ? "before:opacity-0 bg-black-alpha-4 text-black-alpha-24" | |
| : "hover:bg-black-alpha-4 hover:before:opacity-0 active:before:opacity-0", | |
| ], | |
| )} | |
| disabled={isNonInteractive} | |
| > | |
| {variant === "primary" && ( | |
| <div className="overlay button-background !absolute" /> | |
| )} | |
| {/* loading state (spinner) */} | |
| {isLoading && ( | |
| <div | |
| className={cn( | |
| "w-16 h-16 border-2 rounded-full animate-spin", | |
| variant === "primary" || variant === "destructive" | |
| ? "border-white/30 border-t-white" | |
| : "border-black/30 border-t-black", | |
| )} | |
| aria-hidden | |
| /> | |
| )} | |
| {/* Screen reader-only loading label */} | |
| {isLoading && <span className="sr-only">{loadingLabel}</span>} | |
| {attrs.children} | |
| </button> | |
| ); | |
| }, | |
| ); | |
| Button.displayName = "Button"; | |
| export default Button; | |