// UIComponents.jsx — Shared UI primitives
'use strict';
const { useState, useEffect, useRef } = React;
// ── Toast ──────────────────────────────────────────────────────────────────
function Toast({ toast, onRemove }) {
useEffect(() => { const t = setTimeout(() => onRemove(toast.id), 3800); return () => clearTimeout(t); }, []);
const cfg = {
success: { bg:'#ECFDF5', border:'#10B981', icon:'CheckCircle', text:'#065F46' },
error: { bg:'#FEF2F2', border:'#EF4444', icon:'XCircle', text:'#991B1B' },
info: { bg:'#EFF6FF', border:'#3B82F6', icon:'Info', text:'#1E40AF' },
warning: { bg:'#FFFBEB', border:'#F59E0B', icon:'AlertCircle', text:'#92400E' },
}[toast.type || 'success'];
return (
{toast.title &&
{toast.title}
}
{toast.msg}
);
}
function ToastContainer({ toasts, removeToast }) {
return (
{toasts.map(t => )}
);
}
// ── Modal ──────────────────────────────────────────────────────────────────
// Renderizado via portal no document.body para escapar de ancestrais com `transform`
// (que criam novo containing block e quebram `position: fixed`).
function Modal({ open, onClose, title, children, width = 'max-w-2xl', noPad = false }) {
useEffect(() => {
if (open) document.body.style.overflow = 'hidden';
else document.body.style.overflow = '';
return () => { document.body.style.overflow = ''; };
}, [open]);
if (!open) return null;
return ReactDOM.createPortal(
e.target === e.currentTarget && onClose()}>
,
document.body
);
}
// ── ConfirmDialog ──────────────────────────────────────────────────────────
function ConfirmDialog({ open, onClose, onConfirm, title = 'Confirmar exclusão', message, confirmLabel = 'Excluir', danger = true }) {
if (!open) return null;
return ReactDOM.createPortal(
{title}
{message}
,
document.body
);
}
// ── StatusBadge ────────────────────────────────────────────────────────────
const STATUS_CFG = {
'Aberta': { bg:'#DBEAFE', color:'#1D4ED8', dot:'#3B82F6' },
'Em andamento': { bg:'#FEF3C7', color:'#B45309', dot:'#F59E0B' },
'Aguardando peça':{ bg:'#FCE7F3', color:'#9D174D', dot:'#EC4899' },
'Concluída': { bg:'#D1FAE5', color:'#065F46', dot:'#10B981' },
'Entregue': { bg:'#E0E7FF', color:'#3730A3', dot:'#6366F1' },
'pago': { bg:'#D1FAE5', color:'#065F46', dot:'#10B981' },
'pendente': { bg:'#FEF3C7', color:'#B45309', dot:'#F59E0B' },
'entrada': { bg:'#D1FAE5', color:'#065F46', dot:'#10B981' },
'saída': { bg:'#FEE2E2', color:'#991B1B', dot:'#EF4444' },
'Ativo': { bg:'#D1FAE5', color:'#065F46', dot:'#10B981' },
'Inativo': { bg:'#F1F5F9', color:'#64748B', dot:'#94A3B8' },
};
function StatusBadge({ status, size = 'sm' }) {
const cfg = STATUS_CFG[status] || { bg:'#F1F5F9', color:'#64748B', dot:'#94A3B8' };
const px = size === 'xs' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-xs';
return (
{status}
);
}
// ── EmptyState ─────────────────────────────────────────────────────────────
function EmptyState({ icon = 'FileText', title, message, actionLabel, onAction }) {
return (
{title}
{message}
{actionLabel && onAction && (
)}
);
}
// ── Skeleton ───────────────────────────────────────────────────────────────
function Skeleton({ className = '', style = {} }) {
return ;
}
function SkeletonRow() {
return (
);
}
// ── Form Controls ──────────────────────────────────────────────────────────
function FormField({ label, required, error, children, className = '' }) {
return (
{label &&
}
{children}
{error &&
{error}
}
);
}
function Input({ className = '', error, ...props }) {
return (
);
}
function Select({ className = '', children, ...props }) {
return (
);
}
function Textarea({ className = '', ...props }) {
return (
);
}
function Btn({ variant = 'primary', size = 'md', icon, children, className = '', ...props }) {
const base = 'inline-flex items-center justify-center gap-2 font-semibold rounded-xl transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed';
const sz = { sm: 'px-3 py-1.5 text-xs', md: 'px-4 py-2.5 text-sm', lg: 'px-6 py-3 text-sm' }[size];
const v = {
primary: 'text-white hover:opacity-90 active:scale-95',
ghost: 'text-slate-600 hover:bg-slate-100 active:bg-slate-200',
outline: 'border border-slate-200 text-slate-600 hover:bg-slate-50',
danger: 'text-white bg-red-500 hover:bg-red-600 active:scale-95',
}[variant];
const bg = variant === 'primary' ? { background: '#FF6B35' } : {};
return (
);
}
// ── PageHeader ─────────────────────────────────────────────────────────────
function PageHeader({ title, subtitle, action }) {
return (
{title}
{subtitle &&
{subtitle}
}
{action}
);
}
// ── Card ───────────────────────────────────────────────────────────────────
function Card({ children, className = '', style = {}, onClick }) {
return (
{children}
);
}
// ── Table ──────────────────────────────────────────────────────────────────
function Table({ columns, children, loading }) {
return (
{columns.map((c, i) => (
| {c.label} |
))}
{loading ? Array.from({ length: 5 }).map((_, i) => ) : children}
);
}
function TR({ children, className = '' }) {
return {children}
;
}
function TD({ children, className = '' }) {
return {children} | ;
}
// ── SearchInput ────────────────────────────────────────────────────────────
function SearchInput({ value, onChange, placeholder = 'Pesquisar...', className = '' }) {
return (
onChange(e.target.value)} placeholder={placeholder}
className="w-full pl-9 pr-4 py-2 rounded-xl border border-slate-200 bg-white text-sm focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-colors"
style={{ color: '#1E293B' }} />
);
}
// ── Tab ───────────────────────────────────────────────────────────────────
function Tabs({ tabs, active, onChange }) {
return (
{tabs.map(t => (
))}
);
}
// ── Helpers ────────────────────────────────────────────────────────────────
function fmtCurrency(v) { return new Intl.NumberFormat('pt-BR', { style:'currency', currency:'BRL' }).format(v); }
function fmtDate(s) {
if (!s) return '—';
const [y, m, d] = s.split('-');
return `${d}/${m}/${y}`;
}
function fmtPhone(p) { return p; }
function uid() { return Math.random().toString(36).slice(2, 9); }
function nextId(items, prefix) {
const nums = items.map(x => parseInt(x.id.replace(/\D/g, ''), 10) || 0);
return prefix + String(Math.max(0, ...nums) + 1).padStart(4, '0');
}
Object.assign(window, {
Toast, ToastContainer, Modal, ConfirmDialog,
StatusBadge, EmptyState, Skeleton, SkeletonRow,
FormField, Input, Select, Textarea, Btn,
PageHeader, Card, Table, TR, TD, SearchInput, Tabs,
fmtCurrency, fmtDate, fmtPhone, uid, nextId, STATUS_CFG,
});