import React, { ComponentPropsWithoutRef, forwardRef, isValidElement, } from "react" import { tv } from "tailwind-variants" import { type VariantProps } from "tailwind-variants" import type { IconComponent, IconProps } from "../../icons/types" import { backgroundVariants, BorderColorVariantProps, borderVariants, disabledVariants, } from "../../styles" import { buttonLayoutVariants } from "../../styles/utils/buttonLayout" import { classNames } from "../../utils" import { Flex } from "../Flex" import { Link, LinkProps } from "../Link" import { Slot } from "../Slot" import { Spinner } from "../Spinner" export const buttonVariants = tv({ extend: buttonLayoutVariants, base: "justify-center font-medium focus-visible:outline-hidden", variants: { rounded: { true: "rounded-full", }, variant: { primary: backgroundVariants({ background: "blue-3", interactive: true, className: "text-white", }), secondary: classNames( backgroundVariants({ background: "primary", interactive: true, }), borderVariants({ border: "border-1", interactive: true, }), ), "secondary-transparent": classNames( backgroundVariants({ background: "primary", interactive: true, transparent: true, }), borderVariants({ border: "border-1", interactive: true, transparent: true, }), ), tertiary: backgroundVariants({ background: "additional-1", interactive: true, }), destructive: backgroundVariants({ background: "error", interactive: true, class: "text-error", }), ghost: backgroundVariants({ background: "ghost", interactive: true, }), frosted: classNames( backgroundVariants({ background: "frosted", interactive: true, }), borderVariants({ border: "frosted", }), ), }, onlyIcon: { true: "", }, isLoading: { true: "", }, }, compoundVariants: [ { size: "xs", onlyIcon: true, isLoading: false, className: "w-6 p-1" }, { size: "sm", onlyIcon: true, isLoading: false, className: "w-8 p-1.5" }, { size: "md", onlyIcon: true, isLoading: false, className: "w-10 p-2.5" }, { size: "lg", onlyIcon: true, isLoading: false, className: "w-12 p-3" }, { size: "xs", onlyIcon: true, isLoading: true, className: "size-6 px-0" }, { size: "sm", onlyIcon: true, isLoading: true, className: "size-8 px-0" }, { size: "md", onlyIcon: true, isLoading: true, className: "size-10 px-0" }, { size: "lg", onlyIcon: true, isLoading: true, className: "size-12 px-0" }, ], }) type BaseButtonProps = ComponentPropsWithoutRef<"button"> & Omit< VariantProps, "onlyIcon" | "leftIcon" | "rightIcon" > & VariantProps & { /** * Used to show a loading indicator in the button for async actions. */ isLoading?: boolean renderLink?: React.ComponentType iconSide?: "left" | "right" } & Omit export type ButtonWithIconProps = BaseButtonProps & { /** * Icon can either be used as a material icon or as a custom icon. */ icon: IconComponent overrides?: { Icon: { props: IconProps } } } export type ButtonWithCustomIconProps = BaseButtonProps & { icon: React.ReactNode } type ButtonWithAnyIconProps = ButtonWithIconProps | ButtonWithCustomIconProps export type ButtonProps = BaseButtonProps | ButtonWithAnyIconProps export type ButtonSize = ButtonProps["size"] const hasCustomIcon = ( props: ButtonProps, ): props is ButtonWithCustomIconProps => { return isValidElement((props as ButtonWithCustomIconProps).icon) } const hasAnyIcon = (props: ButtonProps): props is ButtonWithAnyIconProps => { return (props as ButtonWithAnyIconProps).icon !== undefined } const LinkButton = forwardRef( function LinkButton({ renderLink, ...rest }, ref) { const Comp = renderLink ?? Link return }, ) /** * Button is our standard interactive element that triggers a response. * You can place text and icons inside of a button. * Our Buttons can also act as a link by passing `href`. */ export const Button = forwardRef( function Button( { renderLink, children, className, disabled = false, href, variant = "primary", size = "md", isLoading, type = "button", iconSide = "left", rounded, ...propsWithIcon }, ref, ) { const hasIcon = hasAnyIcon(propsWithIcon) const onlyIcon = hasIcon && !children const renderContent = () => { if (isLoading) { let color: BorderColorVariantProps["color"] switch (variant) { case "primary": color = "white" break case "destructive": color = "error" break } return } const renderIcon = (buttonProps: ButtonWithAnyIconProps) => { const iconSize = size === "lg" ? 24 : size === "xs" ? 16 : 20 return ( ) } return ( <> {hasIcon && iconSide === "left" && renderIcon(propsWithIcon)} {children} {hasIcon && iconSide === "right" && renderIcon(propsWithIcon)} ) } const { icon: _, ...props } = propsWithIcon as typeof propsWithIcon & { icon?: unknown } return ( {href ? ( {renderContent()} ) : ( )} ) }, )