# Changelog — @adia-ai/web-components

## [0.6.41] — 2026-05-26

### Maintenance

- **Lockstep version bump only — pure ride-along.** No source changes in any of the 9 packages this cycle. Substantive v0.6.41 work is REPO-LEVEL: adia-ui-release skill v1.8.0 → v1.9.0 (catalog-drift recurring graduation — §Step 5.5 pre-commit trip-wire + supersedes-WT contract + §Step 5 allowlist convention; closes FEEDBACK-75). Version bump preserves 9-package lockstep coherence; published tarballs identical to v0.6.40 modulo version metadata.

## [0.6.40] — 2026-05-26

### Fixed — `index.js` barrel side-effect-imports `core/provider.js` (cold-start `<router-ui>` registration)

- **`packages/web-components/index.js`** — the barrel now side-effect-imports `core/provider.js`, which registers `<router-ui>` from the default cold-start path. Closes the FB-69-class regression where consumers importing the cluster-barrel hit "router-ui is not registered" when first navigating in a cold session. Files: `packages/web-components/index.js`.

## [0.6.39] — 2026-05-26

### Added — `<tour-ui>` spotlight-driven product tour primitive (Wave 5e last P1, #259)

- **`<tour-ui>`** — spotlight-driven product tour primitive. Closes the last Wave 5e P1. Component count 177 → 179 (+2 with tour + tour-step sub-primitive).

### Fixed — substrate defensive slot-selector sweep + admin-entity-item icon size

- **Substrate sweep**: slot selectors changed from direct-child to descendant across remaining primitives. Defensive companion to the v0.6.38 `<list-item-ui>` fix — same drift class in adjacent components.
- **`<admin-entity-item>`**: icon size now matches `<nav-item-ui>`'s row icon (24px) instead of hardcoded 16px. Brings the canonical brand-chrome pattern into visual parity with the nav-item it sits next to.
- **`<list-item-ui>`**: now declares `slot="action"` — closes P2 from the v0.6.38 `dogfood:status` gate.

## [0.6.38] — 2026-05-26

### Added — Wave 5e cohort: 3 new primitives + slot-vocab-vs-css backlog closed

- **`<mark-ui>`** — inline highlighted text primitive (#357)
- **`<inline-edit-ui>`** — click-to-edit text in place (#212)
- **Notification deep-link demo + Mark All Read verify** for `<feed-ui>` (#344/#343)

### Fixed — substrate polish across the v0.6.38 window

- **`<list-item-ui>`**: slot selectors changed from direct-child to descendant — description row now lands on row 2 as intended
- **`<segmented-ui>`**: stale indicator stripping — duplicate pills on re-render eliminated
- **`<stat-ui>`**: chart slot bottom-aligns to value baseline; aspect 4:3
- **`<toolbar-ui>`** mixed-controls demo: `<segment-ui>` (not `<button-ui>`) inside `<segmented-ui>`

### Maintenance — slot-vocab-vs-css audit reached 0 critical findings

- `audit-slot-vocab-vs-css` substrate backlog closed → gate now enforced. Multi-yaml + @scope-aware pairing landed in the audit script.
- 5 Wave 5a/b/c/d gap-analysis flips landed (substrate-verified as covered; no new code).

## [0.6.37] — 2026-05-25

### Changed — `--a-warning-bg` redirected from `-strong` to `-20-tint` (bright caution-tape amber, scheme-independent)

- **Token-system fix for the muddy warning surface class.** Previously `--a-warning-bg` was an alias for `--a-warning-strong` (= `-50`, mid-tone amber). Paired with `--a-warning-fg` (= `-text-strong` = `-10-shade`, dark brown — per `semantics.css:309` "warning is light-colored — text on warning fills should be dark"), the pair collapsed to brown-on-brown chips. The local fix shipped in `<tag-ui>` / `<badge-ui>` (literal `--a-warning-20-tint` override) is now hoisted to the L3 token layer so every consumer benefits: progress-row, rating, password-strength, agent-artifact, text[color=warning], timeline. `<button-ui color="warning">` updated to read `--a-warning-bg` (was `-strong`) so it joins the fix. Local tag/badge `-20-tint` overrides removed — they now read the canonical `-bg` token cleanly.
- **`-strong` unchanged** — still the L2 saturated mid-tone, used as a foreground/icon color by alert / chart / code / empty-state / agent-trace (those use it AS a color, not as a surface to put text on, so the `light-dark` swap stays appropriate).
- **Why `-20-tint` is bright in both schemes**: the lightness math `l-max - 0.2*(l-max - l-min)` is scheme-independent (no `light-dark()` swap), so warning surfaces keep the caution-tape look in dark mode too. Other family `-bg` tokens stay scheme-flipping because they pair with `-fg` (= `-05-tint` near-white) which has enough contrast either way.
- Files: `styles/colors/semantics.css`, `components/button/button.css`, `components/tag/tag.{css,test.js}`, `components/badge/badge.css`.

### Added — `<tag-ui>` + `<badge-ui>` gain `[tone="outline"]` third chip style

- **Outline tone**: transparent bg + family-colored 1px border + family-colored text. The lightest visual weight; good in dense data tables or faceted filter rows where multiple chips would otherwise compete. Family variants resolve to `--a-{family}-text` + `--a-{family}-border`; neutral default resolves to `--a-fg-muted` + `--a-border`.
- **Vocabulary mirror across the two primitives**: tag-ui defaults to `solid` tone (filter chip / status pill identity); badge-ui defaults to `muted` (quiet metadata identity). Both accept all three values (`solid` | `muted` | `outline`) so authors can swap shape without learning a new prop. Badge-ui's existing `primary` variant remains as a shortcut for `<badge-ui variant="accent" tone="solid">`.
- New regression guards: 6 source-grep tests on tag-ui for outline-tone rules.
- Files: `components/tag/tag.{css,yaml,test.js,examples.html}`, `components/badge/badge.{css,yaml,examples.html}`, regenerated `.a2ui.json` sidecars.

## [0.6.36] — 2026-05-24

### Changed — `<pagination-ui>` composes `<button-ui>` for every cell + adds universal `[size]` thread-through

- **Native-primitive leak closed.** `pagination.class.js:128` previously stamped raw `<button>` for every page / prev / next cell — the exact failure mode `dogfood-sweep` mode 4 audits for, and the same shape that produced the FB-55 silent-failure trap on `<admin-shell>`. Substrate authors reading pagination as the canonical numbered-button pattern would carry the leak into downstream components. Substrate now stamps `<button-ui>` for every cell so chrome / hover / focus-ring / disabled-state / active-fill come from button-ui's token chain (the canonical surface matrix), not a pagination-tier re-impl.
- **`[size]` prop added** — reflected enum (`sm` | `md` | `lg`, default `md` — aligned with `<button-ui>`'s default since pagination is now a button-ui composite). Threads through to every nested `<button-ui size=…>` so pagination honors the substrate's 24/30/36 px universal size system (with `[density]` modifier). Previously pagination hardcoded `--a-size-sm` (24 px), so consumers on md/lg surfaces had no escape hatch.
- **Active state now `variant="primary"`** instead of a pagination-tier `[data-active]` + custom-bg-token chain. The filled-accent state inherits from button-ui's primary surface matrix — one token chain, one source of truth.
- **Prev / Next now use Phosphor `caret-left` / `caret-right`** via `<button-ui icon=…>` (was: `‹` / `›` Unicode glyphs). Declared in `static requiredIcons` so the audit picks them up.
- **`variant="button"` mode** swaps each non-active cell to `variant="outline"` (the bordered 1×1 mode). `pagination.css` shrunk from 165 → 71 lines — the 94 lines of button styling rules collapsed into button-ui composition.
- **Constant-width compact-mode layout** — `#buildRange` redesigned around an invariant `W = 2*siblings + 5` (= 7 cells at siblings=1; 9 at siblings=2). Three layouts (near-start / middle / near-end) each yield exactly W cells so the row width stays stable when the current page advances. When `total ≤ W`, every page renders (no ellipsis — compressing wouldn't save horizontal slots). Replaces the "bridge if only 1 is skipped" heuristic from an earlier draft, which produced asymmetric cell counts under page advances (page 3 → 6 cells, page 4 → 7 cells — visibly jumpy).
- **17 new DOM + source-grep regression tests** (`pagination.test.js`, NEW). Guards: no raw `<button>` in source; `requiredIcons` declared; size threads to every nested button-ui; active page is `variant="primary"`; rest variant tracks the host's `variant=` (ghost/outline); **width invariance under all page positions for total=10 siblings=1 (always 7 cells) and total=20 siblings=2 (always 9 cells)**.
- Files: `components/pagination/pagination.{class.js,yaml,css,test.js}`, regenerated sidecar + corpus.

### Fixed — `<tag-ui>` dismiss X + slotted icons now inherit the variant text color

- **Visible after the solid-default flip**: the dismiss X (`[slot="dismiss"]`) was hardcoded to `--a-fg-muted` (a neutral grey) regardless of the host's variant, so the X disappeared against saturated solid pills — a near-invisible grey X on the Info/Success/Warning/Error bgs. Same trap on any slotted leading icon: tag had no `currentColor` inheritance rule, so `<icon-ui>` children rendered in their own foreground color rather than the variant's.
- **Fix**: dismiss `color` token now defaults to `currentColor` at `opacity: 0.85` (quieter than the label, but tracks every variant — near-white X on saturated bg, dark X on warning amber, fg-muted X on quiet chrome). Hover restores `opacity: 1` and overlays a `color-mix(in oklch, currentColor 18%, transparent)` bg highlight — same color family as the host, not a stark neutral wash. The dismiss `<icon-ui>` now stamps with `weight="bold"` so the X reads crisply at small size on every variant (was implicit regular — too thin against bright bgs like warning amber). NEW `:scope > icon-ui { color: currentColor; flex-shrink: 0 }` rule mirrors `<badge-ui>`'s convention so leading-icon legends/status chips read as a single color-coded unit.
- Two new component tokens for the dismiss opacity (`--tag-dismiss-opacity`, `--tag-dismiss-opacity-hover`) replace the prior color-swap tokens (`--tag-dismiss-fg-hover` removed — opacity-driven hover instead). Files: `components/tag/tag.css`.

### Fixed — `<tag-ui variant="warning">` solid pill: bright amber bg instead of muddy mid-brown

- **Visible after the solid-default flip**: the canonical `--a-warning-bg` + `--a-warning-fg` pair collapses for warning. `--a-warning-fg` resolves to `--a-warning-10-shade` (a dark brown — per `semantics.css:309` "warning is light-colored — text on warning fills should be dark"), but `--a-warning-bg` (= `--a-warning-strong` = `-50`) is a mid-tone amber. Dark text on mid-tone amber gives the muddy brown-on-brown look the user reported. The other family variants (info/success/danger) all have `-text-strong = -05-tint` (near-white) which contrasts cleanly against their `-50` saturated bg, so they don't hit this trap.
- **Fix at the tag layer**: `:scope[variant="warning"]` now uses the literal `--a-warning-20-tint` step (bright caution-tape amber, scheme-independent) as bg + keeps `--a-warning-fg` as dark text. Restores the bright-amber-with-dark-text shape `semantics.css` intends. Other family variants unchanged. Regression guard added that hard-fails any revert to `--a-warning-bg` for the warning solid bg.
- **Token-system follow-up flagged (not shipped)**: this is a symptom of `--a-warning-bg` pointing at the wrong step. Same muddy contrast likely surfaces wherever `--a-warning-bg` + `--a-warning-fg` is paired (`button-ui variant="warning"`, `chart-ui` warning bars, `rating-ui` filled fg, `progress-row variant="warning"`). A token-side fix that redirects `--a-warning-bg` to `--a-warning-20-tint` would resolve all consumers in one stroke but needs a visual sweep across those components first. Files: `components/tag/tag.{css,test.js}`.

### Changed — `<tag-ui>` family variants default to **solid** fill (BREAKING-visual)

- **`<tag-ui variant="info|success|warning|danger">` now renders as a saturated bg + on-strong (near-white) text** — the chip IS the state, not a tinted metadata label. Aligns the chip vocabulary so consumers can read tag-ui as a status pill at a glance. Previously: muted tinted bg with `--a-{family}-bg` (saturated solid) text — an off-canonical pair that read as "passive metadata with bold color" and diverged from `<badge-ui>`'s canonical muted pair.
- **Opt-out**: NEW `[tone="muted"]` attribute drops a family variant back to the canonical muted pair `--a-{family}-muted` + `--a-{family}-text` (scheme-flipping text — the same pair `<badge-ui>` uses as its default). Use on metadata chips in dense lists where the saturated default would compete for attention.
- **The `default` variant (no family) is unchanged** — it stays as quiet `--a-bg-muted` + `--a-fg` chrome. Opt in to a high-contrast inverse stamp via `<tag-ui tone="solid">` (no variant); resolves to `--a-fg` bg + `--a-bg` fg.
- **Author migration**: existing `<tag-ui variant="info|success|warning|danger">` markup auto-flips to the new saturated default — every existing tag in the codebase becomes louder. If any specific surface needs the prior look, append `tone="muted"`. 75 occurrences across 30 substrate/app/playground files identified; review per surface.
- Source-grep regression guards added: 4 family variants verified to use the solid pair as default; 4 `[tone="muted"][variant=…]` rules verified to use the canonical muted pair; `[variant="default"]` verified unchanged; `[tone="solid"]` neutral inverse verified. Files: `components/tag/tag.{css,yaml,test.js,examples.html,a2ui.json}` + regenerated corpus catalog.

### Fixed — placeholder pseudo no longer pushes the caret to its right after type-then-delete

- **Bug report**: with `<input-ui placeholder="Select country…">`, typing text then deleting it left the caret visually at the END of the placeholder text instead of at position 0. Same trap on `<textarea-ui>`, `<combobox-ui>`, and `<tags-input-ui>`. Root cause: the empty-state placeholder is implemented via `[data-empty]::before { content: attr(data-placeholder); }`, rendered as an **inline** pseudo box. When the contenteditable is empty, the caret IS at offset 0 of the empty text node — but the in-flow `::before` pseudo precedes the text content, occupying inline space, so the caret renders to the right of the pseudo's box. (Only happens after type-then-delete because that path leaves the contenteditable focused, making the caret visible at its true position-0 location.)
- **Fix**: pseudo is now `position: absolute; inset: 0; padding: inherit` — fills the host's content-box without occupying inline-flow space, so the caret renders at the actual content-start where it belongs. Host elements gain `position: relative` as the positioning anchor. For `<tags-input-ui>` + `<combobox-ui>` the pseudo also uses `display: flex; align-items: center` so the placeholder text vertically aligns with the host's flex-centered line-box.
- Regression guards added to `input.test.js` — three source-grep assertions (`:scope` declares `position: relative`, pseudo declares `position: absolute`, pseudo carries `inset: 0` + `padding: inherit`) that hard-fail if the in-flow shape ever re-appears.
- Files: `components/input/input.{css,test.js}`, `components/textarea/textarea.css`, `components/tags-input/tags-input.css`, `components/combobox/combobox.css`.

## [0.6.35] — 2026-05-24

### Fixed — `<popover-ui>` + `<menu-ui>` yaml `slots:` blocks declare the canonical `trigger` / `content` vocabulary (FB-62)

- **CSS positioned slots that the yaml said didn't exist.** `popover.yaml` pre-patch line 43: `slots: {}` (empty). But `popover.css` positions `[slot="trigger"]` and `[slot="content"]` across 7 selectors (open/closed states + first/last-child margin guards), and the yaml's own a2ui rule already states "popover-ui wraps a focusable trigger (slot=\"trigger\") + arbitrary interactive content (slot=\"content\")". Parallel case in `menu.yaml`: `slots:` declared only `default`, but the a2ui rule said "MUST have exactly one child with slot=\"trigger\"". Three sources said the slots existed, one source (the structured `slots:` SoT) said they didn't. Surfaced as 6 INFO findings in the adia-ui-kit v3.4.X recipe-staleness audit; suppressed via `KNOWN_PARENT_SLOT_DRIFT`.
- **Fix**: `popover.yaml` `slots:` now declares `trigger` (focusable element that opens the popover; MUST be focusable for keyboard accessibility — bare `<span>`/`<div>` won't work) and `content` (the body, hoisted to top-layer via `:popover-open`). `menu.yaml` `slots:` adds `trigger` (Required — exactly one child must have `slot="trigger"`; without it the menu cannot open). Pure yaml backfill — no JS, no CSS, no API change. Sidecar `.a2ui.json` files regenerated. Kit's `KNOWN_PARENT_SLOT_DRIFT` allowlist entries can be removed next cycle. Files: `components/popover/popover.{yaml,a2ui.json}`, `components/menu/menu.{yaml,a2ui.json}`.

### Fixed — chat-input + textarea + input substrate prose accurately documents Enter→submit (FB-63)

- **`submit-on-enter` was a documentation phantom across the substrate, and the prose around it carried two adjacent lies.** Three input primitives all dispatch a bubbling `submit` event on Enter (without Shift) unconditionally: `textarea.class.js:97-106`, `input.class.js:713-727`, and `chat-input.js` (which delegates to the inner `<textarea-ui>`'s submit event at line 147). None of them observe a `[submit-on-enter]` attribute — yet the substrate's own demos, yamls, and a2ui rules used `<chat-input-ui submit-on-enter>` as canonical authoring shape. Two adjacent lies discovered in the same cluster: (1) `textarea.yaml` description + a2ui rule + `textarea.examples.html` claimed "Enter inserts a newline — textarea-ui does NOT emit a `submit` event" — false per `textarea.class.js:97-106`. (2) `input.yaml` + `input.examples.html` claimed "only chat-input-ui reflects the [submit-on-enter] attribute" — false, NONE of them reflect it.
- **Fix**: `submit-on-enter` stripped from `patterns/chat-shell/chat-shell.examples.html` (1 occurrence). `chat-input.yaml` event description + `chat-input.js` docstring now state the truth — Enter→submit is unconditional, delegated from inner `<textarea-ui>`, no opt-in/opt-out. `textarea.yaml` description (line 10-18) + a2ui rule (line 119-123) + `textarea.examples.html:253` corrected: "Enter (without Shift) dispatches a bubbling `submit` event; Shift+Enter inserts a newline. Unconditional — no opt-in/opt-out attribute." `input.yaml` + `input.examples.html` adjacent prose: stripped "only chat-input-ui reflects [submit-on-enter]" claim; replaced with accurate "the plain `<input-ui>` primitive ALSO fires `submit` on Enter (unconditional, no opt-in attribute); `<chat-input-ui>` simply builds on that semantic." Companion strip in `@adia-ai/web-modules` chat-cluster (see its changelog). Sweep-grep `submit-on-enter` in authored sources now returns only the two truth-note phrasings (both say "no `[submit-on-enter]` opt-in/opt-out"). Sidecar `.a2ui.json` files regenerated; corpus catalog + rules text rebuilt. Files: `components/chat-thread/chat-input.{yaml,js,a2ui.json}`, `components/textarea/textarea.{yaml,examples.html,a2ui.json}`, `components/input/input.{yaml,examples.html,a2ui.json}`, `patterns/chat-shell/chat-shell.examples.html`.

### Fixed — `<spinner-ui variant="dots">` dot spacing now even (was lopsided)

- **The pre-fix dots layout used `::before` + `::after` pseudo-elements** as the outer two dots and a `box-shadow` on `::before` to paint the middle dot. Three breakages: (1) the box-shadow middle dot doesn't participate in the host's flex-gap math, so the gap between dot1↔dot2 and dot2↔dot3 was unequal (the screenshot showed two dots close together on the left, then one isolated on the right). (2) The box-shadow dot couldn't carry its own `@keyframes` animation, so the middle stayed at default scale while the outer two bounced — a "middle is anchored" look. (3) The author comment explicitly acknowledged the hack ("simpler, more robust shape is to overlay three identical dot pseudos via a flexbox container of pseudo + two child pseudos isn't possible without a stamped child"). Fix: `class.js` `#syncDots()` now stamps three real `<span data-spinner-dot=N>` children when `variant="dots"`. Flex-gap distributes the row evenly; each child gets `animation-delay: 0 / T/3 / 2T/3` so the bounce reads as a left-to-right wave. Reduced-motion + `[paused]` selectors updated to include `> [data-spinner-dot]`. Files: `components/spinner/class.js`, `components/spinner/spinner.css`, `components/spinner/spinner.test.js`.

### Added — `<spinner-ui variant="knight">` knight-rider sliding-bar variant

- **NEW `knight` variant** — a horizontal track with a thumb that slides back-and-forth via `animation-direction: alternate`. Reads as a determinate-looking bar but is indeterminate by intent (no progress value; same loading semantics as the other variants). Tokens added: `--spinner-bar-track-width` (default `4rem`), `--spinner-bar-track-height` (default `var(--spinner-stroke)` — visual continuity with arc/ring), `--spinner-bar-thumb-ratio` (default `0.3` — thumb is 30% of track). Keyframe math: `translateX((1 / ratio - 1) * 100%)` of thumb-relative width = `(track - thumb)` px regardless of track-width override. Reduced-motion path hides the thumb and falls through to the shared ellipsis fallback. yaml `variant` enum + description updated. Files: `components/spinner/spinner.css`, `components/spinner/spinner.yaml`, `components/spinner/spinner.examples.html`.

### Added — `<integration-card-ui>` demo page now ships a live CRUD drawer wiring

- **`integration-card.examples.html` previously showed cards only as static visual showcases** — clicking `Connect` / `Configure` dispatched the `connect` / `configure` events into the void (no consumer wiring on the docs page). Added a "Wired to drawer (CRUD)" section at the top with three cards (`slack` / `github` / `linear`) bound to a shared `<drawer-ui>` form. NEW `integration-card.examples.js` setup module: listens for bubbled events at the grid level, opens the drawer in `connect` or `configure` mode, persists token to an in-memory store keyed by `provider`, and on Save / Disconnect mutates the originating card's `status` attribute. Demonstrates the full Create / Update / Delete loop expected of consumers per SPEC-062 §134-136. Sitemap entry gains `"setup": ".../integration-card.examples.js"` so the docs router auto-imports the module on route load (same pattern as `agent-feedback-bar`, etc.). Files: `components/integration-card/integration-card.examples.html`, `components/integration-card/integration-card.examples.js` (NEW), `site/sitemap.json`.

### Changed — `<date-range-picker-ui>` trigger surface mirrors `<select-ui>` / `<calendar-picker-ui>`

- **`date-range-picker.css` had only `:scope > [slot="trigger"] { min-width: 14em }`** — the entire trigger chrome relied on `<button-ui variant="outline">`'s defaults, which produced an off-pattern surface vs. the rest of the input/picker family. Trigger now styled with the canonical contract: flex row, `min-height: var(--a-size)`, `padding: 0 var(--a-ui-px)`, `border-radius: var(--a-radius)`, `border: 1px solid var(--a-ui-border)`, `background: var(--a-ui-bg)`, `justify-content: space-between` so leading icon + label sit start, trailing caret sits end. Adds canonical `--date-range-picker-trigger-{height,px,gap,radius,bg,bg-hover,fg,border,border-hover,focus-ring}` token shelf — same tokens `<calendar-picker-ui>` already exposes, so consumers can override either family-side. Hover/focus-visible rules attached for parity. Files: `components/date-range-picker/date-range-picker.css`.

### Fixed — `<spinner-ui>` rotational cadence (was 300ms, now 0.8s)

- **`--spinner-duration-default` retargeted from `var(--a-duration-slow)` (300 ms — interaction-duration scale, tuned for hover/focus transitions) to `0.8s`** — canonical loader cadence. A full revolution at 300 ms reads as a frantic blur; 0.8 s gives smooth, perceptible motion that signals "loading" without alarming the eye. The `--a-duration-*` token family (120 / 250 / 300 ms) is now correctly reserved for state transitions; spinner uses its own rotational tempo. Files: `components/spinner/spinner.css`.

### Fixed — `<modal-ui>` header no longer reserves a phantom flex slot for empty `text=`

- **`modal.css`'s `:scope [slot="header"]::before { content: attr(text); flex: 1 }` rule generated an empty-content pseudo-element with `flex: 1` whenever the host's `text` attr was empty or unset.** That phantom flex slot claimed the row's free space and pushed any slotted header content (e.g., `<confirm-dialog-ui>`'s stamped `<text-ui part="title">` + auto-stamped close button) to the trailing edge — the title rendered tight against the X close button instead of left-aligned. Same drift class as the `tag-ui` / `badge-ui` / `segment-ui` / `button-ui` phantom-gap arc (April–May 2026).
- **Fix (defense-in-depth)**: CSS gate the `::before` on `[text]:not([text=""])` so the pseudo isn't generated when text is absent or empty; class.js no longer writes `text=""` on the header when `this.text` is falsy. Either side alone would fix the symptom; both prevent recurrence from independent edit paths. Visible effect: `<confirm-dialog-ui>` now renders "Save changes?" left-aligned with X on the right, matching the canonical header pattern. Files: `components/modal/modal.css`, `components/modal/class.js`.

### Added — `<calendar-grid-ui>` `range-start` / `range-end` props for in-range cell highlighting

- **`<calendar-grid-ui>` gains two new reflected string props — `range-start` and `range-end` (ISO `YYYY-MM-DD`).** When both are set + ordered, day cells strictly between the endpoints get `[data-in-range]` stamped on them. New CSS state styles in-range cells with `--calendar-grid-day-bg-in-range` (defaults to `--a-accent-muted` — the muted accent strip) and `--calendar-grid-day-fg-in-range`. The endpoints themselves continue rendering via the `value` prop's `[data-selected]` state (full `--a-accent` fill); the in-range middle is visually subordinate. Per-day cells also get `data-range-start` / `data-range-end` markers for future shape work (rounded caps, etc.).
- **`<date-range-picker-ui>` now wires this contract**: in `render()`, the effective from/to (pending click-state OR committed value) is pushed onto BOTH calendar-grid panes via `range-start` / `range-end` attributes, so each pane independently lights up its in-range cells. Closes the "selected start + end but no fill between" trap. Files: `components/calendar-grid/calendar-grid.css`, `components/calendar-grid/class.js`, `components/calendar-grid/calendar-grid.yaml`, `components/date-range-picker/class.js`.

## [0.6.34] — 2026-05-23

### Added — Wave 1 + Wave 2: 10 P1 components shipped from SPEC pilot

- **Wave 1 — 8 components** (commit `5ad89318d`): SPEC-001, SPEC-022, SPEC-036, SPEC-037, SPEC-043, SPEC-049, SPEC-056, SPEC-062. First wave of the SPEC-driven component-authoring pilot. Each ships full yaml + a2ui.json + .js class + .css + .examples.html + .test.js per the four-axis contract.
- **Wave 2 — 2 components** (commit `501053880`): SPEC-019, SPEC-038. Completes the 10-component pilot.
- **`<calendar-grid-ui>` substrate primitive extracted** (commit `dcf16cdf8`): a substrate-tier primitive supporting the calendar-family Wave 2 components.
- **Wave 1/2 visual-QA fixes** also landed in `dcf16cdf8`.
- Component count: 92 → 100 primitives (8 net additions). yaml/a2ui.json/d.ts coherence preserved.

### Fixed — OD-5 token-shadowing closed across 98 components (`-default` suffix sweep)

- **`-default` suffix added to all leaf component tokens** (commit `e694be9d0`, single-component fix at `cbdf11f09`): unlocks themability/consumer override across all 98 component CSS files. Pattern: `--button-bg: var(--a-fg);` → `--button-bg-default: var(--a-fg); ... background: var(--button-bg, var(--button-bg-default));`. Consumer overrides at any ancestor surface now apply correctly without being shadowed by the leaf-token default. Closes OD-5 (consumer-side override-discoverability regression).

### Changed — `dist/web-components.min.js` + `dist/web-components.min.css` rebuilt

- Bundle pass absorbed Wave 1+2 + OD-5 + `<calendar-grid-ui>` source changes. Bundle sizes adjusted accordingly.

### Maintenance — `<spinner-ui>` exempted from size-wiring audit

- **`scripts/release/check-size-wiring.mjs`** YAML_SIZE_EXEMPT: added `spinner` — `size:` prop drives diameter via `--spinner-size` matching the icon-ui ladder (sm 14px / md 16px / lg 20px), parallel to existing exemptions for `icon`, `rating`, `swatch`. Content-driven loading glyph, not row-family. Surfaced by the audit after spinner joined the 100-component roster.

## [0.6.33] — 2026-05-23

### Changed — `dist/icons-manifest.js` comment header updated (FB-53 §1 companion)

- **`dist/icons-manifest.js`** is regenerated as a companion to the web-modules co-emit (see `@adia-ai/web-modules` [Unreleased] entry). The manifest file content is byte-identical except for the header comment, which now mentions co-location with both `web-components.min.js` AND `web-modules/everything.min.js`. `dist/web-components.min.js` rebuilt as part of the same bundle pass; no functional change. Files: `dist/icons-manifest.js`, `dist/web-components.min.js`.

### Fixed — `<table-ui raw>` is now a true visual-only passthrough (FB-53 §2)

- **`<table-ui raw>` previously applied the chrome reset but still ran the full data lifecycle**, so wrapping a consumer-authored native `<table>` with no `.data` set produced a phantom `[data-empty]` "No data" overlay above the consumer's content. Root cause: `class.js` `render()` had no `this.raw` short-circuit; `#renderOverlays` injected the empty-state placeholder unconditionally whenever `this.#data.length === 0`. Fix: `render()` now early-returns when `this.raw` is set — no header injection, no body reconciliation, no overlays, no aggregation, no pagination. Raw is now the documented "consumer owns the body shape" mode. yaml description expanded with the full semantic. NEW 4 vitest cases in `table.test.js` (lock the contract: no `[data-empty]`, no `[data-header]`, no `[data-body]`, no rowgroups; non-raw default still injects them); 18/18 passing. Files: `components/table/class.js`, `components/table/table.yaml`, `components/table/table.test.js`.

### Fixed — `<field-ui>` no longer reserves row-gap for an absent message row (FB-54)

- **`field.css` `:scope` declared `row-gap: var(--field-gap)` unconditionally**, so when no `[slot="hint"]` and no `[slot="error"]` were present, the grid's zero-content message row still reserved `--field-gap` (~8 px) between itself and the control row. Result: `<field-ui inline>` rendered ~7-8 px taller than non-field-ui siblings in a `<row-ui align="center">`, visually "lifting" the label relative to sibling control text. Fix: move `row-gap: var(--field-gap)` into a `:has(> [slot="hint"], > [slot="error"])` guard so the gap is reserved only when a message slot is actually present. Existing consumers with hints/errors get the same rendering; consumers without get a tighter, sibling-matching height. `:has()` is baseline (Chromium 105 / Safari 15.4 / Firefox 121 — well within ADR-0007's floor). NEW source-grep regression tests in `field.test.js` lock the contract (19/19 passing). Files: `components/field/field.css`, `components/field/field.test.js`.

### Changed — `<tabs-ui>` row height now anchors to `var(--a-size)` (was hardcoded `2.25rem`)

- **`--tabs-button-height` retargeted from the hardcoded `2.25rem` (36 px, equivalent to `[size="lg"]`) to `var(--a-size)`.** Tabs now respond to the universal `[size]` attribute system like `button-ui`, `select-ui`, `nav-item-ui`, and other row-family primitives — `<tabs-ui size="sm">` → 24 px, default → 30 px, `<tabs-ui size="lg">` → 36 px (all at default `--a-density: 1`). Default-rendered tabs visibly shrink from 36 px → 30 px. **Behavior change** for consumers depending on lg-by-default tabs: set `[size="lg"]` on the host (or override `--tabs-button-height: var(--a-size-lg)`) to restore. Caught by NEW `check:size-wiring` audit (commit `2d931b980`). Files: `components/tabs/tabs.css`, `components/tabs/tabs.examples.html`.

### Maintenance — `<kbd-ui>` exempted from size-wiring audit

- **`scripts/release/check-size-wiring.mjs`** EXEMPT + YAML_SIZE_EXEMPT: added `kbd-ui` to both — `--kbd-height: 1.25rem` is intentional (keyboard-key cap matching small-text rhythm, not row-family) and the `size:` prop drives font-size scale, parallel to existing exemptions for `badge`, `tag`, `icon`. Pre-existing technical debt surfaced by the new audit in v0.6.32; not a v0.6.33-introduced violation. Files: `scripts/release/check-size-wiring.mjs`.

## [0.6.32] — 2026-05-23

### Fixed (P0) — `everything.js` CDN bundle now includes all primitives (FB-51)

- **`packages/web-modules/everything.js` was shells-only in v0.6.29–v0.6.31** — it registered all four shells but did NOT register the 95+ primitives (`search-ui`, `select-ui`, `field-ui`, `stat-ui`, `row-ui`, etc.). Consumers using the CDN-first "just drop in `everything.min.js`" path hit a hard `TypeError: Cannot read properties of undefined (reading 'toLowerCase')` on any interaction with a primitive element. Root cause: `bundle-js.mjs` assembled the bundle from only the web-modules `index.js`, which re-exports shells but not primitives. Fix: bundle now explicitly includes `web-components/index.js` (all primitives) before web-modules. File: `packages/web-modules/everything.js`.

### Added — `dist/icons-manifest.js` CDN icons-manifest; `core/icons-phosphor.js` CDN prefix (FB-51)

- **`packages/web-components/dist/icons-manifest.js`** is now included in the npm tarball's `files:` field and exposed via the `./dist/icons-manifest` export path. Consumers loading via CDN received a 404 on the manifest fetch because the file was excluded from the published package. File: `packages/web-components/dist/icons-manifest.js`.
- **`core/icons-phosphor.js`** — the non-Vite fallback path now destructures `{ cdn: cdnBase, ...weights }` from the manifest. When `cdnBase` is present (as it is in the new generated manifest), SVG fetches use the CDN URL (`https://cdn.jsdelivr.net/npm/@phosphor-icons/core@X.Y.Z/assets/`) instead of `/node_modules/...` (localhost-only). File: `core/icons-phosphor.js`.

### Added — `site/llms.txt` + `scripts/build/llms.mjs` machine-readable docs

- **`site/llms.txt`** is an LLMs.txt-spec document generated by `scripts/build/llms.mjs` and included in every EXE site deploy. It exposes the full component API surface and usage patterns in a format suitable for LLM context injection. Build step wired into `build:site` as a pre-step.

### Added — `status: stable` field on all 141 component YAMLs

- Every component YAML now carries `status: stable`, making the stability signal machine-readable in A2UI catalog queries and corpus retrieval. The field is optional in the schema; previously it defaulted to absent (implicitly stable for all released components).

### Documentation — anatomy sections added to all `components/`, `styles/`, and `traits/` example pages

- **Canonical anatomy sections** (`data-property="slots|data-attrs|keyboard|css-vars|a2ui|related"`) added to all 124 component example pages, 56 trait example pages, and 10 token (`styles/`) example pages via `scripts/docs/anatomy-sweep.mjs`. The sweep generates slot tables, keyboard interaction tables, CSS custom property tables, A2UI composition rules, and related-component links directly from each component's `.a2ui.json` sidecar data. Existing `properties`, `events`, `CSS Tokens`, and `usage` sections were normalized to the canonical `data-property` vocabulary. Files: `components/**/*.examples.html`, `styles/**/*.examples.html`, `traits/**/*.examples.html`.

### Fixed — `<tag-ui>` / `<badge-ui>` / `<segment-ui>` phantom trailing gap when content is slotted (not `[text]`)

- **Three primitives in the same family rendered `::after { content: attr(text) }` unconditionally.** When consumers passed text via the default slot (`<tag-ui>Beta</tag-ui>`) instead of the `[text]` attribute, `attr(text)` resolved to `""` — an empty string IS generated content, so the pseudo-element existed as a flex item. Combined with the host's `display: inline-flex` + `gap`, gap was applied between the slotted text and the empty `::after`, adding a phantom `--{component}-gap`-wide trailing space (~4 px). Fix: gate the rule on `:scope[text]` so the pseudo-element is only generated when the attribute exists — matches the already-correct form in `button.css`. NEW `tag/tag.test.js` (3 cases) pins the contract via source-grep so the ungated form can't drift back. Files: `components/tag/tag.css`, `components/tag/tag.test.js`, `components/badge/badge.css`, `components/segment/segment.css`.

## [0.6.31] — 2026-05-23

### Documentation — 3 legacy `patterns/*` example files rewritten as canonical hubs

Three pre-v0.4.0 `patterns/*.examples.html` files used legacy markup retired by ADR-0024 (`<aside data-sidebar>`, `<main>`, `<dialog data-command>` for admin-shell; raw `<header data-chat-name>` + `<section data-chat-messages>` + `<chat-input-ui data-chat-input>` for chat-shell; raw `<header>` + `<div data-editor-body>` + `<pane-ui data-left|data-right>` + `<div data-canvas>` for editor-shell). All three rewritten as canonical-hierarchy pattern hubs — code-ui block with the bespoke vocabulary + part-by-part table linking to `/site/components/*` reference pages + authoring rules + "where to next" cross-links. Affects EXE-rendered docs only (`patterns/` is NOT in the npm tarball's `files:` field). Files: `patterns/admin-shell/admin-shell.examples.html`, `patterns/chat-shell/chat-shell.examples.html`, `patterns/editor-shell/editor-shell.examples.html`.

### Maintenance

- Lockstep version bump tied to docs surface scrub. No source changes in this package; bumped to maintain the 9-package version coherence enforced by `scripts/release/check-lockstep.mjs`.

## [0.6.30] — 2026-05-23

### Added — `dist/web-components.min.js` rolled-up CDN-ready JS bundle

- **`dist/web-components.min.js`** ships as part of the tarball (1.01 MB raw / ~330 KB gzipped / ~250 KB brotli). Single esbuild-bundled file — flattens every relative import from `index.js` through the 95 primitive registrations + core + tokens into one ESM payload. Browser-target Chromium 125+ / Safari 18+ / Firefox 129+ (matches existing `check:lightningcss-build` baseline).
- **New `./js/bundled` export** in package.json `exports` map points at the bundle. Existing `./` (root) export unchanged — bundler consumers (Vite, webpack, esbuild) continue using it.
- Companion to the v0.6.29 CSS bundle: HTML-first / CodePen / marketing pages can now drop in two `<link>` + one `<script type="module">` and have working AdiaUI primitives. See [`/site/getting-started/cdn-usage`](https://ui-kit.exe.xyz/site/getting-started/cdn-usage) for the full template.
- Generation: `npm run build:bundle-js` (esbuild bundle mode). Freshness enforced at release time via `check:js-bundles-fresh` gate (mirrors `check:css-bundles-fresh` discipline). `npm run build:bundles` runs both CSS + JS together.

### Documentation — README CDN section + AI-agent discoverability

- **README.md** grows two new sections: "CDN (no bundler — CodePen, marketing pages, static HTML)" with the bundle URLs + choice-tree, and "Using with AI agents" pointing at the `adia-ui-kit` skill for Claude Code / Cursor / Windsurf / Claude Desktop harnesses.

## [0.6.29] — 2026-05-23

### Added — `dist/web-components.min.css` rolled-up CDN-ready bundle

- **`dist/web-components.min.css`** ships as part of the tarball (443.1 KB unmin → ~50KB gzipped through any CDN edge). Single LightningCSS-bundled file — flattens every `@import` from `index.css` through the 95 per-component CSS files + tokens + resets into one minified payload. Replaces the @import-waterfall behavior for HTML-first consumers (no bundler) and enables one-line CDN usage: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@adia-ai/web-components@0.6.29/dist/web-components.min.css">`.
- **New `./css/bundled` export** in package.json `exports` map points at the bundle. Existing `./css` export (@import barrel) is unchanged — bundler consumers (Vite) continue using it. Files: `dist/web-components.min.css`.
- Generation: `npm run build:bundle-css` (LightningCSS bundle mode, Vite-8-default targets matching `check:lightningcss-build`). Freshness enforced at release time via `check:css-bundles-fresh` gate (mirrors `check:embeddings-fresh` discipline).


### Fixed — `text-align: start` reset on 18 components to block inherited centering (§text-align-reset)

- **18 components — `input-ui`, `textarea-ui`, `search-ui`, `select-ui`, `check-ui`, `radio-ui`, `switch-ui`, `toggle-group-ui`, `segmented-ui`, `range-ui`, `slider-ui`, `stat-ui`, `badge-ui`, `tag-ui`, `pagination-ui`, `nav-item-ui`, `progress-row-ui`, `kbd-ui` — lacked a `text-align: start` reset in their `:where(:scope)` rule.** `text-align` is an inherited CSS property; when any of these components is a descendant of a container declaring `text-align: center` (or any non-default alignment), the alignment propagated into the component and visibly misaligned its content. Most critically: `input-ui` placeholder and typed value would center-align inside the input chrome; `select-ui` selected value and placeholder would shift; `textarea-ui` text would center. Also affected: `::after`-rendered label text on check/radio/switch/toggle-group/segmented (label text would visually shift); inline labels on badge/tag/kbd; stat values; pagination page numbers; nav-item labels. Fix: added `text-align: start;` to the `:where(:scope)` block of all 18 affected components. Files: `components/{input,textarea,search,select,check,radio,switch,toggle-group,segmented,range,slider,stat,badge,tag,pagination,nav-item,progress-row,kbd}/*.css`.

## [0.6.28] — 2026-05-23

### Maintenance
- **Lockstep version bump only.** No source changes in this package; bumped to maintain the 9-package version coherence enforced by `scripts/release/check-lockstep.mjs`. Substantive v0.6.28 work shipped in editor-sidebar rAF debounce for wide->narrow display-flip transient (FB-50, 3rd iteration of the latch saga; belt-and-braces with v0.6.26 width guard + v0.6.27 computedStyle guard). See `packages/web-modules/CHANGELOG.md#0628--2026-05-23` for details.

## [0.6.27] — 2026-05-22

### Added — `chart-ui[loading]` skeleton placeholder (§FB-12)

- **`<chart-ui loading>` shows a `<skeleton-ui>` placeholder** instead of the chart body while data is being fetched. Preserves the element's aspect-ratio dimensions so the skeleton occupies the same space the chart will fill once data arrives. Parity with `stat-ui[loading]` and `table-ui[loading]` (both shipped v0.6.18). Closes the remaining open item from FEEDBACK-12. File: `components/chart/class.js` + `components/chart/chart.yaml`.

## [0.6.26] — 2026-05-22

### Added — regression tests for FB-46 (select-ui) and FB-47 (table-ui)

- **`components/select/select.test.js`** (new, 4 tests) — locks the FB-46 `[multiple]` pre-init fix: `<option selected>` captures all values, `.value="a,b"` syncs `aria-selected` via Set, single-select backward compat preserved.
- **`components/table/table.test.js`** (2 new tests) — locks the FB-47 Enter-key fix: Enter on a focused body cell fires `cell-click` with correct row data; Enter on a header cell does NOT fire `cell-click` (sort path only).
- **`components/select/select.examples.html`** — multi-select pre-selected demo variants added (option-selected form and value= form).

### Maintenance

- Lockstep version bump. Substantive v0.6.26 source fix: `editor-sidebar` `#syncCollapsed()` 0-width guard applied (FB-42/FB-48, `@adia-ai/web-modules`). See `packages/web-modules/CHANGELOG.md`.

## [0.6.25] — 2026-05-22

### Fixed — `table-ui` Enter key activates focused body cell (FB-47)

- **Enter on a focused body cell now dispatches a click**, triggering the existing `cell-click` → `row-click` pipeline — the same event chain that fires on mouse click. Previously Enter was a no-op on body cells; only header cells triggered sort. Aligns with WAI-ARIA Grid Pattern §6.2.2. File: `components/table/class.js`.

### Fixed — `select-ui[multiple]` pre-initialization via `<option selected>` and `.value=` setter (FB-46)

- **`[multiple]` now correctly captures ALL `<option selected>` children** as the initial value (joined with commas), not just the first. The single-value `preSelected` accumulator was replaced with `preSelectedArr` that collects every selected option before setting `this.value = arr.join(',')`.
- **`#renderOptions()` and the aria-selected sync loop** now use a `Set` derived from `this.value.split(',')` for `[multiple]` mode, so every pre-selected option gets `aria-selected="true"` rendered. File: `components/select/class.js`.

## [0.6.24] — 2026-05-22

### Fixed — `slider-ui` label and suffix template snapshot trap (FB-45)

- **`[slot="label"]` and `[slot="suffix"]` now re-sync in `render()`**, resolving the snapshot trap where `label=${expr}` via the template engine rendered the literal `{{p:N}}` placeholder instead of the resolved value. Both spans are updated on every reactive cycle; a missing label span is also created if absent at connection time. File: `components/slider/class.js`.

### Fixed — `toast-ui` demo shell missing `feed.css` import

- **`toast.html` now imports `feed/feed.css` + `feed/feed.js`.** `<toast-ui>` renders via `<feed-ui>` — the demo shell was not loading feed's stylesheet, so feed items rendered without background, typography, or spacing. Files: `components/toast/toast.html`.

## [0.6.23] — 2026-05-22

### Added — `row-ui` `wrap-at="bp"` responsive wrap (`components/row/`)

- **`wrap-at="bp"`** enables `flex-wrap: wrap` from the named breakpoint upward (`xs/sm/md/lg/xl`). Complements the boolean `[wrap]` (always-wrap, unchanged) for the common "single row on desktop, wraps on mobile" toolbar pattern: `<row-ui gap="2 3@md" wrap-at="sm">`. Uses inline `flex-wrap` to avoid reactive re-entrancy from toggling the boolean attribute inside `render()`. File: `components/row/row.js` + `components/row/row.yaml`.

### Added — `--a-ui-2xs: 11px;` UI typography scale slot (`styles/typography.css`)

- **`--a-ui-2xs`** (11px) slots cleanly into the existing UI scale between `--a-ui-tiny` (10px) and `--a-ui-xs` (12px). Currently unused — pre-staged for upcoming responsive UI-text variants that need a denser-than-`xs` step. File: `styles/typography.css`.

### Documentation — `components/` examples third sweep (§428/§432)

- **22 `components/*/*.examples.html` files** updated with @bp annotations + missing prop / token tables across the docs sweep arc (badge, block, button, card, check, code, col, fields, grid, image, link, list, modal, nav family, pane, select, stat, stream, table, tabs, tag, text-ui, toggle-group, tooltip, tree). Picks up the v0.6.22 `@bp` substrate in living demos so consumers see responsive composition shapes inline. Source journal sections: §428 (third sweep + token tables) + §432 (collision fix).

## [0.6.22] — 2026-05-22

### Added — `@bp` responsive attribute system (`core/responsive.js`)

- New `core/responsive.js` exports `BREAKPOINTS`, `BP_NAMES`, a shared `breakpoint` signal (matchMedia-driven; SSR-safe), and `parseResponsive(value, activeBp)`. Mobile-first: unannotated value = base; `@bp` applies at that breakpoint and wider. Reading `breakpoint.value` inside any UIElement `render()` auto-subscribes — no manual listener wiring needed.
- **`grid-ui`**: `columns`, `gap` accept `@bp` (e.g. `columns="2 4@md"`). `render()` also sets `gridAutoFlow:row` + `gridAutoColumns:auto` so items wrap correctly — the default `grid-auto-flow:column` would otherwise spread them horizontally.
- **`col-ui`**: `gap`, `align`, `justify` accept `@bp`. `align` sets `text-align` inline to mirror the CSS attribute side-effect.
- **`row-ui`**: `gap`, `align`, `justify` accept `@bp`.
- **`text-ui`**: `size` accepts `@bp` (`size="sm md@lg"`).
- **`block-ui`**: `padding`, `margin` accept `@bp` (`padding="sm md@lg"`).
- Subscription is lazy: each component only subscribes to the breakpoint signal when at least one prop contains `@`.

### Added — `empty-state-ui` `variant="danger|warning"`

- New `variant` prop (default|danger|warning). Colors icon and text via danger/warning semantic tokens — **no background tint or border** (by design: an empty-state occupies the full content surface; a tinted box would frame absent content). Use `variant="danger"` for full-section error states where content cannot be shown; the centered layout is more appropriate than an inline alert banner when data is absent. File: `components/empty-state/empty-state.css`.

### Fixed — `card-ui` sparkline cards fill available height

- `card.css`: `:has(> section[bleed] > chart-ui)` switches matching cards to `flex-direction: column`; bleed section gets `flex: 1; min-height: 0`. Scoped tightly — no effect on other card layouts.
- `chart.css`: sparkline type gains `height: 100%`. Computes to `auto` when parent has no explicit height (backward-compatible); fills flex-grown height when available.

### Fixed — `alert-ui` missing `[slot="action"]` CSS rule

- `slot="action"` was documented in `alert.yaml` but had no CSS. Added `flex-shrink: 0; align-self: start` so action content behaves as a proper trailing flex item.

### Fixed — `segmented-ui` overflow: labels now truncate with ellipsis

- `segmented.css`: added `min-width: 0` to `:scope`. `inline-grid` previously never shrank below its max-content width in flex contexts, leaving the `::after { text-overflow: ellipsis }` rule in `segment.css` (already correct) unreachable.
- `card.css`: changed card header action column from `max-content` → `auto` (`minmax(min-content, max-content)`). `max-content` prevented the action column from shrinking. Added `min-width: 0` to `[slot="action"]`. Chain: card narrows → action column shrinks → action flex shrinks → segmented-ui shrinks → segments share available width → `::after` ellipsis fires.

### Fixed — `stat-ui [slot="label"]` now truncates with ellipsis

- **`[slot="label"]` (the eyebrow line above the value) was missing the overflow-ellipsis triad** that `[slot="value"]` already carried. Long label strings clipped hard at the grid cell boundary with no `…`. Added `overflow: hidden; text-overflow: ellipsis; white-space: nowrap` to bring the two sibling slots into parity. File: `components/stat/stat.css`.

### Fixed — `table-ui` column headers now truncate with ellipsis

- **Column headers had `white-space: nowrap` but no overflow clipping**, so long header text overflowed into adjacent cells. `overflow: hidden` cannot be applied to the `[role="columnheader"]` directly — it would clip the `position: absolute` filter dropdown rendered inside it. Fix: converted `[role="columnheader"]` to `display: flex; align-items: center` and added `flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap` on the label `<span>` child (already created by `class.js`). Sort icons and filter buttons remain fully visible at their natural size; only the label text truncates. File: `components/table/table.css`.

### Fixed — `table-ui` body cells now show ellipsis (not just hard clipping)

- **Body cells declared `text-overflow: ellipsis` but showed hard clipping with no `…` character.** Root cause: `text-overflow: ellipsis` on a `display: flex` container has no effect on anonymous flex items (text nodes set via `cell.textContent`). All 6 built-in text-scalar cell types (`text`, `number`, `currency`, `percent`, `date`, `datetime`) and the two `class.js` fallback paths now wrap text in a `<span>` child element. CSS rules targeting `[role="gridcell"] > span` and `> a` (link type) carry `flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap`. Rules for `> row-ui { min-width: 0; overflow: hidden }` and `> row-ui > span` cover the avatar and progress composite types. The `[wrap]` opt-in is extended to reset child-element overflow properties so per-table or per-column wrap still works. Files: `components/table/cell-types.js`, `components/table/class.js`, `components/table/table.css`.

### Changed — UI context inset/gap scale reduced

- **`--a-inset-sm/md/lg` and `--a-gap-sm/md/lg` shifted down the space scale** to better suit dense UI surfaces. New values at `--a-density: 1`: inset sm 16→8px, md 24→16px, lg 32→24px; gap sm 12→8px, md 16→12px, lg 24→16px. The `[prose]` context is unchanged — it stays coarser by design. File: `styles/tokens.css`.

### Changed — `stat-ui` gap tokens cascade with container size context

- **`--stat-column-gap` and `--stat-row-gap` are now `var(--a-gap)` and `var(--a-gap-sm)`** instead of hardcoded space tokens. When a parent container sets `size="sm"` or `size="lg"`, stat-ui gaps adapt automatically. File: `components/stat/stat.css`.

### Documentation — 21 component examples pages corrected

- **YAML-vs-HTML audit of all 93 components** found 58 with missing or stale API documentation. 21 fixed: missing props added, stale enum values corrected (`block`→`stretch` for button, `error`→`danger` for tag/alert, size ranges extended), full API sections added where absent (link, field, nav, nav-group, nav-item). See journal §411–§418 for the complete list.

### Documentation — `packages/web-components/patterns/` + `packages/web-components/traits/` examples sweep

- **§407 ripple sweep on `patterns/` + `traits/` examples.html files.** As a side-effect of the §407 spacing-scale rebase + table-truncation work, demo pages for `conversion-funnel`, `editor-shell`, `form-drawer`, `record-detail-drawer`, `retention-cohort`, `wizard-drawer` (patterns) and `active-state`, `arrow-grid-nav`, `draggable-list-item`, `drop-target`, `focus-restore`, `focus-trap`, `focusable`, `hotkey`, `hoverable` (traits) were re-stamped to use the new inset/gap tokens consistently.

## [0.6.21] — 2026-05-21

### Changed — `table-ui` body cells truncate by default (opt-in wrap via `[wrap]` / `[data-wrap]`)

- **`<table-ui>` body cells now default to single-line with ellipsis truncation** instead of `overflow-wrap: anywhere`. Long unbreakable strings (URLs, IDs) clip gracefully rather than re-wrapping the row, matching the row convention shared by `<select-ui>` + `<nav-item-ui>`. Three opt-in paths to wrapping: (1) `<table-ui wrap>` — new reflected boolean prop, whole-table opt-in; (2) `col.wrap: true` in `columns[]` — renderer sets `[data-wrap]` on each body cell in that column (per-column opt-in); (3) `<td data-wrap>` — surgical per-cell opt-in for hand-authored cells. Behavior change for consumers who relied on the legacy wrap default — set `[wrap]` on the host to restore. New tests cover the three paths. Files: `components/table/{table.css,table.yaml,class.js,table.test.js}`.

### Fixed — toast / feed-item variant text colour now safe in light + dark mode

- **`<feed-item-ui>` — the element `<toast-ui>` / `UIToast.show()` render through — paired a scheme-locked foreground with a scheme-flipping background.** The `info` / `success` / `warning` / `danger` variants set the text to `--a-{variant}-fg` (= `--a-{variant}-text-strong`, locked for solid `--variant-strong` fills) over a `--a-{variant}-muted` tint that flips light↔dark — going low-contrast in one scheme (e.g. a warning toast in dark mode). They now use the scheme-aware `--a-{variant}-text`, which flips with the background — matching `alert.css` and `toast.css` variant wiring. Files: `feed.css`.

### Fixed — `input-ui` inline `[slot="label"]` no longer crowds the value text

- **An inline `<span slot="label">` sat flush against the value** with only the field gap between them, so a short label and the value text read as one run-on string. `[slot="label"]` now carries a `min-width` floor (new `--input-label-min-width` token, default `8ch`) — it reserves a label column and the value start aligns down a stack of inputs. `flex-shrink: 0` keeps it from collapsing; a label wider than the floor still grows past it. Files: `input.css`.

## [0.6.20] — 2026-05-21

### Added — `table-ui` `row-click` event (FEEDBACK-35)

- `<table-ui>` now dispatches a bubbling `row-click` event (detail:
  `{ row, dataIndex }`) once per row click, alongside the existing per-cell
  `cell-click`. Master-detail / open-flyout wiring no longer has to
  deduplicate `cell-click` by `dataIndex`. `table.yaml` documents it.

### Changed — `select-ui` honours declarative `<option selected>` (FEEDBACK-36)

- `<select-ui>` now promotes an `<option selected>` child to the initial
  value when the host carries no `[value]` attribute — matching native
  `<select>` semantics. Previously the `selected` attribute was silently
  ignored and the placeholder shown. Guarded on an empty `this.value`, so
  consumers who set `[value]` explicitly are unaffected.

### Changed — `chart-ui` wrong-`.data`-shape warning shows the correct shape (FEEDBACK-34)

- The one-shot `console.warn` emitted when `.data` is set to a non-array
  now embeds a minimal correct-shape example
  (`[{ label: "Jan", value: 100 }, … ]`) instead of only naming the wrong
  API and pointing at `chart.yaml`.

### Docs — `table.yaml` / `select.yaml` description clarifications (FEEDBACK-35 §2, FEEDBACK-36)

- `components/table/table.yaml` `sort` event: the `key` / `dir` detail
  descriptions now name the fields explicitly (`key` not `column`, `dir` not
  `direction`) — closing the guess the bare names invited.
- `components/select/select.yaml` `value`: documents that a declarative
  `<option selected>` child sets the initial value when the host `[value]`
  attribute is absent.

## [0.6.19] — 2026-05-21

### Fixed — `drawer-ui` `innerHTML`-on-host orphaned the internal `<dialog>` (FEEDBACK-30)

- Setting `.innerHTML` on a mounted `<drawer-ui>` wiped the stamped
  `<dialog>` part; `#dialogRef` (bound once in `connected()`) was left
  detached, so `#syncDialog()` called `showModal()` on a detached node and
  the drawer silently never reopened. `render()` now re-binds `#dialogRef`
  + its listeners when `ensure('dialog')` re-stamps; `#syncDialog()` guards
  `dialog.isConnected`; a failed `showModal()` surfaces a `console.error`.
  `drawer.yaml` gains an `anti_patterns` entry for the innerHTML trap.

### Added — `drawer-ui` `opened` event (FEEDBACK-27)

- `<drawer-ui>` now dispatches a bubbling `opened` event after the open
  transition completes (mirrors `close`). Lets tests + consumers await full
  interactivity instead of `waitForTimeout` heuristics. `drawer.yaml`
  `events:` documents it.

### Added — `chart-ui` `.data` shape docs + wrong-shape warning (FEEDBACK-24)

- `chart.yaml` now documents the `data` prop: an array of plain objects
  keyed by the `x`/`y` attributes — not the Chart.js `{labels, datasets}`
  envelope. Setting `.data` to a non-array (the common envelope mistake)
  emits a one-shot `console.warn` instead of silently rendering blank.
  (`x` and `y` were already documented props — the ticket's Finding 2
  claim otherwise was inaccurate.)

### Added — `segmented-ui` non-`<segment-ui>`-child warning (FEEDBACK-23)

- `<segmented-ui>` emits a one-shot `console.warn` when a direct child is
  not a `<segment-ui>` (a bare `<segment>` renders text but gets no sliding
  indicator, `role`, or `aria-checked`). `segmented.yaml` `description` +
  `slots` now document the `<segment-ui>` child contract.

### Changed — `UIElement.ensure()` part-wipe invariant (FEEDBACK-31)

- `UIElement.ensure()` (`core/element.js`) marks stamped structural parts
  with `_uiPart = true` and its doc comment states the invariant: call
  `ensure()` from `render()`, never cache a part reference once in
  `connected()` — a host `innerHTML` wipe detaches it.

### Added — `types` condition on CSS subpath exports (FEEDBACK-26)

- `@adia-ai/web-components/css` and the per-component `.css` subpath exports
  now carry a `types` condition (`css-module.d.ts` stub), so TypeScript
  under `moduleResolution: bundler` no longer raises TS2882 on CSS
  side-effect imports.

### Docs — no-`.js`-suffix import rule (FEEDBACK-22)

- `USAGE.md` notes that component subpath imports use the directory name
  (`…/components/button`), never the explicit file (`…/button/button.js`) —
  the `exports` wildcard rejects the suffixed form under Vite 8.

## [0.6.18] — 2026-05-21

### Added — `[data-scheme]` attribute on `toggle-scheme-ui` (FB-14)

- **`toggle-scheme-ui` now writes the `[data-scheme]` attribute** on the target
  element alongside the existing `color-scheme` inline style. `styles/themes.css`
  selects on `[data-scheme="light|dark|system"]`; consumer CSS patterned after
  it never fired because the component wrote only the inline style.
  `#writeTarget()` sets the attribute, `#clearTargetOverride()` removes it —
  paired with the style so the two never diverge. Non-breaking: consumers
  relying on the `color-scheme` style alone are unaffected. (~6 LOC `class.js`.)

### Added — `button-ui` text/icon symbol-duplication warning (FB-15)

- **`button-ui` emits a one-shot `console.warn`** when `text=` begins with a
  symbol that `icon=` already renders — e.g. `text="+ New Item" icon="plus"`
  renders a doubled `+`. Covers the high-frequency `plus` / `minus` / `x` /
  `check` / `arrow-*` collisions. Warning-only (WeakSet-guarded, GC-friendly);
  the button still renders and `text=` is never mutated. `button.yaml` gains the
  matching `anti_patterns` entry so A2UI validators flag it at generation time.
  (~36 LOC `class.js` + `button.yaml` + a2ui regen.)

### Added — Loading state as first-class citizen (FB-12 P2)

- **`stat-ui loading` prop.** New `loading: Boolean` (reflect: true). When set,
  the value + change slots render `<skeleton-ui>` shimmer placeholders and the
  host gets `aria-busy="true"`. Label and icon are preserved as static
  metadata. Toggle off when data arrives — slot textContent setters restore
  cleanly. `composes:` now includes `skeleton-ui`. (~28 LOC `stat.js` + 9 LOC
  `stat.yaml` + a2ui chunk regen.)
- **`table-ui` skeleton rows.** `loading=true` now renders N ghost
  `[data-skeleton-row]` rows (N = `paginate` capped at 8, else 5) instead of
  the legacy `<progress-ui>` overlay. Header + columns stay intact so layout
  is preserved while data fetches. Cell widths cycle 60/80/70/50/90% so rows
  read as natural data. Host gets `aria-busy="true"`. `composes:` now
  includes `skeleton-ui`. Fixes yaml-vs-impl drift: the yaml description
  always promised "skeleton rows" since the initial cut; the impl only
  shipped the overlay. (~42 LOC `class.js` rewrite of `#renderOverlays` +
  CSS rewrite of the `:scope[loading] [data-body]` rule.)

### Added — `text-ui` overlay attributes (FB-10)

- **`size` / `color` / `weight` / `text-align` props on `text-ui`.** Pre-v0.6.18
  the skill documented an overlay API (`<text-ui color="subtle" size="sm"
  weight="semibold" text-align="center">`) but the substrate ignored those
  attributes — they were valid HTML but no-op CSS. v0.6.18 implements them
  as `:scope[attr="value"]` rules that override the `--text-*` CSS vars set
  by `[variant]`. Enums:
  - `size` — `sm | md | lg` → `--a-body-sm | --a-body-md | --a-body-lg`
  - `color` — `default | subtle | strong | accent | danger | success | warning`
  - `weight` — `regular | medium | semibold | bold`
  - `text-align` — `start | center | end | justify` (sets `text-align` directly;
    only takes effect when text-ui is block-like)
  Permissive: unknown values are no-ops (variant default wins). Substrate now
  matches the skill's already-documented intent.

### Tests

- 9 new tests in `stat.test.js` (FB-12 P2 #1 coverage).
- 9 new tests in `table.test.js` (FB-12 P2 #3 coverage; uses RAF-based settle
  helper because table-ui renders via `requestAnimationFrame`, not microtasks).
- 18 new tests in `text.test.js` for the overlay attributes (attribute
  reflection × 18 values + CSS rule presence × 18 + 3 enum-vs-CSS
  consistency checks + permissive-fallback smoke).
- **Total: 99/99 passing** (text 82 + stat 9 + table 9 — the existing text
  variant tests still pass alongside the new overlay tests).

### Deferred (see `outbox/FEEDBACK-12--...--followup.md`)

- **`chart-ui` `loading` prop (FB-12 P2 #2)** — design decision upstream.
  `chart.examples.html` documents intentional "compose loading wrappers"
  pattern; pivoting in a patch cut would silently lock in a direction.
- **`skeleton-ui` `variant` reconcile (FB-12 P3)** — two valid dispositions
  (purge a2ui corpus refs / implement prop) need maintainer signoff.

### No-op (FB-11 P3 substrate)

- Verified that `import { UIFeed } from '@adia-ai/web-components/components/feed/class'`
  resolves correctly via the existing `./components/*/class` exports pattern.
  No package.json change needed. The original ticket flagged
  `/components/feed/feed.js` as failing — confirmed (Node exports wildcards
  can't substitute `*` across multiple path segments) but the `/class`
  shorthand is the canonical path and works today. Skill v2.11.0 documents
  this.

## [0.6.17] — 2026-05-21

### Maintenance
- **Lockstep version bump only.** No source changes in this package; bumped to maintain the 9-package version coherence enforced by `scripts/release/check-lockstep.mjs`. Substantive v0.6.17 work shipped in `@adia-ai/web-modules` — collapsed-sidebar rules grew a forgiving fallback block for vanilla HTML consumers (`<button class="nav-item">`, `[slot="heading"]`) so text labels don't overflow the 48px rail. See `packages/web-modules/CHANGELOG.md#0617--2026-05-21` for details.

## [0.6.16] — 2026-05-21

### Maintenance
- **Lockstep version bump only.** No source changes in this package; bumped to maintain the 9-package version coherence enforced by `scripts/release/check-lockstep.mjs`. Substantive v0.6.16 work shipped in `@adia-ai/web-modules`: (1) `<admin-page>` selector relaxed to descendant combinator so router-wrapped layouts no longer collapse to `display: inline` at narrow viewports (was the root of the page-header/KPI overlap regression); (2) `--page-content-header-bg` rebased from `--a-canvas-2` to `--a-canvas-1` so the page-header reads as shell-chrome continuation. See `packages/web-modules/CHANGELOG.md#0616--2026-05-21` for details.

## [0.6.15] — 2026-05-21

### Fixed
- **`installIconLoaders` / `installIconLoadersForRegistered` no longer silently accept a malformed shape (FB-07).** The previous behavior was `weightModules = { ...EMPTY_WEIGHTS, ...modules }` which spread any top-level keys verbatim. A flat path→svg glob (the documented quick-start shape; see FB-09) had its `/path/foo.svg` keys spread at the top level of `weightModules`, leaving every per-weight slot empty. Every `<icon-ui>` then rendered the icon NAME as visible text in its slot — confusing to debug because the per-icon `warnMissingIcon` console message points at name-resolution issues (Phosphor alias, `registerIcon`, etc.), not at loader-shape issues. The fix shape-detects three cases: (1) per-weight map keyed by `regular`/`thin`/`light`/`bold`/`fill`/`duotone` installs verbatim; (2) flat path→svg map auto-promotes to `{ regular: <flat> }` with a one-time `console.warn` so the caller knows to wrap explicitly (and so bold/fill/etc. aren't silently lost); (3) anything else logs `console.error` with the received key sample and aborts the install (registry stays unchanged). Net: the documented flat shape from `docs/guides/02-quick-start.md` continues to work but emits one diagnostic warn per install; consumers on prior versions writing the flat shape needed to wrap manually to get icons at all. Files: `packages/web-components/core/icons.js` (+59 LOC: `WEIGHT_KEYS` const, `isFlatGlobShape()`, `warnedShapes` WeakSet, three-branch dispatch inside `installIconLoaders`).
- **`core/icons-phosphor.js` no longer silently swallows `import.meta.glob` failures (FB-08).** Two silent-fail paths fixed. (a) **Empty-glob path** — `import.meta.glob('/node_modules/@phosphor-icons/core/assets/<weight>/*.svg', …)` returns `{}` (not an error) when zero files match. This happens when the consumer's Vite root sits below `node_modules`, or with pnpm/yarn-berry virtual paths, or when this module is loaded from a published package directory (the absolute-style glob is resolved against the Vite root, not this file's directory). Previously `hasViteGlob` was set true and `installIconLoaders({ regular: {}, … })` ran anyway, leaving the registry empty with no diagnostic. The fix sums per-weight entry counts after the `try` block; if all six weights have zero entries, emits a `console.warn` with the canonical manual-install snippet. (b) **Caught-error path** — the bare `catch {}` was scoped to handle "no Vite at all" (`ReferenceError` because `import.meta.glob` doesn't exist in static serving), but it also swallowed every OTHER error type (plugin order issues, Vite-config misconfigs, etc.). The catch now logs non-`ReferenceError` failures via `console.warn` before falling back to the manifest fetch. Files: `packages/web-components/core/icons-phosphor.js` (+38 LOC, -2 LOC).

### Docs
- **Quick-start + migration guide examples corrected (FB-09).** `docs/guides/02-quick-start.md` lines ~76-81 (boot sequence) and ~126-131 (the dedicated `installIconLoadersForRegistered` section) both showed the flat-glob form passed directly to `installIconLoadersForRegistered`. Both now wrap the glob in `{ regular: … }` to match the canonical contract. New `### Pitfall: flat glob silently auto-promoted (v0.6.15+)` section explains the FB-07 auto-promote behavior and when the per-weight form is strictly required (bold/fill/etc.). `docs/MIGRATION GUIDE.md` lines ~279-282 (the v0.5.3 §154 recommended upgrade example) had the same flat-glob bug — corrected. The skill `adia-ui-kit` v2.9.0 ships the same scaffold-template correction.

### Verification
- `verify:traits` 56/56 clean.
- `components.mjs --verify` clean (142 files up-to-date).
- `smoke:engines` green; `smoke:register-engine` 11/11.
- `npm run check` omnibus PASSES (LightningCSS minify, link check, primitive composes coverage, demo shell imports, phosphor wiring, MCP server smoke 3/3, register-engine 11/11, traits 56/56).
- `eval:diff --engine zettel` cov=49% / avg=90 (baseline-identical).
- Browser-verified four cases against `installIconLoaders`: flat glob → 1 warn + install; garbage shape → 1 error + abort; null → 1 error + abort; per-weight map → silent. Live `/site/examples/admin-dashboard` shows 406/412 icon-ui elements with rendered SVGs — same baseline as pre-fix; substrate change non-regressing.

## [0.6.14] — 2026-05-21

### Maintenance
- **Lockstep version bump only.** No source changes in this package; bumped to maintain the 9-package version coherence enforced by `scripts/release/check-lockstep.mjs`. Substantive v0.6.14 work shipped in `@adia-ai/web-modules` — `<admin-page-header>` now uses `var(--page-content-header-bg)` (= `--a-canvas-2`) + `var(--a-shadow-sm)` so the page-header band reads as elevated chrome over the body canvas. See `packages/web-modules/CHANGELOG.md#0614--2026-05-21` for details.

## [0.6.13] — 2026-05-20

### Maintenance
- **Lockstep version bump only.** No source changes in this package; bumped to maintain the 9-package version coherence enforced by `scripts/release/check-lockstep.mjs`. Substantive v0.6.13 work shipped in `@adia-ai/web-modules` — `<admin-shell>` default `mode` is now `"rounded borderless"` (bare-tag auto-promote on connect; any explicit `mode` attribute still wins). See `packages/web-modules/CHANGELOG.md#0613--2026-05-20` for details.

## [0.6.12] — 2026-05-20

### Maintenance
- **Lockstep version bump + examples-doc cleanup.** Bumped to maintain the 9-package version coherence enforced by `scripts/release/check-lockstep.mjs`. The only file change in this package: `patterns/admin-shell/admin-shell.examples.html` — removed a stale `data-content-root` row from the data-attribute hooks reference table (ADR-0032 retired that legacy hook). No runtime `.js` / `.css` changes. Substantive v0.6.12 work shipped in `@adia-ai/web-modules` — admin-shell legacy CSS bridges retired per ADR-0032 (continuation of ADR-0023 + ADR-0024). See `packages/web-modules/CHANGELOG.md#0612--2026-05-20` for the breaking-change details + migration path.

## [0.6.11] — 2026-05-20

### Fixed
- **`<canvas-ui>` — `.d.ts` hand-authored with full runtime API (claims-ui-v2 inbound FB-02 finding 2, P2).** Pre-fix: `canvas.d.ts` was 27-line codegen output declaring only `theme: string` + the `canvas-interaction` event listener — all 8 imperative runtime methods (`process`, `processAll`, `reset`, `pushVersion`, `back`, `forward`, `historyLength`, `historyIndex`) plus `getHTML()` were absent, forcing TS consumers to write `(canvas as any).processAll(...)`. The yaml/a2ui.json sidecar can only express declarative props per ADR-0027; codegen correctly skips imperative methods, but no compensating hand-authored layer existed. **Fix**: replaced `canvas.d.ts` with 63-line hand-authored declaration covering the full public API surface (each method documented with JSDoc matching the JS contract). Added `'canvas'` to the `HAND_AUTHORED_DTS` skip-set in `scripts/build/dts-codegen.mjs` with referencing comment so the next codegen run preserves the hand-authored file. Consumers post-upgrade can drop `(canvas as any).processAll(...)` casts entirely. Closes claims-ui-v2 inbound FEEDBACK-02 finding 2.

## [0.6.10] — 2026-05-21

### Added
- **`<table-toolbar-ui>` — one-shot dev-mode `console.warn` when unknown opt-out attributes are set (claims-ui FB-04 enhancement).** Triggered by claims-ui FB-04 (RESPONSE-04, v0.6.9 §379): consumers writing `<table-toolbar-ui searchable>` (or `exportable` / `sortable` / `filterable` / `columns-visible`) thinking those attributes toggle visibility saw silent no-ops — the canonical attrs are `no-search` / `no-filter` / `no-sort` / `no-columns`. Pre-v0.6.10 behavior: the unknown attrs simply did nothing. Post-fix: a one-shot `console.warn` per element fires from `connected()`, names the unknown attrs, and points consumers at `npx adia-ai-doc table-toolbar` and the yaml SoT for the canonical reference. Pattern: warn-once-per-element via static `WeakSet` (`UITableToolbar._warnedUnknownAttrs`), matching the `tooltip-ui._warnedMissing` discipline. Scope: case-insensitive attrs starting with `search` / `export` / `sort` / `filter` / `column`, minus the 4 canonical opt-outs. Standard HTML/ARIA attrs (`id`, `class`, `role`, `aria-*`, `data-*`) are never flagged. Pinned by 12 new vitest cases in `table-toolbar.test.js`.

### Internal
- **`scripts/release/check-browser-safe.mjs` — strict-by-default + `/bin/` allowlist.** Flipped `STRICT = args.has('--strict')` → `STRICT = !args.has('--loose')` to match the convention shared by the other 6 release gates (`check:absolute-imports`, `check:no-self-import-css`, `check:scope-bare-descendants`, `check:lightningcss-build`, `check:rolldown-glob`, `check:with-css-pairing`). Paired with `/\/bin\//` added to `NODE_ONLY_PATTERNS` — silences the only pre-existing warning (`packages/web-components/bin/doc.mjs`, the `adia-ui-doc` CLI shipped v0.6.7). The CLI's `node:fs` / `node:path` / `node:url` imports are by design (#!/usr/bin/env node shebang); the broad `/bin/` pattern covers any future package-bin CLIs. Tracked at journal §378 as deferred-to-v0.6.10. No consumer-facing API change.

## [0.6.9] — 2026-05-21

### Fixed
- **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.

### Documentation
- **`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).

## [0.6.8] — 2026-05-20

### Fixed
- **`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).
- **`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).
- **`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.

### Docs
- **`USAGE.md` — three new template-related sections** documenting the FB-55/56/57 cluster:
  - **§-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'`").
  - **§-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.
  - **§-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).
- **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).

## [0.6.7] — 2026-05-19

### Added
- **`<name>.examples.md` ships in npm package** (96 components, ~67KB total).
  Generated by `scripts/build/generate-examples-md.mjs` from `<name>.examples.html`.
  External consumers can now read canonical markup fragments directly from
  `node_modules/@adia-ai/web-components/components/<name>/<name>.examples.md`.
- **`custom-elements.json` ships in npm package** (W3C Custom Elements Manifest v2.1.0).
  Generated from per-component yaml files. Enables IDE LSP autocomplete and
  cross-tool interop (Storybook, VS Code WC extensions).
- **`npx @adia-ai/web-components doc <name>` CLI** prints yaml summary + live
  demo URL + first example fragment for any component. Bare `doc` lists all.
- **`USAGE.md § Data binding with data-stream-*`** new top-level section
  covering the declarative `data-stream-*` attribute family (was previously
  undocumented despite being used in 12+ components).

## [0.6.6] — 2026-05-18

### Added
- `<field-ui align="end">` — new `align` prop (reflected string, enum: `start`|`end`).
  `start` is the current default (no visual change). `end` right-aligns the label
  within its grid cell (`justify-self: end; text-align: end`), useful in multi-field
  forms where labels share a visual column and trailing-flush alignment aids scanability
  (right-to-input reading path). CSS-only; no JS logic change. §304.
## [0.6.5] — 2026-05-18

### Changed
- Lockstep version bump for TS Phase 6 sealing (ADR-0029).
Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
[Semantic Versioning](https://semver.org/).

Scope: custom elements, core (`AdiaElement` + reactivity + templates),
trait system, style tokens, patterns (composite elements). The A2UI
runtime ships in the sibling `@adia-ai/a2ui-runtime` package
(renamed from `@adia-ai/a2ui-utils` in v0.3.0) as of `0.0.4`.
## [0.6.4] - 2026-05-18

### v0.6.4 — Lockstep ride-along

TS Phase 5: a2ui.schema.json schema-driven codegen (§359). No source changes in this package.



## [0.6.3] - 2026-05-18

### v0.6.3 — Lockstep ride-along

TS Phase 5: runtime real declarations (§358) + compose branded IDs.
No source changes in this package. Internal dep ranges stay at `^0.6.0`.



## [0.6.2] - 2026-05-18

### v0.6.2 — Lockstep ride-along

TS Phase 4: retrieval (§356) + mcp (§357) `.ts` sources + TS-MIG-001 closed.
No source changes in this package. Internal dep ranges stay at `^0.6.0`.



## [0.6.1] - 2026-05-18

### v0.6.1 §354 — `<link-ui>` `--link-color-*` → `--link-fg-*` token rename

**BREAKING** for consumers overriding `--link-color`, `--link-color-hover`, or
`--link-color-visited`. Renamed to `--link-fg`, `--link-fg-hover`, `--link-fg-visited`.
No in-tree consumer references found. Internal dep ranges stay at `^0.6.0`.



## [0.6.0] - 2026-05-18

_Lockstep ride-along (no source change in this package; companion to web-components v0.6.0 — see root CHANGELOG)._

**MINOR-line internal-dep range update:** internal `@adia-ai/*` dependency ranges move from `^0.5.0` to `^0.6.0` per AdiaUI lockstep policy.

## [0.5.21] - 2026-05-18

_Lockstep ride-along (no source change in this package; companion to web-components v0.5.21 — see root CHANGELOG)._

## [0.5.20] - 2026-05-18

### Added — §353 (v0.5.20) — `<pane-ui>` + `<tabs-ui>` bar-rule alignment

Both primitives' header/tablist bars converted from `border-bottom` to `box-shadow: inset 0 -1px 0`. Header height fixed at `--pane-bar-height` / `--tabs-button-height` (was `min-height` + the border added a pixel). Pane header gains `box-sizing: border-box` + `justify-content: flex-start`. Collapsed pane header drops the box-shadow. Visually identical to consumers; measurable change in computed height (1px shorter than border-bearing version). Brings these two primitives in line with the rest of the catalog using box-shadow for bar rules.

### Docs
- §351 (FB-53 #2) — USAGE.md `§349 Event handling inside reactive lists` section gains a `CSS child-combinator caveat` paragraph. Calls out that `display:contents` wrapper-spans ARE in the DOM tree, so the CSS child combinator (`>`) walks past them: `.parent > .my-item` will NOT match items inside `repeat()` because the wrapper-span sits between. Recommends descendant combinator (single space) instead. Same caveat extended to `:nth-child` / positional pseudo-classes. Closes the post-Lane-AA1 migration docs gap surfaced by Tokens Studio.
- §352 (FB-53 #3) — USAGE.md `§345 .map() vs repeat() for reactive lists` section gains a `Sizing heuristic` paragraph. Two cases where `repeat()` pays for itself: (a) ≥20 items re-rendering on every signal change, OR (b) any-size list whose items host custom elements with internal state (chrome slots, focus, scroll). Discourages reflexive `.map()` → `repeat()` migration when neither threshold applies.

### Fixed
- §-headers (v0.5.20) — USAGE.md headers for `§345` and `§349` renumbered from `§-TBD (v0.5.19)` to `§345 (v0.5.19)` / `§349 (v0.5.19)` — peer-agent's v0.5.19 pre-cut docs sweep renumbered the CHANGELOG but missed the USAGE.md section headers. Sync restored so the section anchors match between CHANGELOG and USAGE.md.

## [0.5.19] - 2026-05-17

### Added
- §348 (FB-51 #3; P3) — `<description-list-ui>` `align="stretch"` enum value (joins `start`, `between`). For `layout="inline"`, sets `justify-self: stretch` + `width: 100%` on `<dd>` (and `width: 100%` on its direct children) so block-level form controls — `<slider-ui>`, `<select-ui>`, `<color-picker-ui>` — reach the column edge rather than shrink-wrapping to content. Closes the Tokens Studio dts-right-panel migration path for property-row pairs where the value is a control, not text. Schema + generated `.d.ts` + `a2ui.json` updated. NEW `description-list.test.js` with 3 cases (CSS source contract, host attribute precondition, yaml enum shape).
- §347 (FB-50 #3; P3) — `<command-ui>` `select` event detail now carries `category` from the parent `<optgroup label="…">`. Items without a group surface `category = ''` so destructuring stays safe. Schema (`command.yaml`) + generated `.d.ts` + `a2ui.json` updated. Closes Tokens Studio's consumer pattern of routing selected commands by section (Navigation / Edit / etc.) without re-walking the DOM to find the parent optgroup label.
- §342 (FB-44; P1) — `<swatch-ui>` `MutationObserver` for late-arriving `[slot="chrome"]` children. `#absorbChromeSlot()` runs from `#syncCore()` which only fires on attribute / property changes; `scan()` walks `element.attributes` BEFORE descending to child nodes, so the parent template's `update()` loop assigns properties (→ `render()` → `#syncCore()` → `#absorbChromeSlot()`) BEFORE interpolating child node parts. The once-only absorb pass therefore sees an empty children set and chrome — arriving moments later — sits at its arrival position rather than the canonical post-tile region. Fix: arm a `MutationObserver` on host `childList` in `connected()` that re-runs `#absorbChromeSlot()` whenever a direct `[slot="chrome"]` child OR a `display:contents` wrapper-span containing chrome is added. Observer is torn down in `disconnected()`. Closes the post-§341 follow-up gap in Tokens Studio's C1.3 main-palette dogfood (gamut badges + override / tracked dots invisible despite §341 placeholder-preservation). 3 NEW vitest cases in `swatch.test.js`'s `FB-44 / §-TBD` describe block.
- §343 (FB-46; P2) — `<tree-ui>` `tree-select` event detail now forwards `ctrlKey` / `metaKey` / `shiftKey` from the originating `MouseEvent` or `KeyboardEvent`. Unblocks consumer-side Ctrl/Cmd+click multi-select implementations (Tokens Studio token-tree). `select(item, originatingEvent = null)` is the new public signature; programmatic calls (no event) surface all three modifiers as `false`. Schema (`tree.yaml`) + generated `.d.ts` + `a2ui.json` updated. 4 NEW vitest cases in new `tree.test.js`. Backward-compatible: existing `detail.{item,text,value}` shape unchanged; new fields are additive.

### Tests
- §344 (FB-47; partial / misdiagnosed) — 3 NEW vitest cases in `core/template.test.js`'s `FB-47` describe block lock the actual contract: (1) `stamp()` with same template strings reuses the mounted instance (no `replaceChildren()`); (2) `repeat()` with same key preserves wrapper children across re-renders; (3) the `Array.isArray(v)` branch in `applyValue()` DOES rebuild on every re-render (the actual `.map()` destruction surface, documented behavior). The FB-47 author's "`stamp()` is unconditional" + "`repeat()` keyed diffing defeated" claims are empirically false; the tests pin current behavior to catch future drift. See RESPONSE-47 for the consumer-side `.map()` → `repeat()` migration recommendation.
- §347 (FB-50 #3) — NEW `command.test.js` (first dedicated test file for `<command-ui>`) with 3 cases: emits `category=""` for ungrouped options, emits `category="<optgroup label>"` for items inside an optgroup, backward-compat `{ value, label }` destructuring still works. 3/3 passing.

### Docs
- USAGE.md — new `§345 (v0.5.19) — .map() vs repeat() for reactive lists (FB-47)` section under the template parser invariants. Documents the keyed-reuse contract, when `repeat()` matters, and the key-selection guidance (use a stable consumer-supplied identifier, NOT array index).
- USAGE.md — new `§349 (v0.5.19) — Event handling inside reactive lists (FB-49)` section. Explains that `closest()` works through `display:contents` wrapper-spans (DOM walk, not box walk) for typical bubble-phase handlers; reserves `composedPath()` for cross-subtree / shadow-DOM / whitespace-retarget cases with explicit examples. Closes the documentation gap that caused three consumer `composedPath()` workarounds in Tokens Studio to ship without rationale.
- §346 — NEW `components/command/USAGE.md` (FB-50 #1) — first dedicated USAGE doc for `<command-ui>`. Documents the native `<option data-shortcut data-icon>` content API (the actual shape; there is no `command-item-ui` custom element), the `<optgroup label>` grouping pattern, the new `category` field in `select` detail (per FB-50 #3 above), the Cmd+K wiring pattern, `setItems()` imperative API, recent-items behavior, and the `empty` + `footer` slots. Closes Tokens Studio's discoverability gap that drove ~350 LOC of hand-rolled `<dts-command-palette>` fallback.

## [0.5.18] - 2026-05-16

### Fixed
- §341 (FB-43; P1) — `<swatch-ui>` chrome destroyed when interpolated via parent `html\`\`` template `${chromeFrag}`. `template.js`'s `scan()` replaces `<!--p:N-->` comments with empty text nodes inside the swatch element when the parent template is cloned; `wrap()` later mutates those text nodes in-place via `replaceWith()`. Pre-§341: the swatch's `#stamp()` ran `innerHTML = ''` between the parent's `mount()` and `update()` (synchronous custom-element connectedCallback timing), wiping the text-node placeholders. Parent's `replaceWith()` then ran on detached nodes — silent no-op — leaving wrapper-spans detached and chrome stamped into a subtree that never reached the live DOM. Symptom: chrome interpolated as `${chromeFrag}` was completely invisible (queryable from neither swatch nor host). Blocked Tokens Studio's C1.3 main-palette dogfood migration (~80 swatches inside `repeat()`). **Fix:** preserve empty text nodes + comment nodes (template-engine placeholders) AND display:contents wrapper-spans through `innerHTML = ''`. Detach them before the wipe, re-attach at the canonical chrome position (between tile + badge) after the canonical structure is stamped. Wrapper-spans preserved in-place keep the template-engine's `part.n` reference stable across re-renders → chrome content updates land in the same DOM location automatically. `#absorbChromeSlot()`'s late-arriving wrapper handler also updated: MOVE the wrapper (not hoist chrome out of it) to canonical position — same identity-preservation rationale. **§331 contract refined:** previously chrome was hoisted to be a direct sibling of the swatch's internal elements (`parentElement === swatch`); §341 keeps chrome inside the wrapper-span which is a direct sibling. Visually identical (display:contents collapses the wrapper's box) but the DOM identity is now wrapper-bearing. 3 NEW vitest cases (`FB-43 / §341`) + 3 §331 (FB-39) tests updated for new contract. 15/15 swatch tests passing.

## [0.5.17] - 2026-05-16

### Fixed
- §339 (FB-41; P2) — `<swatch-ui>` `[auto-contrast]` overrode `[label-position="overlay"]` position. Two `@scope (swatch-ui)` rules at equal specificity (0,3,1) collided on `> [data-label] { position }`; source order favored auto-contrast's `position: relative` over overlay's `position: absolute` → label dropped below tile (host doubled to 2× block-h). Blocks the canonical `label-position="overlay" + auto-contrast` pattern §253 enabled. Fix: drop `position: relative` from `[auto-contrast]` data-label + data-detail rules — z-index works on positioned elements (overlay supplies position: absolute), and non-overlay defaults are flex flow where z-index is irrelevant. 3 NEW vitest cases lock the CSS source structurally.
- §340 (FB-42; P2) — `<list-item-ui>` `render()` text-branch asymmetry. Icon + description branches had create-if-missing paths; text branch only updated existing stamped spans. Template engine `.text=${expr}` (canonical authoring pattern per USAGE.md) arrives AFTER `connectedCallback` `#stamp()` — `this.text` was `''` at stamp time so no span created, `render()` fired but couldn't recover → primary text silently invisible. Fix: mirror description branch's create-if-missing + remove-when-cleared pattern. NEW `list.test.js` with 5 vitest cases (create-on-prop-set / update / remove / parity-with-description / consumer-stamp non-interference).

### Tests
- §338 (FB-40; misdiagnosed) — 4 NEW vitest cases in `core/template.test.js` lock the existing FEEDBACK-06 comment-aware `inTag()` fix (line 55-78). FB-40 reported apostrophes-in-HTML-comments crashes claiming the parser mis-classifies subsequent `${…}` placeholders; the cited mechanism is incorrect — `inTag()` has skipped past `<!--…-->` wholesale since FEEDBACK-06 (pre-v0.5.x). The 4 new tests empirically verify the fix covers apostrophe-comment + double-quote-comment + multi-apostrophe + apostrophe+em-dash cases. RESPONSE-40 requests stack-trace evidence for the actual crash mechanism since the cited apostrophe diagnosis is empirically wrong against current source.

## [0.5.16] - 2026-05-16

### v0.5.16 §331 — `<swatch-ui>` chrome-slot wrapper-span deep-walk (FB-39; P1)

Closes FB-39. P1 lifecycle gap that FB-38's "C1.3 unblocked" close-out missed: §326 (v0.5.15) fixed the late-direct-append case but NOT the template-render wrapper-span case. AdiaUI's `html\`\`` template engine wraps each interpolated child fragment in a `<span style="display: contents">` (per `core/template.js:212` `wrap()`). When consumer writes `html\`<swatch-ui>${badgeFrag}</swatch-ui>\``, the `<span slot="chrome">` ends up nested INSIDE the wrapper, not as a direct child. `#stamp()`'s `Array.from(this.children).filter(n => n.getAttribute?.('slot') === 'chrome')` returned `[]`; `innerHTML = ''` wiped the wrapper spans + chrome content; `#absorbChromeSlot()` ran on destroyed DOM.

Blocked Tokens Studio's C1.3 dogfood migration (chrome elements silently destroyed) + >95% of AdiaUI consumers using `html\`\`` templates.

#### Fixed — `components/swatch/class.js` (dual-site deep-walk)

- `#stamp()`'s chrome extraction extends from direct-children-only to also walk `querySelectorAll('[slot="chrome"]')` on non-chrome wrapper-span children. Catches both static-HTML (direct child) AND template-render (nested in wrapper) paths.
- `#absorbChromeSlot()` gains a parallel deep-walk for wrapper-spanned chrome children that arrive AFTER `#stamp()` (parent template re-render path). Hoists nested chrome out of any non-internal wrapper into the canonical sibling-of-tile slot via `this.insertBefore(child, this.#badgeEl)`.
- Wrapper spans stay in place post-hoist (`display: contents` makes them invisible to CSS layout; removing them risks interfering with the parent template engine's diff tracking via the `PART` symbol).

#### Added — `components/swatch/swatch.test.js` (6 → 9 cases)

3 NEW vitest cases under "FB-39 / §331":
- `hoists chrome child from inside wrapper span at #stamp() time (template-interpolation path)` — single-wrapper-single-chrome case.
- `handles MULTIPLE chrome children nested in separate wrapper spans` — multi-`${...}` template interpolation; each fragment lands in its own wrapper.
- `hoists chrome from late-arriving wrapper span (post-stamp template re-render)` — parent re-renders + appends a new wrapper-span carrying chrome after `#stamp()` already ran.

Total `swatch.test.js`: 9/9 passing (4 §325 + 2 §326 + 3 §331).

#### Migration

Consumers using `<swatch-ui>` with chrome-slot composition via `html\`\`` templates (`<swatch-ui>${badge}${dot}</swatch-ui>`) now get chrome children placed at canonical position post-§331. The consumer-side workaround (skip chrome composition or use bare `<swatch-ui>` without chrome) can be retired. No consumer code changes required — the §331 fix is invisible to consumers writing canonical templates.

### v0.5.x (Unreleased) §330 — `slider-ui` "Next" design + `segmented-ui` initial-render fix

**`slider-ui`** — full visual redesign to iOS/macOS-style pill slider. Pill-shaped track (`var(--a-toggle-size)` = 16/20/24px for sm/md/lg), primary-color progressive fill (`--a-primary-bg`), white chrome thumb (`--a-chrome-light`) with 2:1 width:height ratio and 2px internal padding on all sides. Mathematical geometry documented inline in CSS: `fill_width(p) = t + p·(W−t)`, `thumb_center(p) = t/2 + p·(W−t)`; fill starts at thumb-width (p=0) and grows incrementally to full track width (p=1). Two-layer thumb architecture: transparent full-height container provides grab area; `::before` renders the inward-padded white pill. `class.js` `#valueFromX()` maps full track width to [min,max]; `#valueFromX()` simplified back to direct rect ratio. `data-dragging` CSS transition lock kills the `left` transition during pointer moves for instant visual feedback. `slider.examples.html` gains Sizes section (lg/md/sm/14px/12px) + updated CSS token table.

**`segmented-ui`** — initial rendering race fix. Added `ResizeObserver` on self + `document.fonts.ready` promise in `connected()`; both call `#updateIndicator()` when layout stabilizes so the animated pill positions correctly after font load or resize. Previously the indicator computed once at mount and stayed misaligned — only corrected on next user interaction.

Files: `slider.css`, `slider.class.js`, `slider.examples.html`, `slider.yaml`, `segmented.class.js`.

### Added
- §333 `<color-input>` gains canonical demo-shell loader pair to restore catalog symmetry with the other 92 primitives. NEW `color-input.html` (52-line shell pattern matching `color-picker.html`: tokens base + composed primitive CSS + composed primitive JS per ADR-0027 + `fetch('./color-input.examples.html')`) + NEW `color-input.examples.html` (7 sections: default · initial values · format hex vs oklch · popover placement · §313 generation constraints `max-chroma`/`max-l`/`min-l`/`hue-drift-max`/`base-hue` · disabled · inside a form). Closes gemini-review refactor-backlog P3-1.
- §332 `<swiper-ui>` gains `chrome="toolbar"` layout mode with companion `label` + `counter` props. Default chrome (overlay paddles + centered dots below) is unchanged. Toolbar chrome stamps a header row (label on the left, paddles on the right) above the track and a footer row (counter on the left, dots on the right) below — supports gallery-style compositions where consumers want the chrome out of the slide area. `#stampToolbarChrome()` builds the head + foot rows; `#updateCounter()` writes "N / M" (page-based, snap-aware) into `[data-swiper-counter]` on every scroll. New examples at `/site/components/swiper` § chrome (photo gallery / featured products / autoplay testimonials). Counter is page-based so it tracks the dot count correctly for multi-column + center-snap.
- §329 `<code-ui>` gains a `<template>`-child authoring path — wrap the source in `<template>…</template>` and write literal `<` / `>` without entity-escaping. `#stampBlock` reads `template.innerHTML`, dedents the wrapper column via a new static `#dedent()` helper, and feeds the result to CodeMirror. The HTML parser treats `<template>` content as inert (it lives in a `DocumentFragment`, not rendered), so embedded `<nav-ui>` / `<button-ui>` / etc. don't get upgraded inside the template. Three authoring paths now in priority order: (1) `[text]` attribute, (2) `<template>` child, (3) escaped textContent (back-compat — existing examples unchanged). Demo at `/site/components/code` § "escape-free authoring via &lt;template&gt;"; dogfooded on `patterns/app-nav` structure example. (Carry-forward from worktree §318.)

### Fixed
- §332 `<swiper-ui>` — comprehensive consumer-feedback hardening pass (9 distinct fixes consolidated). (a) **Click-and-drag preempted by native HTML5 image-drag**: `<img>` inside slide cards fired `dragstart` before our 5px pointermove threshold was reached, suspending pointer events. Fix: `dragstart` listener gated on `this.#drag !== null` (set by `#onPointerDown`) so genuine drag-out of intentionally draggable slide content stays available. (b) **Pagination dots ignored `--swiper-columns`**: stamping one dot per slide regardless of `slides-per-view`. New `#getPageCount()` returns `max(1, slides − cols + 1)` for start/end snap (reads `--swiper-columns` via `getComputedStyle(track)`); `ResizeObserver` on the host re-stamps when container queries shift the cv across breakpoints. `next/prev/play` cap at last page; dot labels read "Page N" (was "Slide N", misleading once dot-index ≠ slide-index). (c) **CSS `container-type` on the wrong element** — `container-type: inline-size` was on `[data-swiper-track]` but `@container` rules targeted `:scope` (the host = track's PARENT, not a descendant). Container queries match descendants of the container, so the rules fell through to whatever further-up ancestor had a container set (the docs shell) and incorrectly stamped `cols=2-3` on every swiper. Moved `container-type: inline-size; container-name: swiper;` to host; `@container swiper (...)` rules now target `:scope > [data-swiper-track]` (a real descendant). JS `#getColumns()` reads from the track via `getComputedStyle` so both `[slides-per-view]` (cascades from host) and `@container` (writes on track) converge. (d) **Snap-aware active tracking** — IntersectionObserver "leftmost intersecting" couldn't distinguish the last two snap positions for center/end snap (both kept the last two slides >50% visible). Replaced with `scroll` listener + `#computeActiveIndex()` that walks every slide, computes its snap target (`left` / `left + width/2 − trackWidth/2` / `left + width − trackWidth` per snap mode), clamps to `[0, maxScroll]`, and picks the closest target to `scrollLeft`. `#pageToSlideIndex` / `#slideToPageIndex` map between page and slide indices (identity for start/center; end-snap shifts by `cols − 1`); snap-aware `goTo()` scrolls to the computed target so dot clicks land without a "scroll to left edge, then scroll-snap re-correct" jank step. `#getPageCount()` returns `slides.length` for `snap="center"` (each slide is its own page; 4-slides-@-cols=2 now stamps 4 dots, not 3). (e) **Loop / autoplay demos didn't visibly demonstrate looping** — at responsive widths (≥960px → cols=3) the 3-slide demos had `pageCount=1`, paddles produced no visible effect. Pinned demos to `slides-per-view="1"`; added a new combined "loop + autoplay" example. (f) **`gap` only accepted token indices despite docs claiming "any CSS length"**: `render()` blindly wrapped `gap` in `var(--a-space-${gap})`. For `gap="1rem"` that became `var(--a-space-1rem)` (undefined token) → property invalidated → CSS gap fell back to 0; three demo variants (`gap=0/1rem/2rem`) all rendered identically. Fix: numeric-only strings resolve to space tokens, anything else used as a raw CSS length. (g) **Dot CSS lost in toolbar chrome**: `:scope > [data-swiper-dots] > button` (direct-child combinator) stopped matching when dots moved inside `[data-swiper-foot]`; buttons fell back to browser defaults (wide gray bars). Relaxed all five dot rules to `:scope [data-swiper-dots]` (descendant combinator). (h) **Active dot rendered much larger than inactive** — `background: <color>` shorthand in `:hover` and `[aria-current="true"]` rules silently RESET `background-clip` back to its initial `border-box`, so the colored area filled the full 1rem outer hitbox and `border-radius: 100rem` rounded that into a ~16px circle (vs the 6px content-box-clipped circle for inactive). Fix: longhand `background-color: <color>` so `background-clip: content-box` from the base rule is preserved. Active dot now has a deliberate +4px (`--a-space-1`) size delta via NEW `--swiper-dot-size-active` token; padding back-computed so the 1rem hitbox stays invariant. Focus ring moved from outer `var(--a-focus-ring)` to inset 2px shadow so focus doesn't change footprint either. **Pitfall worth knowing for future CSS work: `background: <color>` shorthand resets `background-clip`/`background-origin`/`background-position`/etc. to initial — use `background-color: <color>` to preserve them.**
- §327 P2 correctness — `<code-ui language="…">` (every `/site/patterns/*` page) rendered as flow-wrapped plain text with chrome header below the content + no syntax highlighting. Root cause: the `textContent` key in `static properties` of `code/class.js`, `button/class.js`, `tag/class.js`, `badge/class.js` made `UIElement.installProps()` shadow the native `HTMLElement.prototype.textContent` setter. `this.textContent = ''` in `code-ui`'s `#stampBlock` became a signal-write no-op — original authored HTML-escaped source stayed in the element above the stamped chrome, and CodeMirror mounted with an empty doc (1 cm-line, 19.5px tall editor below leftover authored text). Same hazard on `<tag-ui>` produced visible empty pills across every `/site/traits/*` page's API table (the `chip()` helper in `traits/_api-table.js` and 3 other call sites all do `t.textContent = text` after `createElement('tag-ui')`). Fix: removed the `textContent` entry from each of the 4 class.js files. Yaml + `.d.ts` entries kept (renderer routes via `setAttribute('text', …)` for non-text-bearing tags; doesn't depend on JS prop declarations). New drift class #7 (native-DOM-accessor collision). **Carry-forward note:** initially staged as §316 on a worktree branch (2026-05-15) before v0.5.14 cut; §316-§318 were claimed by parallel peer work that shipped via v0.5.15. Renumbered to §327 on re-apply to main against v0.5.15 HEAD (2026-05-16).

### Docs
- §328 source-side polish: re-flowed 6 one-line `<code-ui>` blocks across `stat`/`pagination`/`rating`/`count-up`/`theme-panel`/`theming` USAGE sections — multi-attribute single tags now author each attribute on its own line for readability + consistency with how the rest of the corpus is authored. (Carry-forward from worktree §317.)

## [0.5.15] - 2026-05-16

> **Scope note:** §325 + §326 were originally planned as a separate v0.5.16 cycle (see retired `docs/plans/0.5.16-release-plan.md`); absorbed into v0.5.15 since both landed within the same release window (same-day commits prior to bump). The peer's v0.5.16 plan-doc explicitly anticipated this absorption option ("Hermes's call per `/lockstep-release`"). v0.5.16 plan-doc marked SHIPPED-IN-V0.5.15.

### v0.5.15 §326 — `<swatch-ui>` `[slot="chrome"]` lost in template contexts (FB-37; P1)

Closes FB-37. Pre-§326, `<swatch-ui>` consumed via any `html\`\`` template engine (or any engine that batches child appends separately from element creation) lost its `[slot="chrome"]` children entirely. Root cause: `#stamp()` is gated by a `#stamped` once-only guard + scans `this.children` for chrome slots at `connectedCallback` time. Template engines create the host via `createElement` + connect it FIRST (children empty), then batch-append children AFTER. `#stamp()`'s chrome scan returns `[]`; the `#stamped` guard prevents re-runs; chrome children landed at the END of host's children (after `data-copy`) instead of the canonical sibling-of-tile position. Blocked Tokens Studio's C1.3 dogfood migration (9 failing Playwright tests root-caused to this) — and >95% of AdiaUI consumers using `html\`\`` templates.

#### Fixed — `components/swatch/class.js`

- NEW `#absorbChromeSlot()` helper. Idempotent. Walks `this.children`; for any child with `slot="chrome"` that's not one of the internal stamped elements + not in the canonical position, calls `this.insertBefore(child, this.#badgeEl)` to move it to the slot between `#tileEl` + `#badgeEl`.
- `#syncCore()` calls `#absorbChromeSlot()` at the top of every render pass. Late-arriving chrome children (template-render path) get picked up on the next reactive cycle; pre-positioned chrome children (static-HTML path) are no-op-moves.
- `#stamp()`'s once-only eager scan stays intact for the static-HTML path; `#absorbChromeSlot()` is the safety net for late-append.

#### Added — `components/swatch/swatch.test.js`

- 2 NEW vitest cases under "FB-37 / §326":
  - `absorbs chrome child appended AFTER connectedCallback (template-render path)` — mimics the bug's exact timing (create + connect with empty children → late append → property mutation triggers render → fix picks up the chrome child).
  - `still handles chrome child appended BEFORE connect (static-HTML path)` — regression check that §314's original behavior still works.
- Total suite: 6 cases (4 from §325 + 2 from §326).

#### Migration

Consumers using `<swatch-ui>` via `html\`\`` template engines (lit-html, AdiaUI's own `html\`\``, Preact, Vue, etc.) get the §314 chrome-slot feature working as documented in v0.5.13's `swatch.yaml slots:` block. No consumer-side changes required — the §326 fix is invisible to consumers writing canonical templates.

### v0.5.15 §325 — `<swatch-ui>` default-slot wiped by label-attr (FB-36)

Closes FB-36 + FB-36-followup. Pre-§325, `<swatch-ui label="500"><span>tip</span></swatch-ui>` written as indented multi-line HTML dropped the default-slot child entirely; only `label="500"` rendered. Root cause: `#stamp()`'s `Array.from(this.childNodes).filter(...)` captured pure-whitespace text nodes from the indentation; those landed in `#labelEl.firstChild`; `#syncCore()`'s "stale text-node" branch (designed for runtime `el.label = 'X'` mutations on attr-only swatches) then matched the whitespace + did `textContent = this.label`, wiping both the whitespace AND the consumer's `<span>`.

Pre-§325 the yaml spec ("Use the default slot for richer content" — `swatch.yaml:40`) and the implementation contradicted each other; post-§325 they agree.

#### Fixed — `components/swatch/class.js`

- `#stamp()`'s `slotted` filter chain gains `&& !(n.nodeType === 3 && !n.textContent.trim())` — strips pure-whitespace text nodes from the capture. `#labelEl.firstChild` now becomes the consumer's element (or non-whitespace text), so `#syncCore`'s stale-text-node branch fires only for genuinely attr-driven cases.

#### Added — `components/swatch/swatch.test.js`

- NEW 4-case vitest suite covering FB-36's 3 input axes (default-slot present/absent × label-attr present/absent × runtime mutation). Locks the §325 fix structurally — future regressions of any of the 4 paths fail CI.

#### Migration

`<swatch-ui label="500"><span>tip</span></swatch-ui>` now behaves per yaml spec: default-slot wins, label-attr drops. Consumers using the workaround `.label=` property binding (per FB-36-followup) to bypass the bug can switch back to the canonical `[label]` attribute form post-upgrade.

### v0.5.15 §324 — Yaml `enum_descriptions:` codegen extension (v0.6.0 FB-23 §3 retire carry-forward, PATCH-eligible)

Closes the v0.6.0 carry-forward "Codegen yaml `enum:` per-member descriptions (FB-23 §3 retire)" from the RESPONSE-29 carry-forwards table. v0.5.15 ships the additive codegen surface on PATCH cadence; v0.6.0 no longer needs to carry this item.

**The surface change:** `scripts/schemas/component.yaml.schema.json` `Prop` definition gains an optional `enum_descriptions:` object field — when present alongside `enum:`, dts-codegen emits an exported type alias (e.g. `UITextVariant`) above the class declaration with per-variant JSDoc carrying each description. IDE hover surfaces the disambiguation at authoring time without requiring hand-authored .d.ts.

#### Added

- `scripts/schemas/component.yaml.schema.json` — NEW optional `enum_descriptions: { type: object, additionalProperties: { type: string } }` field per prop. Backward-compatible: existing yamls without the field continue to emit inline enum unions.
- `scripts/build/components.mjs` — forwards `enum_descriptions:` through `compileProp()` into the a2ui.json sidecar.
- `scripts/build/dts-codegen.mjs` — `emitEnumTypeAlias()` helper detects props with both `enum:` and `enum_descriptions:`; emits a typed union alias `${className}${PascalCase(propName)}` with per-variant JSDoc; class property references the alias instead of inlining the union.

#### Changed — `components/text/text.yaml`

- `variant:` prop gains 12-entry `enum_descriptions:` block (one description per variant). Prop `description:` block also gains the FB-23 §2 ARIA-role disclaimer that previously lived in the hand-authored .d.ts.

#### Changed — `scripts/build/dts-codegen.mjs` `HAND_AUTHORED_DTS`

- `text` removed from skip-list. `text.d.ts` now codegen-driven; per-variant JSDoc surfaces via the new `enum_descriptions:` field. Net effect: 1 of 3 remaining `HAND_AUTHORED_DTS` entries closed (`toast` + `feed` remain — both carry runtime API the yaml shape can't express).

#### Regenerated — `components/text/text.d.ts`

- Replaces hand-authored .d.ts with codegen output. `UITextVariant` type alias preserved as exported type; per-variant JSDoc preserved verbatim. Net result: zero consumer-facing diff in the .d.ts surface; codegen takes over maintenance.

### v0.5.15 §322 — NEW audit slot 32: `check-dead-constants.mjs`

Graduates the FB-35 bug class (declared-but-unread module-level consts) to a CI gate. Walks every `.js` file under `packages/<pkg>/`; extracts SCREAMING_SNAKE_CASE / PascalCase `const NAME = ...;` declarations at module level; flags zero-reference cases. Skips `export const NAME` (consumed externally) + test/spec/examples/config files.

First sweep surfaced + removed 2 real dead constants:
- `packages/a2ui/validator/validator.js:38` — `WIRING_WEIGHTS` (vestigial from planned weighted-scoring system; safe deletion).
- `packages/llm/adapters/openai.js:10` — `DEFAULT_MAX_TOKENS = 4096` (replaced by explicit `opts.max_tokens` plumbing).

### v0.5.15 §323 — NEW audit slot 33: `check-hover-guard-discipline.mjs`

Graduates the `analyze-css` bare-`:hover` discipline class to a CI gate. Detects `:hover` rules on host primitives + interactive slots/roles that lack a `:not([disabled])` guard AND set visual properties (background/color/border/box-shadow/etc.). Skips cursor-only / outline-style-only rule bodies.

First sweep surfaced 19 findings across 11 components — all baselined in EXPECTED_DRIFT for cycle-by-cycle close:
- **Real drift** (rely on `pointer-events: none` for click-suppression but hover styles still apply): action-list (4×), menu (1×), nav-item (2×), nav-group (1×).
- **Slotted-trigger drift** (mitigated via parent guard rules; cosmetic-OK but worth tightening): select (5×), calendar-picker (1×), range (2×), tag (1×).
- **Decorative-pop hover** (intentional): avatar (1×). Long-term whitelist candidate.
- **@scope-Safari-17.x-workaround**: action-list (3 of 4) + nav-item (2) + rating (1) — guard can't live inside @scope; lift alongside the hover rule.

#### Removed — dead module-level constants

- `packages/a2ui/validator/validator.js` — `WIRING_WEIGHTS` constant (zero refs).
- `packages/llm/adapters/openai.js` — `DEFAULT_MAX_TOKENS` constant (zero refs).

### v0.5.15 §319 — NEW audit slot 29: `check-exports-map-resolves.mjs`

Graduates the FB-32 bug class to a CI gate. Closes the recurring drift where a package.json `exports` map entry resolves (via wildcard expansion or static path) to a file that doesn't exist on disk — consumer TS imports fail with `TS2882` even though Vite/Node can fall through `node_modules/<pkg>/<path>` directly.

#### Added — `scripts/release/check-exports-map-resolves.mjs`

- ~270 LOC. Walks every `packages/<pkg>/package.json` `exports` map. For each STATIC entry, verifies the referenced file path exists. For each WILDCARD entry matching the FB-32 shape (`./X/*` key + `./X/*/*.<ext>` value), enumerates filesystem candidates that would match the wildcard + verifies each resolved path exists.
- Skips wildcard expansions where an explicit non-wildcard entry overrides the resolution (Node exports-map resolution algorithm precedence).
- Skips CSS-only / no-JS components (header / footer / section / aside / various web-modules shells) — these intentionally ship no `.js`.
- Result: 9 packages / 52 static + 39 wildcard exports entries / 59 resolutions verified / 0 unresolved.

### v0.5.15 §320 — NEW audit slot 30: `check-composes-passthrough.mjs`

Graduates the FB-33 bug class to a CI gate. Closes the recurring drift where a form-bearing primitive that programmatically creates ANOTHER form-bearing primitive in its lifecycle (light-DOM composition) doesn't expose / forward the composed primitive's documented prop + event-detail surface.

#### Added — `scripts/release/check-composes-passthrough.mjs`

- ~270 LOC. Static `COMPOSITION_PAIRS` list declares the required pass-through surface per pair. For each pair: verifies composing primitive's yaml `composes:` block lists the composed primitive; verifies yaml `props:` block exposes every required prop; verifies yaml `events:` block declares each event with required detail keys.
- Initial pair: `color-input → color-picker` (8 props + 12 event-detail keys across `change` + `input`).
- Adding a new pair: any new primitive that programmatically wraps a form-bearing primitive declares its contract in `COMPOSITION_PAIRS`; the audit fires at PR time.
- Companion to slot 23 (`check-form-bearing-dts.mjs`): that audit checks .d.ts type-coverage; this audit checks yaml-declared shape.

### v0.5.15 §321 — NEW audit slot 31: `check-token-name-canonical-shape.mjs`

Graduates the stepper / list state-first naming drift class to a CI gate. Closes the recurring drift where a component-local CSS token uses state-first (`--stepper-active-bg`) or color-infix-state (`--link-color-hover`) naming instead of the canonical property-first state-suffix shape (`--<comp>(-<sub>)*-<prop>(-<state>)?`).

#### Added — `scripts/release/check-token-name-canonical-shape.mjs`

- ~210 LOC. Extracts `--<comp>-*` declarations from each component CSS file. Classifies each by segment shape: canonical-prop / canonical-prop-state / canonical-leaf / canonical-domain / drift-state-first / drift-infix-color-state.
- Vocabularies (PROP_TOKENS + STATE_TOKENS) capture the catalog's recognized property + state terms (bg/fg/border/ring/shadow/color/outline/size + hover/active/selected/disabled/focus/checked/done/...).
- Result: 92 components / 1809 component-local tokens audited / 0 drift findings.
- `link.css` `--link-color-hover` not flagged at v0.5.15 ship: `color` is a recognized PROP_TOKEN + `hover` is a recognized STATE_TOKEN — the shape matches canonical. A future, stricter audit (alternative-property-naming) could flag the `color` vs `fg` divergence; deferred to v0.6.0 BREAKING window since link.css's `--link-color-*` are documented public API.

### v0.5.15 §316 — Naming-convention drift fix (stepper + list state-first → property-first)

`<stepper-ui>` and `<list-ui>` declared internal tokens with state-first naming (`--stepper-active-bg`, `--list-selected-fg`, etc.) — the catalog convention is property-first (`--<comp>-{prop}-{state}`). 18 token names renamed; usage sites updated in-place. `<link-ui>` skipped — its `--link-color-*` tokens are documented public API per `link.yaml tokens:` block; renaming is BREAKING and defers to v0.6.0.

#### Changed — `components/stepper/`, `components/list/`

- `stepper.css` — 12 tokens renamed: `--stepper-{active,done}-{bg,border,fg}` + `--stepper-item-{active,done}-{bg,border,fg}` → `--stepper-{bg,border,fg}-{active,done}` + `--stepper-item-{bg,border,fg}-{active,done}`.
- `list.css` — 3 tokens renamed: `--list-selected-{bg,fg,border}` → `--list-{bg,fg,border}-selected`.

### v0.5.15 §316 — Hand-rolled transition-duration literals → token references (swatch + heatmap + page)

Three components hand-rolled `100ms ease-out` / `120ms ease-out` / `0.1s ease-out` / `150ms ease` instead of routing through the canonical `var(--a-duration-fast)` + `var(--a-easing*)` token chain. Theme designers couldn't swap transition cadence centrally.

#### Changed — `components/swatch/`, `components/heatmap/`, `components/page/`

- `swatch.css:294,311` — `100ms ease-out` + `120ms ease-out` → `var(--a-duration-fast) var(--a-easing-out)`.
- `heatmap.css:80` — `0.1s ease-out` → `var(--a-duration-fast) var(--a-easing-out)`.
- `page.css:61` — `150ms ease` → `var(--a-duration-fast) var(--a-easing)`.

### v0.5.15 §317 — NEW audit slot 27: `check-raw-fg-in-component-css.mjs`

Graduates the §313/§315/§316 bug class to a CI gate. Closes the recurring drift class where component CSS rules bypass their own `--<comp>-<family>-<state>` semantic-layer token and reference the raw global `--a-<family>-<state>` token directly. The audit slot found 5 additional bypass instances across the codebase (nav-group + select + toolbar) — all routed through the component-local semantic layer in this commit.

#### Added — `scripts/release/check-raw-fg-in-component-css.mjs`

- ~190 LOC audit script. Parses each `<comp>.css` for the `:where(:scope) { ... }` declaration block + the set of `--<comp>-*` tokens defined therein. Scans the remainder for `<standard-css-property>: var(--a-<family>-<state>?)` usage where the parallel `--<comp>-<family>-<state>?` is defined — emits a finding per bypass.
- Whitelisted CSS properties: `color`, `background`, `background-color`, `border` (+ all sub-axis variants), `outline`, `fill`, `stroke`, `caret-color`, `accent-color`, `text-decoration-color`, `column-rule-color`.
- Distinguishes declaration sites (`--<comp>-X: var(--a-X)`) from usage sites mechanically — declaration sites are NOT flagged (those are correct semantic-layer aliasing).
- Companion to slot 25 (`check-component-css-vs-styles-tokens.mjs`): that audit catches orphan references (`var(--a-X)` where `--a-X` doesn't exist); this audit catches the inverse (`var(--a-X)` where it exists but `--<comp>-X` should be preferred).
- EXPECTED_FINDINGS baseline: 0 entries at v0.5.15 ship (sweep landed every concrete bypass).

#### Fixed — 5 component CSS bypass sites

- `nav-group.css:243,252` — `background: var(--a-bg-hover)` → `var(--nav-group-bg-hover)`.
- `select.css:199` — listbox container `color: var(--a-fg)` → `var(--select-fg)`.
- `select.css:319` — hint slot fallback `var(--a-fg-muted)` → `var(--select-fg-muted)`.
- `toolbar.css:107` — overflow popover `color: var(--a-fg)` → `var(--toolbar-fg)`.

### v0.5.15 §317 — NEW audit slot 28: `check-form-bearing-events-symmetric.mjs`

Graduates the FB-31/§315 bug class to a CI gate. Closes the recurring drift class where UIFormElement-extending primitives emit only `input` (or only `complete`/`search`/`pick`) without the canonical `change` event with `detail.value`. The audit confirms all 17 form-bearing primitives are now symmetric post-v0.5.14 §315.

#### Added — `scripts/release/check-form-bearing-events-symmetric.mjs`

- ~145 LOC audit script. Walks every `class.js` for classes that `extends UIFormElement`. For each, verifies a `dispatchEvent(new CustomEvent('change', { detail: { value: ... } }))` occurs (statically or via dynamic-name dispatch + nearby `'change'` string literal + `detail = { value: ... }` construction).
- Heuristic limitations documented: false negatives possible if event name is computed via complex variable assignment; manual review at addition time required.
- ALLOW_LIST mechanism for primitives where the commit model legitimately omits `change` (none at v0.5.15 ship).
- Companion to slot 23 (`check-form-bearing-dts.mjs`): that audit verifies typed `<Comp>ChangeEvent` exists in `.d.ts`; this audit verifies the runtime dispatch.

### v0.5.15 §318 — ADR-0028: L3 state-matrix orphan retirement (78 unused tokens deleted)

ADR-0028 ratifies the deletion of 78 orphaned L3 state-matrix tokens from `semantics.css`. The `maintain-tokens` skill audit during v0.5.14 §315 surfaced that 73 of ~80 L3 `--a-{family}-{prop}-{state}` tokens have zero consumers across the entire codebase. The per-primitive `--<comp>-*` token pattern (validated across v0.5.13/14/15 sweeps) is the actual consumer-facing API.

#### Removed — `styles/colors/semantics.css`

- 78 `--a-{accent,primary,brand,info,success,warning,danger,fg,border}-{fg,bg,border}-{active,selected,disabled,invalid}` declarations deleted. ~80 LOC reduction.
- Token count dropped 1301 → 1217 (-6.5%).
- Kept (have consumers): `--a-bg-{active,selected,disabled,invalid}`, `--a-fg-{selected,disabled}`, `--a-accent-bg-active`, `--a-primary-bg-active` (transitive via accent-bg-active).
- Kept (consumer-facing hover state): all `--a-*-hover` variants — hover is the only state where the generic-L3 layer is load-bearing.

#### Added — `.brain/adrs/0028-l3-state-matrix-orphan-retirement.md`

- ADR documents the audit-driven discovery, the retention rationale for the 7 used tokens, the deletion rationale for the 78 orphans, alternatives considered (keep all / delete only family-specific / add audit), and the per-primitive convention as the canonical consumer-facing override surface.

## [0.5.14] - 2026-05-15

### v0.5.14 — Form-control hover-fg token unification (precursor to §315; commit `178418525`)

Form-control primitives diverged on hover-text token routing. `button-ui` correctly used `--a-ui-text-hover` (resolves to `--a-fg-strong` — canonical hover-lift for form-control text); every other form-control primitive used the generic `--a-fg` (no actual lift from default `--a-canvas-text`) — so "hover" had zero text-color effect on `input`/`textarea`/`segment`, and `select-ui`'s listbox/caret hover paths bypassed the `--select-*` token layer entirely with raw `--a-fg-*` references.

#### Fixed — `components/{input,textarea,segment,select}/`

- `input-ui`: `--input-fg-hover: var(--a-fg)` → `var(--a-ui-text-hover)`; `--input-affix-fg-hover: var(--a-fg-subtle)` → `var(--a-ui-text-subtle)`.
- `textarea-ui`: matching routing on hover/affix-hover.
- `segment-ui`: same routing change.
- `select-ui`: listbox + caret + option-hover tokens previously raw `--a-fg-*` references now route through their `--select-*` semantic layer (which itself anchors at `--a-ui-text-*` family).

Net effect: hover-text colors now consistent across all form-control primitives + theme designers can swap them all by changing one semantic token (`--a-ui-text-hover`) at the `--a-ui-*` family root in `semantics.css`.

### v0.5.14 §315 — Drift-class sweep follow-up (`ui-audit-coherence` + `maintain-tokens` + `analyze-css` triple-pass)

Three parallel skill-driven audits surfaced 7 mechanical drift-classes; landed as a single sweep in one commit (`025a981db`). Follow-up to the form-control hover-fg unification at `178418525` — same bug shape (component CSS bypassing its own semantic-layer routing through raw global tokens), broader scope.

#### Fixed — `components/select/`, `components/nav-group/`, `components/chat-thread/chat-input.css`

- `select.css:254,286,293,300` — 4 raw `var(--a-fg-*)` callsites routed through `--select-fg-{muted,fg,muted,muted}` (semantic layer already existed but was bypassed).
- `nav-group.css:225,234,244,253,286,298` — 6 raw `var(--a-fg-{muted,subtle,strong})` callsites routed through `--nav-group-fg-{muted,muted,hover,selected,muted,muted}`.
- `chat-input.css:40,50,58` — placeholder/hover/disabled tokens routed through the canonical `--a-ui-text-{placeholder,hover,disabled}` field-family chain instead of the generic `--a-fg-*` family. Continues the v0.5.13 hover-fg unification arc.

#### Fixed — `components/{check,radio,switch,tabs,action-list,calendar-picker,menu}/`

- 6 form-adjacent primitives' `--{comp}-fg-disabled` token routed through canonical `--a-ui-text-disabled` instead of `--a-fg-muted` (the disabled-text version of the v0.5.13 hover-fg drift class).
- `menu.css:96,99` also fixed `--menu-item-fg-disabled` + `--menu-item-icon-disabled` (the third variant `--a-fg-disabled` shape; same canonical target).

#### Added — `components/otp-input/`, `components/search/`

- `<otp-input-ui>` + `<search-ui>` now dispatch `change` alongside `input` on every value-altering event. Both primitives previously emitted only `input` (the sole exceptions across all UIFormElement-bearing primitives). Closes consumer event-binding gap — `@change=${handler}` now works against every form-bearing tag uniformly.
- `.d.ts` files gain `OtpInputChangeEvent` + `SearchChangeEvent` type exports (reuse existing detail interfaces; matches v0.5.10 `check-form-bearing-dts` audit convention).
- yaml `events:` blocks declare the new `change` event with `detail.value`.

#### Fixed — `styles/typography.css`

- `:where(mark)` background reference fixed: `var(--a-highlight-default)` → `var(--a-highlight)` (`--a-highlight-default` token does not exist; pre-fix `<mark>` silently fell back to UA-default highlight). Single-line typo discovered via `maintain-tokens` skill sweep.

#### Fixed — `components/{textarea,upload,option-card,pagination}/` transition consistency

- `textarea.css:76` transition list expanded from `border-color` only → `background + border-color + color + box-shadow` (matches `input-ui`'s 4-property parity; pre-fix hover/focus slammed in instead of easing).
- `upload.css:64-67` + `option-card.css:102-104` + `pagination.css:71-73` each gain `box-shadow` (and `border-color` for pagination) so focus-ring eases in alongside the other state changes. Cross-primitive hover/focus polish parity restored.

#### Fixed — `components/input/`, `components/select/` `-active` token routing

- `--input-fg-focus: var(--a-fg-strong)` → `var(--a-ui-text-active)` (closes orphan: `--a-ui-text-active` semantic-layer alias existed at `semantics.css:552` with zero consumers).
- `--select-option-fg-active: var(--a-fg)` → `var(--a-ui-text-active)` (same routing; cross-primitive form-control consistency).

#### Fixed — `components/heatmap/`

- `class.js:disconnected()` now cancels `#raf` via `cancelAnimationFrame()`. Pre-fix the `requestAnimationFrame` handle was stored but never cleaned up on detach — minor memory leak in scroll-heavy demos. ~3 LOC.

## [0.5.13] - 2026-05-15

### v0.5.13 §310 — `apcaContrast` soft-clamp constant fix (FEEDBACK-35; P2 correctness)

`@adia-ai/web-components/color`'s `apcaContrast()` was using `APCA_LO_CLAMP = 0.06` for the soft-clamp threshold where the canonical APCA spec (`apca-w3@0.1.9`'s `SAPC_BLACK_THRESHOLD`) calls for `0.022`. The cubic-root soft-clamp `Math.pow(threshold - Y, 1.414)` operating on the wrong threshold lifted near-black luminance too aggressively. Pre-fix `apcaContrast('#000','#fff')` returned 99.49; post-fix returns 106.04 — matches canonical reference within ±0.01 Lc across all 8 reference pairs. Consumers using `<swatch-ui auto-contrast>` (which transitively calls `pickContrastingFg → apcaContrast`) silently got wrong contrast verdicts for any swatch with `Y < 0.06`.

#### Fixed — `color/`

- `color/index.js` `apcaContrast()` soft-clamp now uses `APCA_LO_FG`/`APCA_LO_BG = 0.022` (declared correctly since v0.5.12 but never read). Dead constants (`APCA_NORM_L`, `APCA_REVERSE_FACTOR`, `APCA_NORM_FACTOR`, `APCA_LO_CLAMP`, `APCA_RESCALE`) removed; new `APCA_BLACK_CLAMP = 1.414` factored out for readability.

### v0.5.13 §311 — `@adia-ai/web-components/color` P3 helpers (FEEDBACK-31)

`@adia-ai/web-components/color` (v0.5.12 §301) shipped sRGB-only gamut helpers; the v0.6.0 §301 plan-doc spec lines 121-126 committed both sRGB AND Display-P3. v0.5.13 §311 closes the gap.

#### Added — `color/`

- `inP3Gamut(L, C, H): boolean` — returns true if the OKLCH triple is inside Display-P3 gamut (tolerance `0.001`).
- `gamutMapChromaP3(L, C, H): number` — 8-iteration chroma bisection in P3 (matches `gamutMapChroma`'s shape).
- Internal `linearSrgbToLinearP3()` matrix sourced from CSS Color 4 § 12.3 (D65 → D65; no Bradford chromatic adaptation needed).
- `color/index.d.ts` ships matching declarations.

### v0.5.13 §312 — `<stat-ui>` explicit exports map entries (FEEDBACK-32)

v0.5.12 §253a hand-authored `stat-ui.d.ts` on disk but the package.json wildcard `./components/*` expanded `./components/stat` → `./components/stat/stat.js` (non-existent; actual file is `stat-ui.js`). Consumer TS imports failed at the package-export layer with `TS2882` even though the file exists + Vite resolved at runtime.

#### Changed — `package.json`

- `exports` map adds 2 explicit entries: `./components/stat` + `./components/stat/stat-ui.js` both pointing at `stat-ui.{d.ts,js}`.
- **Drops at v0.6.0 §303** when the `git mv stat-ui.{js,css,d.ts} → stat.{js,css,d.ts}` rename normalizes the filename + the wildcard naturally covers `stat.{js,d.ts}`.

### v0.5.13 §313 — `<color-input-ui>` constraint pass-through + parsed-detail forwarding (FEEDBACK-33)

`<color-input-ui>` (v0.5.12 §302) composed `<color-picker-ui>` but dropped 5 generation-constraint props (`maxChroma` / `maxL` / `minL` / `hueDriftMax` / `baseHue`) and 3 parsed-OKLCH-channel-scalars (`{l, c, h}`) from the re-emitted event detail. Consumers using L-range constraints or OKLCH-native logic (Tokens Studio's `defaults.Lmin`/`Lmax`) couldn't migrate to `<color-input-ui>` without a UX regression.

#### Added — `components/color-input/`

- `class.js` adds 5 generation-constraint props to `static properties` + sets them on inner picker at `#mount()` + diff-detect re-set on `render()` (Object.is handles NaN-equality for `baseHue`/`hueDriftMax`).
- `#commit()` re-emits with all 6 detail fields including parsed scalars `l: detail.l, c: detail.c, h: detail.h`.
- `color-input.yaml` declares the 5 props + 3 new detail fields per `change`/`input` events.
- `color-input.d.ts` `ColorInputChangeEventDetail` interface gains `l: number; c: number; h: number;` fields; class gains 5 typed prop declarations.

### v0.5.13 §314 — `<swatch-ui>` `[slot="chrome"]` named slot (FEEDBACK-34)

`<swatch-ui>`'s `#stamp()` lifecycle wiped `innerHTML` then funneled surviving children into `#labelEl`. Consumer chrome positioned absolutely against the swatch tile (gamut badges + override/tracked dots per C1.3 dogfood) landed inside the label's text bbox where `position: absolute; top/right/bottom/left` anchored to the small label geometry instead of the tile. v0.5.13 §314 adds a `[slot="chrome"]` named slot whose children survive stamping as direct host siblings of `#tileEl`.

#### Added — `components/swatch/`

- `class.js` `#stamp()` extracts `[slot="chrome"]` children BEFORE the wipe; re-attaches them as direct host siblings IMMEDIATELY after `#tileEl`. The host's `position: relative` anchor (when `shape="block"`) lets consumer absolute-positioning anchor to the tile's geometry.
- `swatch.yaml` `slots:` block declares the `chrome` named slot + clarifies `default` slot's funnel-into-label behavior.

#### Migration

```diff
 <swatch-ui shape="block" label-position="overlay" selectable>
-  <badge-ui class="dts-gamut-badge" ...>P3</badge-ui>
-  <span class="dts-override-dot"></span>
+  <badge-ui slot="chrome" class="dts-gamut-badge" ...>P3</badge-ui>
+  <span slot="chrome" class="dts-override-dot"></span>
 </swatch-ui>
```

Single `slot="chrome"` attribute on each chrome element. Existing absolute-positioning CSS unchanged.

## [0.5.12] - 2026-05-15

### v0.5.12 §252 — `package.json` `files` array completion (FB-28 close-out)

Closes FB-28 (CHANGELOG.md tarball asymmetry). `packages/web-components/package.json` `files` array gains `README.md` + `CHANGELOG.md` entries (both files already existed at source; previously absent from publish whitelist). 4-cycle lag (v0.5.8 → v0.5.11) since the original FB-22 §1 P3 adjacent ask went silently un-responded in RESPONSE-22.

Sibling-package sweep verified — all 9 published packages (`@adia-ai/web-components` + `@adia-ai/web-modules` + 7 `a2ui-*` + `@adia-ai/llm`) now have symmetric `files` arrays containing `README.md` + `CHANGELOG.md`. No drift.

### v0.5.12 §253 — `stat-ui.d.ts` hand-author + slot 26 widening (FB-30 close-out)

Closes FB-30 (display-primitive .d.ts drift). Drift surface #1 (`.d.ts` axis) post-§253: 6 sub-axes mechanically protected, slot 26 now scoped for BOTH form-bearing AND display sub-axes. v0.5.10's "100% structural protection" milestone holds; v0.5.12 §253b extends slot 26 scope without changing the slot count.

#### Added — `components/stat/stat-ui.d.ts`

`packages/web-components/components/stat/stat-ui.d.ts` NEW hand-authored matching `stat.yaml` props (5: `label` / `value` / `change` / `trend` / `icon`). Filename mirrors `stat-ui.js` (not `stat.js`) for TS side-effect import resolution. Post-v0.6.0 §303 rename, files normalize to `stat.{js,d.ts}` + codegen takes over.

Root cause deeper than FB-30 framing: `dts-codegen.mjs:189` `isCssOnly()` looks for `${name}.js` — for `stat/`, that's `stat.js`, but the actual file is `stat-ui.js` (irregular filename). False-positives stat as CSS-only; codegen never writes `.d.ts`. v0.6.0 §303 rename fixes this; v0.5.12 §253a is the bridge.

#### Changed — `scripts/release/check-yaml-vs-dts-coverage.mjs` slot 26 widened

- `hasShippedJs(dir, comp)` — looks for `<comp>.js` OR `<comp>-ui.js` (handles irregular-filename case)
- `findDtsPath(dir, comp)` — looks for `<comp>.d.ts` OR `<comp>-ui.d.ts` (mirrors JS filename)
- NEW `CSS_ONLY_PRIMITIVES` exclusion set (`aside` / `footer` / `header` / `section`) — intentionally yaml-only stubs
- NEW finding kind: `missing-dts-file` (display-primitive sub-axis catch)
- Scope: FORM_BEARING-only → all primitives with shipped JS + yaml

Validated by smoke test: removing `stat-ui.d.ts` fires `missing-dts-file: stat`; restoring closes clean.

### v0.5.12 §301 — NEW `@adia-ai/web-components/color` subpath (FEEDBACK-29 re-bucket)

NEW subpath bundling OKLCH ↔ sRGB conversion, hex ↔ OKLCH, sRGB gamut-mapping, APCA contrast, and OKLab-distance helpers as pure functions. Consumer-side `src/lib/color-engine.ts` ~250 LOC of duplicated helpers can retire.

#### Added — `color/`

- `color/index.js` — `oklchToOklab`, `oklabToOklch`, `oklabToLinearSrgb`, `linearToSrgb`, `srgbToLinear`, `oklchToRgb`, `rgbToHex`, `oklchToHex`, `hexToOklch`, `isInGamut`, `isOklchInGamut`, `gamutMapChroma`, `apcaContrast`, `pickContrastingFg`, `oklchDistance`, plus `MAX_CHROMA` + `GAMUT_EPSILON` constants. Pure functions; no DOM / runtime side-effects.
- `color/index.d.ts` — full TS surface, `OKLab` + `OKLCH` interfaces, comprehensive JSDoc with APCA threshold guidance.

#### Changed — `package.json`

- `exports["./color"]` added pointing at `color/index.{js,d.ts}`.
- `files: ["color/", …]` added so the subpath ships in the tarball.

### v0.5.12 §302 — NEW `<color-input-ui>` compact form-bearing primitive (FEEDBACK-29 re-bucket)

Canonicalizes the USAGE.md §221f recipe (popover + button + color-picker) into a single form-associated tag for inline form contexts (settings drawer "source color" field, swatch-row inline-edit). Light-DOM composition; the inner picker fires `change` + `input` which the host re-emits with a flattened detail payload that always carries both hex + oklch string forms regardless of `[format]`.

#### Added — `components/color-input/`

- `color-input.yaml` — schema declares 6 props (`name`, `value`, `format`, `disabled`, `placement`, `open`) + 2 events with flattened parallel-views detail.
- `class.js` — `UIColorInput extends UIFormElement`. Builds its inner DOM (`<popover-ui>` + `<button-ui>` + `<color-picker-ui>`) as light-DOM children in `connected()`; wires picker → host event re-emission + popover `open` mirror via MutationObserver.
- `color-input.css` — compact swatch chip + tabular-num value label; honors `--color-input-swatch-size` token.
- `color-input.d.ts` — hand-authored (FORM_BEARING in dts-codegen); declares `ColorInputChangeEventDetail` with parallel `hex` + `oklch` views.
- `color-input.js` — auto-register shim.
- `color-input.a2ui.json` — codegen sidecar.

#### Changed

- `components/index.js` — re-exports `UIColorInput`.
- `styles/components.css` — `@import` for `color-input/color-input.css`.
- `scripts/build/dts-codegen.mjs` — `FORM_BEARING` set adds `color-input` so codegen preserves hand-authored event types.
- `scripts/release/check-enum-vs-css-selector.mjs` — parser now honors yaml `attribute:` overrides when matching CSS selectors (closes the swatch `labelPosition`/`label-position` false-positive class). Baselines added for color-input's behavioral enums (`format`, `placement`) which are JS-routed not CSS-routed; stale baselines for `heatmap:colorScheme:success/warning` + `page:maxWidth:*` (closed by the attribute-override fix) swept from the expected-findings set.

### v0.5.12 R23 §1 — `<swatch-ui label-position="overlay">` (FB-23 §1)

NEW enum value on `<swatch-ui>` — `[label-position]` (default `below`) accepts `overlay` to render the label inside the tile via absolute positioning. Pair with `[auto-contrast]` for OKLab-L-driven legibility. Unblocks Tokens Studio's C1.3 dogfood migration (blocked 3 cycles on this design call).

#### Added — `components/swatch/`

- `swatch.yaml` — `labelPosition` prop (kebab attribute `label-position`) with `below|overlay` enum.
- `swatch.css` — `:scope[shape="block"][label-position="overlay"]` rules positioning the label absolutely over the tile.
- `swatch/class.js` — static property declaration; reflected.

## [0.5.11] - 2026-05-15

### v0.5.11 §250 — `?attr=${bool}` silent-failure trap closed (FEEDBACK-27)


Closes the fourth template-parser-contract trap in the R6-R27 series. v0.5.3 §155 closed HTML-comment quote-state edge cases; v0.5.3 §152 closed partial-attribute interpolation with a load-time `console.warn`. §250 graduates the Lit-style `?attr=${bool}` boolean-attribute shape to the same warn + degrade contract.

#### Fixed — §250a (v0.5.11) — `core/template.js` `scan()` rejects `?`-prefixed attribute names with a console.warn

`packages/web-components/core/template.js` — `scan()` attribute-walk adds a 4th branch alongside `@event=` / `.prop=` / canonical `attr=`. When `name[0] === '?'`:

1. Strip the bogus `?attr` attribute from the DOM (prevents the literal `?disabled="true"` poison-pill that consumers saw pre-§250).
2. Fire `console.warn` naming the offending attribute + the corrected migration path (`use .${prop}=${'${value}'}` — property binding).
3. Degrade the part slot to a no-op text-node binding (`parts[i] = { t: 'n', n: document.createTextNode(''), ... }`) so the failure mode is "no attribute, with a warn" rather than "inert attribute, no warn".

Pre-§250 (silent inert binding):

```js
// Pre-§250 — silently registered `?disabled="true"` on the DOM; consumer's
// `<color-picker-ui>` ignored it; picker stayed interactive.
html`<color-picker-ui ?disabled=${isTracked}>`;
```

Post-§250 (warn + safe-degrade):

```
console.warn: [template] Lit-style boolean attribute "?disabled=" is not supported.
  Element: <color-picker-ui>
  Use .disabled=${value} (property binding) instead — the primitive reflects
  the property to the DOM attribute for you.
  See USAGE.md § Template parser — invariants + unsupported syntaxes.
```

Why NOT actually implement Lit's `?attr=` semantics: AdiaUI primitives declare reflective property/attribute mapping via `static properties = { disabled: { type: Boolean, reflect: true } }`. The runtime handles property-to-attribute reflection automatically. Adding `?attr=` would create two valid syntaxes for the same effect — discoverability tax with no behavioral benefit.

7 new vitest cases in `packages/web-components/core/template.test.js` lock in the §250 contract:

- `?attr=` fires warn (truthy + falsy)
- Does NOT register literal `?attr` attribute on the DOM
- Does NOT set the matching attribute either (no Lit-attr semantics)
- No false positives on canonical `attr=` / `.prop=` / `@event=`

#### Changed — §250b (v0.5.11) — USAGE.md adds "Template parser — invariants + unsupported syntaxes" section

`packages/web-components/USAGE.md` § Template parser — NEW section enumerates all 4 documented parser contracts:

| Syntax | Status | Migration |
|---|---|---|
| `attr=${val}` (full interpolation) | ✅ Supported | n/a |
| `.prop=${val}` (property binding) | ✅ Supported | canonical for boolean state |
| `@event=${handler}` (event listener) | ✅ Supported | n/a |
| `?attr=${bool}` (Lit-style boolean) | ❌ NOT supported — `console.warn` v0.5.11 §250 | Use `.attr=${bool}` |

Also enumerates the 3 historical parser-bug fixes (apostrophes in HTML comments + quoted attributes in HTML comments + backticks-in-comments) so the parser's tacit contract is now a fully-enumerated invariants list.

Pattern matures: every documented-but-easy-to-miss template-parser failure now emits a `console.warn` at scan time. Consumer onboarding from Lit / native lit-html no longer hits the silent-failure class.

## [0.5.10] - 2026-05-15

### v0.5.10 §248 — `check-static-methods-vs-dts.mjs` slot 20 base-class spread-walk extension (FB-25 audit-extension)


Closes the FB-25 base-class blindspot mechanically. The audit grows a 5th declaration-axis on the `.d.ts` surface:

- For each registered base class (currently `{ UIFormElement }`), walk its `static get properties()` return-object body + cross-reference against base `.d.ts` instance-field declarations.
- New finding kind: `base-property-undeclared`. Hard-fails strict mode when a base class's runtime property set has no matching field declaration in the base's `.d.ts`.

Closes the recurrence path for FB-25: any future addition to `UIFormElement.properties` without a matching `core/form.d.ts` field declaration fails the audit at release time. Subclasses using `...UIFormElement.properties` inherit the property at runtime; without the base-class `.d.ts` declaration, consumers using TS `extends` get TS2339 on `el.<prop> = ...`.

Caveat in the audit: the base's `.d.ts` typically declares the metadata-type shape `static properties: { ... };` whose keys overlap the instance-field names. The audit strips the `static properties: { ... };` braced block before scanning for `DTS_FIELD` matches to avoid false-positive coverage.

Validated by deliberate-removal test: removing `throttle: number;` from `core/form.d.ts` fires `base-property-undeclared: throttle`; restoring closes clean.

### v0.5.10 §249 (slot 26 NEW) — `check-yaml-vs-dts-coverage.mjs` (FB-26 audit-extension)

NEW audit slot 26. Closes the FB-26 hand-authored `.d.ts` drift class mechanically. Walks every form-bearing primitive (`FORM_BEARING` set per `dts-codegen.mjs` — 17 primitives that skip codegen because they hand-author their `.d.ts` for rich event-detail typing) and verifies yaml `props:` reflection:

- For each form-bearing primitive: parse `<name>.yaml` `props:` block → set of declared prop names
- Parse `<name>.d.ts` class body → set of instance-field declarations (including getters)
- Subtract `UIFormElement`-inherited fields (via `core/form.d.ts` walk; 16 fields including the §247d `throttle` addition)
- Set-diff yaml-only → flag as `yaml-vs-dts-missing` (hard finding)

Slot 26's purpose is parallel to slot 19's: slot 19 catches yaml → class.js drift (schema vs implementation); slot 26 catches yaml → hand-authored .d.ts drift (schema vs typed-API surface). Together they bound the "yaml is the SoT, but the SoT can drift from the type" gap that v0.5.9 §220 + v0.5.6 §192 surfaced from different directions.

`EXPECTED_FINDINGS = new Set([])` — post-§247 closures, the audit runs clean across all 17 form-bearing primitives. Any subsequent yaml-prop addition without the matching `.d.ts` hand-author fires strict-mode hard-fail.

Validated by deliberate-removal test: removing `inputmode: string;` from `input.d.ts` fires `yaml-vs-dts-missing: input:inputmode`; restoring closes clean.

### v0.5.10 §247 — post-v0.5.9 `.d.ts` drift close-out (FB-23 / FB-24 / FB-25 / FB-26)

Six items address consumer-reported drift between v0.5.9 runtime + yaml + `.d.ts` overlay. All PATCH-class additive (no breaking changes; types narrow what was previously `any` or undeclared). Single commit lands all six.

#### Added — §247 `HAND_AUTHORED_DTS` skip-list extension (`text` + `feed`)

`scripts/build/dts-codegen.mjs` `HAND_AUTHORED_DTS` set grows `{ 'toast' }` → `{ 'toast', 'text', 'feed' }`. Pattern source: v0.5.9 §246. Two new entries:

- **`text`** — preserves per-`UITextVariant`-member JSDoc that the yaml `enum:` schema can't carry (one description per enum value, not just the property). Closes FB-23 §3 (§221k Part A — the JSDoc-per-variant deferred mid-cycle in v0.5.9).
- **`feed`** — preserves the `UIFeed` static-API class + `UIFeedPostOptions` + `FeedHandle` interfaces that the v0.5.9 §228 sibling-yaml walk can't generate (UIFeed has no tag-registered yaml). Closes FB-24 §1.

#### Fixed — §247 `text.d.ts` per-variant JSDoc + ARIA-role disclaimer (FB-23 §2 + §3)

`packages/web-components/components/text/text.d.ts` hand-authored. Two surfaces:

1. **`UITextVariant` named type** with per-member JSDoc (12 variants × 1-line description). IDE hover on `<text-ui variant="?">` surfaces the picker heuristic inline; complements the §221k chooser guide in USAGE.md (which only the consumer who reads docs sees).
2. **Property-level JSDoc** on `variant: UITextVariant` carries the **presentational-only** disclaimer: `<text-ui variant="heading">` does NOT set `role="heading"` + `aria-level`. For semantic headings, wrap with native `<h1>`-`<h6>` OR add `role` + `aria-level` to the host. Closes the FB-23 §2 ARIA-role discoverability gap; the actual API addition (`level=` / `as=`) is deferred to v0.6.0.

#### Fixed — §247 `feed.d.ts` UIFeed static-API surface (FB-24 §1)

`packages/web-components/components/feed/feed.d.ts` hand-authored. Adds:

- `UIFeed` class with `static get(position?)` / `static post(opts)` / `static clear(position?)` / `static purge()` / `static releaseContainerIfEmpty(container)` — the imperative one-shot API consumers use without instantiating `<feed-ui>` declaratively.
- `UIFeedPostOptions` interface mirroring `UIFeedItem`'s reflected props + `position` + the phase-2 `action?` field.
- `FeedHandle` interface (id + dismiss + update) — the return type of `UIFeed.post()`. Parallels `UIToastFeedHandle` from `toast.d.ts` (post-v0.5.6 §197 + v0.5.9 §246).
- `FeedPosition` named type for the 7 lane anchors (6 corners + inline).

#### Fixed — §247 `core/form.d.ts` adds `throttle: number` (FB-25 §1)

`packages/web-components/core/form.d.ts` adds `throttle: number` field declaration + 3 method signatures (`scheduleThrottledInput` + `flushPendingInput` + `dropPendingInput`) on `UIFormElement`. Closes FB-25's drift: v0.5.9 §220 added `throttle` to the runtime base class's `static get properties()` but never propagated the type declaration. All 17 form-bearing primitives (input / select / slider / range / textarea / switch / check / radio / segmented / search / otp-input / option-card / color-picker / rating / code / calendar-picker / upload) inherit the field via TS `extends`. Single 1-LOC fix covers all 17 instead of 17 per-component edits.

**Pre-existing slider.d.ts `throttle: number;` declaration** becomes redundant (now inherited). Cleanup deferred to v0.6.0 hand-author de-dup pass; no functional impact.

#### Fixed — §247 5 missing prop declarations on form-bearing `.d.ts` (FB-26 §1)

Per FB-26's wider-scope audit beyond FB-25's `throttle`-only scope:

- `input.d.ts` adds `inputmode: string` + `autocomplete: string` (HTML5 attribute forwards to contenteditable surface)
- `select.d.ts` adds `variant: 'default' | 'outline' | 'ghost' | 'soft'` + `size: 'xs' | 'sm' | 'md' | 'lg' | 'xl'` (universal `[size]` family + variant chrome)
- `rating.d.ts` adds `variant: 'star' | 'heart' | 'thumbs'` (icon-set treatment)

5 props × ~3 LOC each. All yaml-declared + runtime-functional pre-v0.5.10; only TS type surface was missing.

#### Changed — §247 USAGE.md adds two clarification sections (FB-23 §1 + §2)

`packages/web-components/USAGE.md` grows two notes inside the v0.5.9 §221 sweep section:

1. **`<text-ui>` ARIA-role clarification** — three authoring patterns (native `<hN>` wrap / ARIA-only on host / presentational-only) with the picker heuristic. Cross-references the `text.d.ts` property-level disclaimer.
2. **`<swatch-ui shape="block">` label-position** — confirms label renders BELOW the tile (flex-column); label-on-tile use case (Tokens Studio C1.3 pattern) is NOT supported in v0.5.x; the auto-contrast classes (`data-on-light` / `data-on-dark`) are designed for a future overlay mode. v0.6.0 candidate: `<swatch-ui label-position="overlay">` (or `shape="block-overlay"`, or sibling `<swatch-tile-ui>` slot composition).

### v0.6.0 candidates surfaced by FB-23/24/25/26

- Codegen yaml-schema extension to carry per-`enum:`-member descriptions (retires `text.d.ts` from `HAND_AUTHORED_DTS`)
- Audit slot 20 spread-walk: when encountering `...Base.properties` in a static-properties literal, recursively walk the base's keys (closes the FB-25 base-class blindspot mechanically)
- NEW audit slot 26 `check-yaml-vs-dts-coverage.mjs` for form-bearing primitives (closes the FB-26 hand-authored `.d.ts` drift class mechanically)
- Audit-extension to walk all `export class` (not just `extends UIElement`) — closes the FB-24 ambient static-API class drift
- `<text-ui level="N">` or `<text-ui as="hN">` semantic-heading attribute (API design call — closes FB-23 §2 with runtime ARIA-role setting, not just docs)
- `<swatch-ui label-position="overlay">` (or equivalent) — API for label-on-tile use case (FB-23 §1 feature)

## [0.5.9] - 2026-05-15

### Added — §246 (v0.5.9) — `dts-codegen.mjs` `HAND_AUTHORED_DTS` skip-list + restore `toast.d.ts` static-method surface


Post-§228 codegen regenerated all `.d.ts` files including `toast.d.ts`, which lost its `static show(opts): UIToastFeedHandle` declaration (yaml's `props:`/`events:` shape can't express imperative static methods). The runtime API was missing from the TS surface.

Fix:

- `scripts/build/dts-codegen.mjs` — new `HAND_AUTHORED_DTS = new Set(['toast'])` checked in `generateForComponent()` before regen; status returns `'skip-hand-authored'`. Pattern matches the existing `FORM_BEARING` skip mechanism.
- `packages/web-components/components/toast/toast.d.ts` — restored full hand-authored shape: `UIToastShowOptions` interface + `UIToastFeedHandle` interface + `static show(opts?): UIToastFeedHandle` declaration. Header doc flags HAND-AUTHORED status + references §246 + cross-links the skip list.

Verified: `node scripts/build/components.mjs` runs codegen with `--force` flag; toast.d.ts is preserved across builds.

v0.6.0 follow-up candidate: yaml `methods:` schema + codegen support for static-method emission would retire this skip-list entry by making toast's surface fully codegen'd. Filed mentally; not yet on plan-doc.

### Added — §246 (v0.5.9) — regression tests for §220 throttle + §223 empty-state minimal

`packages/web-components/components/input/input.test.js` — 5 new tests for the §220 throttle wiring on `<input-ui>`: inheritance from UIFormElement, attribute reflection, synchronous dispatch when throttle=0, trailing-debounce when throttle>0, flushPendingInput() + dropPendingInput() semantics. Passes 13/13.

`packages/web-components/components/empty-state/empty-state.test.js` — NEW test file (component had no tests). 6 tests for the §223 minimal mode: default non-minimal state, `[minimal]` attribute reflection, icon-size opt-out (no `size="lg"` when minimal), no-regression for canvas-style stamping, heading + description slots in both modes, consumer-provided `[slot="action"]` preserved. Passes 6/6.

Authoring pitfall captured in the input.test.js diff: when testing trailing-debounce, surface-event bubbling pollutes host-event count — `<input-ui>` listens for native `InputEvent` on its contenteditable surface, which bubbles to the host alongside the host's own repackaged `CustomEvent('input', {detail:{value}})`. Filter assertions by `e.detail?.value !== undefined` to count only host-emitted events. Generalizes to any primitive testing repackaged-event semantics.

### Fixed — §230-bundle wave 2 (v0.5.9) — close remaining 9 of 16 baselined orphan-token references

The remaining 9 architect-decision entries in `check-component-css-vs-styles-tokens.mjs` (audit slot 25, shipped v0.5.8 §231) resolved. After this commit, the baseline shrinks 9 → 0; `EXPECTED_FINDINGS = new Set([])`. All 16 v0.5.8 orphans closed across the two waves:

**Wave 2 architect decisions:**

- **NEW token** `--a-radius-xs` (parametric: `--a-radius-xs-k: 0.166`) in `styles/tokens.css`. Resolves heatmap + swatch callsites.
- **NEW tokens** `--a-chrome-fg` (= `--a-chrome-light` — white text) + `--a-chrome-bg` (= `oklch(0 0 0 / 0.4)` — dark scrim) in `styles/colors/semantics.css`. Canonical badge-on-chrome family. Resolves swatch badge callsites; documents the pattern for video-controls / color-readout / overlay use cases.
- **NEW token** `--a-ui-line-height` (= `var(--a-body-leading)` = 1.5) in `styles/typography.css`. Resolves alert callsite.
- `components/nav/nav.css` `var(--a-duration-base, 200ms)` → `var(--a-duration)` (canonical mid-tier, 250ms).
- `components/code/code.css` `var(--a-surface-floating)` → `var(--a-bg-strong)` (elevated panel surface).
- `components/feed/feed.css` `var(--a-feed-max-width, 22rem)` → component-local literal `22rem`. Hoisted per RESPONSE-21 guidance.
- `components/swatch/swatch.css` `var(--a-bg-2)` → `var(--a-bg-muted)` (canonical canvas-2 alias).

Closes drift surface #7 (token-reference vs definition) fully. `check-component-css-vs-styles-tokens --strict` exits 0 with zero baseline entries.

### Fixed — §230-bundle wave 1 (v0.5.9) — close 7 of 16 baselined orphan-token references + new `--a-ui-xs: 12px`

Wave 1 (kimba prep batch at `73d06956c`):

- **NEW token** `--a-ui-xs: 12px` defined in `styles/typography.css` (one notch below `--a-ui-sm: 13px`).
- `components/tooltip/tooltip.css` (×2) — `var(--a-weight-regular)` → `var(--a-weight-normal)`.
- `components/range/range.css` — `var(--a-ui-fg)` → `var(--a-fg)`.
- `components/{select,slider,switch,tree}/*.css` (×4) — `var(--a-fine-size)` fallback → `var(--a-ui-xs)`.
- `components/swatch/swatch.css` `--a-ui-xs` references (×2) now resolve cleanly via the new definition.

### Added — §232 (v0.5.9) — NEW audit slot 24 `check-enum-vs-css-selector.mjs` (drift surface #6 closed)

`scripts/release/check-enum-vs-css-selector.mjs` (~330 LOC). Walks each `<name>.yaml` for `enum:` blocks, cross-references against `:scope[<prop>="<value>"]` selectors in the component's `.css`. Flags enum values declared in yaml but with no matching CSS rule — silent fall-through to default variant (FEEDBACK-17 §1 bug class).

Baseline: 119 findings across multiple components (chart type / popover placement / tooltip placement etc. — many JS-routed via Floating UI or chart libraries, false-positive for CSS-cascade routing). Strict mode hard-fails on NEW drift beyond baseline; architect-triage cycle-by-cycle as primitives migrate to true CSS-cascade routing.

Closes the 6th drift surface in the 7-surface lockdown taxonomy.

### Added — §222 (v0.5.9) — NEW audit slot 21 `check-connected-reads-attr.mjs` (drift surface #5 closed)

`scripts/release/check-connected-reads-attr.mjs` (~300 LOC). Walks each `class.js` for `connected()` / `connectedCallback()` bodies that synchronously read a declared `static properties.X` value, without an `attributeChangedCallback` to re-sync when the attribute applies after connection.

Bug class: FEEDBACK-10 §1 `<toggle-scheme-ui scheme="${reactiveValue}">` race — `#initState()` reads `this.scheme` before the framework's attribute-application phase, so declarative-rebinds lose to AUTO fallbacks. v0.5.7 §200 closed for `<toggle-scheme-ui>` specifically; §222 catches the next instance mechanically.

Baseline: 67 findings across components. Heuristic intentionally narrow (one-hop method chase, narrow gate), but accepts false positives are expected on first ship. Architect-triage cycle-by-cycle. Strict mode hard-fails on NEW drift beyond baseline.

Closes the 5th drift surface in the 7-surface lockdown taxonomy.

### Added — §220 (v0.5.9) — `throttle` parity on form-bearing primitives via shared `UIFormElement` helper

`packages/web-components/core/form.js` graduates the v0.5.5 §184 slider trailing-debounce pattern to the shared `UIFormElement` base class:

- `throttle` static property (default 0) — declarative ms window for the `input` event
- `scheduleThrottledInput()` — call from input-event sites; trailing-debounces dispatch when `throttle > 0`
- `flushPendingInput()` — call from change / blur / commit paths; ensures trailing `input` fires before `change`
- `dropPendingInput()` — called automatically by `disconnected()` to prevent late dispatches

Callsite migrations:

- `components/slider/class.js` — drops local `#inputTimer` + `#flushInput()`; delegates via the shared helpers (semantics preserved)
- `components/input/class.js` `#onInput` — now calls `scheduleThrottledInput()` (was unconditional dispatch)
- `components/textarea/class.js` `#onInput` — same migration; `#onBlur` + `#onKeydown` call `flushPendingInput()` before commit

`input.yaml` + `textarea.yaml` document the new `throttle` prop. Use for expensive `input`-driven computation (server-side autocomplete, large list filter, palette regen) where consumers want trailing-debounce semantics without authoring their own timer.

Closes FEEDBACK-14 §3.

### Added — §223 (v0.5.9) — `<empty-state-ui minimal>` single-line muted layout

`empty-state.yaml` + `class.js` + `empty-state.css` — new `minimal: boolean` prop drops the centered-column placeholder chrome (no canvas-style padding, no icon-size lg, no max-width on description). Renders as `[icon] heading [description]` inline in muted-fg, with `[slot="action"]` margin-collapsed to trailing edge. Use for inline empty-table-row / empty-list / placeholder cells where the canvas placeholder is too loud.

Closes FEEDBACK-13 §4.

### Added — §228 (v0.5.9) — `dts-codegen.mjs` sibling-yaml walk + 5 NEW sibling yamls + 2 parent yaml `name:` fixes

`scripts/build/dts-codegen.mjs` (~40 LOC change) — walks ALL `*.a2ui.json` sidecars in each component dir + emits ONE `<dir>.d.ts` with one `export class UI<Name>` declaration per sidecar. Uses `x-adiaui.name` (canonical class name) instead of `UI${title}` (fragile — yaml `name:` and `component:` can drift, e.g. `feed.yaml` `name: UIFeedContainer` + `component: Feed` → old codegen wrote `UIFeed` extending `UIElement` which is wrong since `UIFeed` is the imperative host class, not a custom element).

5 NEW sibling yamls authored:

- `feed/feed-container.yaml` (later retired in favor of `feed.yaml` canonical-class) — actually `feed-container` redundant since `feed.yaml` already declares `UIFeedContainer`
- `menu/menu-item.yaml` (`UIMenuItem`)
- `menu/menu-divider.yaml` (`UIMenuDivider`)
- `stepper/stepper-item.yaml` (`UIStepperItem`)
- `toggle-group/toggle-option.yaml` (`UIToggleOption`)
- `toolbar/toolbar-group.yaml` (`UIToolbarGroup`)

2 parent yaml `name:` fixes (yaml was wrong; class.js was right):

- `col/col.yaml`: `name: UIColumn` → `UICol` (class.js exports `UICol`)
- `richtext/richtext.yaml`: `name: UIRichtext` → `UIRichText` (class.js exports `UIRichText`)

`index.d.ts` tag-map: `UIColumn` → `UICol` (`col-ui`) + `UIRichtext` → `UIRichText` (`richtext-ui`).

13 component `.d.ts` files regenerated by codegen with both parent + sibling classes declared cleanly. After this commit + §229 baseline shrink, `check-static-methods-vs-dts --strict` runs clean across all 4 sub-axes.

Closes FEEDBACK-18 §2 (child-class drift) and the 8 audit-driven additions on top of the 7-instance enumeration.

### Fixed — §229 (v0.5.9) — class-export walk baseline shrinks 15 → 0

`scripts/release/check-static-methods-vs-dts.mjs` `EXPECTED_CLASS_EXPORT_FINDINGS` shrunk to empty Set after §228 codegen ships. All 15 sibling-class drift entries close mechanically.

Drift surface #1 (`.d.ts` axis) now fully strict-mode-protected across all 4 sub-axes (method + event + property + class-export).

### Added — §227 (v0.5.9) — `check-styles-barrel-completeness.mjs` `--package=web-modules` flag

`scripts/release/check-styles-barrel-completeness.mjs` — `runWebModulesAudit()` walks all 6 web-modules clusters (shell / chat / editor / simple / runtime / theme) and verifies each `*.css` file is reachable via one of the three `package.json` export-map patterns (`./<cluster>/*.css`, `./<cluster>/*/*.css`, `./<cluster>/*/css/*.css`).

Companion audit to §226 (v0.5.9 prep batch's web-modules `package.json` exports map fix). Together they close the second half of drift surface #3 (CSS-barrel — web-modules side).

### Added — §225 (v0.5.9) — `<select-ui>` `#parseOptions()` once-per-element console.warn

`packages/web-components/components/select/class.js` — class-level `static #warnedNonOption = new WeakSet()` + `#parseOptions()` collects unknown-child tags + emits a single `console.warn` per element when consumer-authored children aren't native `<option>`/`<optgroup>`. Pattern mirrors v0.5.5 §184 §8 + v0.5.7 §201. `select.yaml` gains `usage:` block documenting native `<option>` children + programmatic `.options = [...]` paths. Closes FEEDBACK-10 §3.

### Added — §224 (v0.5.9) — `<feed-ui>` `data-spawned-by="toast"` marker on toast-spawned containers

`packages/web-components/components/toast/class.js` — `UIToast.show()` stamps `data-spawned-by="toast"` on the spawned `<feed-ui>` container at creation. Idempotent (once per container). Lets DOM-query consumers distinguish toast-spawned containers from user-authored ones. `toast.yaml` gains `usage:` block documenting the marker + query example. Closes FEEDBACK-10 §2.

### Changed — §221a-k (v0.5.9) — 11-item docs sweep

`packages/web-components/USAGE.md` grows a new section "v0.5.9 — Consumer-feedback discoverability sweep" (~218 lines) covering 11 items surfaced across consumer-feedback rounds R9, R10, R13, R15, R16, R20:

- **§221a** Universal `[size]` prop family — token wiring + size-cascade authoring patterns
- **§221b** `<input-ui>` migration guide from native `<input>` — common gotchas + payload-shape table
- **§221c** `<switch-ui>` vs `<check-ui>` vs `<segmented-ui>` — the "named binary state" trap
- **§221d** `<select-ui>` native `<option>` children — declarative + programmatic paths
- **§221e** `<accordion-item-ui>` action buttons in custom headers — `[slot="action"]` exclusion list
- **§221f** `<color-picker-ui>` `<popover-ui trigger=click>` inline pattern
- **§221g** `[data-scheme]` ancestor opt-in for color scheme — patterns + edge cases
- **§221h** Effect-dispatch timing — `queueMicrotask` batching + the double-microtask test-flush idiom (also added to `signals.d.ts` JSDoc on `effect()`)
- **§221i** Backticks inside HTML comments inside `html\`…\`` templates — JS-spec footgun
- **§221j** Typography token cheatsheet — L0/L1 token families + common misreads (`--a-font-semibold` doesn't exist; use `--a-weight-semibold`)
- **§221k** `<text-ui variant>` chooser guide — 12 canonical variants with picker heuristics

`packages/web-components/core/signals.d.ts` — JSDoc on `effect()` documents the queueMicrotask batching semantics + the double-microtask test-flush idiom (the canonical fix for "my effect didn't run yet" in tests).

### Added — §225 (v0.5.9) — `<select-ui>` `#parseOptions()` once-per-element console.warn

`packages/web-components/components/select/class.js` — class-level `static #warnedNonOption = new WeakSet()` + `#parseOptions()` collects unknown-child tags + emits a single `console.warn` per element when consumer-authored children aren't native `<option>`/`<optgroup>`. Pattern mirrors v0.5.5 §184 §8 + v0.5.7 §201. `select.yaml` gains `usage:` block documenting native `<option>` children + programmatic `.options = [...]` paths. Closes FEEDBACK-10 §3.

### Added — §224 (v0.5.9) — `<feed-ui>` `data-spawned-by="toast"` marker on toast-spawned containers

`packages/web-components/components/toast/class.js` — `UIToast.show()` stamps `data-spawned-by="toast"` on the spawned `<feed-ui>` container at creation. Idempotent (once per container). Lets DOM-query consumers distinguish toast-spawned containers from user-authored ones. `toast.yaml` gains `usage:` block documenting the marker + query example. Closes FEEDBACK-10 §2.

### Fixed — §229 (v0.5.9) — `check-static-methods-vs-dts.mjs` (slot 20) class-export walk extension

`scripts/release/check-static-methods-vs-dts.mjs` gains the **4th declaration-axis** on the `.d.ts` surface: walks `export class UI<Name> extends UI<Base>` declarations in `class.js` against `export class` in every `.d.ts` in the component dir. Codegen'd `.d.ts` files NOW INCLUDED for the class-export axis (codegen emits primary; sibling-class drift is §228's gap). Static-method / event / property axes continue to skip codegen'd files (yaml-vs-runtime via slot 19).

Audit-driven discovery surfaced **15 class-export-undeclared findings across 13 components** — 8 more than FEEDBACK-18 §2's original enumeration. All 15 baselined as `EXPECTED_CLASS_EXPORT_FINDINGS`; strict mode hard-fails on NEW class-export drift beyond the baseline. After §228 codegen sibling-yaml walk ships, the baseline shrinks 15 → 0; `.d.ts` axis fully strict-mode-protected across all 4 sub-axes.

## [0.5.8] - 2026-05-15

### Changed — `patterns/` demo-page refresh (v0.5.8)

- `patterns/component-tokens/component-tokens.examples.html` + `patterns/responsive-shell-sidebar/responsive-shell-sidebar.examples.html` — demo refresh consuming v0.5.7 canonical fixes (`<text-ui>` retired-variant cleanup landed in v0.5.7 §210; component-tokens demo updated to reference real tokens; responsive-shell-sidebar demo aligned with v0.5.3 §157 pattern). Companion to §230 token-reference cleanup.

### Fixed — §230 (v0.5.8) — `--a-font-weight-*` orphaned references closed in 3 components


3 component CSS files referenced `var(--a-font-weight-*)` tokens that had been removed from the canonical token namespace in v0.5.0 (typography.css's "to be removed" comment was acted on but 3 consumers weren't updated). The references resolved to empty → CSS-keyword default (inherited 400 happened to match intent for nav-item / nav-group; feed.css had an inline `, 600` fallback masking the breakage). Consumer-override path was silently broken.

- `components/nav-item/nav-item.css:42` — `var(--a-font-weight-normal)` → `var(--a-weight-normal)`
- `components/nav-group/nav-group.css:12` — `var(--a-font-weight-normal)` → `var(--a-weight-normal)`
- `components/feed/feed.css:136` — `var(--a-font-weight-strong, 600)` → `var(--a-weight-semibold)` (preserves rendering weight; drops inline fallback)
- `styles/typography.css` comment block updated to reflect actual state (REMOVED in v0.5.0; was "to be removed" before).

Closes FEEDBACK-20 §1.

### Added — §231 (v0.5.8) — audit slot 25 `check-component-css-vs-styles-tokens.mjs`

NEW: `scripts/release/check-component-css-vs-styles-tokens.mjs` — closes the **7th drift surface** in the v0.5.x drift-class lockdown taxonomy: **token-reference vs token-definition axis**. Walks every `components/<dir>/*.css` for `var(--a-<name>)` references, walks `styles/**/*.css` (recursive — includes `styles/colors/semantics.css` + the 18-file color tree) for `--a-<name>:` definitions, reports orphaned references.

Audit-driven discovery cadence (per memory `feedback_audit_driven_discovery_cadence`): running the audit against `main` post-authoring surfaced **19 orphaned references across 12 components** — 16 more than FEEDBACK-20 §1's original 3-reference filing. The 16 additional findings are baselined as `EXPECTED_FINDINGS` (matching the v0.5.6 §192 schema-vs-impl-coverage baseline pattern) and filed for v0.5.8+ peer-architect triage. Strict mode hard-fails on NEW orphans beyond the baseline; stale-baseline detection reports entries that no longer fire.

Drift-class lockdown taxonomy now covers all 7 surfaces (v0.5.7 closed 4; v0.5.8 closes the remaining 3 via §222 + §232 + §231).

**Audit-script family graduation**: v0.5.4 → v0.5.8: 13 → 22+ trip-wires.

## [0.5.7] - 2026-05-15

### Changed — `traits/catalog.examples.js` consumer sweep (§210 follow-on, v0.5.7)

`traits/catalog.examples.js` swept for the §210 retired `<text-ui variant="subheading">` — replaced with the canonical `subsection` token at 2 call sites (catalog header rendering). Companion mechanical cleanup for the §210 phantom-variant removal.

### Fixed — §210 (v0.5.7) — `<text-ui>` variant enum drift (token-vs-rule + phantom entries)


Close FEEDBACK-17 §1. `text.yaml` + `text.d.ts` + `text.a2ui.json` documented 18 `variant` enum values; `text.css` shipped `:scope[variant=…]` rules for only 9. The 9 stranded values fell through to `body` defaults silently — no console warning, no visual cue. Sixth instance of the recurring "static-declaration lies about runtime" drift class (R5/R7/R8/R9/R14/R17). Two distinct sub-fixes in one arc:

**Token-backed variants restored to rendering (3 values):**
- `subsection`, `deck`, `metric` — each has a full `--a-<role>-{family,weight,size,leading,tracking,case,color}` token family already shipping in `styles/typography.css`, but no matching `:scope` rule consumed the tokens. Added 3 `:scope[variant=…]` rules to `text.css` matching the existing 9-rule pattern. Verified via the new `text.test.js` suite that each rule references `var(--a-<role>-family|weight|size)` (not `var(--a-body-*)` defaults).

**Phantom enum entries retired (6 values):**
- `subheading`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6` — these had no `--a-<role>-*` tokens shipped anywhere (verified across `styles/typography.css` + `styles/prose.css` + `styles/tokens.css`). The yaml + d.ts + a2ui.json advertised them; the project shipped no implementation surface to render them. Removed from all three documentation surfaces. SemVer-minor compatibility note: any consumer who authored `<text-ui variant="subheading">` or `variant="h<N>">` was already getting body-default rendering — the behavior change is from "silent body-default" to "type-error-at-compile-time + clearer authoring contract."

42 new tests in `text.test.js` cover (1) mount smoke for all 12 surviving enum values, (2) every documented variant has a matching CSS rule, (3) the 3 restored variants reference role tokens (not body defaults), (4) the 6 phantom entries do NOT appear in `.d.ts` union OR `.a2ui.json` enum, (5) `.a2ui.json` enum + `.d.ts` union + CSS rules are mutually consistent 1:1. Plus a section-level demo in `text.examples.html` showcasing the 3 restored variants (subsection/deck/metric) inline with kicker/display/heading/body so the demo page visually surfaces the new contract.

Audit-slot extension queued for v0.5.8: extend slot 19 (`check-yaml-impl-coverage.mjs`) one step further — for each `enum` value on a prop, assert a matching `:scope[<name>="<value>"]` rule exists in the component's `.css`. Catches both halves of this drift class (token-vs-rule + phantom enum) mechanically. Reference: FEEDBACK-17 §1 audit-slot suggestion.

### Fixed — §200 (v0.5.7) — `<toggle-scheme-ui>` post-connect reactive-scheme race

`packages/web-components/components/toggle-scheme/class.js` — overrides `attributeChangedCallback` to re-run `#initState()` when the `scheme` attribute changes AFTER `connectedCallback` AND before first user interaction.

The bug class: `template.js scan()` strips placeholder attributes pre-insertion; `replaceChildren()` upgrades the element + fires `connectedCallback`; the old `#initState()` synchronously reads `this.scheme` and locks to AUTO fallback because the attribute is still stripped; then `template.js update()` re-applies `setAttribute('scheme', '<reactive>')` but the AUTO state is already cached. Consumers using `<toggle-scheme-ui scheme="${reactiveValue}">` saw incorrect scheme on every page load.

Fix introduces a `#userTouched` flag set by three paths: (1) the internal button-press handler, (2) `setScheme()` programmatic, (3) `toggle()` programmatic. Until `#userTouched` flips true, post-connect `scheme` attribute changes re-run `#initState()` so the reactive value wins. Once user picks explicitly, the flag locks the choice across subsequent reactive re-renders.

6 new tests in `toggle-scheme.test.js` cover the race repro, initial-attr passthrough, all 3 user-touch paths locking the choice, and pre-touch attribute removal back to auto. Closes FEEDBACK-10 §1.

### Fixed — §201 (v0.5.7) — `<color-picker-ui>` `#parseValue()` accepts `NaN` / `none` / `%`

`packages/web-components/components/color-picker/class.js` — `#parseValue()` regex extended to accept three CSS Color L4 / culori-convention channel shapes that the v0.5.6 baseline silently discarded:

- **`NaN`** — culori chromaless-hue convention; coerced to `0`.
- **`none`** — CSS Color L4 powerless-component syntax; coerced to `0` (matches the L4 powerless-cascade spec).
- **`%`** on L — `oklch(53% 0.05 240)` → divides by 100.

Plus: when input doesn't match any documented shape, a `console.warn` fires **once per element** (deduped via class-level `static #warnedBadParse = new WeakSet()`). Matches the v0.5.5 §184 §8 `<button-ui>` icon-only safety-net pattern + v0.5.7 §215 (queued) `<select-ui>` `#parseOptions` pattern.

7 new tests in `color-picker.test.js` cover all 4 input shapes (numeric, NaN, none, %), hex passthrough, warn-once dedup, and per-instance warn budget. Closes FEEDBACK-13 §1 + FEEDBACK-14 §2 (co-credited per the 2026-05-15 slot-dance reconciliation).

### Added — §207 (v0.5.7) — audit slot 20 `check-static-methods-vs-dts.mjs`

NEW: `scripts/release/check-static-methods-vs-dts.mjs` cross-checks three runtime surfaces in `class.js` against the sibling hand-authored `.d.ts`:

1. **Static methods** — `static <name>(...)` declarations should appear as static signatures in `.d.ts`. Caught FEEDBACK-09 §9 (`<toast-ui>.show()`).
2. **Dispatched events** — `dispatchEvent(new CustomEvent('<name>', ...))` literals should appear as `addEventListener(type: '<name>', ...)` overloads in `.d.ts`. Caught FEEDBACK-07's event-type drift class + R9 §10.
3. **Static-properties keys** — `static properties = { <key>: ... }` keys should appear as public field declarations in `.d.ts`. NEW per FEEDBACK-14 §1 (`<slider-ui>` `throttle` + `hint` missing) — 5th instance of the runtime-vs-`.d.ts` drift class family.

Codegen'd `.d.ts` files (73 of 90; identified via the "Type declarations generated by scripts/build/dts-codegen.mjs" marker) are skipped — yaml is their SoT, and slot 19 (`check-yaml-impl-coverage.mjs`, v0.5.6 §192) covers the yaml-vs-runtime drift surface.

Heuristics: balanced-brace walker to extract `static properties = { ... }` body; depth-tracked key extraction for top-level prop names only; regex extraction for event literals + static method signatures + `.d.ts` field/method declarations.

### Changed — §207-bundled — close 15 `.d.ts` drift findings surfaced by audit slot 20

Running the new slot 20 against `main` surfaced 15 hard findings across 6 hand-authored form-bearing primitives — all closed in this same cycle to complete the drift-class lockdown narrative. Like §205 (`requiredIcons` 2 → 5 primitives), the audit-driven discovery materially expanded scope:

- `calendar-picker.d.ts` — adds `label`, `placeholder`, `format`, `min: string | null`, `max: string | null` fields.
- `code.d.ts` — adds `name`, `required`, `disabled`, `readonly` fields.
- `input.d.ts` — adds `addEventListener(type: 'submit', ...)` overload (the runtime dispatches `Event('submit')` on Enter-key + form-submit at `class.js:717+725`).
- `select.d.ts` — adds `hint` field.
- `switch.d.ts` — adds `hint` field.
- `upload.d.ts` — adds `label`, `accept`, `multiple` fields.

All declarations carry `§207 (v0.5.7)` JSDoc cross-refs. Post-fix audit reports clean across all 95 primitives.

### Added — §208 (v0.5.7) — audit slot 22 `check-styles-barrel-completeness.mjs`

NEW: `scripts/release/check-styles-barrel-completeness.mjs` cross-checks `packages/web-components/styles/components.css` `@import` lines against the per-component CSS files on disk. Catches the "primitive ships CSS but barrel forgot to @import it" drift class surfaced by FEEDBACK-11 §1.

Three drift classes caught:
1. `file-exists-not-imported` (hard) — CSS file exists at `components/<X>/<X>.css` (or irregular `<X>-ui.css`) but barrel doesn't @import it.
2. `imported-not-existing` (hard) — barrel @imports a file that doesn't exist on disk.
3. `duplicate-import` (warn) — same file @imported twice.

Filename handling: supports both `<name>.css` (canonical) AND `<name>-ui.css` (irregular — stat-ui-only as of v0.5.7; planned rename in v0.6.0 §303 per R11 §2). CSS-only components (`header`, `footer`, `section`) are skipped via `CSS_ONLY_COMPONENTS` allowlist.

Post-§203 audit reports clean across all 95 primitives + 92 @import lines.

### Added — §202 (v0.5.7) — `<slider-ui>` `.d.ts` declares `throttle` + `hint`

`packages/web-components/components/slider/slider.d.ts` — adds `throttle: number` + `hint: string` to the `UISlider` class declaration. Closes FEEDBACK-14 §1 — runtime + yaml + a2ui.json all shipped these props since v0.5.5 §184; the `.d.ts` lagged behind. 5th recurrence of the runtime-vs-`.d.ts` drift class (R5 / R7 / R8 / R9 / R14). Consumers using `<slider-ui throttle="50" hint="…">` no longer need `as any` casts or augment shims.

### Changed — §206 (v0.5.7) — `<accordion-item-ui>` action-slot opt-out

`packages/web-components/components/accordion/class.js` — `#onClick` now skips toggle when the click originates inside `[slot="action"]`, `[slot="actions"]`, or `[data-no-toggle]`. Lets consumers author rich custom headers with action buttons (Copy / Download / settings) inside `[slot="header"]` without bubbling-toggle UX. Slot vocabulary matches the canonical `drawer-ui` + `pane-ui` naming. Additive — existing consumers see no behavior change. Closes FEEDBACK-16 §2.

`packages/web-components/components/accordion/accordion-item.yaml` — gains `slots:` block documenting `header` + `action` slots; `events:` block formalizing the `toggle` event detail; sidecar regenerated.

`packages/web-components/components/accordion/accordion.yaml` — `slots.default.description` corrected from `pane-ui children` to `accordion-item-ui children`. Closes FEEDBACK-13 §2 (folded into §206 since adjacent yaml).

### Changed — §205 (v0.5.7) — `requiredIcons` completeness on 5 primitives (2 from FEEDBACK-16, 3 from §209 audit-extension discovery)

Five primitives' `static requiredIcons` declarations updated to mirror their runtime dynamic-icon stamps:

**Filed by FEEDBACK-16 §1:**
- `packages/web-components/components/color-picker/class.js:130` — `['copy']` → `['copy', 'check', 'warning']`. Copy-button dynamic-swaps to `check` (success) / `warning` (failure) at line 496.
- `packages/web-components/components/table/class.js:98` — `['caret-right', 'caret-up-down', 'table']` → adds `'arrow-up'`, `'arrow-down'`, `'funnel-simple'`, `'funnel-simple-fill'`. Dynamic sort-direction + filter-state stamps at lines 679 + 707.

**Surfaced by §209 audit-regex extension (see below):**
- `packages/web-components/components/agent-artifact/class.js` — NEW `static requiredIcons = ['caret-right', 'caret-down']`. Chevron stamps on collapse/expand state at lines 119 + 188.
- `packages/web-components/components/table-toolbar/class.js` — NEW `static requiredIcons = ['arrow-up', 'arrow-down', 'caret-up-down']`. Nested-ternary sort-indicator stamp at line 576.
- `packages/web-components/components/timeline/class.js` — NEW `static requiredIcons = ['caret-down', 'caret-right']`. Expanded-state chevron at line 167.

(In all 3 newly-surfaced primitives, `this.icon` reads are consumer-supplied — not declared in `requiredIcons` per the established convention.)

Yaml `requiredIcons:` blocks mirror the class.js change for all 5; sidecars regenerated. Closes FEEDBACK-16 §1 + 3 additional drift instances that the pre-§209 audit didn't catch.

### Fixed — §209 (v0.5.7) — `check-required-icons.mjs` (audit slot 11) extended to catch ternary-style icon stamps

`scripts/release/check-required-icons.mjs` — regex extended to extract icon-name literals from `.setAttribute('name', <expr>)` where `<expr>` is a ternary (or nested-ternary) rather than a singleton string literal. Uses a balanced-paren walker to capture the 2nd-argument expression (handles inner calls like `this.#filters.has(col.key) ? 'A' : 'B'`), then matches literals that appear in ternary-branch position (preceded by `?` or `:`). Avoids false-positives on comparator literals (e.g. `dir === 'asc'`).

Closes the audit-blind-spot bug class that allowed FEEDBACK-16 §1's drift to slip past CI: pre-§209 the slot reported "all primitives clean"; post-§209 + §205 the audit reports clean while §205 closes the actual drift. The audit now reliably catches the runtime-vs-`static requiredIcons` drift class on both singleton-literal AND ternary-expression stamp sites.

Discovery surface: running the post-§209 audit against `main` surfaced 3 additional primitives with the same drift class (`agent-artifact`, `table-toolbar`, `timeline`) — all 3 fixed under §205 above.

### Added — §204 (v0.5.7) — `package.json` `exports` accepts nested-CSS specifier shapes

`packages/web-components/package.json` `exports` map gains two entries:

```json
"./components/*/*.css": "./components/*/*.css",
"./components/*/css/*.css": "./components/*/css/*.css"
```

Consumers can now import component CSS with the path-mirroring shape that matches every JS subpath import:

```js
// Both shapes now resolve:
import '@adia-ai/web-components/components/toggle-scheme.css';
import '@adia-ai/web-components/components/toggle-scheme/toggle-scheme.css';
```

The second new entry handles `editor-shell`'s nested sub-stylesheets (`components/editor-shell/css/{layout,tokens,bespoke}.css`). Closes FEEDBACK-15 §1.

### Fixed — §203 (v0.5.7) — `styles/components.css` barrel completeness

`packages/web-components/styles/components.css` — adds 2 `@import` lines for `<link-ui>` + `<toggle-scheme-ui>`. Both primitives ship per-component CSS files (`components/link/link.css`, `components/toggle-scheme/toggle-scheme.css`) that previously weren't `@import`-ed into the canonical barrel. Consumers using `<link href="@adia-ai/web-components/styles/components.css">` previously rendered both elements unstyled. Closes FEEDBACK-11 §1 (the 2 genuinely-missing instances; `stat-ui` was already imported at its irregular filename per R11 §2, deferred to v0.6.0 §303).

## [0.5.6] - 2026-05-14

### Added — §191 (v0.5.6) — close §178 baseline-missing 7 `.d.ts` siblings

`core/anchor.d.ts`, `core/controller.d.ts`, `core/markdown.d.ts`, `core/polyfills.d.ts`, `core/provider.d.ts`, `core/streams-bridge.d.ts`, `core/transport.d.ts` — all 7 public-API JS modules now have hand-authored TypeScript declarations mirroring the runtime exports. Pattern follows §154 `icons.d.ts` (v0.5.4 `cf7c14aaa` hotfix): types live alongside the runtime SoT, documented inline, regenerated only if the runtime changes shape.

**Type surfaces now exposed to TS consumers**:

- `anchor.d.ts` — `Placement` type (12-value union), `AnchorOptions` interface, `supportsAnchorCSS: boolean`, `anchorPopover(anchor, popover, options?): () => void`.
- `controller.d.ts` — `BaseController` class (public surface — `host`, `connect`, `disconnect`, `subscribe`, `notify`, lifecycle hooks, `getState`), `RouteController extends BaseController`, plus supporting `ControllerSchema`/`Route`/`RouteState`/`RouteCommands`/`RouteControllerOptions` interfaces.
- `markdown.d.ts` — `renderMarkdown(src: string): string`.
- `polyfills.d.ts` — side-effect-only module declaration (`export {}`). Documents the runtime feature checks + the two opt-in polyfill packages.
- `provider.d.ts` — `RouteController` re-declaration (matches `controller.d.ts` for historical compatibility), `UIRouter extends UIElement`, `TemplateResolver` type, `HTMLElementTagNameMap['router-ui']` declaration so `document.querySelector('router-ui')` returns `UIRouter`.
- `streams-bridge.d.ts` — `BridgeRenderer` interface (duck-typed renderer contract), `BridgeOptions` interface, `bridgeStream` / `bridgeStreamAsync` signatures.
- `transport.d.ts` — `TransportErrorKind` discriminator (4-value union), `TransportOptions` interface, `request` / `json` signatures (`json` is generic over response body type), `TransportError` class with `kind`/`status`/`body` fields.

**Audit-script closure**: `scripts/release/check-dts-sibling-presence.mjs` (audit slot 17) `EXPECTED_MISSING` allowlist emptied — baseline 7 → 0. Strict mode now hard-fails on any future regression (audit reports 14/14 public-API entries with sibling `.d.ts`, 0 missing). Defensive stale-baseline detection retained — every future entry treated as policy gap requiring justification.

**Verification**: `node scripts/release/check-dts-sibling-presence.mjs --strict` → exit 0. `node scripts/build/components.mjs --verify` → clean (135 files). `npm run check` → mcp:smoke OK.

### Changed — §197 (v0.5.6) — FEEDBACK-09 §9 + §10 typing fixes

**§9 — `<toast-ui>` `.d.ts` adds the static `show()` declaration**

`UIToast.show()` was authored in `class.js` since the imperative path
shipped but absent from `toast.d.ts`. TS consumers calling
`UIToast.show({ text: 'Saved' })` got `Property 'show' does not exist on typeof UIToast`. §197 adds two interfaces (`UIToastShowOptions`,
`UIToastFeedHandle`) and the static method declaration. Same drift class as the v0.5.3 §154 `core/icons.d.ts` hotfix.

**§10 — Generic `<V extends string = string>` on Select / Segmented / Switch ChangeEventDetail**

`SelectChangeEventDetail`, `SegmentedChangeEventDetail`, and `SwitchChangeEventDetail` previously hard-coded `value: string`, forcing Tokens Studio's 11 consumer call-sites to widen via `as` casts. Adding a generic parameter `<V extends string = string>` defaulting to `string` is backwards-compatible (existing consumers see no change) and lets type-aware call-sites pin the literal value type:

```ts
type Size = 'sm' | 'md' | 'lg';
select.addEventListener('change', (e: SelectChangeEvent<Size>) => {
  e.detail.value; // typed as Size, not string
});
```

Applies to `SelectChangeEvent`, `SegmentedChangeEvent`, `SwitchChangeEvent` aliases as well. `SwitchChangeEventDetail.checked` stays `boolean` (only `value` is generified).

### Changed — §197 (v0.5.6) — audit slot 11 regex tolerates generic params

`scripts/release/check-form-bearing-dts.mjs` (slot 11) was rejecting the new generic-parameterized `EventDetail` interfaces because the old regex required `<X>EventDetail\s*\{`. §197 widens both the interface-parse regex AND the `Event = CustomEvent<...>` alias regex to accept an optional `<...>` generic-params block. Audit stays at 0 errors / 0 warnings against the updated .d.ts files.
## [0.5.5] - 2026-05-14

### Added — §176 (v0.5.5) — six new sibling sub-component yamls

Six sibling yamls authored per the v0.5.4 §172 scanner pattern (sibling-yaml discovery in the build pipeline). Each describes a co-located sub-component class that previously existed as a registered custom element without its own catalog entry — surfaced as the §175 baseline-orphan class.

- `list/list-item.yaml` — `UIListItem` (3 props: icon, text, description)
- `avatar/avatar-group.yaml` — `UIAvatarGroup` (2 props: max, size)
- `accordion/accordion-item.yaml` — `UIAccordionItem` (2 props: text, open)
- `timeline/timeline-item.yaml` — `UITimelineItem` (8 props: text, description, time, duration, icon, variant, status, spinner)
- `action-list/action-item.yaml` — `UIActionItem` (5 props: icon, text, value, variant, disabled)
- `tabs/tab.yaml` — `UITab` (4 props: text, value, icon, disabled)

After §184's `tree-item.yaml` addition, sibling-yaml pattern is now at **7 components**. Catalog: 125 → 132 components (+7 across §176 + §184).

Commit `443365913`. See root CHANGELOG and journal §176 for full context.

### Added — §184 (v0.5.5, FEEDBACK-08 response cycle) — 6 new APIs + 1 build-time audit

FEEDBACK-08 (2026-05-14) reported 9 items post-v0.5.3 dogfooding. Items 3 (`<segment-ui>` `aria-label` auto-wire) + 9 (`<segment-ui disabled>` schema docs) were already resolved in v0.5.5 in-flight work; 7 remaining items land here as a single arc:

- **`<tree-item-ui>` `badge` attribute (FEEDBACK-08 §1, P1).** Mirrors the `<nav-item-ui>` `badge` API — trailing muted small text alongside the row, right-aligned. Use for counts/labels (e.g. `<tree-item-ui text="Colors" badge="7">`). Empty span auto-hides. Plus a sibling `tree-item.yaml` (per the v0.5.5 §176 sibling-yaml authoring pattern) so the previously-orphan `UITreeItem` component now has its own catalog entry, `.d.ts`, and `.a2ui.json` sidecar. New CSS tokens: `--tree-badge-{bg,fg,px,radius,size}`.
- **`<tree-ui>` programmatic expand/collapse API (FEEDBACK-08 §2, P1).** Five new methods on `UITree`: `.expand(itemOrValue)`, `.collapse(itemOrValue)`, `.expandAll()`, `.collapseAll()`, `.expandTo(itemOrValue)` (expands ancestors + scrolls into view). All accept either an `HTMLElement` (must match `tree-item-ui`) OR a string (matched against `[value]` first, then `[text]` for ergonomics). Removes the `_expanded[]` + `tree-select`-listener boilerplate consumers were maintaining locally.
- **`<slider-ui>` `throttle` attribute (FEEDBACK-08 §4, P1).** Declarative debounce for the `input` event when driving expensive computation (palette regen, shader compile, large list reflow). When `throttle > 0`, value updates + visual feedback remain immediate but `input` event emission is debounced — only the FINAL value in the throttle window dispatches. `change` fires unthrottled on pointerup / track click / keyboard; any pending `input` flushes BEFORE `change` so consumers always see `input → input → … → input → change` ordering. `throttle="0"` (default) preserves the pre-§184 every-pointer-move-fires-input behavior. Common values: 50–100ms.
- **`<slider-ui>` / `<select-ui>` / `<switch-ui>` `hint` attribute (FEEDBACK-08 §7, P2).** Caption rendered beneath the control + wired to `aria-describedby` on the host. Distinct from the in-component `label` (which becomes `aria-label`). `<switch-ui>` had `hint` declared in yaml since §170 but the template never rendered it — this arc closes the schema-vs-implementation gap on switch in addition to adding the new declarations on slider + select. New CSS tokens per primitive: `--{slider,select,switch}-hint-{mt,size,fg,lh}`.
- **`<button-ui>` icon-only a11y safety net (FEEDBACK-08 §8, P2).** When `<button-ui icon="X">` is used WITHOUT `text`, `aria-label`, `aria-labelledby`, OR `title`, `console.warn()` once per element (deduped via `WeakSet`) so the bug class is diagnosable in dev. When `title="Undo"` IS set, mirror it to `aria-label` automatically — matches the common icon-only-button-with-tooltip pattern (zero-author-cost a11y). Consumers can still set `aria-label` explicitly; the auto-mirror only fires when no other accessible name exists. Per FEEDBACK-08's two options A + B chosen together (warn + auto-derive).
- **`core/template.js` partial-attribute error message (FEEDBACK-08 §5, P2).** The §152 (v0.5.3) runtime warn previously suggested `.${attr.name}=${expression}` as a generic property-assignment recipe. For `class` specifically, `.class=${expr}` sets a JS expando (`element.class = "foo"`) instead of the `className` property — CSS classes never apply. Updated the warning to special-case `class` (suggests `.className=${expression}` first, then `class="${expression}"` for the whole-attribute path) and `style` (suggests `.style.cssText=${expression}`); other attributes keep the generic recipe.

### Added — §184 audit slot 18 (build-time partial-attribute interpolation scanner)

NEW `scripts/release/check-template-interp.mjs` (~220 LOC) closes FEEDBACK-08 §6. Walks every `*.js` / `*.mjs` / `*.ts` / `*.tsx` file under `packages/web-components/`, `packages/web-modules/`, `apps/`, `playgrounds/`, `catalog/` (excludes `node_modules/`, `dist/`, `.test.*`, `.spec.*`), strips comments, finds every `` html` `` tagged template literal via brace-balanced walker (matching the runtime template parser's contract), inspects attribute values for partial-attribute interpolation (`attr="prefix ${var}"` patterns where the value mixes literal + placeholder content). Whole-attribute forms (`attr="${expression}"`) and prop/event forms (`.prop=`, `@event=`) are NOT flagged.

The runtime guard at `core/template.js` (§152) warns at mount time; this static scanner catches the bug class before it ships, even in conditional branches that never run during dev testing (the case FEEDBACK-08 §6 highlights). Wired as `npm run check:template-interp` (advisory exit 0) + `:strict` (exit 1 on findings) + added to `npm run check` aggregate alongside the existing 17 audit-script-family trip-wires. Initial run across all 5 scan roots: zero findings. Audit-script family now spans **18 trip-wires** at v0.5.5.
## [0.5.4] - 2026-05-14

### Added — `core/icons.d.ts` authored (+148 LOC, NEW; pre-cycle FEEDBACK-08-ish gap closure)

NEW `packages/web-components/core/icons.d.ts` (commit `cf7c14aaa`).
Mirrors all 10 runtime exports the `core/icons.js` SoT declares,
including the v0.5.3 §154 helpers (`installIconLoadersForRegistered`,
`discoverRequiredIcons`).

**Why this slipped past v0.5.3's gates**: the audit-script family
had 11 trip-wires at v0.5.3 including §102 `check-core-barrel-dts`
which catches **drift when a `.d.ts` exists** (every exported runtime
symbol has a matching type), but does NOT catch the **totally-missing
`.d.ts` case** for a `core/*.js` file. Result: a new `core/<name>.js`
file shipping without `<name>.d.ts` was silently consumer-broken on
TS-strict surfaces (TypeScript consumers importing from
`@adia-ai/web-components/core/icons` resolved to a missing `.d.ts`
and got `Cannot find module`) but JS-green elsewhere.

In-the-wild detection: `/Users/kimba/Projects/adia/color-app/src/main.ts:48`
(TypeScript strict mode) carried a stale comment noting the workaround
needed because `installIconLoadersForRegistered.d.ts` was missing. The
user wanted to upgrade to the recommended auto-discovering helper but
was blocked.

Filed slot 14 candidate for v0.5.5+ (`check-dts-sibling-presence`):
detect totally-missing `.d.ts` files for `core/*.js`, not just symbol-level
drift within existing `.d.ts` files.

### Fixed — §170 `static labelDeprecated = false` opt-out for 7 form-bearing primitives (v0.5.4)

Closes the `<select-ui label="...">` false-positive deprecation
class observed 2026-05-14 console:

  ```
  form.js:205  [AdiaUI] <select-ui label="…"> is deprecated — wrap in
               <field-ui label="…"> for proper label association.
  ```

`UIFormElement.#warnDeprecatedLabel()` (`core/form.js:197`) fires for
any form-bearing element where `properties.label` exists AND `static
labelDeprecated !== false` AND `hasAttribute('label')`. Pre-§170,
only `Input`, `Slider`, `Range` opted out. The 7 other form-bearing
primitives that declare `label:` in yaml as a first-class prop
spammed false-positive warnings on every gen-ui render.

Added `static labelDeprecated = false` to:

  - `calendar-picker/class.js`
  - `check/class.js`
  - `radio/class.js`
  - `select/class.js`
  - `switch/class.js`
  - `textarea/class.js`
  - `upload/class.js`

Audit method: cross-reference yaml `props.label:` declarations
against `class.js` `static labelDeprecated` opt-out. Found 11
form-bearing primitives with `label:` prop; 3 already opted out
(Input/Slider/Range); 7 missing (this commit); 1 false positive
in audit (field-ui — extends UIElement not UIFormElement; only
mentions UIFormElement in a comment).

v0.5.5 §171 will add audit-script trip-wire to catch this drift
class mechanically going forward.

### Added — §172 NEW `chat-input.yaml` catalog entry; `<chat-input-ui>` no longer orphan (v0.5.4)

NEW `packages/web-components/components/chat-thread/chat-input.yaml`
(+251 LOC). Closes the v0.5.4 ticket
`20260514045025-chatinput-training-data-unclea`: `<chat-input-ui>`
was an orphan component (lived as a sibling file inside
`chat-thread/` rather than its own folder); the build scanner only
looked for canonical `<name>/<name>.yaml`, so the catalog had NO
`ChatInput` entry. Runtime registry mapped `ChatInput → chat-input-ui`
but the LLM had no schema describing what the element stamps. Result:
generator added redundant Button(primary) siblings next to ChatInput
for "send" because the built-in send button was invisible to it.

The yaml encodes:
- 5 props (disabled, loading, placeholder, model, models)
- `submit` event with `{text, model}` detail
- 3 slots (toolbar, send, model — each marked "most authors should
  NOT override")
- 3 a2ui rules explicitly stating "DO NOT add a separate Button
  sibling for send"
- 3 anti-patterns
- 4 examples

Sibling yaml in `feed/` (`feed-item.yaml`, pre-existing orphan)
also unblocked by the build-scanner fix in `@adia-ai/a2ui-compose`
§172.

### Maintenance — audit-script companion §174 ships in v0.5.4 (no source change to web-components)

Repo-level audit script targeting web-components drift:

- **§174 `check-form-element-label-opt-out.mjs`** (slot 15) — codifies the
  §170 audit recipe. For every form-bearing primitive (extends
  UIFormElement): if yaml declares `label:` as first-class, class.js MUST
  opt out via `static labelDeprecated = false;`. Catches future drift if a
  new form-bearing primitive is added without the opt-out.

No web-components source change in §174 — detects future drift in the
yaml-vs-class.js invariant established by §170.

### Changed — `version`: `0.5.3` → `0.5.4`.
## [0.5.3] - 2026-05-14

### Added — `requiredIcons` static export on 12 primitives + `installIconLoadersForRegistered()` helper + audit slot 11 (§154, FEEDBACK-06 §4 + FEEDBACK-07 §4 P1, v0.5.3)

FEEDBACK-06 §4 + FEEDBACK-07 §4 (duplicate ask per author's cross-reference) reported consumers trial-and-error-discovering which Phosphor icons AdiaUI primitives auto-stamp (e.g. `<select-ui>` stamps `caret-up-down`; `<tag-ui>` dismissable stamps `x`; `<tree-ui>` stamps `caret-right`). Without a discoverable contract, consumers built the icon list by refresh-and-look.

**Three surfaces**:

**(a) `static requiredIcons` field** added to 12 auto-stamping primitives — `accordion`, `agent-questions`, `agent-reasoning`, `agent-trace`, `calendar-picker`, `color-picker`, `command`, `pane`, `select`, `table`, `tag`, `tree`. Each declares the Phosphor names it stamps without consumer markup:

```js
export class UISelect extends UIFormElement {
  static requiredIcons = ['caret-up-down'];
  // ...
}
```

**(b) `requiredIcons:` yaml field** on the same 12 yamls; mirrors the class.js declaration. `scripts/schemas/component.yaml.schema.json` updated to accept the field (string array, kebab-case pattern). `npm run components` validates the yaml against the schema + propagates the field into `.a2ui.json` sidecars.

**(c) `installIconLoadersForRegistered(modules)` helper** in `core/icons.js`. Walks `customElements.get()` for the 12 known auto-stamping tag names + collects the union of `static requiredIcons` + installs loaders. Also exposes `.discover()` for consumers who want the icon list without installing:

```js
import { installIconLoadersForRegistered } from '@adia-ai/web-components/core/icons';

const modules = import.meta.glob('/node_modules/@phosphor-icons/core/assets/regular/*.svg', { query: '?raw' });
const discovered = installIconLoadersForRegistered(modules);
// discovered: ['caret-up-down', 'x', 'caret-right', 'check-circle', ...]
```

**(d) Audit-script slot 11** at `scripts/release/check-required-icons.mjs` (~190 LOC). Cross-checks `static requiredIcons` (class.js SoT) vs `requiredIcons:` yaml + runtime `<icon-ui name="X">` literal + `setAttribute('name', 'X')` calls. Catches:

1. **declared-not-stamped** — yaml/class.js says `X` but no runtime stamp site
2. **stamped-not-declared** — runtime stamps `X` but declaration omits it (consumer-failure risk)
3. **missing-declaration** — runtime stamps icons but no class.js `static requiredIcons` field
4. **yaml-class-mismatch** — yaml and class.js disagree on the set
5. **yaml-missing-block** / **class-missing-declaration** — one side has the declaration, the other doesn't

Wired as `npm run check:required-icons` + added to the `npm run check` aggregate after `check:substitutable-set-drift` (slot 10). Audit-script family now spans **11 trip-wires**:

| Slot | Script |
|---|---|
| 1 | check-no-live-readers |
| 2 | check-changelog-coverage |
| 3 | check-browser-safe |
| 4 | smoke:genui-composition-retrieval |
| 5 | check-form-bearing-dts |
| 6 | check-core-barrel-dts |
| 7 | check-yaml-events-vs-runtime |
| 8 | check-free-form-eval-regression |
| 9 | check-standalone-html-phosphor |
| 10 | check-substitutable-set-drift |
| **11** | **check-required-icons — NEW v0.5.3** |

Verification: 95 components scanned; 0 drift. The 12 auto-stamping primitives all match yaml ↔ class.js ↔ runtime.

Heuristic limitations (documented in script header): computed icon names (`setAttribute('name', this.icon)`), trait-emitted icons, and JSDoc/comment example references are filtered or reported as "unresolvable" — manual audit applies.

### Added — `responsive-shell-sidebar` pattern (§157, FEEDBACK-06 §3 P1, v0.5.3)

New pattern at `packages/web-components/patterns/responsive-shell-sidebar/` documents the dual-mount editor-shell sidebar recipe: inline panels at `>900px` viewports, drawer twins below. FEEDBACK-06 §3 reported color-app hand-rolling ~40 LOC of CSS + state plumbing for exactly this pattern in Tokens Studio.

**The pattern** ships:

- `.wide-only` / `.narrow-only` CSS convention (display: contents / display: none with breakpoint media query)
- Dual-mount inline sidebars (wrapped in `.wide-only`) + drawer twins outside shell slots
- Toolbar trigger buttons (inside `.narrow-only`) opening the drawer twins
- **MediaQueryList listener** that auto-closes drawers on viewport widening (the gap FEEDBACK-06 §3 flagged for the peer agent)
- Signal-based state sharing recipe (single signal source, parent reads `.value` once, passes to both mount points)
- Dual-mount caveat documented (signal-bound = safe; local-DOM-state = lost on resize)
- v0.6.0 graduation path noted: Option A `<editor-sidebar responsive-breakpoint="900">` built-in attribute deferred pending editor-shell + pane-ui semantics audit + paired `<show-when breakpoint>` trait

Per FEEDBACK-06 §3's "Option B is cheaper landing and matches the existing pattern catalog" framing. Option A graduation lives in v0.6.0 RFC.

Files added: `responsive-shell-sidebar.html` (shell + iframed examples), `responsive-shell-sidebar.examples.html` (working dual-mount + auto-close MQL + `data-chunk` annotations for corpus harvest), `responsive-shell-sidebar.css` (breakpoint grid + visibility rules), `README.md`.

### Fixed — yaml-events drift cleanup on 5 components + audit-script false-positive fix (§144 + §162, v0.5.3)

`npm run check:yaml-events` (audit slot 7, v0.5.1 §100a) reported 6 components with yaml/runtime drift after v0.5.2 §130's toast.close fix. §144 closed 4 of the 6 with hard-finding fixes; §162 (folded into the same v0.5.3 cycle) closes the remaining 2 via audit-script fixes that surfaced a real `<table-ui>` drift previously masked by false positives.

**§144 — Phantom events dropped** (yaml declared but runtime never dispatched):

- **`accordion`** — phantom `change` → dropped. Runtime dispatches `toggle { open: boolean }` (single-pane) which was undeclared in yaml. Replaced `change` block with proper `toggle` declaration + detail typing. Codegen regenerates `AccordionToggleEvent` with `{ open: boolean }` in `.d.ts`.
- **`canvas`** — 3 phantoms (`change`, `click`, `input`) → all dropped. Runtime dispatches only `canvas-interaction` (at canvas.js:50 + :167); the other three events were never bound.
- **`card`** — phantom `drag-end` → dropped. No runtime drag-end dispatch in card class. (`<row-ui>` had the same yaml drag-end but also no dispatch — fixed in same arc.)
- **`row`** — phantom `drag-end` → dropped. Same as card; trait-emitted events may dispatch from droppable trait but not card/row directly.

**§162 — Audit-script false-positive fix + table `row-collapse` declared**:

The audit's runtime-event extractor used a single regex with `[\s\S]*?detail\s*:\s*(\{[^{}]*\}|...)` between event name and detail. Two bugs: (a) `[^{}]*` couldn't parse `detail: { filters: {} }` (nested braces); the lazy `[\s\S]*?` then greedy-expanded across multiple `dispatchEvent` calls, falsely cross-attributing the NEXT sibling's `detail:` to the current event. (b) `detail: { ...conn }` was parsed as field-named `conn` rather than treating the spread as opaque. Rewritten as a brace-balanced walker with explicit spread detection.

- **`noodles`** — false positive cleared. `noodle-connect`/`noodle-disconnect` correctly emit `detail: { ...conn }` (spread); audit now treats spread as opaque and skips comparison (matches the script's "variable detail" handling).
- **`table`** — false positive cleared AND real drift surfaced + fixed. Pre-§162 the audit reported `filter-change → runtime emits {index, row}` (over-matched from row-expand). Post-§162 the audit honestly found a NEW real drift: `<table-ui>.dispatchEvent(new CustomEvent('row-collapse', { detail: { index, row } }))` at table/class.js:235 was undeclared in yaml (only `row-expand` was declared). Added `row-collapse` declaration mirroring `row-expand`'s shape. Codegen regenerates `TableRowCollapseEvent` + `{ index: number, row: object }` in `.d.ts`.

`check:yaml-events` now reports **0 findings of any kind** — first fully-clean run since the audit's v0.4.9 §99g + v0.5.0 §100a graduation. **All 6 components from the §130 drift report are now resolved**; v0.5.4 follow-up dropped (no remaining cleanup arc).

### Added — `<slider-ui>` function-typed `.value` dev-mode guard (§153, FEEDBACK-07 §3 P1, v0.5.3)

FEEDBACK-07 §3 reported that `slider-ui .value=${() => Math.round(gen.lightness * 100)}` silently stored the arrow function as the property value — thumb stayed at 0% with no error or console output. Root cause: `slider-ui` reads `this.value` synchronously in `render()`; the framework's `isFn` branch normally wraps functions in `effect()` and calls them, but the wrap is bypassed when consumers assign imperatively (`sliderEl.value = fn`) or when a non-AdiaUI binding system doesn't recognize the function-as-value convention.

**Fix**: `slider-ui` `render()` checks `typeof this.value === 'function'` and emits a descriptive `console.warn` naming the two correct patterns:

```
[slider-ui] .value received a function. Did you mean:
  .value=${fn()}            ← call the function to get the current value
  .value=${signal.value}     ← read the signal's current value
Functions are not auto-invoked at render time; the slider reads
.value synchronously. See USAGE.md "Reactive binding" for the
parent-template-re-read pattern that works across frameworks.
```

Plus: `return` early from render so the slider doesn't NaN-math the function ref into a 0% position. Subsequent property assignments (with a numeric value) trigger re-render normally.

**Docs**: USAGE.md "Derived reactive bindings" subsection updated with a note about the dev-mode guard + the two fix patterns. The pre-existing `() => signal.value * 100` reactive pattern docs are unchanged — they correctly described Pattern A; the guard catches Pattern B (imperative-set with function ref).

§153c (deferred to v0.5.4+): generic `UIFormElement`-level guard at the framework's `installProps()` site that warns for ANY Number/Boolean-typed property assigned a function. Larger change requires audit of edge cases (e.g. computed signals that look function-ish); deferred.

### Added — `<drawer-ui>` `close` event detail carries `reason` (§156, FEEDBACK-06 §2 P2, v0.5.3)

`<drawer-ui>` previously dispatched a plain `Event('close', { bubbles: true })` with no detail — and the `.d.ts` lied (declared `CustomEvent<unknown>` while the runtime was actually `Event`). FEEDBACK-06 §2 asked for typed `detail.reason` so consumers can distinguish dismiss-intent (escape / backdrop) from explicit (close-button) from programmatic.

**New type** in `drawer.d.ts`:

```ts
export type DrawerCloseReason = 'escape' | 'backdrop' | 'close-button' | 'programmatic';
export interface DrawerCloseEventDetail { reason: DrawerCloseReason; }
export type DrawerCloseEvent = CustomEvent<DrawerCloseEventDetail>;
```

Runtime captures the reason at each entry point (`#closeReason` private field set in `#onPress` / `#onDialogCancel` / `#onDialogClick`); `#onDialogClose` reads + resets the field, then dispatches `new CustomEvent('close', { detail: { reason }, bubbles: true })`. After dispatch the field resets to `'programmatic'` so a subsequent `.open = false` from consumer code doesn't inherit a stale event-driven reason.

`drawer.yaml` `events.close.detail.reason` declares the 4-value enum so codegen produces the typed literal union in `.d.ts`. `check:yaml-events` audit (slot 7) confirms yaml ↔ runtime alignment.

**Migration**: existing consumers reading `e.detail` from the `close` event got `undefined` (and TypeScript said `unknown`). Now they get `{ reason: DrawerCloseReason }` (and TS narrows to the literal union). Existing code that ignored `detail` continues to work unchanged. Same shape becomes a template for any future `modal-ui` / `popover-ui` close events.

### Fixed — Template `inTag()` HTML-comment handling (§155, FEEDBACK-06 §1 P0, v0.5.3)

`packages/web-components/core/template.js` `inTag()` previously tracked `<`, `>`, `'`, `"` to decide whether each `${…}` placeholder was in tag-position (substitute `{{p:N}}`) or content-position (substitute `<!--p:N-->`). The walker had no HTML-comment awareness — an apostrophe inside `<!-- foo's bar -->` put `q=1` (single-quoted-string state), masked the `-->` closer, and silently mis-classified every subsequent placeholder.

FEEDBACK-06 §1 reproduced the bug end-to-end: an explanatory comment containing `<editor-shell>'s named slots` produced post-classification garbage `<segmented-ui .value=<!--p:10--> …>`; the DOM parser ate the trailing `>` of the substituted comment; `scan()` found no part registered at index 10; `update()` fell through the `isFn` branch and invoked the consumer's arrow handler with no arguments → `Cannot read properties of undefined (reading 'detail')` crash at mount, with a stack trace pointing at user code (not framework code).

**Fix**: skip HTML comments wholesale before the quote/tag walker runs. Comments cannot open a tag and cannot contain real string delimiters as far as the live HTML parser is concerned. Unterminated comments fall through to `last = 0` (content-position; safe default). ~15 LOC inside `inTag()`. Adopted FEEDBACK-06 §1's proposed patch verbatim.

Symptom class closed: any template literal with an HTML comment containing an apostrophe (or any unmatched quote char) no longer mis-classifies subsequent placeholders. The "handler called with `e === undefined`" crash class is gone.

**Companion (deferred)**: the same heuristic also doesn't model `<![CDATA[ … ]]>` blocks or `<script>`/`<style>` raw-text content (FEEDBACK-06 §1 closing note). Deferred to v0.5.4 unless a confirmed bug class surfaces. Comment-skip handles the observed FEEDBACK-06 case.

### Added — Template partial-attribute interpolation runtime guard (§152, FEEDBACK-07 §2 P0, v0.5.3)

`packages/web-components/core/template.js` `scan()` previously skipped attributes whose value didn't match the full-attr regex `^\{\{p:(\d+)\}\}$`. Partial interpolation like `style="background:${a}; color:${b}"` (post-concat: `"background:{{p:0}}; color:{{p:1}}"`) silently failed — no part registered, the literal `{{p:N}}` text reached the DOM as the attribute's static value. FEEDBACK-07 §2 reported color swatches rendering as empty black squares from exactly this pattern.

**Fix**: scan() now detects `{{p:` substrings inside non-matching attribute values + emits a descriptive `console.warn` naming the element + attribute + first 80 chars of the value + the three supported binding modes. Warn (not throw) because hot template paths fire many times — a thrown error would crash render; the warn surfaces the bug class loudly without breaking the page.

**Docs**: USAGE.md "Property binding patterns" section grows a "Template attribute bindings" subsection documenting the three modes (`attr="${value}"` full-replacement, `.prop=${value}` property assignment, partial-interpolation not supported) with the `style="background:${a}; color:${b}"` example reproduced from FEEDBACK-07.

No behavior change for templates that were already correct (full-attr placeholders). Behavior change only for previously-silent-failing partial-attr templates: now they warn loudly + still render the garbage `{{p:N}}` text (so the symptom is recoverable). Consumers should fix their templates to use one of the three documented modes.

### Fixed — CodeMirror transitive dep declarations (§151, FEEDBACK-07 §1 P0, v0.5.3)

`packages/web-components/package.json` previously declared only `@adia-ai/a2ui-runtime` in `dependencies`. The `components/code/code-editor.js` source static-imports 6 packages (`@codemirror/state`, `@codemirror/view`, `@codemirror/commands`, `@codemirror/language`, `@codemirror/lint`, `@lezer/highlight`) and dynamic-imports 6 more (`@codemirror/lang-{javascript,json,css,html,markdown,yaml}`). None were declared. Consumer builds (Vite/Rolldown/esbuild) failed with cascading `Failed to resolve import` errors on `@adia-ai/web-components` install, requiring 12 manual `npm install` commands per FEEDBACK-07 §1.

**Fix**: hybrid declaration — 6 always-loaded packages as `dependencies`; 6 `@codemirror/lang-*` packages as `optionalDependencies` (lazy-imported only when the `language` prop is set). `optionalDependencies` is the right shape because `npm install` doesn't error on missing optionals, but the consumer who passes `language="json"` (etc.) gets a clear missing-optional-dep message at runtime instead of a silent dynamic-import failure.

Consumers affected: anyone importing `@adia-ai/web-components` whose bundler resolves the `code-editor.js` chunk (side-effect-imported via the barrel). Pre-fix workaround: 12 manual `npm install @codemirror/* @lezer/highlight` commands. Post-fix: `npm install @adia-ai/web-components` resolves all 6 core deps automatically + the lang-* on first use of the `language` prop.

No runtime behavior change.

### Fixed — `installIconLoaders` tolerates non-canonical loader-map key paths (§140, v0.5.3-staged)

FEEDBACK-06 from color-app reported `installIconLoaders` → `warnMissingIcon` firing for `caret-right` / `x` / `caret-up-down` even though the consumer had installed a loader map. Root cause: `resolveLoader()` only looked up loaders by the canonical `/node_modules/@phosphor-icons/core/assets/<weight>/<filename>` path. When the consumer's Vite root, package manager (pnpm hoist), or manually-built loader map produced keys with a different prefix — e.g. `/node_modules/.pnpm/@phosphor-icons+core@.../node_modules/.../assets/regular/caret-right.svg` — the lookup missed silently + the warn fired at install time.

**Fix**: `resolveLoader()` adds a filename-suffix fallback. The fast path still tries the canonical phosphor path first (O(1), preserves chat-ui dev convention); on miss, scans the weight's module map for any key ending with `/<filename>` (O(N), but only on miss). Same two-step shape for the `-fill` back-compat name suffix. Public API unchanged; only internal lookup logic.

Consumers affected: any project that calls `installIconLoaders()` with a loader map whose keys aren't prefixed with `/node_modules/@phosphor-icons/core/assets/`. Examples: pnpm projects, projects with custom Vite roots, projects with manually-built loader maps using brace-list globs over absolute paths.

New tests: `packages/web-components/core/icons.test.js` (12 cases) — covers canonical fast path, pnpm-style prefix, custom Vite root, manually-built map, `-fill` back-compat (both paths), non-regular weights, alias resolution via suffix, DOM re-stamp end-to-end, and miss-on-both-paths. Full vitest suite 787/787 (+12 new). No public-API change; soft-internal change only.
## [0.5.2] - 2026-05-13

### Changed — `toast.yaml` drops phantom `events.close` declaration (§130, v0.5.2)


`packages/web-components/components/toast/toast.yaml` previously declared `events: { close: { description: "Fired when the toast is dismissed via click or auto-timeout", ... } }` — but the 92-line runtime is a thin facade over `<feed-ui>` (since v0.4.7 §82) and never dispatched the `close` event. Codegen pipeline (yaml → `.a2ui.json` x-adiaui.events → `.d.ts` `ToastCloseEvent` + `ToastCloseEventDetail` types) was emitting type surfaces for an event that never fired. Same drift class as v0.4.8 §90 (color-picker phantom `format-change`).

**Migration**: zero consumer impact — `<toast-ui>.addEventListener('close', handler)` already silently never fired; consumers wanting close-event semantics wire it on the host `<feed-ui>` where the lifecycle actually lives.

Companion regen via `npm run components --force`:
- `toast.a2ui.json` — `x-adiaui.events` block scrubbed
- `toast.d.ts` — `ToastCloseEvent` + `ToastCloseEventDetail` types stripped
- Catalog `catalog-a2ui_0_9.json` — Toast component events block synced (regen ride-along)

Type API change technically MAJOR-shaped under strict SemVer (consumer types narrow), but runtime behavior is identical → PATCH appropriate.
## [0.5.1] - 2026-05-13

_Lockstep ride-along (no source change)._
## [0.5.0] - 2026-05-13

**v0.5.0-staged** — staged for the next minor cut.

### Fixed — `core/element.d.ts` missing runtime re-exports (§102, FEEDBACK-05 P1)

`core/element.js` re-exports the reactive + template helpers from sibling modules:

```js
export { signal, computed, effect, batch, untracked, isSignal } from './signals.js';
export { html, css, repeat } from './template.js';
```

`core/element.d.ts` only declared the `UIElement` class + a few interface types. The canonical USAGE.md import pattern:

```ts
import { UIElement, html, signal, computed, effect } from '@adia-ai/web-components/core/element';
```

works at runtime but fails `tsc` with `TS2305: Module '"@adia-ai/web-components/core/element"' has no exported member 'html'` (etc.). Tokens Studio surfaced this in FEEDBACK-05 post-v0.4.9 upgrade — the only shim they still carried.

**Fix:** `core/element.d.ts` now mirrors the runtime re-exports:

```ts
export { signal, computed, effect, batch, untracked, isSignal } from './signals.js';
export type { ReadonlySignal, Signal, ComputedSignal, EffectOptions, EffectDisposer } from './signals.js';
export { html, css, repeat } from './template.js';
export type { TemplateResult } from './template.js';
```

Consumers can delete their `declare module` shim after upgrading.

Same drift class as v0.4.8 §90 (color-picker.d.ts) and v0.4.9 §91b / §98 (form-bearing `.d.ts` audit) — but in a `core/*` barrel, not a `components/*/<name>.d.ts`. The §98 audit covered the 17 form-bearing primitives' types; this surface was orthogonal and slipped past both sweeps.

### Added — `core/data-stream.d.ts` (NEW, §102 sibling-fix)

`core/index.js` explicitly re-exports `streams` + `whenStream` from `./data-stream.js` (the public surface of the attribute-driven data ingestion module). `core/data-stream.d.ts` didn't exist, so neither symbol was reachable via TypeScript through the core barrel. Authored `data-stream.d.ts` declaring `StreamEntry`, `streams` (`{ get, has, keys, size }`), and `whenStream(id): Promise<StreamEntry>`.

`core/index.d.ts` adds the matching `export { streams, whenStream } from './data-stream.js'` + `export type { StreamEntry }` so the core barrel's TypeScript surface matches the runtime exactly.

### Added — `scripts/release/check-core-barrel-dts.mjs` audit (§102)

New trip-wire to catch the FEEDBACK-05 drift class going forward. Walks every `packages/web-components/core/*.js` barrel, extracts explicit `export { … } from '…'` re-exports, compares against the sibling `.d.ts`. Flags runtime re-exports missing from the type barrel.

Heuristic limits documented in the script header:
- Only matches `export { X, Y } from '…'` form. Misses `export * from '…'` re-bundles (covered by `tsc --strict` round-trips).
- Type-only re-exports (`export type { X } from '…'`) match either form in the `.d.ts` — the audit only cares that the name is reachable.
- Files without a sibling `.d.ts` are skipped (codegen'd elsewhere or genuinely typeless).

Wired as `npm run check:core-barrel-dts`. The audit-script family now spans **7 trip-wires** (§72 check-no-live-readers + §81 check-changelog-coverage + §83 check-browser-safe + §84 behavioral composition-retrieval + §98 check-form-bearing-dts + §100a check-yaml-events-vs-runtime + §102 check-core-barrel-dts).
## [0.4.9] - 2026-05-13

### Added — `<swatch-ui>` multi-badge + auto-contrast label (§99a + §99b)

Two follow-on enhancements to the v0.4.9 §91a swatch extension. **§99a multi-badge** — `[badge]` now parses comma- or space-separated variant lists; multiple pips render side-by-side in the upper-right (each with its own semantic color + `aria-label`). Each pip stamps as `<span data-badge-variant="X">` so per-variant CSS targets a stable selector. Diff-on-change re-render — no tear-down on no-op renders. **§99b auto-contrast** — new `[auto-contrast]` boolean reflected prop. When set on `shape="block"`, JS computes the swatch color's OKLab lightness via a 1px canvas probe + stamps `[data-on-light]` or `[data-on-dark]` on the label element. CSS swaps `--swatch-label-fg` accordingly (threshold 0.62, WCAG-aligned for sRGB). Detail line follows the label's choice via sibling combinator. Only applies to `shape="block"` where the label sits ON the tile. Handles every CSS color form the browser parses (oklch / hex / hsl / named / var() refs). Backwards compat preserved — both default off.

### Added — `<color-picker-ui>` generation constraints (§100b, FEEDBACK-02 #7)

Closes consumer-friction item #7 from `FEEDBACK-02--adia-packages--2026-05-12.md`. Six new props let consumers constrain the picker to a generation context:

- `[max-chroma]` (number, default `Infinity`) — clamp OKLCH chroma channel to at most this value.
- `[max-l]` (number, default `1`) — clamp OKLCH lightness to at most this value.
- `[min-l]` (number, default `0`) — clamp OKLCH lightness to at least this value (symmetric companion to `[max-l]`).
- `[hue-drift-max]` (number, default `NaN`) — max signed-shortest-path hue deviation in degrees from `[base-hue]`. Wrap-aware — a drift of 350° resolves as -10°.
- `[base-hue]` (number, default `NaN`) — reference hue for `[hue-drift-max]`; falls back to the picker's hue at first commit so consumers can pre-seed `[value]` and constrain drift from that initial value.

Clamps run BEFORE the gamut-mapping pass in `#commit()` so consumer-declared bounds win even when the user's drag would otherwise be in-gamut.

**One new event** — `constraint-clamp` — fires immediately before `change` / `input` when one or more axes were clamped. `detail.clamps` is a non-empty array of `{ axis, requested, clamped, reason }` records. `axis` is `"l" | "c" | "h"`; `reason` names the triggering prop (`max-l` / `min-l` / `max-chroma` / `hue-drift-max`). Consumers can surface UX feedback (toast, validity warning, scope-drift indicator) without polling.

Types: `ColorPickerClampRecord`, `ColorPickerConstraintClampEventDetail`, `ColorPickerConstraintClampEvent` added to `color-picker.d.ts` + re-exported from the package barrel.

### Added — yaml event-detail blocks across 26 non-form-bearing primitives (§100c)

Closes the v0.5.x captured item "x-adiaui.events detail-type sidecar → typed `CustomEvent<DetailShape>`". The §80 codegen reads `events.<name>.detail` from each yaml; pre-§100c, ~77% of non-form events had only a `description:` and codegen emitted `CustomEvent<unknown>`. The sweep authored structured `detail:` blocks for 30 events across 26 yamls:

action-list, agent-artifact, agent-feedback-bar, agent-questions, agent-reasoning (×3), agent-suggestions, agent-trace, chart-legend, command, demo-toggle, list, menu, nav, nav-group, nav-item, pagination, swiper (×2), tabs, tag, toggle-group, toggle-scheme, tree, chat-thread, link, stream, noodles (×2). Plus manual fixes for: timeline (yaml said `{ id, open }` but runtime emits `{ expanded }` — drift fix); table (added `filter-change` + `row-expand`); chart (added `legend-update`); heatmap (added chart-hover/leave/select signal-only re-emissions).

After regen, codegen now produces `CustomEvent<<NamedDetail>>` types for 30+ additional events. TypeScript consumers reading event details get typed access instead of `unknown`.

### Added — `cross_package_composes:` yaml block + check-composes-coverage extension (§99d)

Graduates the hardcoded `CROSS_PACKAGE_ALLOWLIST` from `check-composes-coverage.mjs` to a yaml-driven block. Each component yaml gains an optional `cross_package_composes:` block naming primitives in other `@adia-ai/*` packages it composes:

```yaml
# canvas.yaml
composes: []  # same-package siblings (none)
cross_package_composes:
  - tag: a2ui-root
    from: '@adia-ai/web-modules/runtime'
    note: Hosts A2UI rendering. Required when canvas mounts content.
  - tag: theme-ui
    from: '@adia-ai/web-modules/theme'
    note: Optional — only read when [theme-wrapper] is set.
```

The audit script reads the block at scan time + drops matching tags from the "undeclared composition" finding set. Future cross-package compositions need only their tag declared in yaml — no script change required.

### Changed — `<editor-sidebar>` `[name]` → `[persist]` template sweep (§99c)

Two occurrences in `apps/genui/app/a2ui-editor/a2ui-editor.contents.html` migrated to model the preferred shape. `[name]` continues to work via the v0.4.9 §91c back-compat alias path. Surveyed via background Explore agent; no other `[name]` occurrences across `apps/`, `playgrounds/`, `catalog/`, or `site/`.

### Audit summary — non-form primitives' yaml events vs runtime (§100a)

New `scripts/release/check-yaml-events-vs-runtime.mjs` companion to peer's §98 `check-form-bearing-dts.mjs`. Walks every non-form-bearing component, greps `class.js` for dispatch literals, parses yaml `events:` blocks + detail sub-blocks, reports phantom yaml events / undeclared runtime events / detail drift. Wired as `npm run check:yaml-events`. Heuristic limits accepted + documented in the script header (trait-emitted events, computed event names, variable-detail helpers).

### Fixed — `color-picker.d.ts` event detail shape mismatch (§90, FEEDBACK-04 P0)

Closes FEEDBACK-04 claim #2 — a consumer's 70-line `renderSwatch()` helper composing background color, key label, gamut/contrast badge, copy-to-clipboard, and keyboard-select collapses to a single declarative tag:

```html
<swatch-ui
  shape="block" size="lg"
  color="oklch(0.53 0.18 240)"
  label="500"
  detail="oklch(0.53 0.18 240)"
  badge="apca-pass"
  copyable
  selectable
></swatch-ui>
```

**Six new props** (all additive — existing consumers unchanged):

- `badge` — enum `"" | "out-of-gamut" | "p3-only" | "apca-pass" | "apca-fail"`. Renders △ / ✦ / ✓ / ✗ in the upper-right of the tile with semantic color + `aria-label` for screen readers (e.g. "Outside sRGB gamut", "Contrast passes APCA").
- `detail` — secondary line of text below the label. Typically the raw token value (e.g. `oklch(0.53 0.18 240)`) when the swatch represents a design-token step.
- `copyable` — boolean. Renders an inline copy-to-clipboard button (⧉ → ✓ on success, ⚠ on failure; auto-resets after 1.2s). Uses `navigator.clipboard.writeText`.
- `copy-value` — string. Override what gets copied; defaults to `[color]`.
- `selectable` — boolean. Makes the host keyboard-focusable + clickable. Sets `role="button"` + `tabindex="0"`; wires Enter / Space activation.
- `selected` — boolean reflected. Visual selected state with focus ring; sets `aria-pressed`.

**One new event:**

- `select` — fires when a selectable swatch is activated (click / Enter / Space). `detail: { value, color, label }` where `value` is `[color]` (or `[label]` as fallback).

**CSS variants added** under the existing `@scope (swatch-ui)` block: badge tokens (`--swatch-badge-*`), copy-button tokens (`--swatch-copy-*`), select-ring tokens (`--swatch-select-ring*`). Per-badge color uses semantic tones — out-of-gamut maps to `--a-warning`, apca-pass to `--a-success`, etc.

**Backwards compat:** every new prop defaults off / empty. Existing usage (`<swatch-ui shape="dot" color="..." label="...">`) is byte-equivalent in stamping and behavior.

**Migration for the Tokens Studio FEEDBACK-04 #2 helper:**

```diff
- const swatch = renderSwatch({ color, label, badge, copyValue, selected })
- // ...70 LOC of manual stamping + badge SVG + copy state + a11y
+ html`<swatch-ui
+   shape="block" size="lg"
+   color=${color} label=${label} detail=${detail}
+   badge=${gamutBadge} copyable selectable
+   ?selected=${selected.value}
+   @select=${(e) => onSelect(e.detail.color)}
+ ></swatch-ui>`
```

### Added — `complete` event declaration on `<otp-input-ui>` (§91b, audit sweep)

§90 audit recipe applied to all 17 form-bearing primitives. `otp-input.d.ts` was missing the `complete` event declaration even though the runtime has emitted it at `class.js:156` since the primitive shipped — fires once when the user fills the last digit slot with `detail: { value: combined }`. Added `OtpInputCompleteEvent` + `OtpInputCompleteEventDetail` types + the `addEventListener('complete', ...)` overload. Also fixed `otp-input.yaml` `events.complete.description` — said "Detail contains { code }" but actual emission is `{ value }`; updated description + added `detail:` block matching the runtime.

### Added — `save` + `language-load-error` event declarations on `<code-ui>` (§91b, audit sweep)

Same audit. `code.d.ts` was missing two events the runtime has emitted since the primitive shipped:

- `save` (at `class.js:435`) — fires on Mod+S keybind; `cancelable: true`; `detail: { value }`.
- `language-load-error` (at `class.js:226` and `:237`) — fires when CodeMirror's dynamic language-mode or core bundle fails to load; `detail: { phase: 'core' | 'language', language?: string, error: unknown }`.

Added `CodeSaveEvent` + `CodeSaveEventDetail` + `CodeLanguageLoadErrorEvent` + `CodeLanguageLoadErrorEventDetail` types + both `addEventListener` overloads. Re-exported from `packages/web-components/index.d.ts` barrel.

Also fixed `code.yaml`: removed a phantom `copy` event declaration (no runtime emission); added `detail:` blocks for the four real events (`input`, `change`, `save`, `language-load-error`).

### Audit summary — hand-authored `.d.ts` cleanliness as of v0.4.9

After §90 + §91b sweeps, **all 17 form-bearing primitives' hand-authored `.d.ts` files are runtime-matched.** Audit recipe captured in `docs/plans/0.4.9-release-plan.md` §91b for future cycles — automatable as `scripts/release/check-form-bearing-dts.mjs` in a later cycle if drift returns.

### Documentation — USAGE.md "Derived reactive bindings" subsection (§91d, FEEDBACK-04 #3)

Closes the last open FEEDBACK-04 item (claim #3, P2 — derived-reactive binding discoverability). The consumer narrative was explicit: *"This pattern is correct but discoverable only by reading the template engine source."* Pre-§91d, USAGE.md mentioned the eager-evaluation trap briefly but didn't ship the worked example with ✅/❌ markers that the consumer asked for.

New subsection at `USAGE.md § Property binding patterns › Derived reactive bindings` shows four patterns (two correct, two broken) with explicit annotations:

```js
// ✅ Derived reactive — re-evaluates whenever minL changes
html`<slider-ui .value=${() => minL.value * 100} min="0" max="100" />`
// ✅ Direct signal — re-evaluates whenever minL changes
html`<slider-ui .value=${minL} min="0" max="1" step="0.01" />`
// ❌ Static snapshot — reads minL.value ONCE at render time; thumb never moves
html`<slider-ui .value=${minL.value * 100} />`
// ❌ Function as the value — slider receives the function object, not its result
html`<slider-ui .value=${(v) => v * 100} />`
```

Plus a follow-on showing `computed()` as the documented alternative for complex derived values (memoized, stable handle, works in both `.prop=` bindings and `${…}` text bindings). Cross-links back to the existing "eager-evaluation trap" subsection.

**FEEDBACK-04 closure status:** 7/7 items addressed (P0 #1 §90 + P1 #2 §91a + P2 #3 §91d + P2 #4 §91b + P2 #5 §90 + P2 #6 §91c + P3 #7 deferred to v0.5.x as wishlist).

### Fixed — `color-picker.d.ts` event detail shape mismatch (§90, FEEDBACK-04 P0)

`FEEDBACK-04--adia-ui--2026-05-13.md` reported the consumer's listener typed as `{ hex, l, c, h }` but mistakenly believed the actual detail was `{ value, format }`. **Both were wrong** — the runtime (`class.js:414`) has always emitted `{ value, l, c, h, hex, oklch }`. The hand-authored `color-picker.d.ts` was the source of the consumer's `{ value, format }` belief — it had been stale since the runtime detail shape was extended.

**Fix:** updated `ColorPickerChangeEventDetail` to match the runtime:

```ts
export interface ColorPickerChangeEventDetail {
  value: string;   // format-respecting (hex when [format="hex"], oklch when [format="oklch"])
  l: number;       // OKLCH lightness (0–1)
  c: number;       // OKLCH chroma (0–~0.4)
  h: number;       // OKLCH hue (0–360°)
  hex: string;     // hex string (always present)
  oklch: string;   // oklch string (always present)
}
```

Also removed `ColorPickerFormatChangeEvent` + `ColorPickerFormatChangeEventDetail` from the exports — there is no `format-change` event in the runtime (`#commit()` only dispatches `'input'` and `'change'`). The dead type names are dropped from `index.d.ts` barrel; soft-breaking only for consumers that imported those type names (no runtime listener was firing anyway).

`ColorFormat` simplified from `'hex' | 'rgb' | 'hsl' | 'oklch'` to `'hex' | 'oklch'` to match the yaml + runtime (the two formats actually supported).

### Changed — color-picker.d.ts JSDoc surfaces the dual-form invariant

JSDoc on `ColorPickerChangeEventDetail` now explicitly states that `hex` and `oklch` are always populated regardless of the `[format]` setting — consumers reading the `.d.ts` see the invariant without needing to consult RESPONSE-02.
## [0.4.8] - 2026-05-12

### Changed — `core/icons.js` split into pluggable registry + opt-in Phosphor side-effect (§89, v0.4.8)

Closes the `FEEDBACK-adia-packages-03.md` ask: every `<icon-ui name="X">` in a consumer app was pulling the entire Phosphor set (~1,500 icons × 6 weights = ~9,000 SVGs) through Vite's build pipeline, emitting **9,074 chunk files** and a **38 MB `dist/assets/`** even for apps rendering 2 icons. Root cause: `core/icons.js` ran six lazy `import.meta.glob('.../<weight>/*.svg')` calls at module load — Vite emits one code-split chunk per matched file regardless of usage.

**Split shape:**

- `core/icons.js` — registry surface only. Exports `registerIcon`, `registerIcons`, `getIcon`, `hasIcon`, `isIconName`, `listIcons`, `whenIconRegistryReady`, `installIconLoaders` (NEW), `ICON_WEIGHTS`. No top-level globs, no top-level side effects. Cheap to import.
- `core/icons-phosphor.js` (NEW) — side-effect module that runs the six globs (Vite path) or the `icons-manifest.js` fetch (static-serving path) and installs the resulting loader map via `installIconLoaders()`.

**Three barrels side-effect-import `icons-phosphor.js` to preserve zero-config:**

- `index.js` — main barrel
- `core/index.js` — core barrel
- `components/index.js` — components barrel (used by site/docs)

**Consumer-facing impact:**

| Import path | Phosphor auto-loaded? | Bundle |
|---|---|---|
| `import '@adia-ai/web-components'` | ✅ Yes (unchanged) | full set |
| `import '@adia-ai/web-components/core'` | ✅ Yes (unchanged) | full set |
| `import '@adia-ai/web-components/components'` | ✅ Yes (unchanged) | full set |
| `import '@adia-ai/web-components/components/button'` | ❌ **CHANGED** — was full set, now registry-only | scoped |

**Migration for piecemeal consumers** with bundle-size SLOs:

```js
// Either: side-effect-import the full set explicitly
import '@adia-ai/web-components/components/button';
import '@adia-ai/web-components/core/icons-phosphor';

// Or: register only the icons you use (brace-list glob = static-analyzed → N chunks)
import '@adia-ai/web-components/components/button';
import { installIconLoaders } from '@adia-ai/web-components/core/icons';
installIconLoaders({
  regular: import.meta.glob(
    '/node_modules/@phosphor-icons/core/assets/regular/{caret-right,caret-up-down,moon,sun}.svg',
    { query: '?raw', import: 'default', eager: true }
  ),
});
```

`installIconLoaders` accepts the `import.meta.glob` output shape (`{ '/path/to/star.svg': loader }`) and supports both eager (loader = SVG string) and lazy (loader = `() => Promise<string>`) globs. Calling it re-stamps any `<icon-ui>` elements that asked for an icon before loaders arrived.

**Measured impact** (Tokens Studio consumer, 4-icon app, post-fix):

| Metric | Pre-fix | Post-fix (scoped) |
|---|---|---|
| Files emitted under `dist/assets/` | ~9,074 | ~10 (target — verified consumer-side) |
| Total `dist/assets/` on disk | ~38 MB | ~30 KB (target) |
| Main `index-*.js` post-tree-shake | 1.48 MB | ~50-100 KB (target) |

`sideEffects` array in `package.json` extends to include `./core/icons-phosphor.js` so the registration side effect survives tree-shaking under the documented zero-config paths.

### Added — `test/refactor-class-subpath.test.js` (§85b, v0.4.8)

Unit test pinning the v0.4.7 §77 class-subpath refactor's parser invariants. 158 LOC across 6 cases; synthetic feed-shaped input with one `extends` class + one no-`extends` utility class + module-level listener trailer; asserts the shim re-exports the listener trailer (post-§85a fix) AND that `class.js` does NOT (side-effect-free invariant). The bug class this pins: the §85a fix loosened `classRe` regex + made `generateShimFile` emit `parsed.trailer`; this test prevents future regressions on either path. Refactor script lives at `scripts/refactor/class-subpath.mjs`; the test lives here under `packages/web-components/test/` per project convention (the script's inputs are web-components-shaped).

### Added — event-types codegen for 38 non-form primitives (§80, v0.4.8)

Closes the v0.4.7 deferral from root CHANGELOG "Out of scope": typed event surfaces for non-form primitives. v0.4.6 §68 hand-authored event types for the 17 form-bearing primitives. v0.4.7 §74 added codegen'd property types for 73 non-form primitives but left events as base `CustomEvent` (untyped detail). v0.4.8 §80 fills the gap.

**What consumers see after upgrading:**

```ts
import type { UIModal, UITimeline, TimelineToggleEvent } from '@adia-ai/web-components';

const modal = document.querySelector('modal-ui')!;
modal.addEventListener('close', (e) => { /* e: CustomEvent<unknown> — name typed */ });

const timeline = document.querySelector('timeline-ui')!;
timeline.addEventListener('timeline-toggle', (e: TimelineToggleEvent) => {
  const id: string = e.detail.id;       // typed
  const open: boolean = e.detail.open;   // typed
});
```

**Codegen extension:** `scripts/build/dts-codegen.mjs` (+89 LOC) reads `x-adiaui.events` from each component's `.a2ui.json` sidecar and emits:
- `<Title><EventPascal>EventDetail` interface (when yaml `detail:` block is populated)
- `<Title><EventPascal>Event = CustomEvent<<Detail>>` type alias (`unknown` detail if no `detail:` block)
- Per-class `addEventListener` overloads (one generic `HTMLElementEventMap` overload + one specific per typed event)

Naming convention dedups doubled prefixes — `timeline-toggle` on `UITimeline` → `TimelineToggleEvent`, not `TimelineTimelineToggleEvent`.

**Yaml authoring scope was small.** 35 of the 38 non-form event-emitters already had yaml `events:` blocks. Only `feed`, `pane`, `timeline` had empty `events: {}` blocks needing population. Sidecar regen via `npm run build:components`.

**Tally:** 91 typed event aliases + 21 detail interfaces emitted across the 73 codegen'd `.d.ts` files. ~23% of events carry detail typing.

**Barrel surface — `export type {}` → `export *` per component.** `packages/web-components/index.d.ts` non-form section now re-exports via `export *`, so event type aliases + detail interfaces (e.g. `ModalCloseEvent`, `TimelineToggleEventDetail`) are reachable from the package root.

### Coverage closure — 90 of 95 primitives typed

| Primitive class | Property types | Event types |
|---|---|---|
| 17 form-bearing | ✅ v0.4.6 §68 (hand-authored) | ✅ v0.4.6 §68 (rich event detail types) |
| 38 non-form event-emitters | ✅ v0.4.7 §74 (codegen) | ✅ **v0.4.8 §80 (this release — codegen)** |
| 35 non-form non-emitters | ✅ v0.4.7 §74 (codegen) | n/a (no events to type) |
| 5 CSS-only elements | n/a (no class) | n/a (no class) |

The codegen pipeline is now feature-complete for the typed-API surface. Future event additions: author yaml `events:` block → run `npm run build:components` + `npm run codegen:dts` → ship.
## [0.4.7] - 2026-05-12

### Refactored — class.js helper extractions in agent + toolbar primitives (post-§77 follow-up)

Riding along on the §77 `<name>.js` → `class.js` split. Each affected primitive's class.js now hoists previously-inlined imperative DOM helpers to module scope for readability + reuse:

- **`agent-feedback-bar/class.js`** + **`agent-questions/class.js`** + **`agent-reasoning/class.js`** — `makeButton({ icon, text })` helper (small `<button-ui>` stamper with `variant="ghost" size="sm"` defaults). Replaces ~6 lines of inline `document.createElement` + `setAttribute` per stamp site.
- **`toolbar/class.js`** — six new module-scope helpers + one constant: `SPILLOVER_LABEL_FLAG` attr, `isDivider(el)`, `humanizeIcon(name)` (kebab→Title Case fallback), `applySpilloverLabels(root)` + `stripSpilloverLabels(root)` (auto-label buttons when they spill into the overflow menu so menu items aren't icon-only), `outerWidth(el)` (rect + margins). Spillover now stamps `text` from `aria-label` || humanized `icon` so menu items carry readable labels.
- **`table-toolbar/class.js`** — four new helpers + two thresholds: `emptyHint(text)` + `popoverHead(text)` (popover header/empty-state stampers), `getCellValue(row, col)` (accessor-or-key path resolver), `looksLikeIdKey(key)` (heuristic skip for `id`/`uuid`/`*Id` columns in filter generators), `SELECT_THRESHOLD = 50`, `SAMPLE_LIMIT = 500`.

No behavior change for default consumers (the helpers are pure factorings of existing inline logic). Pairs cleanly with §77 because the new class.js files were the natural home for these extractions.

### Added — `.d.ts` codegen for 73 non-form primitives (v0.4.7, §74)

Closes the v0.4.7 deferral from v0.4.6 §68 (`.d.ts` shipment was 17 of 95 primitives). After this release, **90 of 95** primitives that ship a JS class are now typed (the remaining 5 are CSS-only "elements" — `header`, `footer`, `section`, etc. — that have no class). TypeScript consumers now get:

- `document.querySelector('button-ui')` returns `UIButton` (not `Element`)
- `createElement('card-ui')` returns `UICard`
- Typed properties on every non-form primitive: `<button-ui>.size` is `'xs' | 'sm' | 'md' | 'lg' | 'xl'`, not `string`; `<card-ui>.variant` is the actual union; etc.
- JSDoc descriptions surface in IDE tooltips

**Codegen script:** `scripts/build/dts-codegen.mjs` (~225 LOC; npm `codegen:dts` / `codegen:dts:dry`) reads each component's `.a2ui.json` sidecar (the v0.9 JSON Schema regenerated from `<name>.yaml` by `npm run components`) and emits a typed `<name>.d.ts` alongside `<name>.js` / `class.js`. Property types derive from JSON Schema: `enum` → string-literal union, `type: "string|number|boolean|object|array"` → primitive / `Record<string, unknown>` / array. The 17 form-bearing primitives (input, select, slider, range, textarea, switch, check, radio, segmented, search, otp-input, option-card, color-picker, rating, code, calendar-picker, upload) stay **hand-authored** because they carry rich event types (`CustomEvent<InputChangeEventDetail>` etc.) not representable in the sidecar; codegen explicitly skips them so re-runs don't overwrite the rich types.

**Barrel + tag-map:** `packages/web-components/index.d.ts` gets 73 new `export type { UI<X> }` re-exports + 73 new `HTMLElementTagNameMap` entries. The hand-authored "Note: only the typed primitives are listed here…" caveat is removed — every primitive that ships a class is now typed.

**Coordination with class subpath (§77):** `package.json`'s `./components/*/class` types entry maps to `./components/*/*.d.ts` (the per-component `.d.ts`) — so the class subpath gets typed automatically; no separate `class.d.ts` files needed.

### Added — non-side-effect `class` subpath per component (v0.4.7)

Closes the last roadmap item from the §61/§68 consumer-feedback arc (FEEDBACK-adia-packages.md, claim #7). Every component with an `<x-ui>` tag now ships a sibling `class.js` exporting the class **without** calling `customElements.define`. New subpath:

```js
// Auto-register (default; tag registered immediately):
import '@adia-ai/web-components/components/slider';

// Class import (no auto-register; consumer decides when to define):
import { UISlider } from '@adia-ai/web-components/components/slider/class';
```

**Use cases enabled:**
- **Test isolation** — subclass + register under a unique per-test tag without clobbering the global `slider-ui` registration
- **Tag-name override** — register `<my-slider>` for a subclass while `<slider-ui>` remains the canonical
- **Selective composition** — import the class for inheritance without forcing tag registration on the host page

### Refactored — `packages/web-components/components/` (87 single-class + multi-class)

Mechanical refactor across all components with `customElements.define`:

**Before** — `<name>.js` had imports + class definition(s) + `customElements.define` calls + export.

**After** — split into two files:
- **`class.js`** (NEW) — imports + class definition(s) with `export` keyword. No `customElements.define` call. Importing this file gives you the class without auto-registering. ~95% of the original LOC moves here verbatim.
- **`<name>.js`** (REWRITTEN, ~17 LOC) — imports the class(es) from `./class.js` + imports `defineIfFree` from `../../core/register.js` + calls `defineIfFree('<tag>', UI<Name>)` per registered tag + re-exports the class(es). Preserves the auto-register subpath unchanged for existing consumers.

**Why `defineIfFree` not `customElements.define`?** Two reasons: (1) the file may be imported transitively from multiple paths after the refactor — `defineIfFree` (shipped in v0.4.6) is the conflict-safe variant that no-ops on duplicate registration. (2) Documented consumer-side pattern: when two packages register the same tag, `customElements.define` throws `NotSupportedError`; `defineIfFree` returns `false` gracefully.

**Skipped (5 files)** — `button.js` (refactored manually as the proof), `index.js` (barrel re-export), `code/code-editor.js` + `table/cell-types.js` (helpers, not components), plus 6 CSS-only directories (`section`, `stat`, etc.) that have no `.js` file. Components with multiple co-located `customElements.define` calls (menu/tree/timeline/avatar/etc., 11 total) have their multiple classes all moved to a single `class.js` with multiple `defineIfFree` calls in the shim.

**Script:** `scripts/refactor/class-subpath.mjs` — re-runnable parser that finds class boundaries via brace-counting, identifies `customElements.define` calls, and rewrites both files. Encoded for future similar refactors (per the §69 multi-`*` substitution memory).

### Changed — `package.json` exports

Added `./components/*/class` subpath with TypeScript types mapped to the existing per-primitive `.d.ts` files (no separate `.d.ts` per subpath needed — same class, same types):

```json
"./components/*/class": {
  "types": "./components/*/*.d.ts",
  "default": "./components/*/class.js"
}
```

The 17 form-bearing primitives that received `.d.ts` files in v0.4.6 automatically get typed class imports under the new subpath. The ~80 non-form primitives (button, card, table, chart, popover, etc.) get codegen'd `.d.ts` via §74 in this same v0.4.7 release — typed class imports work for both subpaths from day one.

### Changed — `USAGE.md § Registration` updated

Removed the "FUTURE — not available yet (v0.4.7+)" caveat; documented the class subpath as shipped + added a test-isolation example showing how to register under a unique per-test tag via `defineIfFree`.

### Verification

| Gate | Result |
|---|---|
| `vitest packages/web-components` | 483 passed / 0 failed (byte-equivalent to pre-refactor — every existing test consumes the auto-register path, which still works) |
| `node -e \"require.resolve(...)\"` for new subpaths | 4/4 representative paths resolve correctly (button/class, slider/class, menu/class multi-class, input/class) |
| `customElements.define('tag', class)` not called when `./class` subpath imported alone | Verified — `class.js` files have zero side effects |
| Component count post-refactor | 87 processed + 11 skipped (CSS-only / helpers / button-as-proof) = 98 component dirs; 87 paired with `class.js` |

### Changed — 17 component yamls populate `synonyms.tags` (§72)

The alias data that previously lived in `@adia-ai/a2ui-corpus/patterns/_components.json` (retired in §72 as the §65 carry-over from v0.4.6) has been folded into per-component yamls. 17 components touched: `calendar-picker`, `canvas`, `card`, `command`, `input`, `inspector`, `kbd`, `modal`, `otp-input`, `pane`, `pipeline-status`, `progress`, `richtext`, `segmented`, `swiper`, `switch` (plus `chat-thread` in `@adia-ai/web-modules`). Sidecars regenerated by `npm run components` carry the aliases under `x-adiaui.synonyms.tags`.

Four legacy-only alias entries from the retired `_components.json` were merged into their modern canonicals: `Chat` → `ChatThread` (alongside `chat-log` / `conversation` / `message-thread`), `Panel` → `Pane`, `SegmentedControl` → `Segmented`, `Toggle` → `Switch`.

### Coverage closure on FEEDBACK-adia-packages.md (final)

| # | Friction | Status |
|---|---|---|
| 1 | Typed events | ✅ v0.4.5 (CustomEvent.detail) + v0.4.6 (per-primitive `.d.ts`) |
| 2 | `disconnected()` discoverability | ✅ v0.4.6 (USAGE.md § Lifecycle) |
| 3 | Property reactivity discoverability | ✅ v0.4.6 (USAGE.md § Property reactivity) |
| 4 | `.d.ts` shipment | ✅ v0.4.6 (19 files for 17 form-bearing primitives) + **v0.4.7 (73 codegen'd for non-form primitives — 90/95 total)** |
| 5 | USAGE.md reference | ✅ v0.4.6 (430-line guide) |
| 6 | CSS via package specifier | ✅ v0.4.5 |
| 7 | Registration helpers + non-side-effect class import | ✅ **v0.4.7 (this release — class subpath)** + v0.4.6 (`defineIfFree`) |
| 8 | Form-associated discoverability | ✅ v0.4.6 (USAGE.md § Form participation) |

**All 8 friction points from the original FEEDBACK doc are now closed.** The §61 meta-finding (5 of 8 traced to undiscoverable docs, not missing features) is fully resolved.
## [0.4.6] - 2026-05-12

### Fixed — `<pane-ui>` resize tracking 1:1 + pointer-capture under touch

`<pane-ui>` resize handle previously delegated `pointermove`/`pointerup` to `document` (assuming the cursor stayed close to the handle). Two consequences: (a) on touch / fast pointer races the handle lost grip mid-drag; (b) cursor flicker around the 4-px handle as the browser swapped `col-resize` ↔ default. Refactored to handle-level listeners with `setPointerCapture()`:

- **Pointer capture on the resize grabber.** `setPointerCapture(pointerId)` is attempted (try/catch guards browsers without it); subsequent `pointermove` / `pointerup` / `pointercancel` listen on the grabber itself, not `document`. Pointer events flow to the grabber even if the cursor races past it.
- **`isPrimary` guard** rejects secondary touches / scroll-pointers.
- **Document-level cursor lock** — during a drag, `documentElement.style.cursor = 'col-resize'` is set + restored on `pointerup` / `pointercancel`, so the cursor stays `col-resize` across the entire viewport instead of flickering at the handle boundary. Previous cursor captured into `#prevDocCursor` for restore.
- **`touch-action: none`** on the grabber suppresses native scroll-pan during touch drags.
- **`[data-resizing]` host attribute** — set during drag, removed on release. CSS adds `:scope[data-resizing] { user-select: none; transition: none; }` to bypass any consumer-declared width transition (e.g. editor-shell's collapse animation) so pointer movements track 1:1 instead of interpolating.

Same shape pattern as `<admin-sidebar>` (mirrored — keeps the two resize behaviors consistent).

### Added — `<toggle-scheme-ui>` primitive

New focused primitive that toggles `color-scheme` (light ↔ dark) on a target element (default `:root`). Encapsulates the `prefers-color-scheme` initial-resolution, the inline-style write to the target, optional `localStorage` persistence (default prefix `adia-theme-`, matching `<theme-panel>` for cross-element interop), and the moon ↔ sun icon swap. Replaces the hand-wired `<button-ui id="theme-toggle">` + `applyScheme()` pattern previously duplicated across `apps/genui`, `apps/construct-canvas`, and `playgrounds/chat`.

Authoring API: `[scheme="auto"|"light"|"dark"]`, `[target]`, `[persist]`, `[storage-prefix]`, `[icon-light]` / `[icon-dark]`, `[label-light]` / `[label-dark]`, plus `[size]` / `[variant]` / `[color]` / `[disabled]` forwarded to the internal `<button-ui>`. Reflects `[active-scheme]` (read-only). Public methods: `.toggle()`, `.setScheme("light"|"dark"|"auto")`. Bubbles a `scheme-change` `CustomEvent` with `detail: { scheme, source }` where `source ∈ "press" | "media" | "programmatic"`.

When `scheme="auto"` and there is no persisted override, the element wires a `prefers-color-scheme` `matchMedia` listener and updates `active-scheme` on OS preference flips. The first explicit user choice (click, keyboard, or `.toggle()` / `.setScheme('light'|'dark')`) defeats auto and the listener detaches. `.setScheme('auto')` re-attaches.

### Added — TypeScript declarations + consumer guide + registration helpers (§68 consumer-feedback v0.4.6 deferrals)

Closes the three items deferred from the §61 consumer-feedback follow-up arc (Design Tokens Studio FEEDBACK doc, 2026-05-12). The §61 audit found that 5 of 8 consumer friction points trace to undiscoverable documentation rather than missing features; this release closes 4 of those 5 via hand-written `.d.ts` types + a single landing reference at `USAGE.md`. The fifth (typed events at compile time) is now solved by the `.d.ts` shipment.

**`USAGE.md` — comprehensive consumer guide** (~430 lines) at the package root, also shipped in the published tarball. Single landing surface for engineers + agents integrating AdiaUI into an app. Sections: Install, mental model, property reactivity (signal-backed), property binding patterns (the eager-evaluation trap), event contract (`CustomEvent.detail`), form participation (`UIFormElement` + `ElementInternals`), lifecycle (`connected`/`render`/`updated`/`disconnected`), theming/density/size, registration (auto vs explicit + `defineIfFree`), TypeScript, anti-patterns. Cross-linked from `README.md`.

**Hand-written `.d.ts` declarations** for the public API. Adding the package to dependencies now picks up TypeScript types for:

- **Core types** at `core/*.d.ts` — `UIElement`, `UIFormElement`, signal/computed/effect/batch/untracked, html/css/repeat/stamp templating, register/defineIfFree/isRegistered helpers
- **Per-primitive types** at `components/<name>/<name>.d.ts` for 17 form-bearing primitives: input, select, slider, range, textarea, switch, check, radio, segmented, search, otp-input, option-card, color-picker, rating, code, calendar-picker, upload. Each declares typed properties + typed `CustomEvent` payloads
- **`HTMLElementTagNameMap` augmentation** at `index.d.ts` — `document.querySelector('slider-ui')` now returns `UISlider`, not `Element`
- **`package.json` `types` field + `exports` types conditions** — module resolution under `bundler` / `node16` / `nodenext` picks up the declarations automatically

Eliminates the `(e.target as any).value` pattern at compile time:

```ts
import type { SliderChangeEvent } from '@adia-ai/web-components';
slider.addEventListener('change', (e: SliderChangeEvent) => {
  const v: number = e.detail.value;   // typed; would error if value were string
});
```

Verified with negative type tests — string-to-number assignment errors fire; wrong-tag inference fires; wrong detail-shape fires.

**`core/register.js` + `core/register.d.ts`** — explicit registration helpers exported from the `core` barrel:

- `register(tagName, ctor)` — alias for `customElements.define` (no-magic; provided for symmetry)
- `defineIfFree(tagName, ctor)` — no-op + returns `false` if tag already registered. **Solves the consumer's #7 friction** (`customElements.define` throwing `NotSupportedError` when two packages register the same tag). Backwards-compatible — existing auto-register paths in component `.js` files continue to use `customElements.define` directly
- `isRegistered(tagName)` — returns `true` if the tag is taken

The future non-side-effect `class` subpath per component (allowing class import without auto-registration for test isolation) is documented as roadmap in USAGE.md's Registration section; ships v0.4.7+ when the 95 component-file refactor lands.

**`package.json` `files:` array** updated to include `index.d.ts` + `USAGE.md` in the published tarball. CSS-export negation patterns already cover `.d.ts` (no exclude rules needed).

### Coverage

- 19 new `.d.ts` files (1 root + 6 core + 17 per-component – overlap = 19 total)
- 1 new helper module (`core/register.js`, 56 LOC) + its `.d.ts`
- 1 new documentation file (`USAGE.md`, ~430 lines)
- Updated `README.md` consumer-guide section (collapsed to a quick-reference card + pointer to `USAGE.md`)
- Updated `package.json` `types`, `exports`, `files` to expose the types properly

### Refreshed (§69 stale-audit sweep) — `traits/` docs currency

- `traits/CATEGORIES.md` — primitive count updated 93 → 95 (current verified count).
- `traits/resettable/resettable.examples.html` — `AdiaFormElement` → `UIFormElement` in the "When NOT to use" description prose (per ADR-0016 v0.0.32 rename; stale class-name reference caught by post-§68 audit).
## [0.4.5] - 2026-05-12

### Added — `CustomEvent.detail` sweep across 17 form-bearing primitives (§61)

All `change` / `input` event dispatches across form-bearing components now emit a `CustomEvent` carrying `detail: { value: this.value }` (value-semantic primitives) or `detail: { value: this.value, checked: this.checked }` (boolean-semantic — switch / check / radio / option-card). Backwards-compatible — `e.target.value` still works since the host's `value` property is updated before dispatch.

Driven by [consumer feedback](/Users/kimba/Projects/adia/color-app/docs/outbound/FEEDBACK-adia-packages.md) from Design Tokens Studio (2026-05-12) — eliminates the `(e.target as any).value` pattern in TypeScript consumers.

**Value-semantic primitives swept** (`detail: { value }`):
- `<input-ui>` (input.js — 9 dispatch sites)
- `<select-ui>` (select.js — 2 sites)
- `<slider-ui>` (slider.js — 4 sites)
- `<textarea-ui>` (textarea.js — 2 sites)
- `<range-ui>` (range.js — 3 sites)
- `<rating-ui>` (rating.js — 1 site)
- `<search-ui>` (search.js — 2 sites)
- `<otp-input-ui>` (otp-input.js — 3 sites)
- `<calendar-picker-ui>` (calendar-picker.js — 1 site)
- `<upload-ui>` (upload.js — 1 site; detail also carries `files: this.files`)

**Boolean-semantic primitives swept** (`detail: { value, checked }`):
- `<switch-ui>` (switch.js — 1 site)
- `<check-ui>` (check.js — 1 site)
- `<radio-ui>` (radio.js — 1 site)
- `<option-card-ui>` (option-card.js — 1 site)

**Already canonical** (no change needed):
- `<segmented-ui>` — emits `detail.value` since pre-v0.4.0
- `<code-ui>` — emits `detail.value` (CodeMirror state)
- `<color-picker-ui>` — emits multiple typed events with structured detail
- `<demo-toggle-ui>`, `<toggle-group-ui>`, `<tabs-ui>`, `<swiper-ui>` — already typed

### Added — `slider.test.js` locks in property reactivity (§61)

7-case vitest spec for `<slider-ui>` validating the contract that the consumer feedback questioned (claim #5: "doesn't react to property changes"):

1. Initial render places thumb at correct % for `value=50`
2. **Programmatic `slider.value = 75` moves thumb to 75%** (the contested behavior — UIElement's signal-backed property setter at `core/element.js:31-50` triggers the host's render effect on every property change; verified working)
3. `setAttribute('value', '25')` also propagates through `attributeChangedCallback` → property setter → signal
4. `[slot="value"]` readout text syncs on value change
5. `change` event is a `CustomEvent` with `detail.value` on keyboard step (`ArrowRight`)
6. `input` event is a `CustomEvent` with `detail.value` on keyboard step (`ArrowUp`)
7. `aria-valuenow` reflects current value for screen readers

The consumer's claim was wrong about the library — `<slider-ui>` IS reactive to property changes. Their specific binding pattern (`.value=${signal.value * 100}`) is non-reactive because the expression evaluates eagerly to a number, never reaching the template-system's signal/function detector at `template.js:179-183`. The fix on the consumer side is `.value=${signal}` (signal direct) or `.value=${() => signal.value * 100}` (function); the binding system effect-wraps both. v0.4.6 ships a USAGE.md reference making this pattern obvious.

### Added — `/site/tokens/colors` design-tokens export panel (§52)

In-page exporter that snapshots the live OKLCH token surface as Figma-
importable JSON. Supports W3C DTCG (Figma native direct-import, default
— two files: `light.tokens.json` + `dark.tokens.json` with structured
`{colorSpace, components, alpha, hex}` color objects at float precision),
Variables Pro legacy format (single file, `modes` wrapper, hex/rgba
strings matching the canonical `parseColor` regex), and a float-RGB
sub-byte-precision experimental path. Polarity-collapsed tonal primitives
(`neutral.50` is a single Figma variable with per-mode values; the
underlying `-tint` / `-shade` cssVars are internal implementation detail
that don't surface in the export). Pure-JS OKLCH → OKLab → linear-sRGB
→ sRGB → HSL math (Björn Ottosson's matrices), no canvas quantization.

- **`styles/design-tokens-export.js`** (new ~430 lines) — extraction module:
  CSSOM scanner with authoritative-selector filter (`:root, theme-ui,
  [data-theme]` only — skipping attribute-scoped overrides that would
  pollute the cssVar map under last-wins semantics), symbolic
  `var()` / `light-dark()` chain walker, OKLCH color-math pipeline, four
  output formatters (DTCG / hex / float-RGB / HSL-decimal). Public API:
  `buildFigmaJson({ format })`, `buildDtcgFiles()`, `getExportStats()`,
  `downloadJson(filename, data)`, `copyJson(data)`.
- **`styles/colors.examples.html`** — export panel section: 5-button row +
  stats banner + workflow documentation for Figma's manual multi-mode
  import (add mode → right-click → "Import mode").
- **`styles/colors.examples.js`** — wires the export panel: DTCG (Light +
  Dark) primary download, Light-only / Dark-only single-mode, Variables Pro
  legacy hex, clipboard copy. Theme-aware filenames (`design-tokens-${theme}-
  hex.json` for legacy; DTCG path keeps `light.tokens.json` /
  `dark.tokens.json` because Figma derives mode names from filename).
- **`styles/colors.html`** (standalone path) — loads `button-ui` +
  `swatch-ui` CSS+JS for the panel chrome.

### Fixed — SPA-router-vs-download-anchor interaction (§52)

`UIRouter` in `core/provider.js:185` installs a document-level click listener
that intercepts `<a href>` clicks for client-side navigation. Programmatic
download anchors with `blob:` URLs would trigger `history.pushState(blob:...)`
which throws cross-origin. The export's `downloadJson` helper now dispatches
a non-bubbling synthetic click (`a.dispatchEvent(new MouseEvent('click',
{ bubbles: false }))`) — browser's `<a download>` default action still
fires because it's bound to the event target, not propagation. Worth
mirroring in any future in-page download workflow.
## [0.4.4] - 2026-05-12

### Added — corpus drift yaml gaps closed (§44, §50, §51)

Companion to the corpus simplification arc in `@adia-ai/a2ui-{runtime,compose,corpus}@[Unreleased]`. Yaml + js additions for props that were runtime-supported but yaml-under-declared — surfaced by the new `verify:corpus` validator (§39).

**Text strong attribute (§43):**
- **`text.yaml` + `text.js` — `strong: boolean` prop** (was: styled via `:scope[strong]` selector in text.css line 35 but never declared in yaml/js). Caught by §43's audit as a genuine yaml gap; added with `default: false, reflect: true` so the corpus codemod's `Text.weight="semibold"` → `strong: true` rename has a yaml-declared target.

**Renderer-auto-routed textContent props (§44):**
- **`badge.yaml` + `badge.js` — `textContent: string, dynamic`.** Renderer's `#applyProps` lines 294-297 auto-routes `textContent` → `text` attribute for non-text-bearing tags; yaml under-declared.
- **`button.yaml` + `button.js` — `textContent: string`.** Same pattern.
- **`tag.yaml` + `tag.js` — `textContent: string`.** Same pattern.
- **`code.yaml` + `code.js` — `textContent: string`.** Same pattern.

**Universal layout grow attribute (§44):**
- **`col.yaml` + `col.js` — `grow: boolean, reflect: true`.** `:scope[grow]` selector at col.css line 38 supports it; yaml under-declared.
- **`row.yaml` + `row.js` — `grow: boolean, reflect: true`.** Same pattern at row.css line 56. Discovered during synthesis (Row.grow had only 5 corpus records, didn't make the top-20 audit; added anyway for symmetry with col.yaml).

**Pass-through HTML/ARIA attributes (§44):**
- **`button.yaml` — `aria-label: string`.** Pass-through ARIA attribute, native HTML, no js change needed.
- **`textarea.yaml` — `name: string`.** Inherited from UIFormElement; yaml under-declared.

**#JS_PROPS-resident table/select props (§44):**
- **`table.yaml` — `columns: array` + `data: array`.** Already in renderer's `#JS_PROPS` set (line 233) but yaml under-declared.
- **`select.yaml` — `options: array`.** Runtime supports both prop AND children patterns.

**Universal [size] system (§50):**
- **`select.yaml` — `size: enum [sm, md, lg], reflect: true`.** Sizing via the universal `[size]` attribute system at `packages/web-components/styles/tokens.css:206-260`; select.examples.html already shipped `size="sm"`/`size="lg"` examples. Matches Input's sizing tokens so a Select rendered alongside an Input feels coherent in a form row.

**HTML standard input attributes (§51):**
- **`input.yaml` — `inputmode: enum [text, decimal, numeric, tel, search, email, url, none]`.** Routed via setAttribute to the host element. Mobile keyboard hint per HTML inputmode spec.
- **`input.yaml` — `autocomplete: string`.** Routed via setAttribute. Browser autofill behavior per HTML autocomplete spec. Common values documented inline: `off`, `on`, `cc-number`, `cc-exp`, `cc-csc`, `cc-name`, `email`, `username`, `current-password`, `new-password`, `one-time-code`, `given-name`, `family-name`, `street-address`, `postal-code`.

  Companion to the §51 renderer kebab-casing fix in `@adia-ai/a2ui-runtime` — yaml declarations close `verify:corpus` warns AND tell LLM generators these are first-class Input props with specific enum/value lists. The documentation win matters more than the validation win.

Catalog (`packages/a2ui/corpus/catalog-a2ui_0_9.json`) regenerated to pick up every yaml addition. `verify:traits` clean at 56/56; `components --verify` clean at 125 files.

See root [CHANGELOG.md `[Unreleased]`](../../CHANGELOG.md) for the cross-cutting arc narrative + [docs/journal/2026/05/2026-05-12.md](../../docs/journal/2026/05/2026-05-12.md) §§ 43 / 44 / 50 / 51 for per-§ details.
## [0.4.3] - 2026-05-11

### Added

- **`<input-ui type="number">` — `locale` attribute** (BCP-47 tag, e.g. `de-DE`, `fr-FR`, `en-IN`). When set, the input accepts both `.` AND the locale's decimal separator (so `<input-ui type="number" locale="de-DE">` parses `"1,5"` and `"1.5"` interchangeably) and uses `Intl.NumberFormat` for display — `valueAsNumber = 9.5` renders as `"9,5"` under `locale="de-DE"`. Internal storage stays canonical so `.value` round-trips through `Number(v)` after `#toCanonical`. Default empty = en-US-equivalent path (no behavior change for existing consumers). Closes v0.4.2 known-limitation #1.
- **`<input-ui type="number">` — thousands grouping on blur** when `locale` is set. The textContent renders grouped (e.g. en-US `1,234,567.89`, de-DE `1.234.567,89`, fr-FR `1 234 567,89` with U+202F, en-IN `12,34,567.89`) on initial render and post-blur; on focus the input reverts to the ungrouped form so the user can edit naturally without the group separator jumping around mid-keystroke. `this.value` always stores the ungrouped, locale-decimal form; `valueAsNumber` round-trips correctly. Paste of a grouped value (in either canonical or locale form) sanitizes correctly — e.g. en-US `"1,234,567.89"` and de-DE `"1.234.567,89"` both yield `1234567.89` via `valueAsNumber`.
- **`<input-ui type="number">` — hold-to-repeat on stepper buttons.** Pressing and holding a `[+]` or `[−]` button now fires an initial step on `pointerdown` then autorepeats after a 400ms delay at a 60ms cadence (matches native `<input type="number">` cadence in Chromium/Safari). Release anywhere (`pointerup`/`pointercancel` on `document`) stops it — supports the "drag off then lift" abort path without per-button leave/cancel bookkeeping. Boundary hit (`value === min` or `max`) cancels the autorepeat early so no-op events don't keep firing. Keyboard `↑/↓` auto-repeat (already provided by browser key-repeat) is unchanged; this addition covers the pointer-only path that v0.4.2 documented as "not implemented" in known limitations.

### Fixed

- `<input-ui type="number">` `#commitOnBlur` — pre-existing latent bug exposed by the new `locale` support. Was `Number(raw)` directly; locale-formatted values like `"1,5"` (de-DE) parsed as `NaN` on blur, so the function returned early without formatting/snapping. Fixed to `Number(#toCanonical(raw))` — canonicalize first.
- `<input-ui type="number">` `#sanitizeNumeric` — was accepting `.` as decimal when locale's separator is `,`. Paste of `"1.234.567,89"` (de-DE grouped) parsed as `1,23456789` (first `.` mistaken for decimal). Now only the locale's separator is accepted as decimal; `.` silently drops as group separator when sep=`,`. Programmatic `.value = "1.5"` on a `locale="de-DE"` element still works (UIFormElement's setter bypasses sanitizer; `valueAsNumber` canonicalizes via `#toCanonical`).

### Lockstep

9-package coordinated PATCH cut to v0.4.3 (per [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). Ratifies [ADR-0027](../../.brain/adrs/0027-cross-primitive-composition-imports.md) — cross-primitive composition imports are the consumer's responsibility; the primitive's `.js` does NOT side-effect-import composed primitives. Source files touched: `components/input/input.{js,css,yaml,a2ui.json}`. See root [CHANGELOG.md `## [0.4.3]`](../../CHANGELOG.md) for the cut narrative.
## [0.4.2] - 2026-05-11

### Changed

- **`<input-ui type="number">` — drop native `<input type="number">` wrapping; render contenteditable + `[+]/[−]` stepper-button column instead.** Number mode now matches every other input variant (range / select / check / radio / slider) as a "we own the affordance" widget — no native browser stepper, no `-webkit-appearance` suppression, no native min/max coercion. Composes `<button-ui>` + `<icon-ui>` for the stepper column. Password (`type="password"`) remains the only branch that wraps native, kept for `-webkit-text-security` disc masking which has no contenteditable equivalent. Demo shell `input.html` now imports `button.css` + `icon.css` + `button.js` + `icon.js`; any consumer that imports `input-ui` piecemeal (not via the barrel) **AND** uses `type="number"` must also register `button-ui` + `icon-ui`. The component barrel `packages/web-components/components/index.js` handles registration order automatically.
- `<input-ui type="number">` host now carries `role="spinbutton"` + `aria-valuenow` / `aria-valuemin` / `aria-valuemax` / `aria-valuetext` (suffix-aware: `<input-ui type="number" value="70" suffix="kg">` announces "70 kg") + `aria-readonly` / `aria-required` / `aria-invalid` mirrored from props. Stepper-button wrapper carries `aria-hidden="true"` — redundant with keyboard ↑/↓.
- Existing `type` section's number example in [input.examples.html](./components/input/input.examples.html) upgraded with `min="0" max="100"` so the demo's stepper buttons surface working boundary-disable behavior.

### Added

- `<input-ui type="number">` — four new props:
  - `min` (Number, default `null`) — minimum value. Drives `aria-valuemin` + the `[−]` button's disabled state.
  - `max` (Number, default `null`) — maximum value. Drives `aria-valuemax` + the `[+]` button's disabled state.
  - `step` (Number, default `1`) — increment magnitude for ↑/↓ ArrowUp/Down, PageUp/Down × 10, and the `[+]`/`[−]` buttons. Also drives the implicit decimal count for display formatting unless `precision` is set.
  - `precision` (Number, default `null`) — decimal places to format + clamp to. Overrides the `step`-derived decimal count — e.g. `step=1 precision=2` formats `"10.00"`.
- `<input-ui type="number">` — `valueAsNumber` getter (mirrors the standard HTML5 `<input type="number">.valueAsNumber` read pattern). Returns `NaN` when empty / unparseable / lone-sign-or-decimal. Setter coerces and clamps via `#snap`.
- `<input-ui type="number">` — full WAI-ARIA APG spinbutton keyboard model: `ArrowUp` / `ArrowDown` step by `step`, `PageUp` / `PageDown` step by `step × 10`, `Home` → `min` (no-op when null), `End` → `max` (no-op when null), `Enter` commits + fires `change` + `submit`, `Escape` reverts to value at focus-in. `wheel` deliberately **not** bound — wheel-changes-value-on-scroll is an antipattern; users can scroll past an input without fearing silent value changes.
- `<input-ui type="number">` — `beforeinput` filter enforces digits / single decimal / leading minus (when `min == null || min < 0`). Permissive during typing (allows lone `"-"`, lone `"."`, trailing `"1."`) + normalizes on blur via `#commitOnBlur` → `#snap` → `#format`. Paste handler sanitizes clipboard text to the same alphabet before insertion.
- `<input-ui type="number">` — number-range constraint validation. `#runNumberConstraints` layers `badInput` / `rangeUnderflow` / `rangeOverflow` on top of `UIFormElement`'s base constraints (required / pattern / minlength / maxlength). Custom message data attributes: `data-msg-bad-input`, `data-msg-min`, `data-msg-max` (parallels the existing `data-msg-required` / `data-msg-pattern` convention).
- New dedicated demo section in [input.examples.html](./components/input/input.examples.html) (`data-property="number"`) — 8 variants: Quantity (integer min/max) · Price ($ prefix + precision 2) · Weight (kg suffix + step 0.1) · Discount (% suffix + step 5) · Temperature (negative allowed) · sm size · lg size · disabled state.
- Five new `examples[]` entries in [input.yaml](./components/input/input.yaml) for retrieval signal: `quantity-stepper`, `price-with-currency`, `weight-with-unit`, `percent-bounded`, `temperature-negative`.

### Fixed

- `<input-ui type="number">` chrome height regression. After the rewrite, number-mode fields initially measured **+6-10 px past** the canonical `--a-size` baseline (38/34/46 vs target 30/24/36 at md/sm/lg) because the stepper-button column's children pushed the field's flex height past `min-height: var(--input-height)`. Resolved by switching the controls column from flex flow to `position: absolute` (`[data-number] { position: relative }` + column `inset-block: 0; inset-inline-end: 0`) so the column never participates in field-height calculation. Value/suffix reserve space via `padding-inline-end` / `margin-inline-end`. Number-mode fields now measure exactly 24/30/36 px at sm/md/lg — identical to text/email/password/etc.
- `<input-ui type="number">` stepper-button width collapse. button-ui's `:scope:not([text]) { width: var(--button-height) }` icon-only rule was collapsing the buttons to 0 wide when our scope overrode `--button-height` to 0 (to defeat its square min-width/min-height contract). Resolved by bumping our override's selector specificity from `[slot="controls"] button-ui` (0,1,1) to `[data-number] [slot="controls"] button-ui` (0,2,2).
- `<input-ui type="number">` stepper icon overshoot. `<icon-ui>`'s default `--icon-size: calc(1em + 0.125rem)` overshoots the half-column cell, causing 1-2 px vertical clipping. CSS-custom-property declarations on self override inherited values, so the override has to target `icon-ui` directly: `[data-number] [slot="controls"] icon-ui { --icon-size: calc(var(--input-height) * 0.4) }`. Chevrons now measure 12/9.6/14.4 px at md/sm/lg — proportional and well within the half-column.

### Known limitations

- ~~`<input-ui type="number">` filter assumes en-US numeric formatting (`.` as decimal separator). Locale-aware separators (e.g. German `,`) are not parsed. Tracked as a deferred follow-up; right answer is an opt-in `locale="…"` attribute that swaps the filter regex + uses `Intl.NumberFormat` for display.~~ **Resolved in [0.4.3]** above. `locale` attribute + thousands grouping on blur both shipped.
- Scientific notation (`"1e3"`) is rejected by the filter. Deliberately out of scope.
- ~~Hold-to-repeat on the stepper buttons is not implemented. Browser keyboard auto-repeat on ↑/↓ covers the same affordance.~~ **Resolved post-v0.4.2** — see `[0.4.3]` above. Pointer hold now autorepeats at native cadence.

### Lockstep

9-package coordinated PATCH cut to v0.4.2 (per [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy)). Internal `@adia-ai/*` dep ranges stay at `^0.4.0` (patch-cut asymmetry — `^0.4.0` covers `0.4.x` under semver). ADR-0025 ([`.brain/adrs/0025-no-native-form-controls.md`](../../.brain/adrs/0025-no-native-form-controls.md)) ratified — codifies the "no native or suppressed-native interactive widgets in primitives" principle that the `<input-ui type="number">` rewrite implements; `password` remains the documented exception (physically requires `<input type="password">` for `-webkit-text-security`). See root [CHANGELOG.md `## [0.4.2]`](../../CHANGELOG.md) for the cut narrative.
## [0.4.1] - 2026-05-10

### Ride-along (no source changes)

Lockstep PATCH cut alongside `@adia-ai/web-modules@0.4.1` (simple cluster) + `@adia-ai/a2ui-validator@0.4.1` (Phase 3 foundation) + `@adia-ai/a2ui-corpus@0.4.1` (fragment metrics reconciliation). Source byte-identical to v0.4.0.

Internal `@adia-ai/*` dep ranges bumped from `^0.4.0` to `^0.4.1`. See root [CHANGELOG.md `## [0.4.1]`](../../CHANGELOG.md) for the cut narrative.
## [0.4.0] - 2026-05-10

### Ride-along (no source changes)

Lockstep MINOR cut alongside `@adia-ai/web-modules@0.4.0` (ADR-0024 legacy shell shapes retired). Source byte-identical to v0.3.6.

Internal `@adia-ai/*` dep ranges bumped from `^0.3.0` to `^0.4.0`. See root [CHANGELOG.md `## [0.4.0]`](../../CHANGELOG.md) for the cut narrative.
## [0.3.6] - 2026-05-10

### Ride-along (no source changes)

Lockstep version bump only — source byte-identical to v0.3.5. Internal `@adia-ai/*` dep ranges remain at `^0.3.0`. See root [CHANGELOG.md `## [0.3.6]`](../../CHANGELOG.md) for the cut narrative.
## [0.3.5] - 2026-05-07

### Ride-along (no source changes)

Lockstep version bump only — source byte-identical to v0.3.4. Internal `@adia-ai/*` dep ranges remain at `^0.3.0`. See root [CHANGELOG.md `## [0.3.5]`](../../CHANGELOG.md) for the cut narrative.
## [0.3.4] - 2026-05-07

### Ride-along (no source changes)

Lockstep version bump only — source byte-identical to v0.3.3. Internal `@adia-ai/*` dep ranges remain at `^0.3.0`. See root [CHANGELOG.md `## [0.3.4]`](../../CHANGELOG.md) for the cut narrative.
## [0.3.3] - 2026-05-07

**Lockstep cut.** All 9 published `@adia-ai/*` packages now share version `0.3.3`, governed by [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy).

### Changed

- `version`: `0.3.2` → `0.3.3`.
- `dependencies["@adia-ai/...]`: `^0.3.0` (patch-cut asymmetry — caret already covers 0.3.x; ranges unchanged).

### No source changes

`@adia-ai/web-components` source is byte-identical to `0.3.2`. The cut is a version bump + ride-along on the lockstep policy.
## [0.3.2] - 2026-05-06

**9-package lockstep patch cut to v0.3.2.** All lockstep members share
one version per [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy).
Internal `@adia-ai/*` dep ranges unchanged at `^0.3.0`.

### No source changes

This package's source is byte-identical to v0.3.1. The cut bumps
version only.

### Changed

- `version`: `0.3.1` → `0.3.2`.
## [0.3.1] - 2026-05-06

**9-package lockstep patch cut + folder-per-trait restructure.** All 9 published `@adia-ai/*` packages bump 0.3.0 → 0.3.1 per [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy). Internal `@adia-ai/*` dep ranges remain at `^0.3.0` (covers `0.3.1` under semver — patch-cut asymmetry).

This is a **patch cut on top of v0.3.0, no BREAKING changes for npm consumers.** The barrel API (`@adia-ai/web-components/traits`, `@adia-ai/web-components/components`, etc.) is byte-equivalent to v0.3.0 at the public surface. The change is internal: the trait library moved from a flat `traits/<name>.{html,js,examples.html,examples.js,test.js}` layout to one folder per trait (`traits/<name>/<name>.{...}`), mirroring the existing `components/<name>/` convention.

### Changed

- **Trait file layout: folder-per-trait.** 56 traits restructured. The barrel re-exports remain unchanged — `import { activeState } from '@adia-ai/web-components/traits'` is byte-equivalent. **Internal subpath shape changes**: imports that reach into specific trait files (e.g. `import { confettiBurst } from '@adia-ai/web-components/traits/confetti-burst'`) now point at `traits/confetti-burst/confetti-burst.js`. The `package.json` `exports` field doesn't declare per-trait subpaths, so the formal published API is unchanged; consumers using deep imports through the file system would need to update paths.
- **Stage companions move with their trait.** `confetti-stage.js` lives under `traits/confetti/`, `announcer-stage.js` under `traits/announcer/`. These were never public; they're co-located with the trait that owns them.
- `components/card/card.js` + `components/row/row.js` updated to import `traits/draggable/draggable.js` (was `traits/draggable.js`).
- `version`: `0.3.0` → `0.3.1`. Internal `@adia-ai/*` dep ranges: unchanged at `^0.3.0` (covers `0.3.1` under semver — patch-cut asymmetry).

### Why

Symmetry with `components/<name>/` (already folder-per for months) — easier to find related files when working on one trait, allows trait-specific helpers without name pollution at the top level. Survey of the import graph confirmed safety: 247 of 251 `@traits/<X>.js` imports use the barrel (unchanged); 4 reached into specific files (all in-repo, updated in same commit).

### Repo-only (not in tarball — but commit graph for transparency)

- `packages/web-components/traits/` directory restructure: ~280 file moves preserving `git mv` history.
- `packages/a2ui/compose/transpiler/transpiler-maps.js` — no change since v0.3.0 (the v0.3.0 entry mentioned this file; correction: that update was part of v0.3.0).
## [0.3.0] - 2026-05-05

**9-package lockstep cut.** All 9 published `@adia-ai/*` packages bump 0.2.5 → 0.3.0 per [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy). Internal `@adia-ai/*` dep ranges bump `^0.2.0` → `^0.3.0`.

The lockstep policy expanded from 8 to 9 packages with this cut — `@adia-ai/llm` joins as a foundational primitive (extracted from `@adia-ai/a2ui-compose/llm`), and `@adia-ai/a2ui-utils` was renamed to `@adia-ai/a2ui-runtime`. Both are BREAKING for npm consumers (acceptable under pre-1.0 semver).

This package itself ships **no source changes** in v0.3.0. The cut bumps version + the internal dep range only. Substantive content lives in `@adia-ai/llm` (first publish), `@adia-ai/a2ui-runtime` (rename), `@adia-ai/web-modules` (chat-shell now imports from `@adia-ai/llm`), `@adia-ai/a2ui-compose` (drops `./llm` subpath), and `@adia-ai/a2ui-mcp` (synthesis tool dep update).

### Changed

- `version`: `0.2.5` → `0.3.0`.
- `dependencies["@adia-ai/a2ui-utils"]` removed from any consumer; consumers requiring the runtime should now depend on `@adia-ai/a2ui-runtime`.

### Repo-only (not in tarball)

The following directories saw significant in-repo changes between v0.2.5 and v0.3.0, but the additions are **excluded from the published tarball** via the `files:` array's `!components/**/*.html` negation (per [`feedback_npm_files_array_negation.md`](https://github.com/adiahealth/gen-ui-kit/blob/main/.claude/projects/-Users-kimba-Projects-chat-ui/memory/feedback_npm_files_array_negation.md)) and similar rules for non-shipping demo content. Consumers see byte-identical source for the published surface.

- `components/` — co-located per-component demo trio (`<name>.html` shell + `<name>.examples.html` + `<name>.examples.js`) added across ~85 components per ADR-0021 (page-trio convention). These are dev-only artifacts; not bundled into the tarball.
- `core/` — `streams-bridge.js` import path updated for the runtime rename (`@adia-ai/a2ui-utils` → `@adia-ai/a2ui-runtime`).
- `traits/` — `tilt-hover.examples.html` regenerated from the components build (slot-marker convention update). No trait API changes.
- `styles/` — no source changes; touched only via the build's catalog-regeneration sweep.
- `patterns/` — no source changes since v0.2.5.
## [0.2.5] - 2026-05-04

**Lockstep cut + new `<fields-ui>` form-grid primitive + `draggable-list-item` last-item drop-zone fix.** All 8 published `@adia-ai/*` packages bump 0.2.4 → 0.2.5 per [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy). Patch cut — **no BREAKING changes**.

### Added

- **New `<fields-ui>` primitive** at `components/fields/`. A form-category container that hosts multiple `<field-ui>` children on a shared 6-column grid (configurable via `[columns="1..12"]`). Each `<field-ui rows="N">` spans `N` columns; default span (no attr) fills the row. The host's `[inline]` attribute propagates onto every direct `<field-ui>` child via a MutationObserver — a whole sub-form switches label-position from one host attribute. Solves the wrap-flex jitter pattern where authors stacked `<field-ui>` children inside `<row-ui wrap>` and labels/controls would refuse to align across rows. Yaml + js + css + 6/6 tests. Catalog: 97 → 98 yamls.
  - `components/fields/fields.yaml` — declarative API with 2 examples (3-up stacked, inline-search-form) + 2 anti-patterns flagged.
  - `components/fields/fields.js` — `UIFields extends UIElement`. Properties `[columns]` (Number, default 6, reflective) + `[inline]` (Boolean, reflective). MutationObserver re-syncs `[inline]` propagation on `childList` changes.
  - `components/fields/fields.css` — single `@scope (fields-ui)` block. `display: grid; grid-template-columns: repeat(var(--fields-columns, 6), minmax(0, 1fr))`. Per-`rows` selectors unrolled 1..12 (CSS doesn't do attribute arithmetic).
  - `components/fields/fields.test.js` — covers default no-propagation, propagation on connect, on add (post-mount `appendChild`), on remove, non-field-child isolation, and registration. **6/6 PASS**.

### Fixed

- **`traits/draggable-list-item.js` — last-item drop-zone**. In a 3-item list `[A, B, C]`, dragging `B` onto `C` was impossible to land at the bottom slot — the lifted source's DOM still occupied its old position, and the standard mid-line test resolved upper-half-of-`C` to "before `C`" (no-op). Fix: the LAST visible item's whole height is now the "drop after" zone (`indexWithinTarget` returns `visible.length` whenever cursor `y >= rect.top` on the last item). Non-last items keep the standard mid-line; the gap above the last item still resolves through mid-line (so "between second-to-last and last" stays reachable). Universal trait fix — list, kanban, and any future `droppable-collection` consumer benefit. New visual-test assertion in the Tasks UI Playground covers this case.

### Changed

- `version`: `0.2.4` → `0.2.5`.
- `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.5` under semver — patch-cut asymmetry).
- `components/index.js` adds `UIFields` export.
- `styles/components.css` adds `@import "../components/fields/fields.css"`.
## [0.2.4] - 2026-05-04

**Lockstep cut + Tier 4 capability adds + 2 architectural rewrites.** All 8 published `@adia-ai/*` packages bump 0.2.3 → 0.2.4 per [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy). Patch cut — no breaking changes.

### Changed

- `version`: `0.2.3` → `0.2.4`.
- `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.4`).
- New `components/demo-toggle/` directory; `components/index.js` adds `UIDemoToggle` export; `styles/components.css` adds `@import "../components/demo-toggle/demo-toggle.css"`.
- `traits/index.js` adds 12 new exports (the 11 Tier-4 traits + droppable from the parallel Tasks UI initiative); `traits/_catalog.json` regenerated to 56 entries; `core/icons.js` extended with ~25 new aliases.

The substantive content of the next cut, surfaced from the
2026-05-04 trait-library lift initiative (`docs/PLAN.md`,
`docs/reports/traits-creative-director-audit-2026-05-04.md`).
**No BREAKING changes.** Every public tag, prop, event, and
declarative `traits=` attribute from v0.2.3 still works.

### Added — 11 new traits (Tier 4 capability adds)

Catalog: 41 → 56 traits across 9 categories (12 net new + droppable
already in flight from the Tasks UI parallel session). 100% test
coverage maintained; trait-suite footprint **59 files / 365 tests**
(was 44 / 169 at v0.2.3).

- **`layout-animation`** (motion-positioning) — FLIP primitive:
  capture old bounds, project transform from old to new, hand to a
  spring tick loop. Triggered by parent `MutationObserver` (sortable
  lists) or by toggling `data-layout-animate-trigger`. Closes the
  most-asked-for Framer Motion feature.
- **`drop-target`** (motion-positioning) — declarative drop zone with
  hit-testing + accept-reject (file types, MIME csv list). Counterpart
  to `draggable`/`drag-ghost`; emits `drop-enter` / `drop-leave` /
  `drop-receive` / `drop-rejected`.
- **`view-transition`** (motion-positioning) — wraps
  `document.startViewTransition()` for morph animations between DOM
  states. Sets `view-transition-name` on the host; installs a
  `host.startTransition(callback)` method. Same-document baseline
  Chromium 111+ / Safari 18+ / Firefox 129+; graceful-degrade path
  fires start+end synchronously when the API is missing.
- **`input-mask`** (forms-data) — locale-aware as-you-type formatter
  with 6 built-in patterns (phone-us, phone-intl, credit-card,
  date-iso, time-24h, cvv) and caret preservation against entered
  chars (not mask chars). Closes the structurally-thinnest category
  per the audit.
- **`long-press`** (input-interaction) — press-and-hold trigger with
  configurable duration / tolerance / progress events; suppresses the
  trailing `click` and the native `contextmenu` while in flight.
- **`scroll-progress`** (layout-measurement) — page or element scroll
  progress as `data-scroll-progress` (0..1) + CSS variable + event.
  Three modes: `in-view` (default), `page`, `scrolled`.
- **`error-shake`** (animation-feedback) — horizontal lateral
  oscillation on validation failure. Composes with `validation`
  trait via the `validated` event.
- **`success-checkmark`** (animation-feedback) — SVG stroke-draw
  checkmark on validation success. **Renders to body-level
  `position: fixed`** so the trait can't be clipped by the host's
  `overflow: hidden` ancestor (cards, buttons). Composes with
  `validation` via `validated` event.
- **`focus-restore`** (keyboard-navigation) — captures
  `document.activeElement` on connect, restores via `el.focus({
  preventScroll: true })` on disconnect. Falls back to body when the
  captured target is removed/disabled. Closes the modal-close-leaves-
  focus-dangling a11y bug class.
- **`arrow-grid-nav`** (keyboard-navigation) — 2D arrow-key navigation
  for grids/calendars/menubars (APG `grid` pattern). Two structural
  modes: `row-flat` (infers row/col by index ÷ columns) and
  `row-nested` (each direct child is a row container).
- **`announcer`** (audio-haptics-sensory) — singleton aria-live mirror
  with polite/assertive routing + throttling. Two body-level
  `aria-live` regions (`#adia-live-polite`, `#adia-live-assertive`)
  shared across all announcer instances. Lifts every audio/haptic/
  visual trait above WCAG 4.1.3.

Plus four traits that landed via the parallel Tasks UI initiative,
exported through this package's barrel: `droppable` (input-
interaction), `droppable-collection` (input-interaction),
`draggable-list-item` (motion-positioning), `keyboard-reorderable`
(keyboard-navigation).

### Added — `<demo-toggle-ui>` primitive

New custom element at `components/demo-toggle/`. A side-by-side "with
trait / without trait" comparison wrapper that any trait detail page
(or example) can drop into a hero. Reflects `data-state="on"|"off"`
on the host; emits a `change` event with `detail.state`. Optional
`data-mode="overlay"` stacks the slots in place so layout doesn't
shift between states. 9/9 tests passing. Integrated into 3 trait
pages (`parallax`, `shimmer-loading`, `noise-texture`) and ready for
broader retrofit.

### Changed — `confetti` + `confetti-burst` rendered to top-layer

Both traits now render particles into a singleton body-level
`<div popover="manual">` stage at z-index 99999, derived from the
host's `getBoundingClientRect()` at fire time. Particles escape any
`overflow: hidden` ancestor (modals, drawers, cards), no z-index
wars, no per-trait container. Both traits share the stage via a new
private `confetti-stage.js` helper. Disconnect cleans up the
attribute and the listener; in-flight particles outlive the trait
and self-remove on `animationend` (matching the spec's "disconnect
doesn't yank particles" requirement).

### Changed — `anchor-positioning` uses native CSS Anchor Positioning where supported

On Chromium 125+ and Safari 18.0+, the trait now writes
`anchor-name` on the anchor + `position-anchor` / `position-area` on
the host, calls `host.showPopover()` to promote into the top layer,
and lets the browser handle reflow on its own (no JS scroll/resize
loop). The v0 JS-positioning path remains for Firefox + Safari
< 18.0 as a fallback. The host carries
`data-anchor-mode="native"|"fallback"` so consumers and DevTools
sessions can see which path actually ran. Public attribute API
(`data-anchor`, `data-anchor-placement`, `data-anchor-gap`) is
unchanged.

### Changed — `count-up` adds Intl.NumberFormat + prefix/suffix/decimals/locale

The trait now formats the displayed value via `Intl.NumberFormat` so
`2438000` renders as `2,438,000` by default. Four new optional
config attrs:
- `data-count-prefix` — string prepended to the formatted value
  (e.g., `$`).
- `data-count-suffix` — string appended (e.g., `%`).
- `data-count-decimals` — fraction-digit count for currency
  (`$1,234.56`) or percentage (`12.5%`) cases. Default 0.
- `data-count-locale` — BCP 47 language tag override.

Reduced-motion path also formats via the formatter so the snapped
value matches the animated path's final frame.

### Changed — `noise-texture` strength attribute + visibility default

Earlier defaults stacked SVG rect opacity (0.08) × overlay div
opacity (0.5) = effective 0.04, visually no grain. New defaults: rect
opacity = 0.15, overlay's CSS opacity removed (single multiplier).
New `data-noise-strength` config attr (0..1, default 0.15) lets
consumers dial intensity without touching the trait source.

### Changed — Trait helpers dropped underscore prefix

`_motion.js` → `motion.js`, `_test-helpers.js` → `test-helpers.js`,
`_announcer-stage.js` → `announcer-stage.js`, `_confetti-stage.js`
→ `confetti-stage.js`. The catalog walker
(`scripts/build/traits-catalog.mjs`) switched from filename-prefix
detection to an explicit `HELPERS` Set so the convention can keep a
uniform naming style. Internal consumers updated; no public API
change.

### Changed — `--a-section-weight` flipped semibold → normal

Visible on every `<h2>` and `<h3>` consuming the `section` role
token. Lighter weight reads less heavy at the doc-section level —
closer to a label than a title — and pairs better with the `heading`
role above it (already at semibold). Single-line CSS change at
`packages/web-components/styles/typography.css:259`. No API surface
change; pure visual.

### Fixed — `inertia-drag` / `tossable` / `draggable` translate-read

`parseTranslate()` now reads `style.translate` first (the trait
writes to it), falling back to `style.transform` matrix. Same class
as the v0.2.3 `spring-animate` fix (commit `07fb2841`). Pre-fix:
every drag after the first read the wrong CSS property and got
`m41 = 0`, so subsequent drags jumped the host back near the origin
instead of moving from its current position.

### Fixed — `drag-ghost` rect-snap pixel-identical clone

Position the cloned ghost over the host's exact viewport rect at
`position: fixed; left/top/width/height; box-sizing: border-box`,
copy host's `transform`/`transform-origin`, then call
`setDragImage(ghost, clickX, clickY)`. Pre-fix: ghost was offscreen
with default content-box width/height; for content-box hosts, the
ghost rendered larger than the host and the offset coordinates
landed at the wrong bitmap pixel — the ghost drifted off the cursor
as the click moved away from the host's top-left corner. Hide via
`visibility: hidden` after the synchronous `setDragImage` snapshot.

### Fixed — `<list-item-ui>` `[data-active]` CSS state

`list.css` — added `[data-active]` block inside
`@scope (list-item-ui)`: `--a-accent-muted` background,
`--a-accent-strong` text + icon, 2px accent-strong inset rail.
Reuses the same token pair as the existing
`list-ui[selectable] [aria-selected="true"]` rule so the two
selection mechanisms paint consistently. Closes the keyboard-nav
demo "broken" report at v0.2.3 — events fired correctly, the active
state had no visible style.

### Fixed — `list-item-ui` empty items collapsed to 0 height

Empty `<list-item-ui></list-item-ui>` (no `text`, no `icon`, no
children) rendered with 0 height and silently disappeared from the
layout. Add a 24px floor via `min-height: var(--a-space-6)` so empty
items always show a visible row. Aligns with WCAG 2.2 SC 2.5.8
(Target Size, Minimum) for `list-ui[selectable]` where the row is
the click target.

### Fixed — `<button-ui type="reset">` triggers `form.requestReset()`

Previously a no-op (only `type="submit"` was handled). Pre-fix:
`<button-ui type="reset">` clicks fired `press` but never the form's
`reset` event, so the `resettable` trait's reset chain dead-ended at
the click.

### Fixed — `<input-ui>` / `<textarea-ui>` programmatic value sync

`render()` now syncs `this.value` into the inner contenteditable
when they diverge (preserving caret if already in sync). Pre-fix:
programmatic `host.value = "x"` updated the reactive prop but not
the visible content, so the `resettable` trait's snap-back wrote a
value the user couldn't see.

### Fixed — `resettable` discriminates checkable controls

Previously wrote `host.value = initialValue` for everything, which
silently no-op'd on `switch-ui`/`check-ui`/`radio-ui` (whose primary
state is `checked`, not `value`). The trait now detects boolean
controls (native `[type=checkbox|radio]` + custom elements
declaring `checked`) and resets via `host.checked` for those.

### Fixed — `confetti-burst` fires on press, not just connect

Refactored `setup()` to extract `fireBurst()` and install it as a
`press` listener (composes with the `pressable` trait's published
event surface). Old behavior fired exactly once at
`connectedCallback` and was inert thereafter. Multiple in-flight
bursts tracked in a `Set` so rapid clicks don't clobber each other.

### Fixed — `droppable` self-recursion (RangeError stack overflow)

The trait listened for `dnd-drop` on `document` AND dispatched
`dnd-drop` on the host with `bubbles: true`. The bubbled event
re-reached `document` → re-fired the same handler → recursion to
stack overflow. Renamed the host-emitted event to `dnd-drop-receive`
(distinct from the document-level signal). `droppable-collection`
also updated to listen for `dnd-drop-receive` on its host. Memory
entry filed at `feedback_event_name_collision_recursion.md`.

### Fixed — Icon registry: ~25 new aliases across multiple commits

`core/icons.js` aliases mapping consumer-friendly names to canonical
Phosphor names. Suppresses repeated `[icon-ui] Icon "X" not found`
warnings on every page that referenced these:

- `bold` / `italic` / `underline` → `text-b` / `text-italic` /
  `text-underline`
- `reset` / `undo` → `arrow-counter-clockwise`; `redo` →
  `arrow-clockwise`
- `success-checkmark` → `seal-check`; `circuit` → `circuitry`;
  `contrast` / `theme` → `circle-half`; `dark-mode` → `moon`;
  `light-mode` → `sun`
- `alert-triangle` / `alert-octagon` / `alert-diamond` → Phosphor
  `warning` / `warning-octagon` / `warning-diamond`
- `number` / `numeric` → `hash`; `numbered-list` → `list-numbers`
- `circle-warning` → `warning-circle`; `circle-x` → `x-circle`;
  `circle-check` → `check-circle`; `circle-info` → `info`;
  `circle-question` → `question`; `circle-plus` → `plus-circle`;
  `circle-minus` → `minus-circle`
- `fade` / `fade-presence` → `gradient` (from v0.2.3 cut)

---
## [0.2.3] - 2026-05-04

**Lockstep cut + accumulated fixes.** All 8 published `@adia-ai/*`
packages bump 0.2.2 → 0.2.3 per
[`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy).
Patch cut — no breaking changes. The substantive content is
`web-components`-only; the other 7 packages have no source change.

### Changed

- `version`: `0.2.2` → `0.2.3`.
- `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.3`).

### Fixed — `<list-item-ui>` slot layout CSS (2026-05-02)

`components/list/list.css` — added the `@scope (list-item-ui)` track plan that was missing since the element shipped: a 2-column grid with the icon column at `auto` (icon spans rows, center-aligned), the text + description stacked in column 2, and `[slot="content"]` spanning all columns. Tokens (`--list-item-gap-{column,row}`, `--list-item-{desc,icon}-color`, `--list-item-desc-font-size`) match the rest of the list family. Before this fix, icon / text / description rendered inline as a single line of text in flow order — visible on every consumer of the element. CSS-only; no JS or API change.

### Fixed — `traits/confetti-burst.js` fires on press, not connect (2026-05-02)

Refactored the trait's `setup()` to extract a `fireBurst()` function and install it as a listener on the host's `press` event (composes with the `pressable` trait's published event surface). Old behavior fired exactly once at `connectedCallback` time and never again — meaning the element with `traits="confetti-burst"` was inert after the initial mount. New behavior preserves the connect-time burst (so animation-page demos still self-demonstrate) but additionally re-fires on every press; multiple in-flight bursts are tracked in a `Set` so back-to-back clicks don't clobber each other.

### Fixed — Icon registry: `fade` / `fade-presence` aliases (2026-05-04)

`core/icons.js` — added two registry aliases mapping the trait-doc names `fade` and `fade-presence` to Phosphor's `gradient` glyph (the closest visual proxy). Without these the trait detail pages logged `[icons] unknown icon: fade-presence` warnings on every load.

### Fixed — Trait runtime bugs surfaced by user testing on `/site/traits/*` (2026-05-02)

- `traits/spring-animate.js`: read initial position from `getComputedStyle(host).translate` first, then fall back to the transform matrix. The trait writes to `style.translate`, but the read previously parsed `cs.transform`, which is independent of `translate` in the modern CSS model — callers nudging via `style.translate = '100px 0'` were observed at position 0, so the spring never animated.
- `traits/drag-ghost.js`: capture the pointerdown offset within the host and pass it to `dataTransfer.setDragImage(ghost, x, y)`. The old `(0, 0)` offset snapped the ghost to the cursor's top-left corner regardless of where the user grabbed. The ghost clone is also now sized to the host's bounding rect so it doesn't render undersized.

### Changed — Legacy tag-name sweep across nav cluster + 2 docs (2026-05-04)

Removed 11 references to deleted tag names (`<section-nav-item-ui>`,
`<section-nav-group-ui>`) that survived ADR-0015 + nav consolidation
in non-historical files. Source files swept:
`components/nav-item/{nav-item.yaml,nav-item.css,nav-item.a2ui.json}`,
`components/nav-group/{nav-group.yaml,nav-group.css,nav-group.a2ui.json}`.
Description text + cascade-context comments rephrased to drop the
legacy tag names without losing the design rationale ("section variant
doesn't reserve icon space when absent; section-variant groups render
as kicker labels with always-visible children"). Catalog regenerated
to match.

### Changed — `<agent-trace-ui>` STAGE column + detail alignment (2026-05-04)

`agent-trace.css` row track plan tightened so multi-word stage labels
("Rows returned", "Query duration", "Drift vs. SFDC") stay on one line:
`--agent-trace-row-label-col` shifts from a fixed `80px` to
`minmax(7rem, max-content)`, letting the longest label set the track.
The DETAIL column's text and its column header (`[data-trace-aux]`,
`[data-trace-header]:nth-of-type(3)`) are right-aligned so 1-3 word
qualifiers ("warehouse", "reconciled") sit flush with the row's right
edge instead of leaving the column reading as dead width. Per-row
`subgrid` keeps the alignment in lockstep across every row.

---
## [0.2.2] - 2026-05-02

**Lockstep cut + trait coverage 100% + `<traits-host>` wrapper.** All 8 published `@adia-ai/*` packages bump 0.2.1 → 0.2.2 per [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy). Patch cut — no breaking changes.

### Changed

- `version`: `0.2.1` → `0.2.2`.
- `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.2`).
- `traits/` is the surface that gained the new behavior below; `traits/index.js` adds a side-effect import of the new wrapper, `scripts/build/traits-catalog.mjs` flipped to hard-fail.

### Added — Trait test coverage at 100% + `<traits-host>` wrapper (2026-05-02)

Follow-up landing on top of the v0.2.1 trait-system work — closes the two follow-up items from [ADR-0018](../../.brain/adrs/0018-trait-source-of-truth-and-declarative-attribute.md).

- **31 new per-trait behavior test files.** Every trait in `traits/` now has a sibling `.test.js`. Total trait-suite footprint: **44 files / 169 tests** (was 12 / 68). Coverage: **42/42 = 100%** (41 trait files + `traits-host`).
- **`verify:traits` CI gate flipped from soft-warning to hard-fail.** `npm run verify:traits` exits non-zero if any trait file lacks a sibling `.test.js`, with the offending names listed in the failure message. Verified via mutation test: deleting `active-state.test.js` immediately fails the gate; restoring it returns to clean.
- **`<traits-host>` wrapper element** at [`traits/traits-host.js`](./traits/traits-host.js). Tiny pass-through `UIElement` subclass that lets raw HTML opt into declarative trait composition — wrap any markup in `<traits-host traits="pressable scale-press">…</traits-host>` and the named traits attach to the wrapper. Uses `display: contents` so the wrapper has no layout box; children participate in the parent's flex/grid as if the wrapper weren't there. Auto-registered on import of `traits/index.js`. 6 behavior tests covering attribute swap, child event bubbling, and pure-pass-through with no `traits=`.

---
## [0.2.1] - 2026-05-02

**Lockstep cut + 41-trait library overhaul + `<stat-ui>` ad-blocker rename.** All 8 published `@adia-ai/*` packages bump 0.2.0 → 0.2.1 per [`docs/specs/package-architecture.md` § 15](../../docs/specs/package-architecture.md#15-versioning-policy). Patch cut — no breaking changes.

### Changed

- `version`: `0.2.0` → `0.2.1`.
- `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.1`).
- `styles/colors/semantics.css` — neutral-surface border tokens (`--a-canvas-border-{subtle,strong}` + base) shift to lower-numbered scrim levels (1→0, 3→2, 6→4) for a subtler default polarity. Visible on cards/inputs/buttons that consume `--a-canvas-border*`. No public API change.
- `styles/components.css` — `@import` swept to the new `<stat-ui>` filename (see Fixed below).

### Added — Trait system source-of-truth + declarative `traits=` attribute (2026-05-02)

Rolls up under [ADR-0018](../../.brain/adrs/0018-trait-source-of-truth-and-declarative-attribute.md). Consumer-visible additions and one core change to UIElement's lifecycle.

- **`defineTrait()` schema extended.** Now requires `category` (one of nine canonical values, frozen in `define.js`) and `description` (≥ 11 chars). All 40 trait files updated to declare both inline. Build-time throw on missing fields.
- **New `getTrait(name)` export from `traits/define.js`.** Returns the trait factory for a kebab-case name, or null if not yet imported. `getTraitSchema(name)` now reads from the same registry (was a separate map).
- **New `_motion.js` helper module.** `prefersReducedMotion()` cached check + `onReducedMotionChange()` subscriber. Used by 7 motion-bearing traits (`shimmer-loading`, `attention-pulse`, `typewriter`, `count-up`, `gradient-shift`, `ripple`, `confetti`, `confetti-burst`) to bail or render static fallbacks under `prefers-reduced-motion: reduce`.
- **Generated trait catalog at `traits/_catalog.json`.** Single source of truth for downstream consumers (a2ui-corpus, demo pages, MCP `get_traits`). Regenerated by `node scripts/build/traits-catalog.mjs` (added to `package.json` as `build:traits` / `verify:traits` at the repo root).
- **UIElement now reads the `traits` attribute.** `<comp-ui traits="ripple confetti-burst">` works on every UIElement subclass. `traits` is always in `observedAttributes`; `attributeChangedCallback` swaps declarative trait instances live without disturbing static or `addTrait()` instances. Static + declarative coexist; double-apply is prevented.

### Fixed — Trait brittleness bugs surfaced by new tests (2026-05-02)

- **`confetti.js` and `confetti-burst.js` no longer crash without canvas.** Both called `canvas.getContext('2d').clearRect(...)` on null in environments without canvas (SSR, JSDOM, happy-dom). Now null-check `ctx` and degrade gracefully — set the active attribute, return a no-op cleanup. Caught by `_smoke.test.js` connect/disconnect contract test.

### Test surface

- 12 trait test files / 68 tests passing (was 0). Universal contract in `_smoke.test.js`; high-fidelity behavior tests for `pressable`, `focusable`, `validation`, `dirty-state`, `hotkey`, `typewriter`, `count-up`, `shimmer-loading`, `confetti-burst`, `resettable`; declarative-attribute integration in `declarative.test.js`.

### Added — `resettable` trait

- New 41st trait at `traits/resettable.js`. Listens for the host's enclosing `<form>` reset event and restores the host's connect-time initial value; dispatches `reset-applied` event with the initial value in detail. Outside a form: clean no-op. Closes the one phantom-trait claim from the original 2026-05-02 audit that survived as a real behavior gap.

### Changed — UI field hover-bg flattened to default

`--a-ui-bg-hover` was `var(--a-canvas-2-scrim)` (an elevation-2 lift on
hover); it's now `var(--a-canvas-0-scrim)` (same as the default). The
hover affordance for FIELD UI surfaces (`<input-ui>`, `<textarea-ui>`,
`<nav-item-ui>`, `<chat-input-ui>`) is now carried by border-color +
foreground transitions, not bg lift. Reduces hover noise on dense
form surfaces; matches the visual flatness of the default-elevation
canvas.

Affects 4 components (1-line `semantics.css` change). No API change;
no component code change. `--a-ui-bg-active` (canvas-0) and
`--a-ui-bg-disabled` (canvas-1) unchanged — active still drops,
disabled still raises.

### Changed — `<chat-input-ui>` color isolation

The composite host now owns every color the inner `<textarea-ui>`
ships with. Previously, textarea-ui's hover (bg/border/color), focus
(color brightening + redundant ring), invalid focus, disabled (color),
and placeholder treatments all leaked through `<chat-input-ui>` because
each was wired to the generic `--a-ui-*` family with no host-level
override. Result: a hover bg flash on the editable area, a brightened
fg on focus, two stacked focus rings under invalid state, and a
disabled-bg rectangle inside the host's own disabled chrome.

`chat-input.css` now suppresses every textarea-ui state rule and
re-paints them with `--chat-input-*` tokens:

- `--chat-input-fg` (was implicit via `--a-ui-text`).
- `--chat-input-bg-hover`, `-border-hover`, `-fg-hover` — default to
  `--a-ui-bg-hover` / `--a-ui-border-hover` / `--a-fg`, matching
  `<input-ui>`'s hover affordance. The host paints the lift on
  `chat-input-ui:not([disabled]):hover`; the inner textarea-ui's own
  hover bg/border is forced transparent so the alpha doesn't compound.
- `--chat-input-bg-disabled`, `-fg-disabled` — inner-text disabled
  color is owned here; host-level disabled chrome unchanged.
- `--chat-input-placeholder-fg` — was leaking `--a-ui-text-placeholder`.

Hover lives outside the `@scope` block (Safari 17.x bug — `:scope …
[descendant]:hover` doesn't match in Safari 17.x; same workaround
input.css and textarea.css already use). Focus / invalid / disabled /
placeholder rules stay inside the `@scope` and use the
`:scope textarea-ui:STATE [slot="text"]:STATE` (0,4,1) pattern to beat
textarea.css's matching rules (0,3,0).

`--chat-input-duration` shifted from `--a-duration` to `--a-duration-fast`
(120 ms) to match `<input-ui>`'s transition speed; the host now
transitions `background`, `border-color`, `color`, and `box-shadow`
together — same 4-property surface `<input-ui>` and `<select-ui>` use —
so hover / focus / invalid / disabled all interpolate as one.

Inner `[slot="text"]` color overrides changed from explicit
`var(--chat-input-fg)` to `color: inherit` so the host's transitioned
`color` cascades to the contenteditable. The host's `:scope[disabled]`
now sets `color: var(--chat-input-fg-disabled)` directly; the inner
inherits, and the disabled-fg shift fades on the host's `color`
transition rather than snapping. No JS changes; no API changes.
## [0.2.0] - 2026-05-02

**Lockstep cut.** All 8 published `@adia-ai/*` packages now share one
version, governed by [`docs/specs/package-architecture.md` § 15
(Versioning Policy)](../../docs/specs/package-architecture.md#15-versioning-policy)
and enforced by `npm run check:lockstep` + the
`lockstep-versioning` job in
[`.github/workflows/docs-lint.yml`](../../.github/workflows/docs-lint.yml).

Big version jump (0.0.34 → 0.2.0) signals the lockstep era starts here;
this is **not** a breaking change to the component surface — every tag
+ prop + event from 0.0.34 still works identically. The jump aligns
this package with the highest sibling version (a2ui-mcp@0.1.3 → 0.2.0)
and avoids the `^0.0.x` caret-lock trap that bit `a2ui-mcp@0.1.3 →
a2ui-corpus@0.0.6` (the new `^0.X.0` ranges work properly under npm
pre-1.0 semver — see § 15 for the math).

### Changed

- `version`: `0.0.34` → `0.2.0`.
- `dependencies["@adia-ai/a2ui-utils"]`: `^0.0.2` → `^0.2.0`.

### No source changes

This package's source tree is byte-identical to 0.0.34. The cut bumps
version and the internal dep range only. Consumers who want the
substantive 0.0.34 work (the `<step-progress-ui>` primitive) get it
via either 0.0.34 or 0.2.0 — both ship the same component code.


---

## Pre-0.2.0 history

The 0.0.x release series (0.0.34 → 0.0.1, 2026-04-21 to 2026-05-02) is
archived in [`CHANGELOG-pre-0.2.0.md`](./CHANGELOG-pre-0.2.0.md). That
file covers every entry shipped before the lockstep cut.
