@adia-ai/web-components 0.6.7 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.6.9] — 2026-05-21
4
+
5
+ ### Fixed
6
+ - **9 component CSS files — `@scope` blocks no longer use bare-combinator descendants (F-002, P1).** Pre-fix: 92 selector clauses across `action-list.css`, `card.css`, `chart.css`, `chat-thread.css`, `code.css`, `command.css`, `grid.css`, `pane.css`, `stack.css` used the `@scope { > X { } }` shape (bare `>` combinator with no left-hand selector inside `@scope`). Chromium/Firefox tolerate this; LightningCSS rejects it as "Invalid empty selector" (matches the CSS Nesting + `@scope` spec strictly). Symptom: consumers on Vite 7 (default `cssMinify: 'lightningcss'`) saw build-time failures at `card.css:152`; Vite 6 + earlier Vite 7 (esbuild minify) silently swallowed the same source. Fix: codemod-driven rewrite of all 92 clauses from `> X` to `& > X` (explicit `&` reference matches the modern CSS Nesting idiom; semantically identical; LightningCSS-clean). All 120 CSS files now pass LightningCSS minify against Vite 8 default targets (Chromium 125+, Safari 18+, Firefox 129+). Codemod source: `scripts/build/codemod-scope-bare-descendants.mjs` (committed for future audits; idempotent + `--verify` mode). Paired CI gate `check:scope-bare-descendants` blocks regression at PR time; companion `check:lightningcss-build` smoke-minifies every CSS file end-to-end against Vite 8 targets. Closes claims-ui FEEDBACK-02.
7
+
8
+ ### Documentation
9
+ - **`USAGE.md` — new "Looking up a component's API" subsection.** Three discovery paths now documented in one place: `npx adia-ai-doc <component>` CLI (machine-readable + offline-safe), `<component>.examples.html` per-component live demo + Properties table, and `<component>.yaml` source-of-truth schema. Closes claims-ui FEEDBACK-09 (per-component opt-out attributes are documented; this subsection makes the doc-path discoverable from the top-level USAGE entrypoint).
10
+
11
+ ## [0.6.8] — 2026-05-20
12
+
13
+ ### Fixed
14
+ - **`core/template.js` — `.camelCaseProp=${expr}` property bindings now route to the canonical setter (FB-55, P1).** Pre-fix: the HTML parser lowercases attribute names inside `<template>.innerHTML` per HTML5 §13.2.5.32, so `.className=${expr}` arrived at the binding scanner as `.classname`. The binding then wrote `element['classname'] = v` — an enumerable expando — never invoking the `HTMLElement.prototype.className` setter. Classes never applied; no warning, no error. Trap affected every camelCase DOM property surface (`.className`, `.innerText`, `.tabIndex`, `.ariaLabel`, `.contentEditable`, `.readOnly`, `.maxLength`, `.minLength`, `.colSpan`, `.rowSpan`) AND every camelCase property declared via `UIElement.static properties` (80+ across primitives — `<color-picker>` `.maxChroma`, `<grid>` `.columnGap`, `<breadcrumb>` `.collapseKeepLeading`, etc.). Even the canonical `<my-panel .minL=${minL}>` example from `README.md:212` silently failed. **Fix: two-layer name resolution** — (1) `PROP_CASE_FIX` static map at `scan()` time covers built-in DOM camelCase (zero-cost lookup, no prototype walk for the common case); (2) lazy prototype-walk fallback at `applyValue()` time finds canonical camelCase property names on the element's instance + prototype chain via case-insensitive match, caching resolution on the part after first hit. Catches UIElement custom-element props installed per-instance in `installProps()` (those don't exist at `scan()` time because the element hasn't been upgraded yet). Backward-compatible: when neither layer finds a case-insensitive match, the original lowercase name is preserved (consumers writing to lowercase expandos unaffected). 10 NEW vitest cases in `core/template.test.js`'s `FB-55` describe block pin the contract. Closes RESPONSE-55 (reviewer-#A expansion to UIElement primitives explicitly covered).
15
+ - **`core/template.js` — nested `${html\`<svg-child/>\`}` interpolations now render in SVG namespace (FB-57, P1).** Pre-fix: each nested `html\`\`` is a separate cached template whose `tpl.innerHTML = m` is parsed at document level without SVG insertion-mode context, so `<circle>`/`<line>`/etc. arrived as `HTMLUnknownElement` (NS `http://www.w3.org/1999/xhtml`). The `<span style="display:contents">` wrapper from `wrap()` didn't fix it because `display: contents` is layout-level, not namespace-level; SVG layout is namespace-strict → present-but-invisible output, zero bounding rect, no error. **Fix: namespace-aware `mount()`** — discriminate via `container.closest('svg, math, foreignObject, annotation-xml')` at mount time; when stamping into an SVG/MathML subtree, recursively re-namespace the cloned fragment via `createElementNS`, transferring attributes via `setAttributeNS` to preserve attr-namespaces (e.g., legacy `xlink:href`). `<foreignObject>` (SVG) and `<annotation-xml encoding="text/html"|"application/xhtml+xml">` (MathML) descendants correctly REVERT to HTML namespace per HTML5 §12.2.5 foreign-content insertion mode. Implementation site: `mount()` rather than `getTemplate()` — keeps the template cache coherent (same `html\`\`` source can stamp into either context without doubling cache entries). 6 NEW vitest cases in `core/template.test.js`'s `FB-57` describe block (inline-SVG regression, nested-SVG fix, direct-stamp into SVG container, foreignObject boundary, MathML, HTML-container no-op). Closes RESPONSE-57 (Option A accepted with implementation-site refinement; Option B rejected as half-measure).
16
+ - **`core/template.js` — partial-interp warning text recommends `class="${expr}"` first (FB-55 #2).** The v0.5.5 §184 warn text put `.className=${expression}` first as the recommended fix for partial class interpolation; that path now works post-FB-55 fix above, but `class="${expr}"` (full-attribute interpolation) is the more discoverable form that doesn't require knowing the camelCase property name. Promoting it first. The `.classList=${{foo: true, bar: false}}` aspirational line is removed — classList is a read-only DOMTokenList accessor, no `PROP_CASE_FIX` entry can make it assignable.
17
+
18
+ ### Docs
19
+ - **`USAGE.md` — three new template-related sections** documenting the FB-55/56/57 cluster:
20
+ - **§-TBD (v0.6.8) — Host element display defaults (FB-56)** — surfaces the HTML-spec inline default for custom elements + the `:host(:where(*))` zero-specificity recipe used across the in-tree primitive set (30+ flex-container primitives). Documents why `UIElement` doesn't bake a `display` rule into the host (container vs leaf vs passthrough is a consumer-layout decision). Worked symptom + diagnostic recipe ("inspect the host directly — `getComputedStyle(host).display === 'inline'`").
21
+ - **§-TBD (v0.6.8) — SVG composition + namespacing (FB-57)** — documents three modes (inline, nested-interpolations, imperative-`.innerHTML`) with the `<foreignObject>` boundary semantics and the MathML parallel. Migration is zero — pre-fix code that worked keeps working; pre-fix code that was silently broken (nested SVG interpolations) starts producing visible output.
22
+ - **§-TBD (v0.6.8) — `.camelCaseProp=${expr}` property binding (FB-55)** — explains the two-layer name resolution, lists affected camelCase property surfaces, documents backward-compatibility for genuine expando consumers, and notes the discoverability trade-off (the simpler `class="${expr}"` doesn't require knowing the camelCase property name).
23
+ - **USAGE.md `§-TBD (v0.6.8) — UIElement lifecycle hooks (FB-59 + FB-43/44/47/58 cluster)`** — new section documenting the canonical hook order: `adoptStyles` → `prepareParts` → **`connected()`** (line 122, inside `untracked()`) → effect first-run with **stamp at line 127** → `render()` → `updated()`. Source-cited from `core/element.js:107-142`. Resolves a cluster-wide misunderstanding about when `connected()` fires relative to the template stamp: it runs BEFORE the stamp wipes consumer-supplied light-DOM children, NOT after. Includes the "Container UIElement pattern" example showing `connected()` → capture children → arm MutationObserver, plus the defensive `if (present.length > 0) keep` cache-fallback pattern (consumer-discovered) that makes capture robust to ordering. Distinguishes `connected()` (recommended subclass hook) vs `connectedCallback()` (rarely overridden W3C hook). Closes FB-59 P3 docs ask + corrects the §B mechanism walkthrough in RESPONSE-58 (sibling followup-RESPONSE retracts the §B order claim).
24
+
3
25
  ## [0.6.7] — 2026-05-19
4
26
 
5
27
  ### Added
package/USAGE.md CHANGED
@@ -59,6 +59,35 @@ import { signal, computed, effect, html, UIElement, UIFormElement } from '@adia-
59
59
  import '@adia-ai/web-components/traits';
60
60
  ```
61
61
 
62
+ ### Looking up a component's API
63
+
64
+ Three reliable surfaces, in order of immediacy:
65
+
66
+ ```bash
67
+ # 1. CLI (since v0.6.7) — prints yaml summary + live demo URL + first example
68
+ npx adia-ui-doc table-toolbar # full prop / slot / event surface
69
+ npx adia-ui-doc # bare invocation lists all 96 components
70
+ ```
71
+
72
+ ```bash
73
+ # 2. Per-component examples (ship in npm tarball since v0.6.7)
74
+ cat node_modules/@adia-ai/web-components/components/table-toolbar/table-toolbar.examples.md
75
+
76
+ # Or the richer .examples.html for Properties tables + multi-section walks
77
+ open node_modules/@adia-ai/web-components/components/table-toolbar/table-toolbar.examples.html
78
+ ```
79
+
80
+ ```bash
81
+ # 3. The component's yaml (authoritative — all props, defaults, descriptions)
82
+ cat node_modules/@adia-ai/web-components/components/table-toolbar/table-toolbar.yaml
83
+ ```
84
+
85
+ The yaml is the source of truth for every property, slot, event, and token.
86
+ The CLI reads it. The `.d.ts` files are generated from it. When this USAGE
87
+ guide and the yaml disagree, the yaml wins — file a doc-bug FEEDBACK.
88
+
89
+ For the live demo of every component: <https://ui-kit.exe.xyz/site/components/>.
90
+
62
91
  ---
63
92
 
64
93
  ## The mental model
@@ -1181,6 +1210,115 @@ Why AdiaUI doesn't implement `?attr=`: custom elements declare their reflective
1181
1210
  - HTML comments containing quoted attributes (`<!-- attr="value" -->`) — same fix (v0.5.3 §155).
1182
1211
  - Backticks inside HTML comments — see §221i above (JS-spec footgun; not a parser bug).
1183
1212
 
1213
+ ### §-TBD (v0.6.8) — Host element display defaults (FB-56)
1214
+
1215
+ Per the HTML spec, **all custom elements default to `display: inline`** — and `UIElement` subclasses are no exception. When you use a `UIElement` subclass as a layout container (filling a flex parent, holding an SVG canvas, wrapping a fluid grid), the inline default collapses the host to zero height and any `height: 100%` descendant collapses with it.
1216
+
1217
+ **Symptom you're hitting this trap:** content renders in DevTools with the expected DOM, but its computed height is 0 because the custom-element host between it and a layout parent has `display: inline`. Inspect the host element directly — `getComputedStyle(host).display === 'inline'` confirms.
1218
+
1219
+ If you author a `UIElement` to be a layout container, set an explicit `display` on the host tag — the in-tree convention is `:host(:where(*))` so consumer-app CSS can still override without `!important`:
1220
+
1221
+ ```js
1222
+ import { css } from '@adia-ai/web-components/core/element';
1223
+
1224
+ class MyContainer extends UIElement {
1225
+ static styles = css`
1226
+ :host(:where(*)) { /* zero-specificity wrap; consumers override at (0,0,1) */
1227
+ display: flex;
1228
+ flex-direction: column;
1229
+ flex: 1 1 auto;
1230
+ min-height: 0;
1231
+ }
1232
+ `;
1233
+ static template = (host) => html`<slot></slot>`;
1234
+ }
1235
+ ```
1236
+
1237
+ Or, if the host should be transparent to layout (its children inherit the parent's grid/flex context directly):
1238
+
1239
+ ```css
1240
+ my-passthrough-element { display: contents; }
1241
+ ```
1242
+
1243
+ See also: the slot-routing pass-through pattern that's the natural consumer of `display: contents` (cross-referenced from §345's `.map()` vs `repeat()` discussion where `wrap()` spans use the same mechanism).
1244
+
1245
+ **`UIElement` does not bake a `display` rule into the host** because container vs leaf vs passthrough is a consumer-layout decision, not a framework one. Per primitive, a global default would risk breaking layouts where the consumer explicitly wants `inline` semantics (e.g. inline badge inside flowing text, `<icon>` rendered alongside surrounding `<text-ui>`, `<kbd-ui>` keystroke marker). The in-tree primitive set spans `inline` / `inline-block` / `block` / `flex` / `grid` / `contents` — no single default fits.
1246
+
1247
+ **In-tree convention reference:** scan `packages/web-components/components/*/<name>.css` for the `:host(:where(*))` pattern. The 30+ flex-container primitives (`<editor-shell>`, `<nav>`, `<pane>`, `<page>`, `<card>`, etc.) all use this shape; copy from the closest-fit primitive.
1248
+
1249
+ ### §-TBD (v0.6.8) — SVG composition + namespacing (FB-57)
1250
+
1251
+ `html\`\`` handles SVG content in three modes, with one historical trap closed in v0.6.8.
1252
+
1253
+ **Mode 1 — inline SVG in a single template (always worked):**
1254
+
1255
+ ```js
1256
+ html`
1257
+ <svg viewBox="0 0 100 100">
1258
+ <circle cx="50" cy="50" r="10"/>
1259
+ <path d="M 0 0 L 100 100"/>
1260
+ </svg>
1261
+ `
1262
+ ```
1263
+
1264
+ When the parser sees `<svg>` in `tpl.innerHTML`, it enters SVG insertion mode for its descendants. Children arrive as proper `SVGElement` instances.
1265
+
1266
+ **Mode 2 — nested template interpolations (now works, post-v0.6.8 FB-57 fix):**
1267
+
1268
+ ```js
1269
+ html`
1270
+ <svg viewBox="0 0 100 100">
1271
+ ${dots.map(d => html`<circle cx="${d.x}" cy="${d.y}" r="3"/>`)}
1272
+ </svg>
1273
+ `
1274
+ ```
1275
+
1276
+ Each nested `html\`<circle/>\`` is a separate template parsed at document level (no SVG context), so pre-v0.6.8 the circles arrived as `HTMLUnknownElement` — present in the DOM but invisible to SVG layout (zero bounding rect, no stroke, no fill, no error). v0.6.8 `mount()` detects when the container is inside an SVG (or MathML) subtree and re-namespaces the cloned fragment via `createElementNS`. The natural composition pattern works.
1277
+
1278
+ **Mode 3 — imperative `.innerHTML` commit (escape hatch for very large SVG):**
1279
+
1280
+ For SVG content too large to template cleanly (thousands of nodes, dynamic shape strings):
1281
+
1282
+ ```js
1283
+ class MyChart extends UIElement {
1284
+ static template = (host) => html`<svg viewBox="0 0 ${host.w} ${host.h}"></svg>`;
1285
+ updated() {
1286
+ const svg = this.querySelector('svg');
1287
+ svg.innerHTML = host._buildSvgString(); // SVG-namespaced via SVGSVGElement.innerHTML
1288
+ }
1289
+ }
1290
+ ```
1291
+
1292
+ `.innerHTML=${str}` as a template binding also works for this (the property name is already lowercase, so the FB-55 PROP_CASE_FIX entry just maps `innerhtml` → `innerHTML`, and the assignment routes through the SVGSVGElement's `innerHTML` setter which DOES enter SVG insertion mode).
1293
+
1294
+ **`<foreignObject>` boundary:** content inside `<foreignObject>` returns to HTML namespace per HTML5 §12.2.5 foreign-content insertion mode. The template engine respects this — interpolations inside `<foreignObject>` are HTML-namespaced even when the outer container is SVG. Use it as the documented escape hatch for HTML widgets embedded in SVG graphs (tooltips, popovers, foreign Cells in dataviz).
1295
+
1296
+ **MathML** uses the same mechanism: nested `${html\`<mi/>\`}` inside `<math>` gets re-namespaced to MathML NS. `<annotation-xml encoding="text/html">` is the MathML equivalent of `<foreignObject>` for HTML re-entry.
1297
+
1298
+ **Migration:** zero. Pre-v0.6.8 code that already worked (inline SVG + `.innerHTML` commits) keeps working. Code that was silently broken (nested SVG interpolations) starts producing visible output.
1299
+
1300
+ ### §-TBD (v0.6.8) — `.camelCaseProp=${expr}` property binding (FB-55)
1301
+
1302
+ Pre-v0.6.8 trap: the HTML parser lowercases attribute names inside `<template>.innerHTML` per HTML5 §13.2.5.32, so `.className=${expr}` arrived at the binding scanner as `.classname`. The binding then wrote `element['classname'] = v` — a lowercase enumerable **expando** — never invoking the `HTMLElement.prototype.className` setter. CSS classes never applied; no warning, no error.
1303
+
1304
+ Same trap affected every camelCase DOM property surface: `.className`, `.innerText`, `.tabIndex`, `.ariaLabel`, `.contentEditable`, `.readOnly`, `.maxLength`, `.minLength`, `.colSpan`, `.rowSpan` — AND every camelCase property declared via `UIElement.static properties` (e.g. `<color-picker>`'s `.maxChroma`, `<grid>`'s `.columnGap`, `<breadcrumb>`'s `.collapseKeepLeading`, plus 80+ others across primitives). Even the canonical reactive-binding example from `README.md` (`<my-panel .minL=${minL}>`) silently failed.
1305
+
1306
+ v0.6.8 fix: a two-layer name resolution in `core/template.js`:
1307
+
1308
+ 1. **`PROP_CASE_FIX` static map** at scan time covers built-in DOM camelCase properties (zero-cost lookup, no prototype walk for the common case).
1309
+ 2. **Prototype-walk fallback** at `applyValue()` time finds canonical camelCase property names on the element's instance + prototype chain via case-insensitive match — caches the resolution on the part after first hit. Catches UIElement custom-element props installed per-instance in `installProps()`.
1310
+
1311
+ ```js
1312
+ // ✅ Works post-v0.6.8 — both forms route to the canonical setter
1313
+ html`<span .className=${'foo bar'}>text</span>`;
1314
+ html`<color-picker .maxChroma=${0.3}></color-picker>`;
1315
+ html`<my-panel .minL=${minL}></my-panel>`;
1316
+ ```
1317
+
1318
+ **Backward compatibility:** when neither the map nor the prototype walk finds a case-insensitive match, the original lowercase name is preserved. Consumers deliberately writing to lowercase expando properties (`.myexpando=${v}`) are unaffected.
1319
+
1320
+ **Discoverability note:** the simpler form `class="${expr}"` (full-attribute interpolation) works without needing to know the camelCase property name and is the v0.6.8 partial-interp warn-text's first recommendation. Reach for `.className=` when you specifically want the property-binding semantics (e.g., when the value carries non-string types, or for performance in tight inner loops).
1321
+
1184
1322
  ### §345 (v0.5.19) — `.map()` vs `repeat()` for reactive lists (FB-47)
1185
1323
 
1186
1324
  Use **`repeat(items, keyFn, tplFn)`** for any list whose items are signal-driven and should preserve identity across re-renders. Plain `.map()` returns an `Array` of template results, which the template engine materializes via `container.replaceChildren()` on every parent update — correct, but per-item DOM is re-created from scratch.
@@ -1277,6 +1415,83 @@ Use the descendant combinator (a single space) instead, which traverses any dept
1277
1415
 
1278
1416
  Same caveat applies to `:nth-child()` / `:nth-of-type()` / `:first-child` / `:last-child` against the parent — those count actual children (the wrapper-spans), not the items inside. If you need nth-item styling, key it via a data attribute on the item itself rather than relying on positional pseudo-classes against the wrapper layer.
1279
1417
 
1418
+ ### §-TBD (v0.6.8) — UIElement lifecycle hooks (FB-59 + FB-43/44/47/58 cluster)
1419
+
1420
+ UIElement orchestrates the custom-element lifecycle and exposes two extension points: the W3C standard `connectedCallback()` (rarely overridden) and the convenience **`connected()` hook (the recommended subclass extension point)**. Same pattern for `disconnected()`.
1421
+
1422
+ #### Canonical hook order
1423
+
1424
+ For a UIElement subclass with `static template = () => html\`<x/>\`` mounted into the DOM with consumer-supplied light-DOM children:
1425
+
1426
+ 1. **`adoptStyles(ctor)`** — adopted stylesheets attached to the document
1427
+ 2. **`prepareParts(ctor)`** — `static template` markup parsed into the part registry
1428
+ 3. **`this.connected()`** — subclass hook fires here, inside `untracked(...)`. **Light-DOM children supplied by the parent template are intact at this point.**
1429
+ 4. **Effect first-run** — `stamp(result, this)` runs here if `ctor.template(this)` returns non-null. This is the call that **wipes consumer-supplied light-DOM children** (via `mount()` → `container.replaceChildren(f)`).
1430
+ 5. **`this.render()`** — explicit render hook for imperative DOM updates
1431
+ 6. **`this.updated(changedProps)`** — change-set hook for property-specific reactions
1432
+
1433
+ **The key insight for container components: `connected()` runs BEFORE the stamp.** If your container UIElement has both a non-null `static template` AND wants to capture consumer-supplied light-DOM children, do the capture in `connected()`. The wipe runs in step 4, which is after step 3.
1434
+
1435
+ Source: `packages/web-components/core/element.js:107-142` — `UIElement.connectedCallback()`. `connected()` fires at line 122 inside `untracked(...)`; the stamp runs at line 127 inside the effect registered at lines 123-137.
1436
+
1437
+ #### Container UIElement pattern (FB-43/44/47/58 cluster — the right shape)
1438
+
1439
+ When your UIElement is a container for consumer-supplied light-DOM children AND has its own internal `static template`, capture the children in `connected()`:
1440
+
1441
+ ```js
1442
+ class DtsGraph extends UIElement {
1443
+ static template = () => html`<div class="dts-graph-root"></div>`;
1444
+
1445
+ // ✅ connected() runs BEFORE the stamp wipes children — capture here
1446
+ connected() {
1447
+ this._series = [...this.querySelectorAll('dts-graph-band, dts-graph-curve, …')];
1448
+
1449
+ // Optional: MutationObserver for late-arriving children (e.g. parent template
1450
+ // toggles `${cond ? html\`<band/>\` : ''}` and the new band lands later)
1451
+ this._mo = new MutationObserver(() => this._captureSeries());
1452
+ this._mo.observe(this, { childList: true, subtree: true });
1453
+ }
1454
+
1455
+ _captureSeries() {
1456
+ const present = [...this.querySelectorAll('dts-graph-band, …')];
1457
+ // Defensive cache-fallback: post-stamp wipe makes `present` empty, but the
1458
+ // initial capture from connected() above already saved the refs. Keep them.
1459
+ if (present.length > 0) this._series = present;
1460
+ }
1461
+
1462
+ disconnected() {
1463
+ this._mo?.disconnect();
1464
+ }
1465
+ }
1466
+ ```
1467
+
1468
+ The `if (present.length > 0) keep` cache-fallback is the architectural pattern that makes the capture robust to either ordering. It works in any lifecycle environment with no specification dependency.
1469
+
1470
+ #### `connected()` vs `connectedCallback()` reference
1471
+
1472
+ | Hook | Who calls it | When (relative to step list above) | Use case |
1473
+ |---|---|---|---|
1474
+ | `connectedCallback()` | The browser, per W3C custom-elements spec | Orchestrates the whole sequence | Almost never override — UIElement runs the lifecycle for you |
1475
+ | **`connected()`** | UIElement, from within `connectedCallback()` at line 122 | Step 3 — after `prepareParts()`, BEFORE the template stamp | **Subclass setup, light-DOM capture, observer arming, signal effects** |
1476
+ | `render()` | The effect, after every signal change | Step 5 — after `stamp()` if the template returned non-null | Imperative DOM updates that don't fit the template engine |
1477
+ | `updated(changedProps)` | The effect | Step 6 — after `render()`, when properties changed | React to specific prop changes |
1478
+ | `disconnected()` | UIElement, from `disconnectedCallback()` | Element leaves the DOM | Teardown observers, timers, listeners |
1479
+
1480
+ #### When NOT to override `connectedCallback()` directly
1481
+
1482
+ `UIElement.connectedCallback()` orchestrates the entire lifecycle. Overriding it directly requires re-invoking `super.connectedCallback()` correctly, and you'd have to choose whether your work goes before or after super:
1483
+
1484
+ - **BEFORE super**: you miss the `prepareParts(ctor)` call that initializes the part registry. `static template` won't stamp. Other downstream initialization breaks.
1485
+ - **AFTER super**: your work runs after step 6 (`updated()`), which means after the stamp wiped your light-DOM children. You'd need to capture in `constructor()` instead — but `constructor()` runs before the element is in the DOM, so light-DOM children aren't appended yet.
1486
+
1487
+ `connected()` is the right hook for 99% of consumer code. It runs at exactly the right time: children present, stamp not yet run.
1488
+
1489
+ #### Why `connected()` is wrapped in `untracked()`
1490
+
1491
+ Per the comment in `element.js:112-121`: `connected()` commonly reads reactive properties via template-literal interpolation (`${this.label}` etc.) when stamping inner DOM. If the outer call path is inside another effect (e.g. a parent's render loop `appendChild`'ing this node), those reads would subscribe the outer effect to this element's signals — invisible cross-element coupling that looks like spooky re-runs on unrelated state changes. The `untracked()` wrapper prevents that subscription leak. The element's own effect (registered at lines 123-137) re-reads the same signals in its own tracking context, so reactivity within the element is unaffected.
1492
+
1493
+ This means: inside `connected()`, you CAN read signals/props for setup logic, but those reads won't establish reactive subscriptions. Use the element's own `render()` or signal effects for reactive work; use `connected()` for one-shot setup.
1494
+
1280
1495
  ### §221j — Typography token cheatsheet
1281
1496
 
1282
1497
  Quick-reference for component-CSS authoring. Cross-reference [`styles/typography.css`](./styles/typography.css):
@@ -99,7 +99,7 @@ action-item-ui:hover [slot="icon"] {
99
99
  }
100
100
 
101
101
  /* Default slot (trailing content: kbd, badge, chevron) pushed to far right */
102
- > :not([slot="icon"]):not([slot="text"]) {
102
+ & > :not([slot="icon"]):not([slot="text"]) {
103
103
  flex-shrink: 0;
104
104
  margin-inline-start: auto;
105
105
  color: var(--action-item-icon-fg);
@@ -149,7 +149,7 @@
149
149
  [padding] switches to padding mode with background.
150
150
  Grid layout: optional icon | heading+description | optional action */
151
151
 
152
- > header {
152
+ & > header {
153
153
  display: block;
154
154
  margin: var(--card-inset);
155
155
  }
@@ -157,24 +157,24 @@
157
157
  /* Activate grid layout only when a DIRECT slotted child is present.
158
158
  Constraining to `:has(> [slot])` prevents deeply nested slots (e.g. an
159
159
  [slot="icon"] inside an <avatar-ui>) from falsely activating the grid. */
160
- > header:has(> [slot]) {
160
+ & > header:has(> [slot]) {
161
161
  display: grid;
162
162
  gap: var(--card-header-gap);
163
163
  align-items: center;
164
164
  }
165
165
 
166
- > header[padding] {
166
+ & > header[padding] {
167
167
  margin: 0;
168
168
  padding: var(--card-inset);
169
169
  background: var(--card-bg-padded);
170
170
  }
171
171
 
172
- > header[divider] {
172
+ & > header[divider] {
173
173
  padding-bottom: var(--card-inset);
174
174
  border-bottom: 1px solid var(--card-divider);
175
175
  }
176
176
 
177
- > header[center] {
177
+ & > header[center] {
178
178
  text-align: center;
179
179
  justify-items: center;
180
180
  }
@@ -182,16 +182,16 @@
182
182
  /* Column templates — match gen-ui-kit pattern. Direct-child `:has(> …)` so
183
183
  nested [slot="icon"] inside a composite (e.g. <avatar-ui>) can't trigger
184
184
  the icon column by accident. */
185
- > header:has(> [slot="icon"]):has(> [slot="action"]) { grid-template-columns: max-content 1fr max-content; }
186
- > header:has(> [slot="icon"]):not(:has(> [slot="action"])) { grid-template-columns: max-content 1fr; }
187
- > header:not(:has(> [slot="icon"])):has(> [slot="action"]) { grid-template-columns: 1fr max-content; }
188
- > header:not(:has(> [slot="icon"])):not(:has(> [slot="action"])) { grid-template-columns: 1fr; }
185
+ & > header:has(> [slot="icon"]):has(> [slot="action"]) { grid-template-columns: max-content 1fr max-content; }
186
+ & > header:has(> [slot="icon"]):not(:has(> [slot="action"])) { grid-template-columns: max-content 1fr; }
187
+ & > header:not(:has(> [slot="icon"])):has(> [slot="action"]) { grid-template-columns: 1fr max-content; }
188
+ & > header:not(:has(> [slot="icon"])):not(:has(> [slot="action"])) { grid-template-columns: 1fr; }
189
189
 
190
190
  /* Unslotted children (zettel fragment injection, etc.) — each on its own row,
191
191
  spanning the full width, stacked ABOVE the heading/description/action grid.
192
192
  This lets compositions inject logos, banners, etc. into a header without
193
193
  having to know the slot vocabulary. */
194
- > header > *:not([slot]):not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(p):not(small) {
194
+ & > header > *:not([slot]):not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(p):not(small) {
195
195
  grid-column: 1 / -1;
196
196
  justify-self: center;
197
197
  }
@@ -200,7 +200,7 @@
200
200
  Default: single row, center-aligned with heading/action.
201
201
  When a description row exists, spans both rows and anchors to the
202
202
  start so the icon visually aligns with the heading. */
203
- > header > [slot="icon"] {
203
+ & > header > [slot="icon"] {
204
204
  grid-column: 1;
205
205
  grid-row: 1;
206
206
  align-self: center;
@@ -209,39 +209,39 @@
209
209
  justify-content: center;
210
210
  }
211
211
 
212
- > header:has(> :is([slot="description"], p, small)) > [slot="icon"] {
212
+ & > header:has(> :is([slot="description"], p, small)) > [slot="icon"] {
213
213
  grid-row: 1 / span 2;
214
214
  align-self: start;
215
215
  }
216
216
 
217
217
  /* Heading — row 1 */
218
- > header > :is([slot="heading"], h1, h2, h3, h4, h5, h6),
219
- > header > [slot="heading"] :is(h1, h2, h3, h4, h5, h6) {
218
+ & > header > :is([slot="heading"], h1, h2, h3, h4, h5, h6),
219
+ & > header > [slot="heading"] :is(h1, h2, h3, h4, h5, h6) {
220
220
  grid-row: 1;
221
221
  line-height: 1.3;
222
222
  margin: 0;
223
223
  }
224
224
  /* Heading slot is a flex container — can hold title + inline badges/metadata */
225
- > header > [slot="heading"] {
225
+ & > header > [slot="heading"] {
226
226
  display: flex;
227
227
  align-items: center;
228
228
  gap: var(--card-header-gap);
229
229
  }
230
- > header:has(> [slot="icon"]) > :is([slot="heading"], h1, h2, h3, h4, h5, h6) { grid-column: 2; }
231
- > header:not(:has(> [slot="icon"])) > :is([slot="heading"], h1, h2, h3, h4, h5, h6) { grid-column: 1; }
230
+ & > header:has(> [slot="icon"]) > :is([slot="heading"], h1, h2, h3, h4, h5, h6) { grid-column: 2; }
231
+ & > header:not(:has(> [slot="icon"])) > :is([slot="heading"], h1, h2, h3, h4, h5, h6) { grid-column: 1; }
232
232
 
233
233
  /* Description — row 2 */
234
- > header > :is([slot="description"], p, small) {
234
+ & > header > :is([slot="description"], p, small) {
235
235
  grid-row: 2;
236
236
  grid-column: 1 / -1;
237
237
  line-height: 1.4;
238
238
  margin: 0;
239
239
  }
240
- > header:has(> [slot="icon"]) > :is([slot="description"], p, small) { grid-column: 2 / -1; }
240
+ & > header:has(> [slot="icon"]) > :is([slot="description"], p, small) { grid-column: 2 / -1; }
241
241
 
242
242
  /* Action — row 1, last column.
243
243
  Flex container so it can hold badge + button + anything inline. */
244
- > header [slot="action"] {
244
+ & > header [slot="action"] {
245
245
  justify-self: end;
246
246
  align-self: center;
247
247
  grid-row: 1;
@@ -256,17 +256,17 @@
256
256
  [padding] switches to padding with background.
257
257
  [bleed] removes all spacing for edge-to-edge content. */
258
258
 
259
- > section {
259
+ & > section {
260
260
  margin: var(--card-inset);
261
261
  }
262
262
 
263
- > section[padding] {
263
+ & > section[padding] {
264
264
  margin: 0;
265
265
  padding: var(--card-inset);
266
266
  background: var(--card-bg-padded);
267
267
  }
268
268
 
269
- > section[bleed] {
269
+ & > section[bleed] {
270
270
  margin: 0;
271
271
  padding: 0;
272
272
  }
@@ -276,7 +276,7 @@
276
276
  [divider] adds top border.
277
277
  [padding] switches to padded mode. */
278
278
 
279
- > footer {
279
+ & > footer {
280
280
  display: block;
281
281
  margin: var(--card-inset);
282
282
  }
@@ -284,8 +284,8 @@
284
284
  /* Activate flex layout when a direct slotted child or multiple children are
285
285
  present. `:has(> [slot])` so nested [slot="…"] inside composites can't
286
286
  trigger the flex row. */
287
- > footer:has(> [slot]),
288
- > footer:has(> :nth-child(2)) {
287
+ & > footer:has(> [slot]),
288
+ & > footer:has(> :nth-child(2)) {
289
289
  display: flex;
290
290
  flex-wrap: wrap;
291
291
  align-items: center;
@@ -293,30 +293,30 @@
293
293
  }
294
294
 
295
295
  /* col-ui in footer takes full width (common for stacked footer: button + divider + social) */
296
- > footer > col-ui { width: 100%; }
296
+ & > footer > col-ui { width: 100%; }
297
297
 
298
- > footer[justify="end"] {
298
+ & > footer[justify="end"] {
299
299
  justify-content: flex-end;
300
300
  }
301
301
 
302
- > footer[divider] {
302
+ & > footer[divider] {
303
303
  margin-top: var(--card-inset);
304
304
  padding-top: var(--card-inset);
305
305
  border-top: 1px solid var(--card-divider);
306
306
  }
307
307
 
308
- > footer[padding] {
308
+ & > footer[padding] {
309
309
  margin: 0;
310
310
  padding: var(--card-inset);
311
311
  background: var(--card-bg-padded);
312
312
  }
313
313
 
314
314
  /* Footer with direct-child description + action = space-between */
315
- > footer:has(> :is([slot="description"], p, small)):has(> [slot="action"]) {
315
+ & > footer:has(> :is([slot="description"], p, small)):has(> [slot="action"]) {
316
316
  justify-content: space-between;
317
317
  }
318
318
 
319
- > footer :is([slot="description"], p, small) {
319
+ & > footer :is([slot="description"], p, small) {
320
320
  font-size: var(--card-description-size);
321
321
  color: var(--card-description-fg);
322
322
  flex: 1;
@@ -327,7 +327,7 @@
327
327
  /* Footer heading — trend-stat line (symmetric with header's [slot="heading"]).
328
328
  Used for chart trend-footer patterns like "Trending up by 5.2% this month".
329
329
  Paired with [slot="description"] for a "Jan – Mar 2024" period caption. */
330
- > footer > [slot="heading"] {
330
+ & > footer > [slot="heading"] {
331
331
  font-size: var(--card-font-size);
332
332
  font-weight: var(--a-weight-medium);
333
333
  color: var(--card-heading-fg);
@@ -339,21 +339,21 @@
339
339
  /* Footer with heading + description = column stack, heading on top. The
340
340
  existing flex-row layout becomes a flex-column when a heading is present,
341
341
  so the trend line and period caption stack naturally. */
342
- > footer:has(> [slot="heading"]) {
342
+ & > footer:has(> [slot="heading"]) {
343
343
  flex-direction: column;
344
344
  align-items: flex-start;
345
345
  gap: var(--a-space-0-5);
346
346
  }
347
347
 
348
- > footer:has(> [slot="heading"])[justify="end"] {
348
+ & > footer:has(> [slot="heading"])[justify="end"] {
349
349
  align-items: flex-end;
350
350
  }
351
351
 
352
- > footer:has(> [slot="heading"])[justify="center"] {
352
+ & > footer:has(> [slot="heading"])[justify="center"] {
353
353
  align-items: center;
354
354
  }
355
355
 
356
- > footer [slot="action"] {
356
+ & > footer [slot="action"] {
357
357
  margin-inline-start: auto;
358
358
  display: flex;
359
359
  gap: var(--card-footer-gap);
@@ -361,14 +361,14 @@
361
361
 
362
362
  /* Multiple direct action-slotted children (A2UI pattern):
363
363
  only the first pushes the group right; subsequent ones flow with gap */
364
- > footer > [slot="action"] ~ [slot="action"] {
364
+ & > footer > [slot="action"] ~ [slot="action"] {
365
365
  margin-inline-start: 0;
366
366
  }
367
367
 
368
368
  /* Dual-cluster footer: leading action (e.g. Delete) on the inline-start
369
369
  edge, trailing action cluster on the inline-end. margin-inline-end:
370
370
  auto fills the gap between the two groups. */
371
- > footer > [slot="action-leading"] {
371
+ & > footer > [slot="action-leading"] {
372
372
  margin-inline-end: auto;
373
373
  display: flex;
374
374
  gap: var(--card-footer-gap);
@@ -376,15 +376,15 @@
376
376
 
377
377
  /* ═══════ Images ═══════ */
378
378
 
379
- > img,
380
- > [slot="media"] {
379
+ & > img,
380
+ & > [slot="media"] {
381
381
  display: block;
382
382
  width: 100%;
383
383
  object-fit: cover;
384
384
  }
385
385
 
386
- > img:first-child,
387
- > [slot="media"]:first-child {
386
+ & > img:first-child,
387
+ & > [slot="media"]:first-child {
388
388
  border-radius: var(--card-radius) var(--card-radius) 0 0;
389
389
  }
390
390
 
@@ -112,7 +112,7 @@
112
112
  /* Empty-state slot — author places <empty-state-ui slot="empty"> inside
113
113
  chart-ui. CSS toggles visibility on [data-has-data] (set by chart.js
114
114
  when .data is non-empty). No data ⇒ empty-state visible; data ⇒ hidden. */
115
- > [slot="empty"] { display: none; }
115
+ & > [slot="empty"] { display: none; }
116
116
  :scope:not([data-has-data]) > [slot="empty"] {
117
117
  display: flex;
118
118
  flex: 1 1 auto;
@@ -65,7 +65,7 @@
65
65
  }
66
66
 
67
67
  /* -- Header -- */
68
- > header {
68
+ & > header {
69
69
  display: flex;
70
70
  align-items: center;
71
71
  gap: var(--chat-gap);
@@ -73,7 +73,7 @@
73
73
  border-bottom: 1px solid var(--chat-border-color);
74
74
  }
75
75
 
76
- > header [slot="avatar"] {
76
+ & > header [slot="avatar"] {
77
77
  width: var(--chat-header-avatar-size);
78
78
  height: var(--chat-header-avatar-size);
79
79
  border-radius: var(--chat-header-avatar-radius);
@@ -87,19 +87,19 @@
87
87
  flex-shrink: 0;
88
88
  }
89
89
 
90
- > header [slot="name"] {
90
+ & > header [slot="name"] {
91
91
  font-weight: var(--chat-header-name-weight);
92
92
  font-size: var(--chat-header-name-size);
93
93
  }
94
94
 
95
- > header [slot="status"] {
95
+ & > header [slot="status"] {
96
96
  font-size: var(--chat-header-status-size);
97
97
  color: var(--chat-header-status-fg);
98
98
  margin-inline-start: auto;
99
99
  }
100
100
 
101
101
  /* -- Messages -- */
102
- > section {
102
+ & > section {
103
103
  flex: 1;
104
104
  overflow-y: auto;
105
105
  padding: var(--chat-messages-padding);
@@ -175,7 +175,7 @@
175
175
  }
176
176
 
177
177
  /* -- Footer -- */
178
- > footer {
178
+ & > footer {
179
179
  min-height: var(--chat-footer-min-height);
180
180
  display: flex;
181
181
  align-items: center;