2026-02-06 23:01:30 -05:00

99 lines
2.8 KiB
TypeScript

'use client';
import React from 'react';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
export type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
children: React.ReactNode;
className?: string;
}
const variantStyles: Record<ButtonVariant, string> = {
primary:
'bg-indigo-500 text-white hover:bg-indigo-400 active:bg-indigo-600 focus-visible:ring-indigo-500/50',
secondary:
'bg-gray-700 text-gray-200 hover:bg-gray-600 active:bg-gray-800 focus-visible:ring-gray-500/50',
ghost:
'bg-transparent text-gray-300 hover:bg-gray-800 hover:text-gray-100 active:bg-gray-700 focus-visible:ring-gray-500/50',
danger:
'bg-red-500/10 text-red-400 hover:bg-red-500/20 active:bg-red-500/30 focus-visible:ring-red-500/50',
success:
'bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 active:bg-emerald-500/30 focus-visible:ring-emerald-500/50',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-xs gap-1.5',
md: 'px-4 py-2 text-sm gap-2',
lg: 'px-6 py-3 text-base gap-2.5',
};
const Spinner: React.FC<{ className?: string }> = ({ className }) => (
<svg
className={twMerge('animate-spin h-4 w-4', className)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<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>
);
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
loading = false,
disabled,
children,
className,
...props
},
ref,
) => {
const isDisabled = disabled || loading;
return (
<button
ref={ref}
disabled={isDisabled}
className={twMerge(
clsx(
'inline-flex items-center justify-center font-medium rounded-lg',
'transition-all duration-150 ease-out',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900',
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
variantStyles[variant],
sizeStyles[size],
),
className,
)}
{...props}
>
{loading && <Spinner className={size === 'sm' ? 'h-3 w-3' : 'h-4 w-4'} />}
{children}
</button>
);
},
);
Button.displayName = 'Button';