bbdb68a6de
Esta commit conteudo a estrutura atomica completa:
- types: Result<T,E>, AsyncState<T>, Paginated<T>, SortConfig<T>
- utils: date, str, num, cn, debounce, throttle, storage, arr, obj
- validators: Zod schemas — email, password, uuid, url, phone, CPF/CNPJ, sanitizedStr, safeParse
- hooks: useToggle, useAsync, useDebounce, useLocalStorage, useMedia, useInterval, useOnClickOutside, useClipboard, useFetch
- components: Button, Input, Alert, Card, Spinner (atomic design pattern)
- build: tsup v8 ESM+CJS + DTS + sourcemaps — 0 erros
- tests: 57 testes 100% usuarios
- docker: multi-stage Dockerfile (node 20-alpine)
- config: vitest, tsup, tsconfig strict, .npmignore
Filosofia atomica:/utils ← /types ← /validators ← /hooks ← /components
Build: npm run build | Test: npm test | Publish: npm publish
🤖 Generated with Pulse (openclaw + nova-self-improver)
153 lines
6.1 KiB
TypeScript
153 lines
6.1 KiB
TypeScript
/**
|
||
* Componentes atômicos — Receptor de className sempre no topo,
|
||
* spread de props SEMPRE por último.
|
||
*/
|
||
|
||
import { cn } from '../utils';
|
||
|
||
// =========================================================
|
||
// 🅱 Button
|
||
// =========================================================
|
||
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
|
||
type ButtonSize = 'sm' | 'md' | 'lg';
|
||
|
||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||
variant?: ButtonVariant;
|
||
size?: ButtonSize;
|
||
loading?: boolean;
|
||
leftIcon?: React.ReactNode;
|
||
rightIcon?: React.ReactNode;
|
||
}
|
||
|
||
const variantStyles: Record<ButtonVariant, string> = {
|
||
primary: 'bg-indigo-600 text-white hover:bg-indigo-700 active:bg-indigo-800 disabled:bg-indigo-300',
|
||
secondary: 'border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 active:bg-gray-100',
|
||
ghost: 'text-gray-600 hover:bg-gray-100 active:bg-gray-200',
|
||
danger: 'bg-red-600 text-white hover:bg-red-700 active:bg-red-800',
|
||
success: 'bg-emerald-600 text-white hover:bg-emerald-700 active:bg-emerald-800',
|
||
};
|
||
|
||
const sizeStyles: Record<ButtonSize, string> = {
|
||
sm: 'px-2.5 py-1 text-xs rounded',
|
||
md: 'px-4 py-2 text-sm rounded-md',
|
||
lg: 'px-6 py-3 text-base rounded-lg',
|
||
};
|
||
|
||
export function Button({
|
||
className, variant = 'primary', size = 'md',
|
||
loading, leftIcon, rightIcon,
|
||
children, disabled, ...rest
|
||
}: ButtonProps) {
|
||
return (
|
||
<button
|
||
className={cn(
|
||
'inline-flex items-center justify-center gap-2 font-medium transition-colors',
|
||
'focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2',
|
||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||
variantStyles[variant], sizeStyles[size], className
|
||
)}
|
||
disabled={disabled || loading}
|
||
{...rest}
|
||
>
|
||
{loading ? <Spinner size={16} /> : (leftIcon ?? null)}
|
||
{children}
|
||
{!loading && rightIcon}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
// =========================================================
|
||
// 🅸 Input
|
||
// =========================================================
|
||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||
label?: string;
|
||
error?: string;
|
||
hint?: string;
|
||
}
|
||
|
||
export function Input({ className, label, error, hint, id, ...rest }: InputProps) {
|
||
const inputId = id ?? label?.toLowerCase().replace(/\s+/g, '-');
|
||
return (
|
||
<div className="flex flex-col gap-1">
|
||
{label && <label htmlFor={inputId} className="text-sm font-medium text-gray-700">{label}</label>}
|
||
<input
|
||
id={inputId}
|
||
className={cn(
|
||
'w-full rounded-md border border-gray-300 px-3 py-2 text-sm',
|
||
'placeholder:text-gray-400',
|
||
'focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500',
|
||
'disabled:bg-gray-100 disabled:cursor-not-allowed',
|
||
error && 'border-red-500 focus:ring-red-500 focus:border-red-500',
|
||
className
|
||
)}
|
||
{...rest}
|
||
/>
|
||
{error && <span className="text-xs text-red-500">{error}</span>}
|
||
{hint && !error && <span className="text-xs text-gray-400">{hint}</span>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// =========================================================
|
||
// 🔔 Toast / Alert
|
||
// =========================================================
|
||
type AlertVariant = 'info' | 'success' | 'warning' | 'error';
|
||
|
||
interface AlertProps {
|
||
variant?: AlertVariant;
|
||
title?: string;
|
||
children: React.ReactNode;
|
||
onClose?: () => void;
|
||
}
|
||
|
||
const alertStyles: Record<AlertVariant, { container: string; icon: string }> = {
|
||
info: { container: 'bg-blue-50 border-blue-200 text-blue-800', icon: 'ℹ️' },
|
||
success: { container: 'bg-emerald-50 border-emerald-200 text-emerald-800', icon: '✅' },
|
||
warning: { container: 'bg-amber-50 border-amber-200 text-amber-800', icon: '⚠️' },
|
||
error: { container: 'bg-red-50 border-red-200 text-red-800', icon: '❌' },
|
||
};
|
||
|
||
export function Alert({ variant = 'info', title, children, onClose }: AlertProps) {
|
||
const s = alertStyles[variant];
|
||
return (
|
||
<div className={cn('flex items-start gap-2 rounded-lg border px-4 py-3', s.container)}>
|
||
<span className="text-base leading-none">{s.icon}</span>
|
||
<div className="flex-1">
|
||
{title && <p className="font-semibold text-sm">{title}</p>}
|
||
<p className="text-sm">{children}</p>
|
||
</div>
|
||
{onClose && <button onClick={onClose} className="ml-auto text-lg leading-none opacity-50 hover:opacity-100">×</button>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// =========================================================
|
||
// 🃏 Card
|
||
// =========================================================
|
||
interface CardProps extends React.HTMLAttributes<HTMLDivElement> { children: React.ReactNode; }
|
||
export const Card: React.FC<CardProps> = ({ className, children, ...rest }) => (
|
||
<div className={cn('rounded-xl border border-gray-200 bg-white shadow-sm', className)} {...rest}>{children}</div>
|
||
);
|
||
export const CardHeader: React.FC<CardProps> = ({ className, children, ...rest }) => (
|
||
<div className={cn('px-5 py-4 border-b border-gray-100', className)} {...rest}>{children}</div>
|
||
);
|
||
export const CardTitle: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||
<h3 className="font-semibold text-gray-900 text-base">{children}</h3>
|
||
);
|
||
export const CardBody: React.FC<CardProps> = ({ className, children, ...rest }) => (
|
||
<div className={cn('px-5 py-4', className)} {...rest}>{children}</div>
|
||
);
|
||
|
||
// ── Spinner (reutilizável) ─────────────────────────────────
|
||
export function Spinner({ size = 16, className = '' }: { size?: number; className?: string }) {
|
||
return (
|
||
<svg className={cn('animate-spin', className)} width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||
<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 12h4z" />
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
// ── Cn helper re-export ────────────────────────────────────
|
||
export { cn } from '../utils';
|