All files / src/components/ui button.tsx

100% Statements 7/7
100% Branches 7/7
100% Functions 1/1
100% Lines 7/7

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117          267x                                                                           267x                           21652x                                               21652x           113x                       21539x                           267x      
import { forwardRef, type ButtonHTMLAttributes, type HTMLAttributes } from "react";
import { Slot, Slottable, type SlotProps } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
 
const buttonVariants = cva(
	"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
	{
		variants: {
			variant: {
				default:
					"bg-primary-500 text-foreground-inverse hover:bg-primary-600 active:bg-primary-700",
				destructive:
					"bg-error text-foreground-inverse hover:bg-error active:bg-error",
				outline:
					"border border-border-default bg-background-elevated text-foreground-default hover:bg-background-muted hover:border-border-strong",
				secondary:
					"bg-secondary-500 text-foreground-inverse hover:bg-secondary-600 active:bg-secondary-700",
				ghost:
					"text-foreground-default hover:bg-background-muted hover:text-foreground-default",
				link: "text-primary-600 underline-offset-4 hover:underline",
			},
			size: {
				default: "h-10 px-4 py-2",
				sm: "h-9 rounded-md px-3 text-xs",
				lg: "h-11 rounded-md px-8",
				icon: "h-10 w-10",
			},
		},
		defaultVariants: {
			variant: "default",
			size: "default",
		},
	},
);
 
interface ButtonProps
	extends ButtonHTMLAttributes<HTMLButtonElement>,
		VariantProps<typeof buttonVariants> {
	asChild?: boolean;
	isLoading?: boolean;
}
 
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
	(
		{
			className,
			variant,
			size,
			asChild = false,
			isLoading,
			children,
			disabled,
			...props
		},
		ref,
	) => {
		const loadingSpinner = isLoading ? (
			<svg
				className="h-4 w-4 animate-spin"
				xmlns="http://www.w3.org/2000/svg"
				fill="none"
				viewBox="0 0 24 24"
				aria-hidden="true"
			>
				<circle
					className="opacity-25"
					cx="12"
					cy="12"
					r="10"
					stroke="currentColor"
					strokeWidth="4"
				/>
				<path
					className="opacity-75"
					fill="currentColor"
					d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
				/>
			</svg>
		) : null;
 
		if (asChild) {
			// Pass children to Slot as an array (not a Fragment): Slot uses
			// React.Children.toArray + find(isSlottable), which only flattens
			// arrays. A Fragment wrapper would be treated as a single element
			// and the className would end up merged onto the Fragment (where
			// it's silently dropped) instead of the Link/anchor inside.
			return (
				<Slot
					className={cn(buttonVariants({ variant, size, className }))}
					ref={ref}
					{...(props as HTMLAttributes<HTMLElement>)}
				>
					{loadingSpinner}
					<Slottable>{children as SlotProps["children"]}</Slottable>
				</Slot>
			);
		}
 
		return (
			<button
				className={cn(buttonVariants({ variant, size, className }))}
				ref={ref}
				disabled={disabled || isLoading}
				{...props}
			>
				{loadingSpinner}
				{children}
			</button>
		);
	},
);
 
Button.displayName = "Button";
 
export { Button };