feat(components): Create UI components - Button, Input, Card, Checkbox

This commit is contained in:
Erik Silva
2025-12-01 01:45:24 -03:00
parent 888e4e4d60
commit 9b3659504e
5 changed files with 351 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
/**
* 🔘 Button Component
* Componente de botão reutilizável com múltiplas variações
*/
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
fullWidth?: boolean;
children: React.ReactNode;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
isLoading = false,
fullWidth = false,
disabled,
className = '',
children,
...props
},
ref,
) => {
// Estilos base
const baseStyles =
'font-semibold rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2';
// Estilos por tamanho
const sizeStyles = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
// Estilos por variante
const variantStyles = {
primary:
'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 disabled:bg-blue-400',
secondary:
'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-400 disabled:bg-gray-100',
ghost:
'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-400 disabled:text-gray-400',
danger:
'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 disabled:bg-red-400',
};
// Largura total
const widthStyle = fullWidth ? 'w-full' : '';
// Estado desabilitado
const isDisabled = disabled || isLoading;
return (
<button
ref={ref}
disabled={isDisabled}
className={`
${baseStyles}
${sizeStyles[size]}
${variantStyles[variant]}
${widthStyle}
${isDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}
${className}
`}
{...props}
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<svg
className="h-4 w-4 animate-spin"
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>
Carregando...
</span>
) : (
children
)}
</button>
);
},
);
Button.displayName = 'Button';
export default Button;

View File

@@ -0,0 +1,100 @@
/**
* 🎯 Card Component
* Componente de card/painel reutilizável
*/
import React from 'react';
interface CardProps {
children: React.ReactNode;
className?: string;
variant?: 'default' | 'outlined';
clickable?: boolean;
onClick?: () => void;
}
export const Card: React.FC<CardProps> = ({
children,
className = '',
variant = 'default',
clickable = false,
onClick,
}) => {
const variantStyles = {
default: 'bg-white border border-gray-200 shadow-sm',
outlined: 'bg-transparent border border-gray-200',
};
return (
<div
onClick={onClick}
className={`
rounded-lg
p-6
transition-all
duration-200
${variantStyles[variant]}
${clickable ? 'cursor-pointer hover:shadow-md' : ''}
${className}
`}
>
{children}
</div>
);
};
interface CardHeaderProps {
children: React.ReactNode;
className?: string;
}
export const CardHeader: React.FC<CardHeaderProps> = ({
children,
className = '',
}) => (
<div className={`mb-4 border-b border-gray-200 pb-3 ${className}`}>
{children}
</div>
);
interface CardTitleProps {
children: React.ReactNode;
className?: string;
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
}
export const CardTitle: React.FC<CardTitleProps> = ({
children,
className = '',
as: Component = 'h2',
}) => (
<Component className={`text-lg font-bold text-gray-900 ${className}`}>
{children}
</Component>
);
interface CardContentProps {
children: React.ReactNode;
className?: string;
}
export const CardContent: React.FC<CardContentProps> = ({
children,
className = '',
}) => <div className={`${className}`}>{children}</div>;
interface CardFooterProps {
children: React.ReactNode;
className?: string;
}
export const CardFooter: React.FC<CardFooterProps> = ({
children,
className = '',
}) => (
<div className={`mt-6 border-t border-gray-200 pt-3 ${className}`}>
{children}
</div>
);
export default Card;

View File

@@ -0,0 +1,48 @@
/**
* ✓ Checkbox Component
*/
import React from 'react';
interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
({ label, error, id, className = '', ...props }, ref) => {
const checkboxId = id || `checkbox-${Math.random()}`;
return (
<div className="flex items-center">
<input
ref={ref}
id={checkboxId}
type="checkbox"
className={`
w-5
h-5
rounded
border-gray-300
text-blue-600
focus:ring-blue-500
cursor-pointer
${error ? 'border-red-500' : 'border-gray-300'}
${className}
`}
{...props}
/>
{label && (
<label htmlFor={checkboxId} className="ml-2 text-gray-700 cursor-pointer">
{label}
</label>
)}
{error && <p className="text-red-500 text-sm ml-2">{error}</p>}
</div>
);
},
);
Checkbox.displayName = 'Checkbox';
export default Checkbox;

View File

@@ -0,0 +1,82 @@
/**
* 📝 Input Component
* Componente de input reutilizável com validação
*/
import React from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
helperText?: string;
required?: boolean;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
(
{
label,
error,
helperText,
required = false,
id,
className = '',
type = 'text',
...props
},
ref,
) => {
const inputId = id || `input-${Math.random()}`;
return (
<div className="w-full">
{label && (
<label
htmlFor={inputId}
className="block text-sm font-medium text-gray-700 mb-1"
>
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
<input
ref={ref}
id={inputId}
type={type}
className={`
w-full
px-4
py-2
border
rounded-lg
font-base
focus:outline-none
focus:ring-2
focus:ring-offset-2
transition-colors
disabled:bg-gray-50
disabled:text-gray-500
disabled:cursor-not-allowed
${
error
? 'border-red-500 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}
${className}
`}
{...props}
/>
{error && <p className="text-red-500 text-sm mt-1">{error}</p>}
{helperText && !error && (
<p className="text-gray-500 text-sm mt-1">{helperText}</p>
)}
</div>
);
},
);
Input.displayName = 'Input';
export default Input;

View File

@@ -0,0 +1,14 @@
/**
* 🎨 UI Components Exports
*/
export { default as Button } from './Button';
export { default as Input } from './Input';
export { default as Checkbox } from './Checkbox';
export {
default as Card,
CardHeader,
CardTitle,
CardContent,
CardFooter,
} from './Card';