/** * A very small element helper. * * No framework, deliberately. The rendering layer is imperative DOM work * (pagination, injected documents, shadow roots, column measurement) where a * virtual DOM adds a layer of indirection between the code and the layout it is * measuring — exactly the layer this product cannot afford to debug through. */ export type Child = Node | string | null | undefined | false; export interface ElementOptions { className?: string; text?: string; html?: string; attrs?: Record; on?: Partial void>>; children?: Child[]; dataset?: Record; } export function el( tag: K, options: ElementOptions = {}, ): HTMLElementTagNameMap[K] { const node = document.createElement(tag); if (options.className) node.className = options.className; if (options.text !== undefined) node.textContent = options.text; if (options.html !== undefined) node.innerHTML = options.html; for (const [key, value] of Object.entries(options.attrs ?? {})) { if (value === undefined || value === false) continue; node.setAttribute(key, value === true ? '' : String(value)); } for (const [key, value] of Object.entries(options.dataset ?? {})) { node.dataset[key] = value; } for (const [event, handler] of Object.entries(options.on ?? {})) { if (handler) node.addEventListener(event, handler); } for (const child of options.children ?? []) { if (child === null || child === undefined || child === false) continue; node.append(typeof child === 'string' ? document.createTextNode(child) : child); } return node; } export function clear(node: Element): void { while (node.firstChild) node.removeChild(node.firstChild); } export function formatDate(ms: number): string { if (!ms) return ''; const date = new Date(ms); const now = Date.now(); const diff = now - ms; if (diff < 60_000) return '刚刚'; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`; if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`; if (diff < 7 * 86_400_000) return `${Math.floor(diff / 86_400_000)} 天前`; return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } export function formatBytes(bytes: number): string { if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let value = bytes; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`; } export function percent(value: number): string { return `${Math.round(Math.min(1, Math.max(0, value)) * 100)}%`; }