// ============================================================
// JuFlow — Shared UI Components
// ============================================================
const { useState, useEffect, useRef, useCallback } = React;
const U = window.JFUtils;
// ─── ICONS (safe wrapper around lucide-react UMD) ─────────
const _LR = window.LucideReact || window.lucideReact || {};
const Icon = ({ name, size = 18, color, strokeWidth = 2, style = {}, className = '' }) => {
const Comp = _LR[name];
if (!Comp) return ;
return ;
};
// ─── AVATAR ───────────────────────────────────────────────
const Avatar = ({ name, color, size = 36, src, style = {} }) => {
const bg = color || '#0A1F44';
const sz = size;
return (
{src
?

: U.initials(name)
}
);
};
// ─── BADGE (status pill) ──────────────────────────────────
const Badge = ({ status, label, style = {} }) => {
const colors = U.statusColor(status);
return (
{label || U.statusLabel(status)}
);
};
// ─── TAG CHIP ─────────────────────────────────────────────
const TAG_COLORS = {
'VIP': { bg:'#FFF7ED', text:'#C2410C', border:'#FED7AA' },
'Aniversariante': { bg:'#FDF4FF', text:'#7E22CE', border:'#E9D5FF' },
'Inadimplente': { bg:'#FEF2F2', text:'#B91C1C', border:'#FECACA' },
'Regular': { bg:'#F0FDF4', text:'#15803D', border:'#BBF7D0' },
};
const TagChip = ({ tag }) => {
const c = TAG_COLORS[tag] || { bg:'#F3F4F6', text:'#374151', border:'#D1D5DB' };
return (
{tag === 'VIP' && '★ '}{tag}
);
};
// ─── BUTTON ───────────────────────────────────────────────
const Button = ({ children, variant='primary', size='md', onClick, disabled, icon, style={}, type='button' }) => {
const [hovered, setHovered] = useState(false);
const sizes = { sm:'6px 12px', md:'9px 18px', lg:'12px 24px' };
const fSizes = { sm:12, md:13, lg:14 };
const variants = {
primary: { bg: hovered ? '#E55A20' : '#FF6B35', color:'#fff', border:'transparent', shadow: hovered ? '0 4px 14px rgba(255,107,53,.35)' : '0 2px 8px rgba(255,107,53,.25)' },
secondary: { bg: hovered ? '#f0f4f8' : '#fff', color:'#0A1F44', border:'#E2E8F0', shadow: 'none' },
ghost: { bg: hovered ? '#f0f4f8' : 'transparent', color:'#0A1F44', border:'transparent', shadow:'none' },
danger: { bg: hovered ? '#DC2626' : '#EF4444', color:'#fff', border:'transparent', shadow:'none' },
outline: { bg:'transparent', color: hovered ? '#FF6B35' : '#0A1F44', border: hovered ? '#FF6B35' : '#CBD5E1', shadow:'none' },
};
const v = variants[variant] || variants.primary;
return (
);
};
// ─── INPUT ────────────────────────────────────────────────
const Input = ({ label, value, onChange, placeholder, type='text', icon, required, error, style={}, inputStyle={}, autoFocus, onKeyDown }) => {
const [focused, setFocused] = useState(false);
return (
{label &&
}
{icon &&
}
onChange && onChange(e.target.value)}
placeholder={placeholder}
autoFocus={autoFocus}
onKeyDown={onKeyDown}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
style={{
width:'100%', boxSizing:'border-box',
padding: icon ? '9px 12px 9px 34px' : '9px 12px',
borderRadius:8, fontSize:13,
border:`1.5px solid ${error ? '#EF4444' : focused ? '#FF6B35' : '#E2E8F0'}`,
outline:'none', color:'#0A1F44', background:'#fff',
transition:'border-color .15s',
...inputStyle
}}
/>
{error &&
{error}}
);
};
// ─── SELECT ───────────────────────────────────────────────
const Select = ({ label, value, onChange, options, placeholder, required, style={} }) => {
const [focused, setFocused] = useState(false);
return (
{label && }
);
};
// ─── TEXTAREA ─────────────────────────────────────────────
const Textarea = ({ label, value, onChange, placeholder, rows=3, style={} }) => {
const [focused, setFocused] = useState(false);
return (
{label && }
);
};
// ─── TABS ─────────────────────────────────────────────────
const Tabs = ({ tabs, activeTab, onTabChange, style={} }) => (
{tabs.map(tab => (
))}
);
// ─── MODAL ────────────────────────────────────────────────
const Modal = ({ open, onClose, title, children, width = 560, footer }) => {
useEffect(() => {
if (open) document.body.style.overflow = 'hidden';
else document.body.style.overflow = '';
return () => { document.body.style.overflow = ''; };
}, [open]);
if (!open) return null;
return (
{ if (e.target === e.currentTarget) onClose(); }}
style={{
position:'fixed', inset:0, zIndex:1000,
background:'rgba(10,31,68,.55)', backdropFilter:'blur(4px)',
display:'flex', alignItems:'center', justifyContent:'center',
padding:16,
}}
>
{/* Header */}
{title}
{/* Body */}
{children}
{/* Footer */}
{footer && (
{footer}
)}
);
};
// ─── CONFIRM DIALOG ───────────────────────────────────────
const ConfirmDialog = ({ open, onClose, onConfirm, title, message, confirmLabel='Confirmar', danger=false }) => (
>}
>
{message}
);
// ─── TOAST ────────────────────────────────────────────────
const Toast = ({ toasts }) => (
{toasts.map(t => {
const icons = { success:'CheckCircle', error:'XCircle', info:'Info', warning:'AlertTriangle' };
const colors = {
success: { bg:'#F0FDF4', border:'#86EFAC', icon:'#22C55E', text:'#15803D' },
error: { bg:'#FEF2F2', border:'#FCA5A5', icon:'#EF4444', text:'#B91C1C' },
info: { bg:'#EFF6FF', border:'#93C5FD', icon:'#3B82F6', text:'#1D4ED8' },
warning: { bg:'#FEF9C3', border:'#FDE047', icon:'#EAB308', text:'#854D0E' },
};
const c = colors[t.type] || colors.success;
return (
{t.message}
);
})}
);
// ─── METRIC CARD ──────────────────────────────────────────
const MetricCard = ({ title, value, subtitle, icon, color, trend, trendLabel }) => {
const [hov, setHov] = useState(false);
return (
setHov(true)}
onMouseLeave={() => setHov(false)}
style={{
background:'#fff', borderRadius:14, padding:'20px 22px',
boxShadow: hov ? '0 8px 24px rgba(10,31,68,.10)' : '0 2px 8px rgba(10,31,68,.06)',
transition:'all .2s ease', flex:1, minWidth:0,
}}
>
{value}
{(trend !== undefined || subtitle) && (
{trend !== undefined && (
= 0 ? '#22C55E' : '#EF4444',
display:'flex', alignItems:'center', gap:2,
}}>
= 0 ? 'TrendingUp' : 'TrendingDown'} size={12} color={trend >= 0 ? '#22C55E' : '#EF4444'} />
{Math.abs(trend)}%
)}
{trendLabel || subtitle}
)}
);
};
// ─── SEARCH BAR ───────────────────────────────────────────
const SearchBar = ({ value, onChange, placeholder = 'Buscar...', style = {} }) => {
const [focused, setFocused] = useState(false);
return (
onChange(e.target.value)}
placeholder={placeholder}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
style={{
width:'100%', boxSizing:'border-box',
padding:'9px 12px 9px 36px', borderRadius:10, fontSize:13,
border:`1.5px solid ${focused ? '#FF6B35' : '#E2E8F0'}`,
outline:'none', background:'#fff', color:'#0A1F44',
transition:'border-color .15s',
}}
/>
);
};
// ─── EMPTY STATE ──────────────────────────────────────────
const EmptyState = ({ icon, title, message, action }) => (
{title}
{message}
{action}
);
// ─── SKELETON ─────────────────────────────────────────────
const Skeleton = ({ width = '100%', height = 16, radius = 6, style = {} }) => (
);
// ─── PROGRESS BAR ─────────────────────────────────────────
const ProgressBar = ({ value, max, color = '#FF6B35', style = {} }) => {
const pct = Math.min(100, Math.round((value / max) * 100));
return (
);
};
// ─── DROPDOWN MENU ────────────────────────────────────────
const DropdownMenu = ({ trigger, items, align = 'right' }) => {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
const handler = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
return (
setOpen(o => !o)}>{trigger}
{open && (
{items.map((item, i) => item === 'divider'
?
: (
)
)}
)}
);
};
// ─── EXPORT ───────────────────────────────────────────────
Object.assign(window, {
Icon, Avatar, Badge, TagChip, Button, Input, Select, Textarea,
Tabs, Modal, ConfirmDialog, Toast, MetricCard, SearchBar,
EmptyState, Skeleton, ProgressBar, DropdownMenu,
});