115 lines
3.5 KiB
TypeScript
115 lines
3.5 KiB
TypeScript
import React from 'react';
|
|
import { Info } from 'lucide-react';
|
|
import Spinner from './Spinner';
|
|
|
|
export type ButtonVariant =
|
|
| 'primary'
|
|
| 'secondary'
|
|
| 'danger'
|
|
| 'success'
|
|
| 'warning'
|
|
| 'outline'
|
|
| 'ghost';
|
|
|
|
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
|
|
|
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: ButtonVariant;
|
|
size?: ButtonSize;
|
|
isLoading?: boolean;
|
|
startIcon?: React.ComponentType<{ className?: string }>;
|
|
endIcon?: React.ComponentType<{ className?: string }>;
|
|
info?: string;
|
|
badge?: string | number;
|
|
fullWidth?: boolean;
|
|
}
|
|
|
|
export default function Button({
|
|
children,
|
|
variant = 'primary',
|
|
size = 'md',
|
|
isLoading = false,
|
|
startIcon: StartIcon,
|
|
endIcon: EndIcon,
|
|
info,
|
|
badge,
|
|
fullWidth = false,
|
|
className = '',
|
|
disabled,
|
|
...props
|
|
}: ButtonProps) {
|
|
// Base styles: strict whitespace-nowrap and touch-friendly cursor
|
|
const baseClasses =
|
|
'inline-flex items-center justify-center font-bold whitespace-nowrap transition-all select-none rounded-xl cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none focus:outline-none focus:ring-2 focus:ring-offset-2';
|
|
|
|
// Size styling
|
|
const sizeClasses: Record<ButtonSize, string> = {
|
|
xs: 'px-2.5 py-1 text-[11px] gap-1.5',
|
|
sm: 'px-3.5 py-1.5 text-xs gap-2',
|
|
md: 'px-4 py-2.5 text-xs sm:text-sm gap-2',
|
|
lg: 'px-6 py-3 text-sm sm:text-base gap-2.5 shadow-md',
|
|
};
|
|
|
|
// Variant styling
|
|
const variantClasses: Record<ButtonVariant, string> = {
|
|
primary:
|
|
'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm shadow-indigo-200 focus:ring-indigo-500 border border-indigo-600',
|
|
secondary:
|
|
'bg-slate-100 hover:bg-slate-200 text-slate-700 focus:ring-slate-400 border border-slate-200',
|
|
danger:
|
|
'bg-rose-600 hover:bg-rose-700 text-white shadow-sm shadow-rose-200 focus:ring-rose-500 border border-rose-600',
|
|
success:
|
|
'bg-emerald-600 hover:bg-emerald-700 text-white shadow-sm shadow-emerald-200 focus:ring-emerald-500 border border-emerald-600',
|
|
warning:
|
|
'bg-amber-500 hover:bg-amber-600 text-white shadow-sm shadow-amber-200 focus:ring-amber-400 border border-amber-500',
|
|
outline:
|
|
'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-indigo-500',
|
|
ghost:
|
|
'bg-transparent hover:bg-gray-100 text-gray-600 focus:ring-gray-300 border border-transparent',
|
|
};
|
|
|
|
const iconSizes: Record<ButtonSize, string> = {
|
|
xs: 'w-3 h-3',
|
|
sm: 'w-3.5 h-3.5',
|
|
md: 'w-4 h-4',
|
|
lg: 'w-5 h-5',
|
|
};
|
|
|
|
return (
|
|
<button
|
|
disabled={disabled || isLoading}
|
|
className={`
|
|
${baseClasses}
|
|
${sizeClasses[size]}
|
|
${variantClasses[variant]}
|
|
${fullWidth ? 'w-full' : ''}
|
|
${className}
|
|
`}
|
|
{...props}
|
|
>
|
|
{isLoading ? (
|
|
<Spinner size={size === 'lg' ? 'md' : 'sm'} />
|
|
) : (
|
|
<>
|
|
{StartIcon && <StartIcon className={iconSizes[size]} />}
|
|
{children && <span>{children}</span>}
|
|
{badge !== undefined && (
|
|
<span className="px-1.5 py-0.5 rounded-full text-[10px] bg-white/20 text-current font-mono">
|
|
{badge}
|
|
</span>
|
|
)}
|
|
{EndIcon && <EndIcon className={iconSizes[size]} />}
|
|
{info && (
|
|
<span
|
|
className="inline-flex items-center text-current/80 hover:text-current cursor-help ml-0.5"
|
|
title={info}
|
|
>
|
|
<Info className="w-3.5 h-3.5" />
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</button>
|
|
);
|
|
}
|