@adia-ai/web-components 0.6.6 → 0.6.8

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/USAGE.md +286 -5
  3. package/bin/doc.mjs +379 -0
  4. package/components/accordion/accordion.examples.md +49 -0
  5. package/components/action-list/action-list.examples.md +33 -0
  6. package/components/agent-artifact/agent-artifact.examples.md +28 -0
  7. package/components/agent-feedback-bar/agent-feedback-bar.examples.md +24 -0
  8. package/components/agent-questions/agent-questions.examples.md +13 -0
  9. package/components/agent-reasoning/agent-reasoning.examples.md +28 -0
  10. package/components/agent-suggestions/agent-suggestions.examples.md +19 -0
  11. package/components/agent-trace/agent-trace.examples.md +19 -0
  12. package/components/alert/alert.examples.md +19 -0
  13. package/components/aside/aside.examples.md +38 -0
  14. package/components/avatar/avatar.examples.md +19 -0
  15. package/components/badge/badge.examples.md +19 -0
  16. package/components/block/block.examples.md +29 -0
  17. package/components/breadcrumb/breadcrumb.examples.md +32 -0
  18. package/components/button/button.examples.md +19 -0
  19. package/components/calendar-picker/calendar-picker.examples.md +23 -0
  20. package/components/canvas/canvas.examples.md +49 -0
  21. package/components/card/card.examples.md +25 -0
  22. package/components/chart/chart.examples.md +19 -0
  23. package/components/chart-legend/chart-legend.examples.md +22 -0
  24. package/components/chat-thread/chat-thread.examples.md +35 -0
  25. package/components/check/check.examples.md +19 -0
  26. package/components/code/code.examples.md +31 -0
  27. package/components/col/col.examples.md +11 -0
  28. package/components/color-input/color-input.examples.md +19 -0
  29. package/components/color-picker/color-picker.examples.md +19 -0
  30. package/components/command/command.examples.md +58 -0
  31. package/components/demo-toggle/demo-toggle.examples.md +19 -0
  32. package/components/description-list/description-list.examples.md +37 -0
  33. package/components/divider/divider.examples.md +31 -0
  34. package/components/drawer/drawer.examples.md +63 -0
  35. package/components/embed/embed.examples.md +19 -0
  36. package/components/empty-state/empty-state.examples.md +21 -0
  37. package/components/feed/feed.examples.md +19 -0
  38. package/components/field/field.examples.md +29 -0
  39. package/components/fields/fields.examples.md +58 -0
  40. package/components/footer/footer.examples.md +18 -0
  41. package/components/grid/grid.examples.md +21 -0
  42. package/components/header/header.examples.md +32 -0
  43. package/components/heatmap/heatmap.examples.md +19 -0
  44. package/components/icon/icon.examples.md +19 -0
  45. package/components/image/image.examples.md +19 -0
  46. package/components/input/input.examples.md +21 -0
  47. package/components/inspector/inspector.examples.md +27 -0
  48. package/components/kbd/kbd.examples.md +29 -0
  49. package/components/link/link.examples.md +25 -0
  50. package/components/list/list.examples.md +33 -0
  51. package/components/menu/menu.examples.md +37 -0
  52. package/components/modal/modal.examples.md +43 -0
  53. package/components/nav/nav.examples.md +39 -0
  54. package/components/nav-group/nav-group.examples.md +35 -0
  55. package/components/nav-item/nav-item.examples.md +25 -0
  56. package/components/noodles/noodles.examples.md +30 -0
  57. package/components/option-card/option-card.examples.md +47 -0
  58. package/components/otp-input/otp-input.examples.md +19 -0
  59. package/components/page/page.examples.md +46 -0
  60. package/components/pagination/pagination.examples.md +19 -0
  61. package/components/pane/pane.examples.md +40 -0
  62. package/components/pipeline-status/pipeline-status.examples.md +19 -0
  63. package/components/popover/popover.examples.md +34 -0
  64. package/components/progress/progress.examples.md +19 -0
  65. package/components/progress-row/progress-row.examples.md +19 -0
  66. package/components/radio/radio.examples.md +19 -0
  67. package/components/range/range.examples.md +19 -0
  68. package/components/rating/rating.examples.md +19 -0
  69. package/components/richtext/richtext.examples.md +19 -0
  70. package/components/row/row.examples.md +11 -0
  71. package/components/search/search.examples.md +19 -0
  72. package/components/section/section.examples.md +37 -0
  73. package/components/segment/segment.examples.md +32 -0
  74. package/components/segmented/segmented.examples.md +31 -0
  75. package/components/select/select.examples.md +49 -0
  76. package/components/skeleton/skeleton.examples.md +19 -0
  77. package/components/slider/slider.examples.md +21 -0
  78. package/components/stack/stack.examples.md +10 -0
  79. package/components/stat/stat.examples.md +19 -0
  80. package/components/step-progress/step-progress.examples.md +19 -0
  81. package/components/stepper/stepper.examples.md +37 -0
  82. package/components/stream/stream.examples.md +19 -0
  83. package/components/swatch/swatch.examples.md +23 -0
  84. package/components/swiper/swiper.examples.md +139 -0
  85. package/components/switch/switch.examples.md +19 -0
  86. package/components/table/table.examples.md +19 -0
  87. package/components/table-toolbar/table-toolbar.examples.md +12 -0
  88. package/components/tabs/tabs.examples.md +49 -0
  89. package/components/tag/tag.examples.md +19 -0
  90. package/components/text/text.examples.md +19 -0
  91. package/components/textarea/textarea.examples.md +23 -0
  92. package/components/timeline/timeline.examples.md +35 -0
  93. package/components/toast/toast.examples.md +19 -0
  94. package/components/toggle-group/toggle-group.examples.md +33 -0
  95. package/components/toggle-scheme/toggle-scheme.examples.md +19 -0
  96. package/components/toolbar/toolbar.examples.md +46 -0
  97. package/components/tooltip/tooltip.examples.md +25 -0
  98. package/components/tree/tree.examples.md +57 -0
  99. package/components/upload/upload.examples.md +19 -0
  100. package/core/template.js +236 -7
  101. package/core/template.test.js +294 -0
  102. package/custom-elements.json +9012 -0
  103. package/package.json +7 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.6.8] — 2026-05-20
4
+
5
+ ### Fixed
6
+ - **`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).
7
+ - **`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).
8
+ - **`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.
9
+
10
+ ### Docs
11
+ - **`USAGE.md` — three new template-related sections** documenting the FB-55/56/57 cluster:
12
+ - **§-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'`").
13
+ - **§-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.
14
+ - **§-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).
15
+ - **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).
16
+
17
+ ## [0.6.7] — 2026-05-19
18
+
19
+ ### Added
20
+ - **`<name>.examples.md` ships in npm package** (96 components, ~67KB total).
21
+ Generated by `scripts/build/generate-examples-md.mjs` from `<name>.examples.html`.
22
+ External consumers can now read canonical markup fragments directly from
23
+ `node_modules/@adia-ai/web-components/components/<name>/<name>.examples.md`.
24
+ - **`custom-elements.json` ships in npm package** (W3C Custom Elements Manifest v2.1.0).
25
+ Generated from per-component yaml files. Enables IDE LSP autocomplete and
26
+ cross-tool interop (Storybook, VS Code WC extensions).
27
+ - **`npx @adia-ai/web-components doc <name>` CLI** prints yaml summary + live
28
+ demo URL + first example fragment for any component. Bare `doc` lists all.
29
+ - **`USAGE.md § Data binding with data-stream-*`** new top-level section
30
+ covering the declarative `data-stream-*` attribute family (was previously
31
+ undocumented despite being used in 12+ components).
32
+
3
33
  ## [0.6.6] — 2026-05-18
4
34
 
5
35
  ### Added
package/USAGE.md CHANGED
@@ -14,11 +14,12 @@ The complete reference for engineers and agents **integrating** AdiaUI into an a
14
14
  4. [Property binding patterns (templates / framework integration)](#property-binding-patterns)
15
15
  5. [Event contract — `CustomEvent` with `detail`](#event-contract)
16
16
  6. [Form participation — `UIFormElement` + `ElementInternals`](#form-participation)
17
- 7. [Lifecycle `connected` / `render` / `updated` / `disconnected`](#lifecycle)
18
- 8. [Theming, density, size](#theming-density-size)
19
- 9. [Registration auto vs explicit](#registration--auto-vs-explicit)
20
- 10. [TypeScript](#typescript)
21
- 11. [Anti-patterns](#anti-patterns)
17
+ 7. [Data binding with `data-stream-*`](#data-binding-with-data-stream-)
18
+ 8. [Lifecycle `connected` / `render` / `updated` / `disconnected`](#lifecycle)
19
+ 9. [Theming, density, size](#theming-density-size)
20
+ 10. [Registration — auto vs explicit](#registration--auto-vs-explicit)
21
+ 11. [TypeScript](#typescript)
22
+ 12. [Anti-patterns](#anti-patterns)
22
23
 
23
24
  ---
24
25
 
@@ -461,6 +462,100 @@ form.addEventListener('submit', (e) => {
461
462
 
462
463
  ---
463
464
 
465
+ ## Data binding with `data-stream-*`
466
+
467
+ Many AdiaUI components (`<chart-ui>`, `<heatmap-ui>`, `<table-ui>`, `<stat-ui>`, `<progress-row-ui>`, and ~7 others) support **declarative remote-data binding** via the `data-stream-*` attribute family. You point a component at a URL, and it self-fetches (REST poll) or self-subscribes (SSE) and pushes the result into one of its properties. No JS glue required.
468
+
469
+ This is the canonical "wire a component to a backend" pattern in AdiaUI. Reach for it before writing a `fetch()` loop in your app code.
470
+
471
+ ### Concept
472
+
473
+ A component that opts in watches its `data-stream-*` attributes. When `data-stream-src` is set on a connected element, the component starts a transport (poll, SSE, or manual) and assigns each payload to a target property (e.g. `data` on `<table-ui>`, `value` on `<stat-ui>`). Change the attribute and the transport restarts; remove the attribute (or disconnect the element) and the transport tears down.
474
+
475
+ Because the binding is attribute-driven, it survives SSR, server-rendered partials, htmx swaps, framework hydration, and templating engines that have no concept of property assignment.
476
+
477
+ ### Attribute reference
478
+
479
+ | Attribute | Purpose | Default |
480
+ |---|---|---|
481
+ | `data-stream-src` | URL — REST endpoint or SSE source. Setting this activates the binding. | (required) |
482
+ | `data-stream-mode` | `"poll"`, `"sse"`, or `"manual"`. `manual` fetches once on connect and on attribute change. | `"poll"` |
483
+ | `data-stream-interval` | Poll interval in milliseconds. Ignored for `sse` / `manual`. | `5000` |
484
+ | `data-stream-path` | Dot-path into the JSON response to extract before binding (e.g. `claims` extracts `response.claims`; `data.items` extracts `response.data.items`). | (whole response) |
485
+ | `data-stream-target` | Which component property receives the payload. | component-specific (`data` for `<table-ui>`, `value` for `<stat-ui>`, `data` for `<chart-ui>`, …) |
486
+ | `data-stream-method` | HTTP method for REST mode. `POST` is supported when you need a request body via headers/config. | `"GET"` |
487
+ | `data-stream-headers` | JSON-stringified object of request headers (e.g. `'{"Authorization":"Bearer …"}'`). | `null` |
488
+ | `data-stream-id` | Logical transport key. Multiple elements with the same `data-stream-id` share one underlying fetch/SSE connection. | (per-element) |
489
+
490
+ ### REST polling
491
+
492
+ ```html
493
+ <table-ui
494
+ data-stream-src="/api/claims"
495
+ data-stream-interval="10000"
496
+ data-stream-target="data"
497
+ ></table-ui>
498
+ ```
499
+
500
+ The table fetches `/api/claims` every 10s and assigns the response to its `data` property. If the response is wrapped (e.g. `{ claims: [...] }`), add `data-stream-path="claims"` to drill in.
501
+
502
+ ### SSE streaming
503
+
504
+ ```html
505
+ <stat-ui
506
+ label="Active"
507
+ data-stream-src="/api/active-count"
508
+ data-stream-mode="sse"
509
+ data-stream-target="value"
510
+ ></stat-ui>
511
+ ```
512
+
513
+ Opens an `EventSource` against `/api/active-count`. Each SSE message's `data` field is JSON-parsed (when possible) and pushed into the stat's `value`. The connection closes on disconnect.
514
+
515
+ ### Shared transport
516
+
517
+ When two or more components need the same source, give them a matching `data-stream-id` and they will share a single fetch/SSE subscription:
518
+
519
+ ```html
520
+ <table-ui
521
+ data-stream-src="/api/claims"
522
+ data-stream-id="shared-claims"
523
+ data-stream-target="data"
524
+ ></table-ui>
525
+
526
+ <stat-ui
527
+ label="Total"
528
+ data-stream-src="/api/claims"
529
+ data-stream-id="shared-claims"
530
+ data-stream-path="length"
531
+ data-stream-target="value"
532
+ ></stat-ui>
533
+ ```
534
+
535
+ One request feeds both. Each element still applies its own `data-stream-path` and `data-stream-target` independently. The shared transport is reference-counted; the last element to disconnect tears it down.
536
+
537
+ ### SSR / server rendering
538
+
539
+ `data-stream-*` is **client-only**. Server-rendered markup that contains these attributes is safe — the attributes are just strings on the element — and the transport only activates once the component is upgraded in the browser. This means you can:
540
+
541
+ - Server-render the initial empty/skeleton state of a `<table-ui>` and let the client take over.
542
+ - Emit `data-stream-*` from any templating layer (Jinja, ERB, Handlebars, htmx fragments) without server-side coupling to the data-stream runtime.
543
+ - Hydrate framework apps without special-casing — React/Vue/Svelte just need to render the attribute; they do not need to subscribe.
544
+
545
+ If you need a server-rendered first paint of *real* data, render it normally (slot or attribute) — the data-stream binding will replace it on first successful payload.
546
+
547
+ ### Authoritative source
548
+
549
+ The implementation lives at:
550
+
551
+ ```
552
+ packages/web-components/core/data-stream.js
553
+ ```
554
+
555
+ A component opts in by declaring a `data_stream:` block in its YAML descriptor (see `packages/web-components/components/*/*.yaml`). That block is the source of truth for which target properties are bindable and which defaults apply for that specific component. When in doubt, read the component's YAML and `core/data-stream.js` — this document is a discoverability summary, not a spec.
556
+
557
+ ---
558
+
464
559
  ## Lifecycle
465
560
 
466
561
  `UIElement` exposes four overridable lifecycle methods. Defaults are no-ops. Override what you need:
@@ -1086,6 +1181,115 @@ Why AdiaUI doesn't implement `?attr=`: custom elements declare their reflective
1086
1181
  - HTML comments containing quoted attributes (`<!-- attr="value" -->`) — same fix (v0.5.3 §155).
1087
1182
  - Backticks inside HTML comments — see §221i above (JS-spec footgun; not a parser bug).
1088
1183
 
1184
+ ### §-TBD (v0.6.8) — Host element display defaults (FB-56)
1185
+
1186
+ 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.
1187
+
1188
+ **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.
1189
+
1190
+ 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`:
1191
+
1192
+ ```js
1193
+ import { css } from '@adia-ai/web-components/core/element';
1194
+
1195
+ class MyContainer extends UIElement {
1196
+ static styles = css`
1197
+ :host(:where(*)) { /* zero-specificity wrap; consumers override at (0,0,1) */
1198
+ display: flex;
1199
+ flex-direction: column;
1200
+ flex: 1 1 auto;
1201
+ min-height: 0;
1202
+ }
1203
+ `;
1204
+ static template = (host) => html`<slot></slot>`;
1205
+ }
1206
+ ```
1207
+
1208
+ Or, if the host should be transparent to layout (its children inherit the parent's grid/flex context directly):
1209
+
1210
+ ```css
1211
+ my-passthrough-element { display: contents; }
1212
+ ```
1213
+
1214
+ 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).
1215
+
1216
+ **`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.
1217
+
1218
+ **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.
1219
+
1220
+ ### §-TBD (v0.6.8) — SVG composition + namespacing (FB-57)
1221
+
1222
+ `html\`\`` handles SVG content in three modes, with one historical trap closed in v0.6.8.
1223
+
1224
+ **Mode 1 — inline SVG in a single template (always worked):**
1225
+
1226
+ ```js
1227
+ html`
1228
+ <svg viewBox="0 0 100 100">
1229
+ <circle cx="50" cy="50" r="10"/>
1230
+ <path d="M 0 0 L 100 100"/>
1231
+ </svg>
1232
+ `
1233
+ ```
1234
+
1235
+ When the parser sees `<svg>` in `tpl.innerHTML`, it enters SVG insertion mode for its descendants. Children arrive as proper `SVGElement` instances.
1236
+
1237
+ **Mode 2 — nested template interpolations (now works, post-v0.6.8 FB-57 fix):**
1238
+
1239
+ ```js
1240
+ html`
1241
+ <svg viewBox="0 0 100 100">
1242
+ ${dots.map(d => html`<circle cx="${d.x}" cy="${d.y}" r="3"/>`)}
1243
+ </svg>
1244
+ `
1245
+ ```
1246
+
1247
+ 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.
1248
+
1249
+ **Mode 3 — imperative `.innerHTML` commit (escape hatch for very large SVG):**
1250
+
1251
+ For SVG content too large to template cleanly (thousands of nodes, dynamic shape strings):
1252
+
1253
+ ```js
1254
+ class MyChart extends UIElement {
1255
+ static template = (host) => html`<svg viewBox="0 0 ${host.w} ${host.h}"></svg>`;
1256
+ updated() {
1257
+ const svg = this.querySelector('svg');
1258
+ svg.innerHTML = host._buildSvgString(); // SVG-namespaced via SVGSVGElement.innerHTML
1259
+ }
1260
+ }
1261
+ ```
1262
+
1263
+ `.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).
1264
+
1265
+ **`<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).
1266
+
1267
+ **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.
1268
+
1269
+ **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.
1270
+
1271
+ ### §-TBD (v0.6.8) — `.camelCaseProp=${expr}` property binding (FB-55)
1272
+
1273
+ 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.
1274
+
1275
+ 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.
1276
+
1277
+ v0.6.8 fix: a two-layer name resolution in `core/template.js`:
1278
+
1279
+ 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).
1280
+ 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()`.
1281
+
1282
+ ```js
1283
+ // ✅ Works post-v0.6.8 — both forms route to the canonical setter
1284
+ html`<span .className=${'foo bar'}>text</span>`;
1285
+ html`<color-picker .maxChroma=${0.3}></color-picker>`;
1286
+ html`<my-panel .minL=${minL}></my-panel>`;
1287
+ ```
1288
+
1289
+ **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.
1290
+
1291
+ **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).
1292
+
1089
1293
  ### §345 (v0.5.19) — `.map()` vs `repeat()` for reactive lists (FB-47)
1090
1294
 
1091
1295
  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.
@@ -1182,6 +1386,83 @@ Use the descendant combinator (a single space) instead, which traverses any dept
1182
1386
 
1183
1387
  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.
1184
1388
 
1389
+ ### §-TBD (v0.6.8) — UIElement lifecycle hooks (FB-59 + FB-43/44/47/58 cluster)
1390
+
1391
+ 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()`.
1392
+
1393
+ #### Canonical hook order
1394
+
1395
+ For a UIElement subclass with `static template = () => html\`<x/>\`` mounted into the DOM with consumer-supplied light-DOM children:
1396
+
1397
+ 1. **`adoptStyles(ctor)`** — adopted stylesheets attached to the document
1398
+ 2. **`prepareParts(ctor)`** — `static template` markup parsed into the part registry
1399
+ 3. **`this.connected()`** — subclass hook fires here, inside `untracked(...)`. **Light-DOM children supplied by the parent template are intact at this point.**
1400
+ 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)`).
1401
+ 5. **`this.render()`** — explicit render hook for imperative DOM updates
1402
+ 6. **`this.updated(changedProps)`** — change-set hook for property-specific reactions
1403
+
1404
+ **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.
1405
+
1406
+ 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.
1407
+
1408
+ #### Container UIElement pattern (FB-43/44/47/58 cluster — the right shape)
1409
+
1410
+ When your UIElement is a container for consumer-supplied light-DOM children AND has its own internal `static template`, capture the children in `connected()`:
1411
+
1412
+ ```js
1413
+ class DtsGraph extends UIElement {
1414
+ static template = () => html`<div class="dts-graph-root"></div>`;
1415
+
1416
+ // ✅ connected() runs BEFORE the stamp wipes children — capture here
1417
+ connected() {
1418
+ this._series = [...this.querySelectorAll('dts-graph-band, dts-graph-curve, …')];
1419
+
1420
+ // Optional: MutationObserver for late-arriving children (e.g. parent template
1421
+ // toggles `${cond ? html\`<band/>\` : ''}` and the new band lands later)
1422
+ this._mo = new MutationObserver(() => this._captureSeries());
1423
+ this._mo.observe(this, { childList: true, subtree: true });
1424
+ }
1425
+
1426
+ _captureSeries() {
1427
+ const present = [...this.querySelectorAll('dts-graph-band, …')];
1428
+ // Defensive cache-fallback: post-stamp wipe makes `present` empty, but the
1429
+ // initial capture from connected() above already saved the refs. Keep them.
1430
+ if (present.length > 0) this._series = present;
1431
+ }
1432
+
1433
+ disconnected() {
1434
+ this._mo?.disconnect();
1435
+ }
1436
+ }
1437
+ ```
1438
+
1439
+ 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.
1440
+
1441
+ #### `connected()` vs `connectedCallback()` reference
1442
+
1443
+ | Hook | Who calls it | When (relative to step list above) | Use case |
1444
+ |---|---|---|---|
1445
+ | `connectedCallback()` | The browser, per W3C custom-elements spec | Orchestrates the whole sequence | Almost never override — UIElement runs the lifecycle for you |
1446
+ | **`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** |
1447
+ | `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 |
1448
+ | `updated(changedProps)` | The effect | Step 6 — after `render()`, when properties changed | React to specific prop changes |
1449
+ | `disconnected()` | UIElement, from `disconnectedCallback()` | Element leaves the DOM | Teardown observers, timers, listeners |
1450
+
1451
+ #### When NOT to override `connectedCallback()` directly
1452
+
1453
+ `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:
1454
+
1455
+ - **BEFORE super**: you miss the `prepareParts(ctor)` call that initializes the part registry. `static template` won't stamp. Other downstream initialization breaks.
1456
+ - **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.
1457
+
1458
+ `connected()` is the right hook for 99% of consumer code. It runs at exactly the right time: children present, stamp not yet run.
1459
+
1460
+ #### Why `connected()` is wrapped in `untracked()`
1461
+
1462
+ 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.
1463
+
1464
+ 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.
1465
+
1185
1466
  ### §221j — Typography token cheatsheet
1186
1467
 
1187
1468
  Quick-reference for component-CSS authoring. Cross-reference [`styles/typography.css`](./styles/typography.css):