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

@ -1,14 +1,20 @@
/* hive-btn.css scoped stylesheet for the <hive-btn> customized built-in
button element (hive-btn.js). Loaded as raw text at build time
(esbuild's `text` loader) and turned into a `CSSStyleSheet` adopted by
the element's own shadow root. `:host` styles the button itself (the
element *is* a real `<button is="hive-btn">`, so `:host(:hover)` /
`:host(:disabled)` select genuine native pseudo-classes, not
hand-rolled state tracking) this is the base look every `<hive-btn>`
gets; `variant` is an attribute (not a class) since it's a semantic
property of the component, not an arbitrary styling hook. */
/* hive-btn.css stylesheet for the <hive-btn> customized built-in button
element (hive-btn.js). Loaded as raw text at build time (esbuild's
`text` loader) and adopted once on `document` (see hive-btn.js) NOT
scoped to a shadow root. A customized built-in (`<button is="hive-btn">`)
can't host one: `Element.attachShadow()` only accepts autonomous custom
elements or a fixed list of native tags that doesn't include `button`,
and explicitly excludes `is=`-upgraded built-ins regardless of tag
so this styles via the `[is="hive-btn"]` attribute selector instead of
`:host`, same light-DOM-scoping approach the rest of the app's `.btn`
consumers already use, just keyed off the attribute instead of a class.
`:hover`/`:disabled` below are still genuine native pseudo-classes on a
real `<button>`, not hand-rolled state tracking that part of the
original design goal survives even without shadow encapsulation.
`variant` is an attribute (not a class) since it's a semantic property
of the component, not an arbitrary styling hook. */
:host {
[is="hive-btn"] {
font-family: inherit;
font-weight: bold;
text-transform: uppercase;
@ -24,17 +30,17 @@
box-shadow: 0 0 0 0 currentColor;
transition: box-shadow 0.15s ease;
}
:host(:hover) {
[is="hive-btn"]:hover {
background: color-mix(in srgb, var(--fg) 6%, transparent);
text-shadow: 0 0 10px currentColor;
box-shadow: 0 0 10px -2px currentColor;
}
:host(:disabled) {
[is="hive-btn"]:disabled {
opacity: 0.32;
cursor: not-allowed;
text-shadow: none;
box-shadow: none;
}
:host([variant="cancel"]) { color: var(--subtext0); }
:host([variant="confirm"]) { color: var(--green); }
:host([variant="danger"]) { color: var(--red); }
[is="hive-btn"][variant="cancel"] { color: var(--subtext0); }
[is="hive-btn"][variant="confirm"] { color: var(--green); }
[is="hive-btn"][variant="danger"] { color: var(--red); }