// Tiny DOM helpers: element builder, toasts and a confirm dialog. /** Build an element. Props: class, on handlers, booleans, attributes. */ export function h(tag, props, ...kids) { const el = document.createElement(tag); for (const [k, v] of Object.entries(props ?? {})) { if (v == null || v === false) continue; if (k === 'class') el.className = v; else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v); else if (v === true) el.setAttribute(k, ''); else el.setAttribute(k, v); } el.append(...kids.flat(Infinity).filter((c) => c != null && c !== false)); return el; } export function clear(el) { el.replaceChildren(); return el; } export function toast(message, kind = 'info') { const host = document.getElementById('toasts'); const t = h('div', { class: `toast ${kind}`, role: kind === 'err' ? 'alert' : null }, message); host.append(t); setTimeout(() => { t.classList.add('out'); setTimeout(() => t.remove(), 400); }, kind === 'err' ? 8000 : 4500); } /** Resolve true if the user confirms. */ export function confirmDialog({ title, body, confirmText = 'CONFIRM', cancelText = 'ABORT', danger = false }) { const dlg = document.getElementById('dialog'); return new Promise((resolve) => { const finish = (v) => { dlg.close(); resolve(v); }; clear(dlg).append( h('div', { class: `panel dialog ${danger ? 'danger' : ''}` }, h('div', { class: 'panel-title' }, title), h('div', { class: 'panel-body' }, ...[].concat(body).map((p) => h('p', null, p))), h('div', { class: 'actions' }, h('button', { class: 'btn', type: 'button', onclick: () => finish(false) }, cancelText), h('button', { class: `btn ${danger ? 'danger' : 'primary'}`, type: 'button', onclick: () => finish(true) }, confirmText), ), ), ); dlg.oncancel = () => resolve(false); dlg.showModal(); }); } export function download(filename, bytes, type = 'application/octet-stream') { const url = URL.createObjectURL(new Blob([bytes], { type })); const a = h('a', { href: url, download: filename }); document.body.append(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); } export function pad(n, width, ch = '0') { return String(n).padStart(width, ch); } export function plural(n, one, many = `${one}S`) { return `${n} ${n === 1 ? one : many}`; }