/** Tiny DOM builder. Everything goes in as text, never as HTML. */
const SVG_NS = 'http://www.w3.org/2000/svg';
/**
* h('div.card#id', { class: 'extra', text: 'hi', onclick: fn, data: {…} }, ...children)
* Falsy children are skipped, so `cond && node` works inline.
*/
export function h(spec, props = null, ...children) {
const [tag, ...rest] = String(spec).split(/(?=[.#])/);
const el = document.createElement(tag || 'div');
for (const token of rest) {
if (token[0] === '.') el.classList.add(token.slice(1));
else el.id = token.slice(1);
}
if (props && (typeof props !== 'object' || props.nodeType || Array.isArray(props))) {
children.unshift(props);
props = null;
}
for (const [key, value] of Object.entries(props ?? {})) {
if (value == null || value === false) continue;
if (key === 'class') el.classList.add(...String(value).split(/\s+/).filter(Boolean));
else if (key === 'text') el.textContent = value;
else if (key === 'style' && typeof value === 'object') Object.assign(el.style, value);
else if (key === 'data') for (const [k, v] of Object.entries(value)) el.dataset[k] = v;
else if (key.startsWith('on') && typeof value === 'function') el.addEventListener(key.slice(2), value);
else if (key === 'value' || key === 'checked' || key === 'disabled') el[key] = value;
else el.setAttribute(key, value === true ? '' : value);
}
append(el, children);
return el;
}
export function append(parent, children) {
for (const child of children.flat(Infinity)) {
if (child == null || child === false || child === '') continue;
parent.append(child.nodeType ? child : document.createTextNode(String(child)));
}
return parent;
}
/** Inline icon from a path spec; keeps markup out of the rest of the app. */
export function icon(name, size = 22) {
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('width', size);
svg.setAttribute('height', size);
for (const d of ICONS[name] ?? ICONS.play) {
const node = document.createElementNS(SVG_NS, d.startsWith('circle:') ? 'circle' : 'path');
if (d.startsWith('circle:')) {
const [cx, cy, r] = d.slice(7).split(',');
node.setAttribute('cx', cx); node.setAttribute('cy', cy); node.setAttribute('r', r);
} else {
node.setAttribute('d', d);
}
svg.append(node);
}
return svg;
}
const ICONS = {
home: ['M4 10.6 12 4l8 6.6V20a1 1 0 0 1-1 1h-4v-6H9v6H5a1 1 0 0 1-1-1z'],
subs: ['M4 7h16M6 11h12M4.5 15h15a1 1 0 0 1 .8 1.6l-2 2.6a1.5 1.5 0 0 1-1.2.6H6.9a1.5 1.5 0 0 1-1.2-.6l-2-2.6A1 1 0 0 1 4.5 15z'],
fire: ['M12 3s5 4 5 8.5a5 5 0 0 1-10 0C7 9 9 7.5 9 7.5s.5 2 1.5 2S12 6 12 3z'],
history: ['M3.2 12a8.8 8.8 0 1 0 2.6-6.2', 'M3 4v4.5h4.5', 'M12 8v4.4l3 1.8'],
like: ['M7 21V10l5-7 1 .6a2 2 0 0 1 .9 2.2L13 10h5.4a2 2 0 0 1 2 2.5l-1.7 7A2 2 0 0 1 16.7 21z', 'M7 10H4v11h3z'],
dislike: ['M17 3v11l-5 7-1-.6a2 2 0 0 1-.9-2.2L11 14H5.6a2 2 0 0 1-2-2.5l1.7-7A2 2 0 0 1 7.3 3z', 'M17 14h3V3h-3z'],
clock: ['circle:12,12,8.6', 'M12 7v5.2l3.2 1.9'],
bookmark: ['M6.5 4h11a1 1 0 0 1 1 1v15.2a.6.6 0 0 1-.94.5L12 16.8l-5.56 3.9a.6.6 0 0 1-.94-.5V5a1 1 0 0 1 1-1z'],
channel: ['circle:12,8,4', 'M4.5 20a7.5 7.5 0 0 1 15 0'],
upload: ['M12 16V4', 'M7.5 8.5 12 4l4.5 4.5', 'M4 16v2.5A1.5 1.5 0 0 0 5.5 20h13a1.5 1.5 0 0 0 1.5-1.5V16'],
comment: ['M20 4H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h3v4l5-4h8a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1z'],
reply: ['M9 8 4 12.5 9 17', 'M4 12.5h9a7 7 0 0 1 7 7V21'],
share: ['circle:18,5.5,2.6', 'circle:6,12,2.6', 'circle:18,18.5,2.6', 'M8.3 10.7 15.7 6.8', 'M8.3 13.3l7.4 3.9'],
trash: ['M4 7h16', 'M9.5 7V5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v2', 'M6.5 7l.9 12a1 1 0 0 0 1 .9h7.2a1 1 0 0 0 1-.9L17.5 7'],
edit: ['M4 20h4L20 8l-4-4L4 16z', 'M14.5 5.5 18.5 9.5'],
play: ['M8 5.5v13l11-6.5z'],
film: ['M3 5h18v14H3z', 'M8 5v14M16 5v14', 'M3 12h18'],
search: ['circle:11,11,7', 'm20 20-3.5-3.5'],
signout: ['M15 7V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-2', 'M10 12h10', 'm17 9 3 3-3 3'],
check: ['m5 12.5 4.5 4.5L19 7'],
x: ['M6 6 18 18M18 6 6 18'],
bell: ['M18 15V10a6 6 0 1 0-12 0v5l-2 3h16z', 'M10 21h4'],
eye: ['M2.5 12S6.5 5.5 12 5.5 21.5 12 21.5 12 17.5 18.5 12 18.5 2.5 12 2.5 12z', 'circle:12,12,3.2'],
dots: ['circle:12,5,1.6', 'circle:12,12,1.6', 'circle:12,19,1.6'],
};
export const clear = (node) => { while (node.firstChild) node.firstChild.remove(); return node; };
export function mount(node, ...children) {
clear(node);
append(node, children);
return node;
}