canina/frontend/admin-panel/src/components/ui/Button.tsx
2026-08-22 17:01:13 +03:30

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>
);
}