/* Shared data, contexts, atoms, and overlays for the Airtm ATS Dashboard variations. */ /* ─────────────── data ─────────────── */ const ROLES = [ { id: 'exec', label: 'Executive', sub: 'C-level overview' }, { id: 'manager', label: 'Hiring manager', sub: 'My reqs & candidates' }, { id: 'recruiter', label: 'Recruiter', sub: 'Full pipeline' }, ]; const ROLE_GROUPS = [ { id: 'sr-eng', label: 'Senior Engineer', ttf: 60, ttfTarget: 30, tth: 7, tthTarget: 7, offer: 100, screen: 92, attr: 4 }, { id: 'eng', label: 'Engineer II', ttf: 60, ttfTarget: 30, tth: 3, tthTarget: 7, offer: 100, screen: 78, attr: 2 }, { id: 'designer', label: 'Product Designer', ttf: 80, ttfTarget: 30, tth: 6, tthTarget: 7, offer: 100, screen: 89, attr: 0 }, { id: 'pm', label: 'Product Manager', ttf: 45, ttfTarget: 30, tth: 3, tthTarget: 7, offer: 100, screen: 94, attr: 1 }, { id: 'analyst', label: 'Data Analyst', ttf: 22, ttfTarget: 30, tth: 2, tthTarget: 7, offer: 100, screen: 88, attr: 0 }, { id: 'ops', label: 'Operations Lead', ttf: 14, ttfTarget: 30, tth: 2, tthTarget: 7, offer: 100, screen: 86, attr: 0 }, ]; const PIPELINE = [ { stage: 'Applied', count: 312 }, { stage: 'Screening', count: 84 }, { stage: 'Interview', count: 41 }, { stage: 'Take-home', count: 18 }, { stage: 'Offer', count: 6 }, { stage: 'Hired', count: 3 }, ]; /* deterministic-ish "AI" candidate set */ const CANDIDATES = [ { id: 'c1', name: 'Mariana Quintero', role: 'sr-eng', loc: 'Bogotá, CO', langs: ['EN','ES'], score: 94, stage: 'Interview', lastTouch: '2h ago', expYrs: 9, source: 'Referral', salary: 92, reasons: ['9y in distributed systems','LATAM time zone','Strong Go + TS','Led 4-eng team at Stripe alum'] }, { id: 'c2', name: 'Daniel Okafor', role: 'sr-eng', loc: 'Lagos, NG', langs: ['EN'], score: 91, stage: 'Screening', lastTouch: '6h ago', expYrs: 8, source: 'Inbound', salary: 78, reasons: ['8y backend at fintech','Owns payments rails','Open-source contributor','Available <30 days'] }, { id: 'c3', name: 'Sofía Restrepo', role: 'designer', loc: 'Medellín, CO', langs: ['EN','ES'], score: 89, stage: 'Take-home', lastTouch: '1d ago', expYrs: 6, source: 'AirtmDirect', salary: 70, reasons: ['Wallet & remittance work','Strong systems thinker','Portfolio matches voice','LATAM time zone'] }, { id: 'c4', name: 'Camila Vargas', role: 'pm', loc: 'Mexico City, MX', langs: ['EN','ES'], score: 88, stage: 'Interview', lastTouch: '4h ago', expYrs: 7, source: 'LinkedIn', salary: 105, reasons: ['Shipped 2 LATAM fintech apps','Direct ops experience','Bilingual EN/ES'] }, { id: 'c5', name: 'Anika Pillai', role: 'eng', loc: 'Bengaluru, IN', langs: ['EN'], score: 86, stage: 'Applied', lastTouch: '12h ago', expYrs: 4, source: 'Inbound', salary: 48, reasons: ['Strong TS + React','Worked on Stripe webhooks','Time-zone fit risk'] }, { id: 'c6', name: 'Thiago Almeida', role: 'eng', loc: 'São Paulo, BR', langs: ['EN','PT'], score: 84, stage: 'Screening', lastTouch: '1d ago', expYrs: 5, source: 'Referral', salary: 65, reasons: ['Crypto-rails background','Speaks PT/EN','LATAM time zone'] }, { id: 'c7', name: 'Lucas Méndez', role: 'analyst', loc: 'Buenos Aires, AR', langs: ['EN','ES'], score: 82, stage: 'Offer', lastTouch: '30m ago', expYrs: 5, source: 'Inbound', salary: 58, reasons: ['SQL+dbt+Looker','Built KPI dashboards','Already past final round'] }, { id: 'c8', name: 'Renata Oliveira', role: 'pm', loc: 'Rio de Janeiro, BR', langs: ['EN','PT'], score: 80, stage: 'Screening', lastTouch: '3h ago', expYrs: 6, source: 'LinkedIn', salary: 98, reasons: ['Risk & compliance PM','Worked at Nubank','Salary slightly over band'] }, { id: 'c9', name: 'Aaron Lee', role: 'ops', loc: 'Manila, PH', langs: ['EN'], score: 78, stage: 'Interview', lastTouch: '5h ago', expYrs: 8, source: 'Inbound', salary: 52, reasons: ['Ran 30-person CS team','Process-first','Time-zone fit risk'] }, { id: 'c10',name: 'Valentina Cruz', role: 'designer', loc: 'Lima, PE', langs: ['EN','ES'], score: 76, stage: 'Applied', lastTouch: '2d ago', expYrs: 4, source: 'Referral', salary: 56, reasons: ['Mobile-first portfolio','Less systems work','LATAM time zone'] }, { id: 'c11',name: 'Jonas Weber', role: 'sr-eng', loc: 'Berlin, DE', langs: ['EN','DE'], score: 74, stage: 'Screening', lastTouch: '1d ago', expYrs: 11, source: 'Inbound', salary: 135, reasons: ['Strong CV','Outside salary band','Time-zone overlap is tight'] }, { id: 'c12',name: 'Pooja Iyer', role: 'analyst', loc: 'Mumbai, IN', langs: ['EN'], score: 71, stage: 'Applied', lastTouch: '4d ago', expYrs: 3, source: 'Inbound', salary: 42, reasons: ['Solid SQL','Junior for the band','Time-zone fit risk'] }, ]; const REQS = [ { id: 'r1', title: 'Senior Backend Engineer', role: 'sr-eng', dept: 'Engineering', loc: 'Remote · LATAM', applicants: 184, days: 38, predicted: 42, target: 30, owner: 'Carla R.', status: 'On track', hires: '0 / 2' }, { id: 'r2', title: 'Product Designer', role: 'designer', dept: 'Design', loc: 'Remote · LATAM', applicants: 96, days: 51, predicted: 58, target: 30, owner: 'Diego F.', status: 'At risk', hires: '0 / 1' }, { id: 'r3', title: 'Product Manager, Risk', role: 'pm', dept: 'Product', loc: 'Remote · Global',applicants: 142, days: 21, predicted: 28, target: 30, owner: 'Carla R.', status: 'On track', hires: '0 / 1' }, { id: 'r4', title: 'Frontend Engineer', role: 'eng', dept: 'Engineering', loc: 'Remote · LATAM', applicants: 211, days: 12, predicted: 24, target: 30, owner: 'Pablo M.', status: 'On track', hires: '0 / 2' }, { id: 'r5', title: 'Data Analyst', role: 'analyst', dept: 'Data', loc: 'Remote · LATAM', applicants: 88, days: 9, predicted: 18, target: 30, owner: 'Diego F.', status: 'On track', hires: '0 / 1' }, { id: 'r6', title: 'Operations Lead, CS', role: 'ops', dept: 'Operations', loc: 'Remote · APAC', applicants: 53, days: 33, predicted: 39, target: 30, owner: 'Pablo M.', status: 'At risk', hires: '0 / 1' }, ]; const UPDATES = [ { t: 'offer-accept', who: 'Lucas Méndez', what: 'accepted offer · Data Analyst', when: '12 min', tone: 'prog' }, { t: 'interview', who: 'Mariana Quintero', what: 'interview scheduled · 3:30pm today', when: '1h', tone: 'brand' }, { t: 'stage', who: 'Daniel Okafor', what: 'moved to Interview · Senior Backend', when: '2h', tone: 'neutral' }, { t: 'offer-sent', who: 'Camila Vargas', what: 'offer sent · Product Manager, Risk', when: '3h', tone: 'brand' }, { t: 'interview', who: 'Sofía Restrepo', what: 'interview scheduled · tomorrow 10:00', when: '4h', tone: 'brand' }, { t: 'stage', who: 'Thiago Almeida', what: 'moved to Screening · Frontend Engineer', when: '6h', tone: 'neutral' }, { t: 'offer-decline',who: 'Jonas Weber', what: 'declined offer · Senior Backend', when: '1d', tone: 'dest' }, { t: 'stage', who: 'Anika Pillai', what: 'moved to Applied · Frontend Engineer', when: '1d', tone: 'neutral' }, ]; const ATS_CONNECTORS = [ { id: 'db', name: 'In-house DB',status: 'Connected', detail: 'PostgreSQL on RDS · live' }, { id: 'greenhouse',name: 'Greenhouse', status: 'Available', detail: 'One-click connect' }, { id: 'workable', name: 'Workable', status: 'Available', detail: 'One-click connect' }, { id: 'ashby', name: 'Ashby', status: 'Available', detail: 'One-click connect' }, { id: 'csv', name: 'CSV import', status: 'Available', detail: 'Upload a file' }, ]; /* ─────────────── time periods ─────────────── */ const TIME_PERIODS = [ { id: 'week', label: 'This week', short: 'Week', bins: 7, binLabel: (i) => ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'][i] }, { id: 'month', label: 'This month', short: 'Month', bins: 4, binLabel: (i) => `W${i+1}` }, { id: 'quarter', label: 'This quarter', short: 'Quarter', bins: 12, binLabel: (i) => `W${i+1}` }, { id: 'year', label: 'This year', short: 'Year', bins: 12, binLabel: (i) => ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][i] }, ]; /* Seeded pseudo-random so charts stay stable across renders */ function _h(s) { let h = 2166136261; for (let i=0;i>> 0; return h; } function rand(seed) { let s = _h(seed); return () => { s = (s * 1103515245 + 12345) >>> 0; return ((s >>> 16) & 0x7fff) / 0x7fff; }; } /* Generate a metric series for a given KPI id + period + reqId scope. * Returns { values, current, target, trendPct }. */ function metricSeries(kpiId, periodId, reqId = 'all') { const period = TIME_PERIODS.find(p => p.id === periodId) || TIME_PERIODS[1]; const r = rand(`${kpiId}-${periodId}-${reqId}`); const reqFactor = reqId === 'all' ? 1 : (0.7 + r() * 0.6); const presets = { ttf: { base: 45, range: 18, target: 30, lowerBetter: true, trend: -8 }, tth: { base: 4.2, range: 1.6,target: 7, lowerBetter: true, trend: -22 }, offer: { base: 96, range: 8, target: 90, lowerBetter: false, trend: 4 }, screen: { base: 86, range: 8, target: 60, lowerBetter: false, trend: 6 }, attr: { base: 1.5, range: 1.0,target: 5, lowerBetter: true, trend: -1 }, reqs: { base: 6, range: 2, target: 6, lowerBetter: false, trend: 1 }, applied:{ base: 38, range: 22, target: 40, lowerBetter: false, trend: 12 }, hired: { base: 1.4, range: 1.2,target: 2, lowerBetter: false, trend: 0 }, }; const p = presets[kpiId] || presets.ttf; const values = []; for (let i = 0; i < period.bins; i++) { const v = (p.base + (r() - 0.5) * p.range) * reqFactor; values.push(Math.max(0, kpiId === 'reqs' ? Math.round(v) : v)); } const current = values[values.length - 1]; return { values, current, target: p.target, lowerBetter: p.lowerBetter, trendPct: p.trend, binLabel: period.binLabel, bins: period.bins, }; } /* ─────────────── helpers ─────────────── */ const fmt = (n) => n.toLocaleString('en-US'); const reqById = (id) => REQS.find(r => r.id === id); const candsForReq = (rid) => { const r = reqById(rid); if (!r) return []; return CANDIDATES.filter(c => c.role === r.role).sort((a,b) => b.score - a.score); }; const roleLabel = (rid) => (ROLE_GROUPS.find(r => r.id === rid)?.label || rid); const initials = (name) => name.split(' ').map(p => p[0]).slice(0,2).join(''); function scoreTone(s) { if (s >= 88) return 'prog'; if (s >= 75) return 'brand'; if (s >= 65) return 'warn'; return 'neutral'; } /* ─────────────── icons (inline, currentColor) ─────────────── */ const I = { Home: (p) => (), Inbox: (p) => (), Users: (p) => (), Briefcase:(p)=> (), Calendar:(p) => (), Chart: (p) => (), Settings:(p) => (), Search: (p) => (), Sparkle: (p) => (), Bell: (p) => (), ArrowUp: (p) => (), ArrowDn: (p) => (), Check: (p) => (), X: (p) => (), Filter: (p) => (), Plus: (p) => (), Dots: (p) => (), Globe: (p) => (), Clock: (p) => (), Tag: (p) => (), Plug: (p) => (), TrendUp: (p) => (), Send: (p) => (), }; /* ─────────────── atoms ─────────────── */ function Avatar({ name, size = 36, tone = 'neutral' }) { const tones = { neutral: { bg: 'var(--bg-tertiary)', fg: 'var(--content-primary)' }, brand: { bg: 'var(--indigo-20)', fg: 'var(--indigo-90)' }, prog: { bg: 'var(--green-20)', fg: 'var(--green-90)' }, warn: { bg: 'var(--yellow-20)', fg: 'var(--yellow-90)' }, violet: { bg: 'var(--violet-20)', fg: 'var(--violet-90)' }, mint: { bg: 'var(--mint-20)', fg: 'var(--mint-90)' }, orange: { bg: 'var(--orange-20)', fg: 'var(--orange-90)' }, }; const t = tones[tone] || tones.neutral; // deterministic tone from name const palettes = ['brand','prog','warn','violet','mint','orange','neutral']; const idx = (name || '').split('').reduce((a,c) => a + c.charCodeAt(0), 0) % palettes.length; const auto = tone === 'auto' ? tones[palettes[idx]] : t; return (
{initials(name)}
); } function Pill({ tone = 'neutral', children, dot, onClick, active }) { const map = { brand: { bg: 'var(--status-bg-brand)', fg: 'var(--status-text-brand)' }, prog: { bg: 'var(--status-bg-progressive)', fg: 'var(--status-text-progressive)' }, warn: { bg: 'var(--status-bg-warning)', fg: 'var(--status-text-warning)' }, dest: { bg: 'var(--status-bg-destructive)', fg: 'var(--status-text-destructive)' }, neutral: { bg: 'var(--status-bg-neutral)', fg: 'var(--status-text-neutral)' }, }; const t = map[tone] || map.neutral; const Tag = onClick ? 'button' : 'span'; return ( {dot && } {children} ); } function ScoreRing({ score, size = 44, stroke = 4, label }) { const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const dash = (score / 100) * c; const tone = scoreTone(score); const color = tone === 'prog' ? 'var(--green-50)' : tone === 'brand' ? 'var(--indigo-50)' : tone === 'warn' ? 'var(--yellow-50)' : 'var(--neutral-50)'; return (
{score}
{label &&
{label}
}
); } function Sparkbar({ values, height = 28, color }) { const arr = Array.isArray(values) && values.length > 0 ? values : [0]; const max = Math.max(...arr) || 1; return (
{arr.map((v,i) => (
))}
); } /* ─────────────── role switcher ─────────────── */ const RoleContext = React.createContext({ role: 'exec', setRole: () => {} }); function RoleSwitcher({ compact }) { const { role, setRole } = React.useContext(RoleContext); return (
{ROLES.map(r => ( ))}
); } /* ─────────────── filter bar ─────────────── */ function Select({ value, onChange, options, icon, width = 220 }) { const [open, setOpen] = React.useState(false); const wrap = React.useRef(null); React.useEffect(() => { if (!open) return; const fn = (e) => { if (!wrap.current?.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', fn); return () => document.removeEventListener('mousedown', fn); }, [open]); const cur = options.find(o => o.value === value) || options[0]; return (
{open && (
{options.map(o => ( ))}
)}
); } function FilterBar({ req, setReq, period, setPeriod, extra }) { const reqOptions = [ { value: 'all', label: 'All jobs', sub: REQS.length }, ...REQS.map(r => ({ value: r.id, label: r.title, sub: r.applicants })), ]; const periodOptions = TIME_PERIODS.map(p => ({ value: p.id, label: p.label })); return (
} /> {extra}
); } /* ─────────────── topbar ─────────────── */ function Topbar({ onSearch, onSettings, title = 'Dashboard' }) { return (

{title}

{/* "Ask anything" search + role switcher hidden for now */}
); } /* ─────────────── sidebar ─────────────── */ function Sidebar({ active = 'dashboard', onNav }) { const items = [ { id: 'dashboard', label: 'Dashboard', icon: }, { id: 'candidates', label: 'Candidates', icon: , count: CANDIDATES.length }, { id: 'pipeline', label: 'Kanban', icon: }, { id: 'reqs', label: 'Jobs', icon: , count: REQS.length }, { id: 'inbox', label: 'Inbox', icon: , count: 12 }, { id: 'interviews', label: 'Interview', icon: }, { id: 'reports', label: 'Reports', icon: }, ]; return ( ); } /* ─────────────── overlay shell ─────────────── */ function Modal({ open, onClose, width = 640, children, title, subtitle }) { if (!open) return null; return (
e.stopPropagation()} style={{ width: '100%', maxWidth: width, background: 'var(--bg-primary)', borderRadius: 16, boxShadow: 'var(--shadow-lg)', overflow: 'hidden', display: 'flex', flexDirection: 'column', maxHeight: 'calc(100% - 24px)' }}> {(title || onClose) && (
{title &&
{title}
} {subtitle &&
{subtitle}
}
)}
{children}
); } /* ─────────────── AI search modal ─────────────── */ function AISearchModal({ open, onClose, onOpenCandidate }) { const [q, setQ] = React.useState(''); const samples = [ 'Senior backend engineers in LATAM with payments experience', 'Designers under 30 days to start, EN/ES bilingual', 'PMs at risk of declining our offer', 'Reqs likely to miss the 30-day target this quarter', ]; const matches = React.useMemo(() => { if (!q.trim()) return CANDIDATES.slice(0, 5); const tokens = q.toLowerCase().split(/\s+/).filter(Boolean); const score = (c) => { const hay = (c.name + ' ' + c.loc + ' ' + c.langs.join(' ') + ' ' + c.reasons.join(' ') + ' ' + roleLabel(c.role)).toLowerCase(); let s = c.score / 4; tokens.forEach(t => { if (hay.includes(t)) s += 25; }); return s; }; return [...CANDIDATES].map(c => ({ ...c, _s: score(c) })).sort((a,b) => b._s - a._s).slice(0,6); }, [q]); React.useEffect(() => { if (open) setTimeout(() => document.getElementById('ai-q')?.focus(), 50); }, [open]); return (
setQ(e.target.value)} style={{ flex: 1, border: 0, outline: 0, fontSize: 16, background: 'transparent', color: 'var(--content-primary)', fontFamily: 'var(--font-sans)' }} /> esc
{!q && (
Try
)} {!q && (
{samples.map((s,i) => ( ))}
)}
{q ? 'Matches' : 'Top candidates'}
{matches.map(c => ( ))}
); } /* ─────────────── settings (ATS connectors) ─────────────── */ function SettingsModal({ open, onClose }) { const [active, setActive] = React.useState('db'); return (
ATS connector
{ATS_CONNECTORS.map(c => ( ))}
AI ranking model
Airtm Rank v2 · trained on 142k hires
Calibrated weekly · 84% precision@10 over the last 90 days
Active
); } /* ─────────────── candidate drawer ─────────────── */ // Map dashboard canonical stage label → pipeline stage ID (matches STAGE_CANONICAL in server.py) const _CANONICAL_TO_ID = { 'Applied': 'applied', 'Screening': 'intro_screening', 'Interview': 'hiring_manager', 'Take-home': 'technical_test', 'Offer': 'offer', 'Hired': 'hired', 'Rejected': 'rejected', }; const _STAGE_NEXT = { 'applied': 'intro_screening', 'intro_screening': 'hiring_manager', 'hiring_manager': 'technical_test', 'technical_test': 'offer', 'offer': 'hired', }; const _STAGE_LABELS = { 'intro_screening': 'Intro screening', 'hiring_manager': 'Hiring manager', 'technical_test': 'Technical test', 'offer': 'Offer', 'hired': 'Hired', 'rejected': 'Rejected', }; const _STAGE_BG = { 'intro_screening': 'var(--neutral-20)', 'hiring_manager': 'var(--indigo-10)', 'technical_test': 'var(--violet-10)', 'offer': 'var(--yellow-10)', 'hired': 'var(--green-10)', 'rejected': 'var(--red-10)', }; function CandidateDrawer({ candidate, onClose }) { const [pendingMove, setPendingMove] = React.useState(null); // stageId string const [moving, setMoving] = React.useState(false); // All hooks must be called before any early return (Rules of Hooks). const commitMove = React.useCallback((toStageId, emailData) => { if (!candidate) return; if (emailData && emailData.to && emailData.subject && emailData.body) { fetch('/api/email/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: emailData.to, subject: emailData.subject, body: emailData.body, opportunityId: candidate.id, }), }) .then(r => { if (!r.ok) console.error('[ATS] email send failed', r.status); }) .catch(err => console.error('[ATS] email send error', err)); } fetch(`/api/opportunities/${candidate.id}/stage`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stageId: toStageId }), }).catch(() => {}); setPendingMove(null); onClose(); }, [candidate, onClose]); if (!candidate) return null; // Live candidates (real DB) have a UUID id; mock candidates have ids like "c1". const isLive = /^[0-9a-f]{8}-/.test(candidate.id || ''); // Use the posting title for live data, role-group label for mock data. const displayRole = candidate.roleLabel || roleLabel(candidate.role); const currentStageId = _CANONICAL_TO_ID[candidate.stage] || 'intro_screening'; const nextStageId = _STAGE_NEXT[currentStageId] || null; const nextStageLabel = nextStageId ? (_STAGE_LABELS[nextStageId] || nextStageId) : null; const req = REQS.find(r => r.role === candidate.role); const initiateMove = (toStageId) => { if (!isLive) return; // demo data — no real opportunity to update // If the email composer is loaded (pipeline.jsx has run), use it. if (window.EmailComposer) { setPendingMove(toStageId); } else { setMoving(true); fetch(`/api/opportunities/${candidate.id}/stage`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stageId: toStageId }), }).then(() => onClose()).catch(() => setMoving(false)); } }; const EC = window.EmailComposer; const toStageLabel = pendingMove ? (_STAGE_LABELS[pendingMove] || pendingMove) : ''; return ( <> {pendingMove && EC && ( commitMove(pendingMove, emailData)} onDiscardAndMove={() => commitMove(pendingMove)} onCancel={() => setPendingMove(null)} /> )}
e.stopPropagation()} style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 480, background: 'var(--bg-primary)', boxShadow: 'var(--shadow-lg)', display: 'flex', flexDirection: 'column' }}>
{candidate.name}
{displayRole} · {candidate.loc}
Airtm Rank score
{candidate.score >= 88 ? 'Strong fit' : candidate.score >= 75 ? 'Good fit' : candidate.score >= 65 ? 'Possible fit' : 'Weak fit'}
{displayRole} applicant
{candidate.reasons && candidate.reasons.length > 0 && (
Why this rank
{candidate.reasons.map((r,i) => (
{r}
))}
)}
{candidate.expYrs > 0 && } {candidate.salary > 0 && } {candidate.source && } {candidate.lastTouch && } {candidate.langs && candidate.langs.length > 0 && } {candidate.stage && }
{req && (
Job
{req.title}
{req.dept} · {req.loc} · {req.applicants} applicants
{req.status} AI-predicted fill in {req.predicted}d · target {req.target}d
)}
{nextStageId && ( )}
); } function Stat({ label, value }) { return (
{label}
{value}
); } /* ─────────────── export ─────────────── */ Object.assign(window, { ROLES, ROLE_GROUPS, PIPELINE, CANDIDATES, REQS, UPDATES, ATS_CONNECTORS, TIME_PERIODS, fmt, reqById, candsForReq, roleLabel, initials, scoreTone, metricSeries, I, Avatar, Pill, ScoreRing, Sparkbar, Stat, Select, FilterBar, RoleContext, RoleSwitcher, Topbar, Sidebar, Modal, AISearchModal, SettingsModal, CandidateDrawer, });