Button.tsx
1 // Copyright (c) 2026 VPL Solutions. All rights reserved. 2 // Licensed under the MIT License. See LICENSE for details. 3 4 import { type ButtonHTMLAttributes, forwardRef } from 'react'; 5 6 interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { 7 variant?: 'primary' | 'secondary' | 'ghost'; 8 size?: 'sm' | 'md' | 'lg'; 9 loading?: boolean; 10 } 11 12 export const Button = forwardRef<HTMLButtonElement, ButtonProps>( 13 ({ className = '', variant = 'primary', size = 'md', loading, children, disabled, ...props }, ref) => { 14 const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'; 15 16 const variants = { 17 primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500', 18 secondary: 'bg-gray-200 dark:bg-white/10 text-gray-900 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-white/20 focus:ring-gray-500', 19 ghost: 'bg-transparent text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10 focus:ring-gray-500', 20 }; 21 22 const sizes = { 23 sm: 'px-3 py-1.5 text-sm', 24 md: 'px-4 py-2 text-sm', 25 lg: 'px-6 py-3 text-base', 26 }; 27 28 return ( 29 <button 30 ref={ref} 31 className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${disabled || loading ? 'opacity-50 cursor-not-allowed' : ''} ${className}`} 32 disabled={disabled || loading} 33 {...props} 34 > 35 {loading && ( 36 <svg className="animate-spin -ml-1 mr-2 h-4 w-4\" fill="none" viewBox="0 0 24 24"> 37 <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> 38 <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /> 39 </svg> 40 )} 41 {children} 42 </button> 43 ); 44 } 45 ); 46 47 Button.displayName = 'Button';