/* ============================================================
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 ``; }
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 = `
${title}
${subtitle ? `
${subtitle}
` : ''}
${body}
${footer ? `` : ''}
`;
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)}
`;
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 = `
`;
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 };
})();