Files
bakery-ia/frontend/src/components/ui/Button/Button.tsx

145 lines
4.6 KiB
TypeScript
Raw Normal View History

2025-08-28 10:41:04 +02:00
import React, { forwardRef, ButtonHTMLAttributes } from 'react';
import { clsx } from 'clsx';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'warning';
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
isLoading?: boolean;
isFullWidth?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
loadingText?: string;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
variant = 'primary',
size = 'md',
isLoading = false,
isFullWidth = false,
leftIcon,
rightIcon,
loadingText,
className,
children,
disabled,
...props
}, ref) => {
const baseClasses = [
'inline-flex items-center justify-center font-medium',
'transition-all duration-200 ease-in-out',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
'border rounded-lg'
];
const variantClasses = {
primary: [
'bg-[var(--color-primary)] text-[var(--text-inverse)] border-[var(--color-primary)]',
'hover:bg-[var(--color-primary-dark)] hover:border-[var(--color-primary-dark)]',
'focus:ring-[var(--color-primary)]/20',
'active:bg-[var(--color-primary-dark)]'
],
secondary: [
'bg-[var(--color-secondary)] text-[var(--text-inverse)] border-[var(--color-secondary)]',
'hover:bg-[var(--color-secondary-dark)] hover:border-[var(--color-secondary-dark)]',
'focus:ring-[var(--color-secondary)]/20',
'active:bg-[var(--color-secondary-dark)]'
],
outline: [
'bg-transparent text-[var(--color-primary)] border-[var(--color-primary)]',
'hover:bg-[var(--color-primary)] hover:text-[var(--text-inverse)]',
'focus:ring-[var(--color-primary)]/20',
'active:bg-[var(--color-primary-dark)] active:border-[var(--color-primary-dark)]'
],
ghost: [
'bg-transparent text-[var(--color-primary)] border-transparent',
'hover:bg-[var(--color-primary)]/10 hover:text-[var(--color-primary-dark)]',
'focus:ring-[var(--color-primary)]/20',
'active:bg-[var(--color-primary)]/20'
],
danger: [
'bg-[var(--color-error)] text-[var(--text-inverse)] border-[var(--color-error)]',
'hover:bg-[var(--color-error-dark)] hover:border-[var(--color-error-dark)]',
'focus:ring-[var(--color-error)]/20',
'active:bg-[var(--color-error-dark)]'
],
success: [
'bg-[var(--color-success)] text-[var(--text-inverse)] border-[var(--color-success)]',
'hover:bg-[var(--color-success-dark)] hover:border-[var(--color-success-dark)]',
'focus:ring-[var(--color-success)]/20',
'active:bg-[var(--color-success-dark)]'
],
warning: [
'bg-[var(--color-warning)] text-[var(--text-inverse)] border-[var(--color-warning)]',
'hover:bg-[var(--color-warning-dark)] hover:border-[var(--color-warning-dark)]',
'focus:ring-[var(--color-warning)]/20',
'active:bg-[var(--color-warning-dark)]'
]
};
const sizeClasses = {
xs: 'px-2 py-1 text-xs gap-1 min-h-6',
2025-09-11 13:01:35 +02:00
sm: 'px-3 py-1.5 text-sm gap-1.5 min-h-8 sm:min-h-10', // Better touch target on mobile
md: 'px-4 py-2 text-sm gap-2 min-h-10 sm:min-h-12',
lg: 'px-6 py-2.5 text-base gap-2 min-h-12 sm:min-h-14',
xl: 'px-8 py-3 text-lg gap-3 min-h-14 sm:min-h-16'
2025-08-28 10:41:04 +02:00
};
const loadingSpinner = (
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4"
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>
);
const classes = clsx(
baseClasses,
variantClasses[variant],
sizeClasses[size],
{
'w-full': isFullWidth,
'cursor-wait': isLoading
},
className
);
return (
<button
ref={ref}
className={classes}
disabled={disabled || isLoading}
{...props}
>
{isLoading && loadingSpinner}
{!isLoading && leftIcon && (
<span className="flex-shrink-0">{leftIcon}</span>
)}
<span>
{isLoading && loadingText ? loadingText : children}
</span>
{!isLoading && rightIcon && (
<span className="flex-shrink-0">{rightIcon}</span>
)}
</button>
);
});
Button.displayName = 'Button';
export default Button;