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 = { 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 = { xs: "p-1.5", sm: "p-1.5", md: "p-2", lg: "p-3", }; const iconSizeClasses: Record = { 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 > = { 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 && } {!loading && iconLeft && renderIcon(iconLeft)} {children && {children}} {!loading && iconRight && renderIcon(iconRight)} ); if ("href" in props && props.href) { return ( {content} ); } return ( ); }