Files
pulse-libs/projetos/@pulse-libs/core/src/components/index.tsx
T
Pulse Agent 0889ee9117 feat(lib): add useLiveStream WS hook + useLiveMetrics + LiveMetricChart
feat(hooks): add useLiveStream generic WebSocket hook
  - supports websocket/sse/polling transports
  - exponential backoff reconnect with jitter
  - circular buffer with configurable size
  - typed filter callback per use case
  - manual disconnect + reconnect + error state

feat(hooks): add useLiveMetrics derived hook
  - sliding time-window cut
  - moving average (configurable window)
  - current / avg / min / max / ratePerSecond
  - zero allocations per tick (memoized)

feat(charts): add LiveMetricChart molecule (Recharts)
  - line + area variants, grid + tooltip
  - moving-average overlay (dashed)
  - ConnectionStatus atom in header
  - status bar + compact mode
  - 100% responsive, GPU via SVG ViewBox

feat(atoms): add ConnectionStatus indicator
  - 5 states: disconnected/connecting/connected/reconnecting/error
  - animated pulse, JetBrains Mono, pill style
  - exported helpers: formatLatency / formatBytes

docs(pkg): bump v0.1.0 → v0.2.0, add recharts peerDep
2026-05-20 22:59:10 -03:00

153 lines
6.1 KiB
TypeScript
Executable File
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';