Spaces:
Sleeping
Sleeping
| import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'; | |
| import { cn } from '../utils/cn'; | |
| import { Spinner } from './Spinner'; | |
| export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'; | |
| export type ButtonSize = 'sm' | 'md' | 'lg'; | |
| interface Props extends ButtonHTMLAttributes<HTMLButtonElement> { | |
| variant?: ButtonVariant; | |
| size?: ButtonSize; | |
| loading?: boolean; | |
| leftIcon?: ReactNode; | |
| rightIcon?: ReactNode; | |
| fullWidth?: boolean; | |
| } | |
| const VARIANT_STYLES: Record<ButtonVariant, string> = { | |
| primary: | |
| 'bg-brand-600 text-white hover:bg-brand-700 active:bg-brand-800 focus-visible:ring-brand-500 disabled:bg-brand-300', | |
| secondary: | |
| 'bg-white text-slate-900 border border-slate-300 hover:bg-slate-50 active:bg-slate-100 focus-visible:ring-brand-500 disabled:bg-slate-50 disabled:text-slate-400', | |
| ghost: | |
| 'bg-transparent text-slate-700 hover:bg-slate-100 active:bg-slate-200 focus-visible:ring-brand-500 disabled:text-slate-400', | |
| danger: | |
| 'bg-red-600 text-white hover:bg-red-700 active:bg-red-800 focus-visible:ring-red-500 disabled:bg-red-300', | |
| }; | |
| const SIZE_STYLES: Record<ButtonSize, string> = { | |
| sm: 'h-8 px-3 text-xs gap-1.5', | |
| md: 'h-10 px-4 text-sm gap-2', | |
| lg: 'h-12 px-6 text-base gap-2', | |
| }; | |
| export const Button = forwardRef<HTMLButtonElement, Props>(function Button( | |
| { | |
| variant = 'primary', | |
| size = 'md', | |
| loading = false, | |
| leftIcon, | |
| rightIcon, | |
| fullWidth, | |
| className, | |
| disabled, | |
| children, | |
| type = 'button', | |
| ...rest | |
| }, | |
| ref, | |
| ) { | |
| const isDisabled = disabled || loading; | |
| return ( | |
| <button | |
| ref={ref} | |
| type={type} | |
| disabled={isDisabled} | |
| aria-busy={loading || undefined} | |
| className={cn( | |
| 'inline-flex items-center justify-center rounded-md font-semibold transition-colors', | |
| 'focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2', | |
| 'disabled:cursor-not-allowed', | |
| VARIANT_STYLES[variant], | |
| SIZE_STYLES[size], | |
| fullWidth && 'w-full', | |
| className, | |
| )} | |
| {...rest} | |
| > | |
| {loading ? ( | |
| <Spinner size={size === 'lg' ? 'md' : 'sm'} /> | |
| ) : ( | |
| leftIcon && <span aria-hidden>{leftIcon}</span> | |
| )} | |
| {children} | |
| {!loading && rightIcon && <span aria-hidden>{rightIcon}</span>} | |
| </button> | |
| ); | |
| }); | |