nico-martin's picture
nico-martin HF Staff
added stats and template
832660f
import { type ReactNode, cloneElement, isValidElement } from "react";
import cn from "../../utils/classnames.ts";
import { Loader } from "../index.ts";
type ButtonColor = "primary" | "secondary" | "mono" | "danger";
type ButtonVariant = "solid" | "outline" | "ghost";
type ButtonSize = "xs" | "sm" | "md" | "lg";
interface BaseButtonProps {
children?: ReactNode;
className?: string;
color?: ButtonColor;
variant?: ButtonVariant;
size?: ButtonSize;
iconLeft?: ReactNode;
iconRight?: ReactNode;
disabled?: boolean;
loading?: boolean;
notDisabledWhileLoading?: boolean;
shiny?: boolean;
}
interface ButtonAsButton extends BaseButtonProps {
onClick?: () => void;
type?: "button" | "submit" | "reset";
}
interface ButtonAsLink extends BaseButtonProps {
href: string;
onClick?: never;
to?: never;
target?: string;
rel?: string;
}
export type ButtonProps = ButtonAsButton | ButtonAsLink;
const sizeClasses: Record<ButtonSize, string> = {
xs: "px-2 py-1.5 text-xs",
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
};
const iconOnlySizeClasses: Record<ButtonSize, string> = {
xs: "p-1.5",
sm: "p-1.5",
md: "p-2",
lg: "p-3",
};
const iconSizeClasses: Record<ButtonSize, string> = {
xs: "h-3 w-3",
sm: "h-4 w-4",
md: "h-5 w-5",
lg: "h-6 w-6",
};
const colorVariantClasses: Record<
ButtonColor,
Record<ButtonVariant, string>
> = {
primary: {
solid:
"bg-yellow-500 text-gray-900 hover:bg-yellow-600 dark:bg-yellow-400 dark:text-gray-900 dark:hover:bg-yellow-500",
outline:
"border-1 border-yellow-500 text-yellow-600 hover:bg-yellow-50 dark:border-yellow-400 dark:text-yellow-400 dark:hover:bg-yellow-950",
ghost:
"text-yellow-600 hover:bg-yellow-50 dark:text-yellow-400 dark:hover:bg-yellow-950",
},
secondary: {
solid:
"bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600",
outline:
"border-1 border-blue-600 text-blue-600 hover:bg-blue-50 dark:border-blue-400 dark:text-blue-400 dark:hover:bg-blue-950",
ghost:
"text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-950",
},
mono: {
solid:
"bg-gray-900 text-white hover:bg-gray-800 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100",
outline:
"border-1 border-gray-900 text-gray-900 hover:bg-gray-50 dark:border-white dark:text-white dark:hover:bg-white/10",
ghost:
"text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-white/10",
},
danger: {
solid:
"bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600",
outline:
"border-1 border-red-600 text-red-600 hover:bg-red-50 dark:border-red-400 dark:text-red-400 dark:hover:bg-red-950",
ghost:
"text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950",
},
};
export default function Button({
children,
className = "",
color = "primary",
variant = "solid",
size = "md",
iconLeft,
iconRight,
disabled = false,
loading = false,
notDisabledWhileLoading = false,
shiny = false,
...props
}: ButtonProps) {
const isIconOnly = !children && (iconLeft || iconRight || loading);
const baseClasses = cn(
"inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900",
isIconOnly ? iconOnlySizeClasses[size] : sizeClasses[size],
colorVariantClasses[color][variant],
{
"cursor-not-allowed opacity-50": disabled || loading,
},
className
);
const isDisabled = disabled || (loading && !notDisabledWhileLoading);
const renderIcon = (icon: ReactNode) => {
if (isValidElement(icon)) {
return cloneElement(icon as any, {
className: cn((icon.props as any)?.className, iconSizeClasses[size]),
});
}
return icon;
};
const content = (
<>
{loading && <Loader size={size} />}
{!loading && iconLeft && renderIcon(iconLeft)}
{children && <span>{children}</span>}
{!loading && iconRight && renderIcon(iconRight)}
</>
);
if ("href" in props && props.href) {
return (
<a
href={props.href}
className={baseClasses}
target={props.target}
rel={props.rel}
aria-disabled={isDisabled}
>
{content}
</a>
);
}
return (
<button
type={(props as ButtonAsButton).type || "button"}
onClick={(props as ButtonAsButton).onClick}
disabled={isDisabled}
className={baseClasses}
>
{content}
</button>
);
}