/** * Islands infrastructure. * * An island is a custom element that attaches behavior to server-rendered * markup. It never re-renders what the server sent, so a page keeps working * with the script blocked and the element only adds what needs a client. * * The payoff over the `DOMContentLoaded` + `htmx:after:swap` re-init dance that * static/mt.js does by hand: the browser upgrades a custom element whenever it * is connected to the document, including markup htmx swapped in, so an island * needs no manual re-initialization. * * Ported from MNW/server/frontend/src/islands/base.ts, deliberately as a copy. * The two apps share no workspace and no registry, and 30 lines duplicated is * cheaper than the mechanism that would let them share it. See the Client * section of the livechat design note. * * Pure logic goes in a sibling `*.logic.ts` with no DOM imports, so * `node --test` exercises it without a DOM; the element file wires that logic * over the light-DOM children. */ /** * Base class: runs `init()` exactly once, the first time the element is * connected. Moving the element in the document will not re-run it. */ export abstract class MtElement extends HTMLElement { #initialized = false; connectedCallback(): void { if (this.#initialized) return; this.#initialized = true; this.init(); } /** Wire behavior over the already-rendered light-DOM children. */ protected abstract init(): void; } /** * Define a custom element once. Idempotent, so importing the module from more * than one entry point does not throw on the second definition. */ export function define(name: string, ctor: CustomElementConstructor): void { if (!customElements.get(name)) customElements.define(name, ctor); }