/* ============================================================ ui.js — Reusable UI primitives & helpers Exposes global `UI` ============================================================ */ (function () { 'use strict'; const ICONS = { 'user-plus': '', 'calendar': '', 'check': '', 'check-circle': '', 'x': '', 'x-circle': '', 'file': '', 'star': '', 'message': '', 'info': '', 'alert': '', 'eye': '', 'edit': '', 'trash': '', 'more': '', 'plus': '', 'download': '', 'filter': '', 'clock': '', 'mail': '', 'phone': '', 'map': '', 'briefcase': '', 'trending-up': '', 'trending-down': '', 'users': '', 'award': '', 'dollar': '', 'target': '', 'send': '', 'video': '', 'search': '', 'chevron-left': '', 'chevron-right': '', 'refresh': '', 'copy': '', 'upload': '', 'linkedin': '', 'inbox': '', 'sparkles': '', 'zap': '', 'grid': '', 'bookmark': '', 'paperclip': '', 'external': '', 'shield': '', 'lock': '', 'flame': '', 'bell': '', 'layers': '', 'list': '', 'check-square': '', 'arrow-right': '' }; function icon(name, cls) { return `${ICONS[name] || ICONS['info']}`; } function avatar(name, initials, color, cls) { const bg = color || DB.avatarColor(name || ''); const init = initials || DB.initials(name || '?'); return `${init}`; } // ---------- badge helpers ---------- const statusMap = { 'Open': 'b-green', 'Closed': 'b-gray', 'On Hold': 'b-amber', 'Draft': 'b-blue', 'Applied': 'b-blue', 'Screening': 'b-purple', 'Assessment': 'b-amber', 'Interview': 'b-indigo', 'Offer': 'b-teal', 'Hired': 'b-green', 'Rejected': 'b-red', 'Scheduled': 'b-blue', 'Completed': 'b-green', 'Cancelled': 'b-red', 'No Show': 'b-amber', 'Sent': 'b-blue', 'Accepted': 'b-green', 'Negotiating': 'b-amber', 'Declined': 'b-red', 'Expired': 'b-gray', 'In Progress': 'b-amber', 'Pending': 'b-gray', 'Active': 'b-green', 'Invited': 'b-amber', 'Strong Hire': 'b-green', 'Hire': 'b-teal', 'Lean Hire': 'b-amber', 'No Hire': 'b-red' }; function badge(text, cls) { return `${text}`; } function scoreChip(score) { // Theme tokens, not fixed hex — the old greens/blues dropped to ~2.6:1 on dark cards. const color = score >= 85 ? 'var(--success)' : score >= 70 ? 'var(--warning)' : score >= 55 ? 'var(--info)' : 'var(--danger)'; return `${score}`; } function pbar(pct, cls) { const c = pct >= 80 ? 'green' : pct >= 50 ? '' : pct >= 30 ? 'amber' : 'red'; return `
`; } function avatarStack(names, max) { max = max || 3; const shown = names.slice(0, max); const extra = names.length - max; let html = '
'; shown.forEach(n => html += avatar(n, DB.initials(n))); if (extra > 0) html += `+${extra}`; return html + '
'; } // ---------- Modal ---------- function modal({ title, subtitle, body, footer, size }) { const root = document.getElementById('modalRoot'); root.innerHTML = ` `; root.classList.add('open'); document.body.style.overflow = 'hidden'; root.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', closeModal)); return root; } function closeModal() { const root = document.getElementById('modalRoot'); root.classList.remove('open'); root.innerHTML = ''; document.body.style.overflow = ''; } // ---------- Toast ---------- function toast(msg, type = 'info', title) { const root = document.getElementById('toastRoot'); const cfg = { success: { i: 'check-circle', c: 'i-green', t: 'Success' }, error: { i: 'x-circle', c: 'i-red', t: 'Error' }, info: { i: 'info', c: 'i-blue', t: 'Notice' }, warning: { i: 'alert', c: 'i-amber', t: 'Warning' } }[type] || { i: 'info', c: 'i-blue', t: 'Notice' }; const el = document.createElement('div'); el.className = 'toast'; el.innerHTML = ` ${icon(cfg.i)}
${title || cfg.t}
${msg}
`; root.appendChild(el); const remove = () => { el.classList.add('out'); setTimeout(() => el.remove(), 300); }; el.querySelector('.toast-close').onclick = remove; setTimeout(remove, 4200); } // ---------- Sortable / paginated table ---------- function dataTable(config) { // config: { columns:[{key,label,sortable,render,align}], rows, pageSize, empty } const state = { sortKey: null, sortDir: 1, page: 1, rows: config.rows }; const pageSize = config.pageSize || 10; const id = 'tbl_' + Math.random().toString(36).slice(2, 8); function sorted() { let r = state.rows; if (state.sortKey) { const col = config.columns.find(c => c.key === state.sortKey); r = [...r].sort((a, b) => { let va = col.sortValue ? col.sortValue(a) : a[state.sortKey]; let vb = col.sortValue ? col.sortValue(b) : b[state.sortKey]; if (typeof va === 'string') { va = va.toLowerCase(); vb = (vb || '').toLowerCase(); } if (va < vb) return -1 * state.sortDir; if (va > vb) return 1 * state.sortDir; return 0; }); } return r; } function render() { const rows = sorted(); const total = rows.length; const pages = Math.max(1, Math.ceil(total / pageSize)); if (state.page > pages) state.page = pages; const start = (state.page - 1) * pageSize; const pageRows = rows.slice(start, start + pageSize); const thead = config.columns.map(c => { const sortedCls = state.sortKey === c.key ? (state.sortDir === 1 ? 'sorted-asc' : 'sorted-desc') : ''; const ind = c.sortable ? `${state.sortKey === c.key ? (state.sortDir === 1 ? '▲' : '▼') : '⇅'}` : ''; return `${c.label}${ind}`; }).join(''); let tbody; if (!pageRows.length) { tbody = `
${icon('search')}

No results found

${config.empty || 'Try adjusting your filters or search.'}

`; } else { tbody = pageRows.map(row => `${config.columns.map(c => `${c.render ? c.render(row) : (row[c.key] ?? '')}`).join('')}`).join(''); } const from = total ? start + 1 : 0, to = Math.min(start + pageSize, total); const pager = pageButtons(state.page, pages); const el = document.getElementById(id); el.innerHTML = `
${thead}${tbody}
`; el.querySelectorAll('th.sortable').forEach(th => th.onclick = () => { const k = th.dataset.sort; if (state.sortKey === k) state.sortDir *= -1; else { state.sortKey = k; state.sortDir = 1; } render(); }); el.querySelectorAll('[data-page]').forEach(b => b.onclick = () => { const p = b.dataset.page; if (p === 'prev') state.page = Math.max(1, state.page - 1); else if (p === 'next') state.page = Math.min(pages, state.page + 1); else state.page = +p; render(); }); if (config.onRender) config.onRender(el); } function pageButtons(cur, pages) { let btns = ``; const list = []; for (let i = 1; i <= pages; i++) { if (i === 1 || i === pages || Math.abs(i - cur) <= 1) list.push(i); else if (list[list.length - 1] !== '…') list.push('…'); } list.forEach(i => btns += i === '…' ? `` : ``); btns += ``; return btns; } // public API return { html: `
`, mount: render, update(rows) { state.rows = rows; state.page = 1; render(); } }; } function fieldError(inputEl, msg) { inputEl.classList.add('err'); let err = inputEl.parentElement.querySelector('.field-error'); if (err) { err.textContent = msg; err.classList.add('show'); } } function clearErrors(form) { form.querySelectorAll('.err').forEach(e => e.classList.remove('err')); form.querySelectorAll('.field-error').forEach(e => e.classList.remove('show')); } window.UI = { icon, avatar, badge, scoreChip, pbar, avatarStack, modal, closeModal, toast, dataTable, fieldError, clearErrors, ICONS }; })();