// Tiny DOM helpers — no framework, just ergonomic element creation. // el('div', { class: 'x', onClick: fn }, child, child, ...) // Attrs: class, dataset(obj), style(obj), html(innerHTML), on(fn), // boolean true -> present attribute, anything else -> setAttribute. export function el(tag, attrs = {}, ...children) { const node = document.createElement(tag); for (const [key, val] of Object.entries(attrs || {})) { if (val == null || val === false) continue; if (key === 'class') node.className = val; else if (key === 'dataset') Object.assign(node.dataset, val); else if (key === 'style' && typeof val === 'object') { // Custom properties (--x) must go through setProperty; plain assignment // silently drops them. for (const [prop, pv] of Object.entries(val)) { if (pv == null) continue; if (prop.startsWith('--')) node.style.setProperty(prop, pv); else node.style[prop] = pv; } } else if (key === 'html') node.innerHTML = val; else if (key === 'value') node.value = val; else if (key.startsWith('on') && typeof val === 'function') node.addEventListener(key.slice(2).toLowerCase(), val); else if (val === true) node.setAttribute(key, ''); else node.setAttribute(key, val); } appendAll(node, children); return node; } function appendAll(node, children) { for (const child of children.flat(Infinity)) { if (child == null || child === false || child === true) continue; node.appendChild( typeof child === 'string' || typeof child === 'number' ? document.createTextNode(String(child)) : child, ); } } export function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); return node; } export function mount(container, ...children) { clear(container); appendAll(container, children); return container; } export function loading(text = 'Loading…') { return el('div', { class: 'loading' }, el('span', { class: 'spinner' }), text); } export function errorBox(message, onRetry) { return el( 'div', { class: 'error-box' }, el('p', {}, message || 'Something went wrong.'), onRetry ? el('button', { class: 'btn', onClick: onRetry }, 'Retry') : null, ); } export function emptyState(title, subtitle, action) { return el( 'div', { class: 'empty-state' }, el('div', { class: 'empty-icon' }, '🧭'), el('h3', {}, title), subtitle ? el('p', {}, subtitle) : null, action || null, ); } let toastHost = null; export function toast(message, type = 'error', ms = 4200) { if (!toastHost) { toastHost = el('div', { class: 'toast-host' }); document.body.appendChild(toastHost); } const node = el('div', { class: `toast toast-${type}` }, message); toastHost.appendChild(node); requestAnimationFrame(() => node.classList.add('show')); setTimeout(() => { node.classList.remove('show'); setTimeout(() => node.remove(), 300); }, ms); }