/** * Base class for web components. * Calls render() on connect and on any observed attribute change. */ export class BaseElement extends HTMLElement { connectedCallback() { this.render() } attributeChangedCallback() { this.render() } render() { } /** * Read an attribute with type coercion based on the fallback type. * - number fallback → parseFloat * - boolean fallback → true if attribute present and not "false" * - otherwise → string or fallback */ attr(name, fallback = null) { const val = this.getAttribute(name) if (val === null) return fallback if (typeof fallback === 'number') return parseFloat(val) if (typeof fallback === 'boolean') return val !== 'false' return val } /** * Dispatch a CustomEvent from this element. */ dispatch(type, detail = {}, options = {}) { this.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail, ...options })) } } /** * Register a custom element and return the class. * Lets you export default define('my-el', class extends BaseElement { ... }) */ export function define(tag, Cls) { customElements.define(tag, Cls) return Cls } /** * Tagged template for inline HTML strings. */ export function html(strings, ...values) { return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '').trim() } /** * Tagged template for inline CSS strings. */ export function css(strings, ...values) { return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '').trim() } /** * Create a DOM element with optional attributes and children. * * @param {string} tag * @param {Record} [attrs] * @param {...(Node|string)} children * @returns {HTMLElement} * * @example * el('button', { type: 'button', class: 'btn' }, 'Click me') */ export function el(tag, attrs = {}, ...children) { const node = document.createElement(tag) for (const [k, v] of Object.entries(attrs)) { if (v === false || v == null) continue if (v === true) node.setAttribute(k, '') else node.setAttribute(k, v) } node.append(...children) return node } /** * Shorthand for querySelector. Scoped to `root` (default: document). * @template {Element} T * @param {string} selector * @param {ParentNode} [root] * @returns {T|null} */ export function qs(selector, root = document) { return root.querySelector(selector) } /** * Shorthand for querySelectorAll. Returns a plain Array. * @template {Element} T * @param {string} selector * @param {ParentNode} [root] * @returns {T[]} */ export function qsa(selector, root = document) { return Array.from(root.querySelectorAll(selector)) } /** * Add an event listener and return a cleanup function. * @param {EventTarget} target * @param {string} event * @param {EventListener} handler * @param {AddEventListenerOptions} [options] * @returns {() => void} unlisten */ export function on(target, event, handler, options) { target.addEventListener(event, handler, options) return () => target.removeEventListener(event, handler, options) } /** * Wrap a plain object in a Proxy that calls onChange on any top-level property set. * Shallow only — nested mutations do not trigger onChange. */ export function reactive(init, onChange) { return new Proxy(init, { set(target, key, value) { target[key] = value onChange() return true }, }) }