/** Tiny DOM helpers (no framework). */ /** * el("button.cls#id", {onclick, ...attrs}, [children|string]) * Tag is optional (defaults to div); `.class` and `#id` tokens may appear in * any order and any count after it (e.g. "section.view#view-compose"). */ export function el(spec, attrs = {}, children = []) { const tagMatch = spec.match(/^[a-z][a-z0-9]*/i); const tag = tagMatch ? tagMatch[0] : "div"; const node = document.createElement(tag); const rest = spec.slice(tagMatch ? tagMatch[0].length : 0); const idMatch = rest.match(/#([\w-]+)/); if (idMatch) node.id = idMatch[1]; const classes = [...rest.matchAll(/\.([\w-]+)/g)].map((x) => x[1]); if (classes.length) node.className = classes.join(" "); for (const [k, v] of Object.entries(attrs)) { if (v == null || v === false) continue; if (k.startsWith("on") && typeof v === "function") { node.addEventListener(k.slice(2).toLowerCase(), v); } else if (k === "class") { node.className = node.className ? `${node.className} ${v}` : v; } else if (k === "html") { node.innerHTML = v; } else if (k in node && k !== "list") { try { node[k] = v; } catch { node.setAttribute(k, v); } } else { node.setAttribute(k, v); } } const kids = Array.isArray(children) ? children : [children]; for (const c of kids) { if (c == null || c === false) continue; node.append(c.nodeType ? c : document.createTextNode(String(c))); } return node; } export function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); return node; } /** Render a morse pattern string ('.-') as styled dot/dash chips. */ export function morsePattern(pattern) { const wrap = el("span.pattern"); for (const ch of pattern) { wrap.append(el(`span.${ch === "." ? "dot" : "dash"}`)); } return wrap; }