| 1 |
// Islands infrastructure. |
| 2 |
// |
| 3 |
// An "island" is a custom element that attaches behavior to server-rendered |
| 4 |
// markup. It never re-renders the server's HTML (no hydration). A key payoff |
| 5 |
// over the old `querySelectorAll` + `DOMContentLoaded` + `htmx:afterSwap` |
| 6 |
// re-init dance: the browser upgrades a custom element automatically whenever |
| 7 |
// it's connected to the document, INCLUDING content HTMX swaps in, so islands |
| 8 |
// need no manual re-initialization after a swap. |
| 9 |
// |
| 10 |
// Pure, testable logic goes in a sibling `*.logic.ts` (no DOM imports) so |
| 11 |
// `node --test` can exercise it without a DOM; the element file wires that |
| 12 |
// logic over the light-DOM children. |
| 13 |
|
| 14 |
/** Base class: runs `init()` exactly once, the first time the element is |
| 15 |
* connected (a move/reconnect won't re-run it). */ |
| 16 |
export abstract class MnwElement extends HTMLElement { |
| 17 |
#initialized = false; |
| 18 |
|
| 19 |
connectedCallback(): void { |
| 20 |
if (this.#initialized) return; |
| 21 |
this.#initialized = true; |
| 22 |
this.init(); |
| 23 |
// `data-island` means this element's behaviour ran. Nothing styles it; it |
| 24 |
// exists to be asserted on. |
| 25 |
// |
| 26 |
// Set AFTER init() on purpose. If init throws, the attribute is absent and |
| 27 |
// the element is visibly un-enhanced to a checker, which is the state that |
| 28 |
// used to be undetectable. |
| 29 |
// |
| 30 |
// 2026-08-14 is why this is here. A stale module in the bundle's import |
| 31 |
// graph made `core/index.js` fail to link, so no island on the site |
| 32 |
// registered at all — and the page looked fine, because every island in |
| 33 |
// this codebase enhances server-rendered markup that stands on its own |
| 34 |
// without it. Progressive enhancement means a dead bundle and a healthy one |
| 35 |
// differ only in behaviour, and behaviour is the thing nothing was |
| 36 |
// checking. `scripts/page-smoke.mjs` checks it now, and this is what it |
| 37 |
// reads. |
| 38 |
this.setAttribute('data-island', ''); |
| 39 |
} |
| 40 |
|
| 41 |
/** Wire behavior over the already-rendered light-DOM children. */ |
| 42 |
protected abstract init(): void; |
| 43 |
} |
| 44 |
|
| 45 |
/** Define a custom element once (idempotent, safe if the module is imported |
| 46 |
* from more than one entry). */ |
| 47 |
export function define(name: string, ctor: CustomElementConstructor): void { |
| 48 |
if (!customElements.get(name)) customElements.define(name, ctor); |
| 49 |
} |
| 50 |
|