// Componentes de UI compartilhados
const { useState, useEffect, useRef, useMemo, useCallback } = React;
// ----- Icon set (inline SVG, small set) -----
function Icon({ name, size = 18, color = 'currentColor', strokeWidth = 2 }) {
const props = {
width: size, height: size, viewBox: '0 0 24 24',
fill: 'none', stroke: color, strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round'
};
const paths = {
home: <>>,
building: <>>,
plus: <>>,
search: <>>,
filter: <>>,
download: <>>,
arrowLeft: <>>,
arrowUp: <>>,
arrowDown: <>>,
check: <>>,
x: <>>,
edit: <>>,
trash: <>>,
more: <>>,
chart: <>>,
pie: <>>,
file: <>>,
bank: <>>,
wallet: <>>,
lock: <>>,
target: <>>,
calendar: (
<>
>
),
list: <>>,
receipt: <>>,
eye: <>>,
chevronDown: <>>,
chevronUp: <>>,
chevronRight: <>>,
alert: <>>,
settings: <>>,
user: <>>,
menu: <>>,
logout: <>>,
upload: <>>,
creditCard: <>>,
};
return ;
}
// ----- Buttons -----
function Btn({ variant = 'primary', size = 'md', icon, children, onClick, disabled, type = 'button', style = {}, title }) {
const pad = size === 'sm' ? '6px 10px' : size === 'lg' ? '12px 20px' : '8px 14px';
const fs = size === 'sm' ? 13 : size === 'lg' ? 15 : 14;
const variants = {
primary: { background: 'var(--c-primary)', color: '#fff', border: '1px solid var(--c-primary)' },
secondary: { background: 'var(--c-surface)', color: 'var(--c-text)', border: '1px solid var(--c-border)' },
ghost: { background: 'transparent', color: 'var(--c-text-muted)', border: '1px solid transparent' },
danger: { background: 'var(--c-surface)', color: '#dc2626', border: '1px solid var(--c-red-bg)' },
success: { background: '#16a34a', color: '#fff', border: '1px solid #16a34a' },
dark: { background: 'var(--c-text)', color: '#fff', border: '1px solid var(--c-text)' },
};
return (
);
}
// ----- Badge -----
function Badge({ status, children, dot = true }) {
const c = statusColor(status) || { bg: 'var(--c-bg)', fg: 'var(--c-text-muted)', dot: '#94a3b8', label: status };
return (
{dot && }
{children || c.label}
);
}
// ----- Card -----
function Card({ children, style = {}, padding = 20, onClick }) {
return (
{children}
);
}
// ----- KPI Card -----
function KPI({ label, value, delta, deltaLabel, icon, color = 'var(--c-text)', sub }) {
return (
{value}
{(delta != null || sub) && (
{delta != null && (
= 0 ? '#16a34a' : '#dc2626', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 2 }}>
= 0 ? 'arrowUp' : 'arrowDown'} size={12} />
{Math.abs(delta).toFixed(1)}%
)}
{sub || deltaLabel}
)}
);
}
// ----- Modal -----
function Modal({ open, onClose, title, children, width = 560, footer, disableBackdropClick = false }) {
const isMobile = useIsMobile();
useEffect(() => {
function esc(e) { if (e.key === 'Escape' && open) onClose(); }
document.addEventListener('keydown', esc);
return () => document.removeEventListener('keydown', esc);
}, [open, onClose]);
if (!open) return null;
return (
e.stopPropagation()} style={{
background: 'var(--c-surface)', borderRadius: 12,
width: isMobile ? '95vw' : width || 520,
maxWidth: '95vw', maxHeight: '90vh',
margin: isMobile ? '20px auto' : 'auto',
display: 'flex', flexDirection: 'column', boxShadow: '0 20px 60px rgba(0,0,0,.2)'
}}>
{children}
{footer &&
{footer}
}
);
}
// ----- Form fields -----
function Field({ label, children, hint, required, span = 1 }) {
return (
{children}
{hint && {hint}}
);
}
const inputStyle = {
width: '100%', padding: '8px 12px', border: '1.5px solid var(--c-border)', borderRadius: 8, fontSize: 13,
background: 'var(--c-surface)', color: 'var(--c-text)', fontFamily: 'inherit',
outline: 'none', transition: 'border-color 0.15s, box-shadow 0.15s'
};
function Input(props) {
const { style, ...rest } = props;
return (
{ e.target.style.borderColor = 'var(--c-primary)'; e.target.style.boxShadow = '0 0 0 3px var(--c-primary-soft)'; }}
onBlur={e => { e.target.style.borderColor = 'var(--c-border)'; e.target.style.boxShadow = 'none'; }}
{...rest}
/>
);
}
function DateInput({ value, onChange, placeholder, style = {} }) {
return (
{
e.target.style.borderColor = 'var(--c-primary)'
e.target.style.boxShadow = '0 0 0 3px var(--c-primary-soft)'
}}
onBlur={e => {
e.target.style.borderColor = 'var(--c-border)'
e.target.style.boxShadow = 'none'
}}
/>
)
}
function CustomSelect({ value, onChange, options, placeholder = 'Selecionar', style = {}, disabled = false, size = 'md' }) {
const { useState, useEffect, useRef } = React;
const opts = options.map(o => typeof o === 'string' ? { value: o, label: o } : o);
const [open, setOpen] = useState(false);
const ref = useRef(null);
const selected = opts.find(o => o.value === value);
const heights = { sm: 32, md: 38, lg: 44 };
const fontSizes = { sm: 12, md: 13, lg: 14 };
useEffect(() => {
function onClick(e) {
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
}
document.addEventListener('mousedown', onClick);
return () => document.removeEventListener('mousedown', onClick);
}, []);
function select(val) {
if (onChange) onChange({ target: { value: val } });
setOpen(false);
}
return (
{open && (
{opts.map((o, i) => (
))}
)}
);
}
function Textarea(props) {
const { style, ...rest } = props;
return (