feat(ui): static shell + router + api wrapper
Three-column grid (sidebar / main / right rail) with Cradle aesthetic: blackflame accent on Cinzel display headings + Cormorant Garamond body in cards, system UI for chrome. Hash-based router covers all entity routes plus search, inbox, sacred-valley. api.js stores OWNER_TOKEN in localStorage and prompts via a modal on 401. dom.js provides safe el() + mount() builders so no component ever assigns innerHTML from API data (the only exception is an explicit, scary-named html: opt-in for sanitizer output, used later by the markdown editor). state.js is a tiny event bus for shared chrome state (pending count). Components and views are loaded as ES modules — sidebar / topbar / rightrail + 9 view stubs that the later Phase E tasks fill in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
48
public/dom.js
Normal file
48
public/dom.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// Safe DOM builder used by all components / views. Never assigns
|
||||
// innerHTML from API data — all text is set via textContent (auto-escaped
|
||||
// by the browser), all structure via createElement.
|
||||
//
|
||||
// Usage:
|
||||
// el('div', { class: 'card', onclick: fn }, 'title', el('span', {}, 'sub'))
|
||||
// Children may be strings (becomes text node), Nodes, arrays of either,
|
||||
// or null/undefined (ignored).
|
||||
|
||||
export function el(tag, attrs = {}, ...children) {
|
||||
const node = document.createElement(tag);
|
||||
for (const [k, v] of Object.entries(attrs || {})) {
|
||||
if (v === null || v === undefined || v === false) continue;
|
||||
if (k === 'class') node.className = v;
|
||||
else if (k === 'style' && typeof v === 'object') Object.assign(node.style, v);
|
||||
else if (k.startsWith('on') && typeof v === 'function') node.addEventListener(k.slice(2).toLowerCase(), v);
|
||||
else if (k === 'dataset' && typeof v === 'object') Object.assign(node.dataset, v);
|
||||
else if (k === 'html') {
|
||||
// Explicit, scary-named opt-in. ONLY use with strings produced by
|
||||
// marked() or other vetted sanitizers — never with raw API data.
|
||||
node.innerHTML = v;
|
||||
} else if (k in node && typeof node[k] !== 'function') {
|
||||
try { node[k] = v; } catch { node.setAttribute(k, v); }
|
||||
} else {
|
||||
node.setAttribute(k, v);
|
||||
}
|
||||
}
|
||||
appendAll(node, children);
|
||||
return node;
|
||||
}
|
||||
|
||||
function appendAll(parent, children) {
|
||||
for (const c of children) {
|
||||
if (c === null || c === undefined || c === false) continue;
|
||||
if (Array.isArray(c)) appendAll(parent, c);
|
||||
else if (c instanceof Node) parent.appendChild(c);
|
||||
else parent.appendChild(document.createTextNode(String(c)));
|
||||
}
|
||||
}
|
||||
|
||||
export function clear(node) {
|
||||
while (node.firstChild) node.removeChild(node.firstChild);
|
||||
}
|
||||
|
||||
export function mount(node, ...children) {
|
||||
clear(node);
|
||||
appendAll(node, children);
|
||||
}
|
||||
Reference in New Issue
Block a user