// UI primitives — LIGHT enterprise theme.
const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React;
// ---------- Toast ----------
const ToastCtx = createContext(null);
function ToastProvider({ children }) {
const [toasts, setToasts] = useState([]);
const push = useCallback((t) => {
const id = Math.random().toString(36).slice(2, 9);
setToasts((cur) => [...cur, { id, ...t }]);
setTimeout(() => setToasts((cur) => cur.filter((x) => x.id !== id)), t.duration || 3800);
}, []);
return (
{children}
{toasts.map((t) => (
{t.title}
{t.body &&
{t.body}
}
))}
);
}
const useToast = () => useContext(ToastCtx);
// ---------- Button ----------
function Button({
children, variant = 'default', size = 'md', icon, iconRight,
className = '', onClick, disabled, type = 'button',
}) {
const sizes = {
sm: 'h-7 px-2.5 text-xs gap-1.5 rounded-md',
md: 'h-9 px-3.5 text-sm gap-2 rounded-lg',
lg: 'h-11 px-5 text-sm gap-2 rounded-lg',
};
const variants = {
default: 'bg-white text-ink-800 border border-line hover:bg-ink-50 hover:border-ink-200',
primary: 'gradient-bg text-white border border-brand-700/30 hover:opacity-95 shadow-sm',
soft: 'bg-brand-50 text-brand-600 border border-brand-100 hover:bg-brand-100',
amber: 'bg-ai-50 text-ai-700 border border-ai-100 hover:bg-ai-100',
ghost: 'text-ink-700 hover:bg-ink-50 border border-transparent',
success: 'bg-emerald-50 text-emerald-700 border border-emerald-100 hover:bg-emerald-100',
danger: 'bg-rose-50 text-rose-700 border border-rose-100 hover:bg-rose-100',
whatsapp: 'bg-emerald-600 text-white hover:bg-emerald-700 border border-emerald-700/40',
call: 'bg-brand-500 text-white border border-brand-600/40 hover:bg-brand-600',
};
return (
);
}
// ---------- IconButton ----------
function IconButton({ icon, onClick, title, variant = 'default', size = 'md', className = '' }) {
const sizes = { sm: 'h-7 w-7', md: 'h-9 w-9', lg: 'h-10 w-10' };
const variants = {
default: 'bg-white border border-line text-ink-700 hover:bg-ink-50 hover:text-ink-900',
ghost: 'text-ink-500 hover:bg-ink-50 hover:text-ink-900 border border-transparent',
primary: 'gradient-bg text-white border border-brand-700/30 shadow-sm',
soft: 'bg-brand-50 text-brand-600 border border-brand-100 hover:bg-brand-100',
};
return (
);
}
// ---------- Pill ----------
function Pill({ children, tone = 'slate', icon, size = 'md', className = '' }) {
const tones = {
slate: 'bg-ink-50 text-ink-700 border-line',
brand: 'bg-brand-50 text-brand-600 border-brand-100',
amber: 'bg-ai-50 text-ai-700 border-ai-100',
emerald: 'bg-emerald-50 text-emerald-700 border-emerald-100',
sky: 'bg-sky-50 text-sky-700 border-sky-100',
rose: 'bg-rose-50 text-rose-700 border-rose-100',
fuchsia: 'bg-fuchsia-50 text-fuchsia-700 border-fuchsia-100',
white: 'bg-white text-ink-700 border-line',
};
const sizes = {
sm: 'h-5 text-[10px] px-1.5 gap-1',
md: 'h-6 text-xs px-2 gap-1.5',
lg: 'h-7 text-[13px] px-2.5 gap-1.5',
};
return (
{icon && {icon}}
{children}
);
}
function StageBadge({ stage }) {
const map = {
'Applied': { tone: 'slate', icon: },
'AI Screened': { tone: 'brand', icon: },
'Shortlisted': { tone: 'amber', icon: },
'Interview': { tone: 'sky', icon: },
'Submitted': { tone: 'fuchsia', icon: },
'Hired': { tone: 'emerald', icon: },
};
const cfg = map[stage] || { tone: 'slate', icon: null };
return {stage};
}
function RecoBadge({ value }) {
if (value === 'Advance') return }>Advance;
if (value === 'Hold') return }>Hold;
if (value === 'Reject') return }>Reject;
return —;
}
function PriorityBadge({ value }) {
if (value === 'High') return } size="sm">Alta;
if (value === 'Medium') return Media;
return Baja;
}
// ---------- Avatar ----------
function Avatar({ name, initials, color = 'from-indigo-500 to-violet-400', size = 36, online, ring }) {
const ini = initials || (name ? name.split(' ').filter(Boolean).slice(0, 2).map((s) => s[0]).join('') : '?');
return (
);
}
// ---------- Card ----------
function Card({ children, className = '', interactive, onClick }) {
return (
{children}
);
}
// ---------- KPI ----------
function KpiCard({ label, value, sub, icon, trend, accent }) {
return (
{accent && (
)}
{value}
{trend && (
{trend.dir === 'up' ? : trend.dir === 'down' ? : null}
{trend.value}
)}
{sub && {sub}
}
);
}
// ---------- Sparkline ----------
function Sparkline({ data, color = '#4f46e5', height = 28, width = 120 }) {
const max = Math.max(...data), min = Math.min(...data);
const span = Math.max(1, max - min);
const pts = data.map((v, i) => {
const x = (i / (data.length - 1)) * width;
const y = height - ((v - min) / span) * (height - 4) - 2;
return `${x},${y}`;
}).join(' ');
const area = `M0,${height} L${pts.split(' ').join(' L')} L${width},${height} Z`;
const gid = `sg-${color.replace('#', '')}`;
return (
);
}
// ---------- Match bar ----------
function MatchBar({ value, size = 'md', showLabel = true }) {
const color = value >= 90 ? '#4f46e5' : value >= 80 ? '#7c3aed' : value >= 70 ? '#a855f7' : value >= 60 ? '#d97706' : '#ef4444';
const heights = { sm: 4, md: 6, lg: 8 };
return (
);
}
// ---------- Tabs ----------
function Tabs({ tabs, value, onChange }) {
return (
{tabs.map((t) => (
))}
);
}
// ---------- Input ----------
function Input({ value, onChange, placeholder, icon, className = '', size = 'md', type = 'text', onKeyDown }) {
const sizes = { sm: 'h-8 text-xs', md: 'h-9 text-sm', lg: 'h-11 text-sm' };
return (
{icon && {icon}}
onChange?.(e.target.value)}
onKeyDown={onKeyDown}
type={type}
placeholder={placeholder}
className="bg-transparent outline-none flex-1 text-ink-900 placeholder:text-ink-400"
/>
);
}
// ---------- Checkbox ----------
function Check({ checked, onChange, label, className = '' }) {
return (
);
}
// ---------- Funnel ----------
function Funnel({ data }) {
const max = Math.max(...data.map((d) => d.count));
return (
{data.map((d, i) => (
{d.stage}
{d.count.toLocaleString()}
{d.conv}%
{i < data.length - 1 && (
)}
))}
);
}
// ---------- Skeleton ----------
function Skeleton({ className = '' }) {
return ;
}
// ---------- Radar ----------
function Radar({ axes, data, size = 240 }) {
const cx = size / 2, cy = size / 2;
const r = size / 2 - 30;
const n = axes.length;
const angle = (i) => (Math.PI * 2 * i) / n - Math.PI / 2;
const polyPoints = (vals) =>
vals.map((v, i) => {
const rr = (v / 100) * r;
return `${cx + Math.cos(angle(i)) * rr},${cy + Math.sin(angle(i)) * rr}`;
}).join(' ');
return (
);
}
// ---------- Modal ----------
function Modal({ open, onClose, children, size = 'md', className = '' }) {
useEffect(() => {
if (!open) return;
const h = (e) => { if (e.key === 'Escape') onClose?.(); };
document.addEventListener('keydown', h);
return () => document.removeEventListener('keydown', h);
}, [open, onClose]);
if (!open) return null;
const widths = { sm: 'max-w-md', md: 'max-w-xl', lg: 'max-w-3xl', xl: 'max-w-5xl' };
return (
);
}
function ModalHeader({ icon, title, subtitle, onClose }) {
return (
{icon &&
{icon}
}
{title}
{subtitle &&
{subtitle}
}
} variant="ghost" onClick={onClose} />
);
}
function Divider({ className = '' }) {
return ;
}
// ---------- Section header ----------
function SectionHeader({ kicker, title, action }) {
return (
{kicker &&
{kicker}
}
{title &&
{title}
}
{action}
);
}
Object.assign(window, {
ToastProvider, useToast,
Button, IconButton, Pill, StageBadge, RecoBadge, PriorityBadge,
Avatar, Card, SectionHeader, KpiCard, Sparkline, MatchBar,
Tabs, Input, Check, Funnel, Skeleton, Radar, Modal, ModalHeader, Divider,
});