import { ComponentProps, RefObject, useImperativeHandle, useRef } from "react"; import clsx from "clsx"; import { Loader, RoundedRectLoader } from "../loader"; import { useRipple } from "../../hooks"; import { Slot } from "../slot"; import { cva } from "class-variance-authority"; import { ThemeColor } from "../../types"; function loaderColor(color: ThemeColor): ThemeColor { switch (color) { case "yellow-alpha": return "yellow"; case "gray-alpha": return "gray"; default: return color; } } export interface ButtonProps extends Omit, "color"> { withRipple?: boolean; variant?: "contained" | "light" | "outlined" | "text" | "ghost"; size?: "small" | "medium" | "large" | "custom" | "input" | "scalable"; color?: ThemeColor; width?: "fit" | "full" | "custom"; loading?: boolean; disabled?: boolean; focusable?: boolean; /** * For a selected item inside a group. */ selected?: boolean; icon?: boolean; asChild?: boolean; ref?: RefObject; } export const buttonVariants = cva( "relative box-border inline-flex items-center leading-5 no-underline focus-visible-has:outline-2", { variants: { clickable: { true: "cursor-pointer active:translate-y-px focus-visible-has:z-10", false: "pointer-events-none", }, size: { small: "[--h-button:1.5rem] h-(--h-button) truncate", medium: "[--h-button:2rem] h-(--h-button) truncate", large: "[--h-button:3rem] h-(--h-button) truncate", input: "[--h-button:calc(var(--h-input)-2px)] h-(--h-button) truncate", custom: "truncate", scalable: "", }, icon: { true: "rounded-[calc(var(--h-button)/2)] min-w-(--h-button) [&_i,&_img,&_svg]:w-(--h-button) justify-center [&_:last-child:not(i,img,svg,strong)]:pr-4", false: "rounded-2xl px-4", custom: "", }, width: { fit: "w-full max-w-fit", full: "w-full justify-center", custom: "", }, variant: { /** * src/tailwind/plugin.ts * addVariant("current", [ * `&[aria-checked="true"]`, * `&[aria-current="page"]`, * `&:has(:checked)` * ]) */ contained: "shadow-lg hover:shadow-xl active:shadow-xl-active outline-offset-0 text-custom-text bg-custom-3 hover:bg-custom-4 current:bg-custom-4 focus-visible-has:outline-custom-5", light: "shadow-lg hover:shadow-xl has-checked:shadow-xl focus:shadow-xl active:shadow-xl-active outline-offset-0 bg-custom-1 text-gray-text hover:text-custom-text hover:bg-custom-3 current:bg-custom-3 focus-visible-has:outline-custom-4", outlined: "bg-custom-0 hover:shadow-sm active:shadow-sm-active text-gray-7 outline-offset-0 border hover:bg-custom-1 border-custom-border focus-visible-has:outline-custom-5 current:bg-custom-3 focus-visible-has:border-transparent", text: "bg-transparent hover:shadow-sm active:shadow-sm-active outline-offset-0 hover:bg-custom-1 dark:hover:bg-custom-1/50 text-gray-7 hover:text-gray-8 current:bg-custom-3 focus-visible-has:outline-custom-5", ghost: "outline-offset-0 current:bg-custom-1 focus-visible-has:outline-custom-5", }, }, defaultVariants: { size: "medium", icon: false, variant: "contained", clickable: true, width: "fit", }, }, ); export function Button({ withRipple = true, variant = "contained", loading = false, color = "yellow", size = "medium", focusable = true, className, disabled = false, children, selected = false, icon = false, asChild = false, width = "fit", ref, ...props }: ButtonProps) { const inputRef = useRef(null!); useImperativeHandle( ref, () => inputRef.current, ); const rippleEnabled = !loading && !disabled && withRipple; useRipple( inputRef, ["text", "outlined", "ghost"].includes(variant) ? true : false, rippleEnabled, ); const Comp = asChild ? Slot : "button"; return ( {asChild ? ( children ) : ( <> {/* {!notClickable && withRipple && ripples} */} {children} {loading && (icon ? ( ) : ( ))} )} ); }