frontend: fix hive-btn crashing every themed dialog — customized built-ins can't host shadow DOM

Element.attachShadow() throws NotSupportedError unconditionally for a
customized built-in (<button is="hive-btn">): the spec only allows
autonomous custom elements or a fixed list of native tags to host a
shadow root, and explicitly excludes any is=-upgraded built-in
regardless of which tag it upgrades. button isn't on that list either
way. This made every themed dialog (any confirm/prompt, since openDialog
always renders at least one button) throw and fail to render in a real
browser, though it passed CI since nothing there exercises actual
browser DOM.

hive-dialog and hive-toast are unaffected — both are genuine autonomous
custom elements (extends HTMLElement, no is= upgrade), which are valid
shadow hosts.

Fix: hive-btn no longer calls attachShadow. Styles adopt onto document
once (module-level guard) instead of per-instance shadow root, scoped
via the [is="hive-btn"] attribute selector instead of :host — same
light-DOM approach the rest of the app's .btn consumers already use.
Native button behaviour is untouched, only the styling mechanism
changed. Build clean.
This commit is contained in:
iris 2026-07-29 22:51:24 +02:00
commit b201f6be88
3 changed files with 61 additions and 51 deletions

View file

@ -5,14 +5,15 @@
// dialog.
//
// Implemented as two shadow-DOM custom elements (`<hive-dialog>`,
// `<hive-toast>`), plus `<hive-btn>` (hive-btn.js) for the dialog's own
// buttons — real per-component style encapsulation, CSS in real `.css`
// files imported as raw text (see each component's own header comment
// for the design rationale: shadow-DOM-vs-light-DOM tradeoffs live atop
// the `<hive-dialog>`/`<hive-toast>` classes below, the customized-
// built-in choice lives in hive-btn.js). Other `.btn` consumers across
// the app stay on the light-DOM `.btn` class for now — migrating them
// is a separate follow-up.
// `<hive-toast>`) — real per-component style encapsulation, CSS in real
// `.css` files imported as raw text (see each component's own header
// comment for the shadow-DOM-vs-light-DOM tradeoffs) — plus `<hive-btn>`
// (hive-btn.js) for the dialog's own buttons, which is NOT shadow-DOM
// encapsulated: a customized built-in (`<button is="hive-btn">`) can't
// host a shadow root at all, so it styles itself via a document-level
// stylesheet instead (see hive-btn.js's header for why). Other `.btn`
// consumers across the app stay on the light-DOM `.btn` class for now —
// migrating them is a separate follow-up.
//
// `openDialog` is the general primitive (any title/message/content + a row of
// buttons); `themedConfirm` is a thin cancel/confirm wrapper with optional