Files
pulse-libs/projetos/@pulse-libs/core/src/components/index.tsx
T
pulse-agent bbdb68a6de feat(lib-core): biblioteca atomica @pulse-libs/core v1.0.0-beta.1
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)
2026-05-19 21:43:03 -03:00

153 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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';