# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Packages: `@colletdev/core`, `@colletdev/react`, `@colletdev/vue`, `@colletdev/svelte`, `@colletdev/angular`, `@colletdev/react-native` share a single version. `@colletdev/docs` is versioned independently.

---

## [0.6.1] — 2026-08-26

### Fixed: one out-of-range number could blank every component on the page

`loading` on `<cx-table>` is a count. A non-finite or huge float saturated to
`usize::MAX` (4,294,967,295 on wasm32), reached `for _ in 0..count` building a
String, aborted the allocator and **trapped the WASM module** — which flips the
runtime's health flag for the whole page. Every other Collet component rendered
empty from that moment, and already-rendered ones froze at their last state.

The trigger is ordinary consumer arithmetic, not an attack:
`<Table loading={total / pageSize} />` with `pageSize` of 0 is `Infinity`, and
the wrapper forwards `String(value)`. Counts are now clamped; `NaN` and
negatives read as zero.

### Fixed: anything derived from `value` silently froze

A fast path wrote the inner `<input>.value` directly and skipped the re-render,
to protect cursor position and IME state. The text stayed right — so it looked
fine — while everything computed FROM the value stopped updating: the slider's
fill and readout, the text input's clear button (unreachable through normal
typing), the chat composer's character counter, the search bar's clear
affordance. Each corrected itself on the next unrelated prop change, which made
it read as flaky rather than broken.

The bypass is removed. What it was protecting is now handled properly:
`_injectHtml` restores focus and selection by identity, and `_scheduleRender`
defers while an IME composition is open — neither existed when the bypass was
written. A test pins that the caret does not jump.

### Fixed: unescaped consumer strings reached `class` and `style` attributes

`height` on `cx-scrollbar`, `cx-scroll-area` and `cx-chat` was interpolated raw.
On the same tags, `id` was already escaped — the risk was understood and one
field was missed. A value containing a quote closed the attribute; a breakout
payload produced a live `<img onerror>` inside the shadow root. `height` is
documented as a CSS length or Tailwind class, which is exactly the kind of value
an application binds from configuration.

### Smaller: −4.9% from nine one-line changes

`to_lowercase()`/`to_uppercase()` pull the full Unicode special-casing machinery
into the binary — `ß`→`SS`, Greek final sigma, the iterator types — 35 KB raw
for nine sites that need none of it. Slug generation and an ASCII needle now use
ASCII folding; the search shortcut badge uses CSS `text-transform`, which is
**locale-aware where Rust's is not**. `aria-keyshortcuts` keeps Rust
uppercasing, because CSS cannot transform an attribute value.

Component WASM: 216,320 → **200.9 KB brotli**. Budget headroom 98% → 94%.


Two defects shipped in 0.6.0 that broke React 19 consumers. Both were found by an
independent reviewer within an afternoon of the release, and both were live on
npm. If you installed 0.6.0 with React, upgrade.

### Fixed: controlled overlays did nothing in React 19

Six components expose an `open` attribute AND an `open()` method — `cx-dialog`,
`cx-drawer`, `cx-command-palette`, `cx-collapsible`, `cx-sidebar`, `cx-top-bar`.

React 19 decides between setting a property and setting an attribute with
`key in domElement`. The method makes that true, so React assigned a string
*over the method*. Measured in a browser:

```
typeof el.open  before: "function"    the method
typeof el.open  after:  "string"      method destroyed
getAttribute('open'):   null          attribute never set
```

Both documented ways to open a dialog were dead at once: `<Dialog open>` set
nothing, and the escape hatch `ref.current.open()` threw because `open` was now
a string. Attributes colliding with an imperative method are now routed through
`setAttribute`.

### Fixed: `disabled={false}` disabled the control, permanently

`disabled` and `required` are setters on `CxFormElement`, so React assigned the
serialized string `"false"` — which is truthy. Every one of the ten
form-associated components became un-interactive whenever a consumer wrote
`disabled={false}`, on any mount occurring after `init()` resolved.

The setters now apply the same canonical rule the attribute path uses: absent or
null → off, the string `"false"` → off, anything else present → on. Fixing the
setter rather than only the routing means the element is correct no matter which
framework writes to it, and how.

### Why these survived

Vue (the `^` attribute prefix) and Angular (`[attr.x]` host bindings) were both
already immune. The hazard was understood and defended in two of four transports
— which is how it passed review: it looked handled.

The React end-to-end test for exactly this dialog behaviour asserted
`shadowRoot.innerHTML.length > 20`. A **closed** dialog satisfies that, and the
whole block sat inside an `if (isVisible())` guard that allowed it to pass with
zero assertions. It now asserts `<dialog>.open`.

Both fixes are pinned by parity check 24j and by falsifiability cases
`method-name-collision` and `bool-setter-coercion` — each verified to fail when
the fix is reverted.

## [0.6.0] — 2026-08-26

The release where the component API stopped being *inferred* and started being
*compiled*.

### The headline: the manifest is emitted by the compiler

Every framework wrapper, every TypeScript type and every parity check is derived
from `custom-elements.json`. That manifest used to be produced by reading Rust
source with regular expressions — a script matched `cfg.string("label")` out of
each adapter and inferred the public API from what it saw.

`cx_fields!` (`crates/wasm-api/src/spec.rs`) replaces it. Each component declares
its fields once, and the macro emits the config struct, its reader, AND a
`&[FieldSpec]` — three things that cannot disagree because they are one
declaration. 60 components, 706 fields, 163 with a Rust-declared default.

**It found a real defect on its first run.** `cx-text-input.rows` is a genuine
field (`rows: u8_or = 3`) driving `TextInput::multiline()`. The regex never saw
it, so **no wrapper in any framework exposed it** and a multiline input was
locked at 3 rows. In Angular, adding it to a template was a build error
(`NG8002: Can't bind to 'rows'`) for a field that had existed all along.

The spec tables are behind a build-time Cargo feature, so consumers ship zero
spec bytes.

### Three frameworks were overriding your defaults

The 163 recorded defaults made a defect class visible that had been invisible in
all three wrappers at once — each with a different mechanism, each with the same
result: **the wrapper substituted a value you never passed.**

- **Vue** — `Boolean` in a prop's type list makes an absent prop resolve to
  `false`, not `undefined`. `<ToggleGroup :items="[{id:'bold', pressed:true}]" />`
  wrote `value="false"`, and Rust reads a non-empty `value` as a *controlled*
  selection — so every item's own `pressed` was discarded and nothing rendered
  pressed. `SearchBar.loading` had the same shape.
- **Svelte** — `$bindable(0)` supplied a fallback the renderer never asked for.
  `<Slider label="Volume" />` rendered hard left instead of centred. Worse,
  binding to undefined state (`let v = $state<string>()`) threw
  `props_invalid_value` from inside a library file, aborting mount, on 7 of 11
  bindables.
- **Angular** — the ControlValueAccessor's fallback was a hand-typed `0` against
  the renderer's `50.0`. An empty `FormControl` rendered the slider hard left in
  Angular and centred everywhere else; nothing disagreed, because the control's
  value is null either way. Only the pixels were wrong.

All three are fixed at the generator, and each is gated so it cannot return.

### Live elements inside WASM-rendered regions

`TableCell.slot`, `TableRow.detail_slot`, `TabItem.content_slot` and
`AccordionItem.panel_slot` make Rust render `<slot name="…">`. You put your own
element in light DOM with a matching `slot` attribute and the browser projects it
in — with its listeners, state and identity intact, because the node is never
re-created.

This is browser-native, so it works identically in every framework and in plain
HTML. React auto-projects JSX; Svelte accepts a snippet; Vue accepts a template
slot.

Previously, passing JSX to those fields rendered **nothing**. The function meant
to handle it, `serializeContent`, was fully written, documented, and never called
from anywhere. `renderToStaticMarkup` would have been the wrong repair: it
returns markup, and markup has no event listeners — the button would have
rendered, looked correct, and done nothing forever.

### Smaller for everyone

`pulldown-cmark` left the component binary. Markdown now arrives through a
separately-shipped binary that was already being lazy-loaded — the old build
carried both copies.

| | brotli |
|---|--:|
| 0.5.1 as published | 268,827 B |
| 0.6.0 | 216,591 B |
| **net** | **−52,236 B (−19.4%)** |

The compiler-emitted manifest costs +12.4 KB of that; the markdown removal
returns −64.6 KB.

**One deployment note worth more than the number:** default nginx does not
compress `application/wasm`. Misconfigured, your users download 710 KB instead of
212 KB. Vercel, Netlify, Cloudflare and CloudFront handle it by default.

### Also in this release

- **Localization** — `setStrings()` / `stringKeys()`, now re-exported from all
  four framework packages, so translating no longer needs a second import from
  core. Scope is honest: 3 keys, DatePicker only.
- **`@colletdev/core` has zero runtime dependencies.** `tailwindcss` was a
  build tool sitting in `dependencies`; it is gone.
- **CSS is published** — `dist/tokens.css`, `tokens-shadow.css`,
  `cx-utilities.css`, `syntax.css`, `fonts/`.
- **`@layer collet`** wraps library defaults, so an ordinary consumer selector
  wins without `!important`.
- **Canonical boolean rule** everywhere: absent → the field's default; exactly
  `"false"` → false; anything else present → true.
- **`mobile-header` / `mobile-footer` slots** on Sidebar. A slot assigns a child
  exactly once, and `header`/`footer` were consumed by the desktop sidebar —
  which is hidden on mobile — so branding was correctly slotted, present in the
  DOM, and invisible on every phone.
- **`rows` on `cx-text-input`**, reachable for the first time.
- **154 props now carry `@defaultValue`** in editor autocomplete.
- **Angular's published bundle is built by the pipeline.** It previously shipped
  whatever FESM artifact sat on the publisher's disk; `_cxMemo` is used by 29
  source files and appeared in it **zero** times. Angular consumers were
  installing 0.5.x and receiving far older code.

### Fixed

- Structured-prop accessors are installed per component, so `key in element`
  answers truthfully. React 19 was routing `selected`/`expanded` down the
  property path on components where they are plain attributes — `expanded={false}`
  arrived as `'"false"'` and read as **true**.
- JSON parsing of attributes is gated by declaration. Any string starting with
  `[` or `{` used to be parsed, so a code sample beginning `[cx]` vanished.
- File attachments worked in no framework build: `__cxFileApi` was assigned in
  exactly one file — the SSR gallery runtime npm consumers never load.
- `disabled={false}` rendered disabled on all 10 form-associated elements.
- Collapsed table row details were 16px tall, not 0.
- Collapsed activity groups were invisible but still tabbable (WCAG 2.4.3).
- Five tags rendered a duplicate attribute; a parser keeps the first, so every
  table lost its corner radius and horizontal scrolling.
- The SSR browser-support warning printed on every server render.
- React's `elements.d.ts` never reached `dist/`, so `<cx-button />` was a type
  error while the docs called it type-checked.

Consumer-reported defect wave. Every item below was hit in real use by the
library's first production content site and verified against the shipped
package or the library source — none are speculative.

### Added: React — live React nodes inside WASM-rendered regions

- **`TableCell.html`, `TableRow.detail`, `TabItem.content` and
  `AccordionItem.panel_content` now accept a `ReactNode`.** Passing JSX
  previously rendered **nothing**: those fields reach Rust as part of a config
  object, which asked for a string and got a React descriptor.

  The node is never serialized. It stays in React's tree, rendered into the
  element's light DOM with a `slot` attribute, while the config carries the slot
  *name*; Rust renders `<slot name>` at that position and the browser projects
  the live node in. `onClick`, `useState`, refs and context all keep working,
  because from React's point of view nothing unusual happened.

  `renderToStaticMarkup` was the obvious alternative and the wrong one: it
  returns markup, and markup has no listeners. `<Button onClick={save} />` would
  have rendered, looked correct, and done nothing forever.

  Slot names derive from stable identity (`row.id`, `item.value`, `item.id`),
  never a counter — inserting a row at the top must not renumber every slot and
  remount every projected node.

### Added: React — DOM attributes and native events pass through

- **Any prop a wrapper does not own is now forwarded verbatim to the host
  element**: `id`, `data-*`, `aria-*`, `title`, `tabIndex`, `slot`, `role`, and
  native React handlers such as `onMouseEnter`. Previously `<Button
  data-testid="save" />` was a TypeScript error and, had it compiled, would have
  been dropped — so a Collet component could not be given a test hook, an
  external ARIA relationship, or a `slot` for composition. Types come from
  `React.HTMLAttributes<HTMLElement>`, so typos are still compile errors and
  `children` is still rejected by components with no default slot.

### Fixed: React 19 assigns properties where we needed attributes (P0)

- **`selected`, `expanded` and `texture` reached the element JSON-quoted.**
  react-dom 19 chooses between property and attribute with a bare `in` test
  (`key in domElement ? el[key] = value : setAttribute(...)`), and
  `@colletdev/core` installs structured-prop accessors on the *shared*
  `CxElement` prototype — so those three names are properties on **every**
  element, including the five where they are plain string/boolean attributes.
  React took the property path and the structured setter JSON-stringified the
  value: `<RadioGroup selected="b">` arrived as `"b"` **with quotes** and no
  attribute at all (no radio selected), `<DatePicker selected="2026-01-15">` the
  same, and `<Fab expanded={false}>` arrived as `'"false"'` — which is not the
  literal `'false'` the boolean rule matches, so an explicit **false read as
  true** and the FAB published `aria-expanded="true"`. Affected `cx-radio-group`,
  `cx-date-picker`, `cx-fab`, `cx-activity-group` and `cx-card`; these five
  attributes are now written with `setAttribute` in a layout effect.

  Note: a routed attribute is written after mount, so it is absent from
  server-rendered HTML and appears on hydration.

### Fixed: React lifecycle and referential identity

- **A callback `ref` is no longer re-attached on every render.** The transport
  used `useImperativeHandle` with no dependency array, which re-runs on every
  commit and nulls the forwarded ref first — so a consumer's callback ref was
  invoked `(null, el, null, el, …)` once per render forever, and any React 19
  ref-cleanup it returned ran just as often. Replaced with a merged callback ref
  that fires only when the node or the ref identity changes, and that propagates
  a React 19 cleanup function.
- **Event listeners are wired before any property is assigned.** They were
  attached in a passive effect, which runs *after* the microtask in which an
  element queues and dispatches — so anything emitted in that window was
  missed. Both are now layout effects, listeners first.
- **The transport no longer writes a ref during render.** `latestProps.current =
  props` in the render body is concurrent-unsafe (a discarded render still wrote
  it) and makes the React Compiler bail out of the component.
- **Layout effects are isomorphic.** `renderToString` no longer warns once per
  Collet component.
- **Node projection is memoised** on the structured prop, so a consumer who
  keeps `rows`/`items` referentially stable pays nothing for it.
- **A projecting component that also declares a `<slot>` no longer drops its
  projected nodes.** The two code paths were mutually exclusive in the
  generator; they are now one.

### Changed: React — `ColletProvider` reports init failures

- **`useCollet()` now returns `{ ready, error }` alongside the config**, and
  `<ColletProvider onError>` is called when `init()` rejects. The provider used
  to `.catch(() => setReady(true))`, so a consumer whose WASM 404'd behind a CDN
  rule got an app of empty custom elements and no way to find out why — every
  `<cx-*>` renders as an inert unknown element until it is upgraded. The failure
  is also logged. Children still render; the provider must not blank the app,
  but it must not stay quiet either. Additive: existing `useCollet()` callers
  are unaffected.

### Fixed: Vue — an explicit `false` did the opposite of what it said (P0)

- **`:readonly="false"` made the field read-only.** Vue's `patchAttr` routes the
  names HTML defines as *special* boolean attributes (`readonly`, `novalidate`,
  `formnovalidate`, `itemscope`, `allowfullscreen`, `ismap`, `nomodule`) through
  `includeBooleanAttr()`, where the string `'false'` is truthy — so the wrapper
  set `readonly=""`, the HTML spelling of "on". Affected `TextInput`,
  `Autocomplete`, `DatePicker`, `SearchBar` and `TagInput`, in the browser, in
  every release that had the prop.

- **Server-rendered `<Dialog :open="false">` shipped `<cx-dialog open>`.** The
  same rule, one layer out: `ssrRenderDynamicAttr()` applies it to the FULL
  boolean-attribute list, so `disabled`, `open`, `checked`, `required`,
  `multiple` and `muted` all rendered as bare, present attributes when the
  consumer had explicitly passed `false`. Under Nuxt that meant a modal open in
  the HTML before a byte of JavaScript ran, corrected only once hydration caught
  up — and never corrected at all without JS.

  Both are one fix: `buildColletAttrs` now emits the JS boolean `false` instead
  of the string. It takes the "absent" branch in both of Vue's special cases and
  still stringifies to `"false"` for every other attribute name, which is what
  the 22 `bool_or(_, true)` Rust fields need in order to be switched off.
  Parity Check 49 pins the value so the two spellings cannot be confused again.

### Added: Vue — live Vue components inside WASM-rendered regions

- **Every wrapper now forwards children.** The ~40 components that declare no
  Shadow DOM slot rendered `h(tag, attrs)` with no third argument and silently
  discarded anything nested inside them — including `<Table>`, `<Tabs>` and
  `<Accordion>`, the three components slot projection exists for. Projection is
  a browser feature that needs no wrapper support, and Vue was the one framework
  that could not use it.

- **Any named slot that is not a declared Shadow DOM slot is projected into
  light DOM under that name.** Name a slot in the config
  (`cells: [{ slot: 'cell-r1-actions' }]`) and fill it from the template
  (`<template #cell-r1-actions>`): the component stays in Vue's tree with its
  handlers, reactive state, refs and identity intact, while the browser displays
  it inside the cell. Applies to `TableCell.slot`, `TableRow.detail_slot`,
  `TabItem.content_slot` and `AccordionItem.panel_slot`. The plain form — a
  child carrying its own `slot` attribute — works too.

### Fixed: Vue — in-place mutation of a structured prop was a no-op

- **`rows.push(row)` updated nothing.** `installStructuredProperties()` opens
  with `if (previous === value) return`, which is right for React, Svelte and
  Angular, whose contract is a fresh object per update. Vue's reactive proxy
  keeps the same identity across a mutation, so the element was handed the
  reference it already held and kept rendering the old row count while
  `el.rows.length` already read the new one. Structured props are now detached
  from Vue's reactivity before assignment, which also stops the element's
  `JSON.stringify` from walking a live proxy inside our `watchEffect` and
  subscribing the effect to the whole structure. `el.rows` is now structurally
  equal to, not identical with, the array you passed.

### Fixed: Vue — `@colletdev/vue/nuxt` was not in the package

- **The Nuxt module resolved to a file no build produced.** `package.json`
  exported `./nuxt` → `dist/nuxt.js` while `tsconfig.json` EXCLUDED
  `src/nuxt.ts` (to dodge the unresolvable `@nuxt/kit` and `#imports`
  specifiers). `dist/` is gitignored, so the file existed only as a months-old
  artefact on the machine that once compiled it. The Nuxt surfaces are now
  declared as ambient types, the module is compiled and type-checked, and parity
  Check 51 fails the build if any declared export subpath has no source inside
  it.

- **Custom-element registration now uses Nuxt's supported option.** The module
  mutated the Vite Vue plugin's private `api.options` from a `vite:extendConfig`
  hook — private state, matched by the plugin's internal name, and a silent
  no-op on webpack or rspack. It now sets
  `vue.compilerOptions.isCustomElement`, which Nuxt forwards to every builder,
  and composes with any predicate the app already set.

### Fixed: Silently-wrong behavior (P0)

- **Overriding design tokens on `:root` now works in light mode.** Collet's own
  rule (`html[data-theme="light"]`, specificity `(0,1,1)`) outranked the
  documented `:root` override `(0,1,0)`, so a consumer's light palette was
  ignored regardless of load order — while their dark palette applied, because
  `:root[data-theme='dark']` reaches `(0,2,0)`. All library defaults in
  `tokens.css` are now wrapped in **`@layer collet`**, so any unlayered consumer
  rule wins by construction, with no `!important` and no specificity counting.
  `@font-face`, `@property`, `@keyframes` and `@view-transition` stay unlayered.
  The reduced-motion safety net is *strengthened*: important declarations invert
  layer order, so it now outranks even a consumer's `!important`.
- **`disabled={false}` no longer disables the component.** Button, FAB,
  FileUpload and CommandPalette read `disabled` with `hasAttribute()`, which is
  `true` for the literal `disabled="false"` every wrapper emits for an explicit
  `false`. The component rendered enabled and silently swallowed every click.
- **`open={false}` no longer renders overlays permanently open.** Dialog, Drawer,
  Sidebar, TopBar, **Collapsible** and **CommandPalette** intercepted the `open`
  attribute and tested presence (`newVal !== null`), returning before the base
  class's `"false"` coercion. A declaratively-closed dialog painted over the
  page. Same defect at mount, via `hasAttribute('open')`.
- **`<cx-collapsible>` now clears `inert` when it opens.** The panel was marked
  `inert` while collapsed (correct) but never un-marked, so a reopened panel was
  visible, sized and `aria-expanded="true"` while being completely dead to
  input. `inert` cannot be overridden from CSS, so consumers had no workaround.
  Fixed in both the packaged element and the SSR gallery behavior.
- **Structured props no longer reset the component on every render.** All four
  wrapper runtimes assigned array/object props unconditionally; consumers pass
  inline literals, so a fresh reference arrived each render and defeated the
  element's reference-equality setter guard. For components that emit on
  interaction this closed an invisible loop — event → `setState` → re-render →
  reset → event — and a carousel with 4+ slides could not be navigated past the
  first. Wrappers now compare **by value** (`Object.is` fast path, then a
  short-circuiting structural walk, scoped to manifest-flagged structured props;
  deliberately *not* a blanket `JSON.stringify` diff, which would be its own
  performance trap on exactly those props). Vue compares against a snapshot so
  its `deep: true` reactivity keeps working; Angular routes structured host
  bindings through a value-stable memo so its own dirty check skips the write.

### Fixed: Lifecycle and registration (P1)

- **`init()` is now idempotent and concurrency-safe.** Re-registering a defined
  element threw `NotSupportedError` from inside `Promise.all`, which aborted
  every *remaining* registration and left the page half-upgraded with no error
  surfaced. Calling `init()` while a framework provider had also called it was
  enough to trigger it, nondeterministically.
- **Elements added while registration was in flight are no longer missed.** The
  MutationObserver started *after* the registration await, so anything mounted
  during that window was never upgraded — which is why the set of defined
  elements differed across loads of the same build, and why a carousel could
  render at 0 px height. The observer now starts first, with a re-scan after.
- **`ColletProvider` accepts `components`** and initialises in a layout effect,
  before first paint, instead of after it.
- **`init()` registers `<cx-theme>`.** `defineCxTheme()` was exported but called
  nowhere, so the element every framework provider wraps the app in never
  upgraded — no `display: contents`, no `color-scheme`.

### Fixed: Contract drift — wrappers were missing real API (P1)

Eleven events were dispatched by element source while absent from
`custom-elements.json`, so **no wrapper exposed them**:

- `cx-sidebar` → `cx-toggle` (`onToggle`, `SidebarToggleDetail`)
- `cx-search-bar` → `cx-clear` (`ClearDetail`)
- `cx-scroll-area`, `cx-chat` → `cx-scroll-end` (`ScrollEndDetail`)
- `cx-table` → `cx-resize` (`ResizeDetail`), `cx-scroll` (`ScrollDetail`)
- `cx-top-bar` → `cx-crush` (`CrushDetail`), `cx-move` (`MoveDetail`)
- `cx-chat` → `cx-focus`, `cx-blur` (`FocusDetail`), `cx-keydown` (`KeyboardDetail`)

- **`separatorsAfter` has one contract again** — `number[]` on both TopBar and
  Sidebar. Sidebar was typed `string` while its own JSDoc described "an array of
  group indices"; passing the documented shape failed silently (`"0"` → 0
  separators, `[0]` → 2).
- **Mobile slots accept framework nodes.** `mobile-leading` / `mobile-trailing`
  are real `<slot>` elements in the Rust renderer but were typed as string
  attributes, so passing JSX serialized to `[object Object]`. They are now
  declared slots on TopBar and Sidebar, and slot props are camelCased
  (`mobileLeading`, `mobileTrailing`).
- **Sidebar's mobile string props are no longer discarded.** `header` and
  `mobileActions` were dropped entirely when slotted — which the packaged
  element always is — so setting them did nothing. They now render as slot
  fallback content, matching TopBar.

### Fixed: Events and accessibility

- **`cx-click` no longer swallows a link's navigation.** The 150 ms action
  cooldown suppressed the second `cx-click` of a double-tap on a link-mode
  Button, so consumer interception never ran and `preventDefault()` was never
  called — the browser performed a full page navigation. The cooldown no longer
  applies to elements that carry an `href`.
- **Link-mode Button keeps its native link role.** It set `role="button"` on a
  real `<a href>`, removing it from screen-reader link lists and promising Space
  activation the browser does not provide — contradicting the component's own
  `keyboard_pattern()`, which reports `Link`.
- **`FileChangeDetail.files` carries real `File` objects**, not a
  `{name,size,type}` projection. Reading bytes previously required reaching into
  the shadow root for `input[type=file].files`. `File` structurally satisfies the
  old shape, so existing code keeps compiling.

### Changed

- **The jsdelivr CDN fallback for the WASM binary is now opt-in** via
  `init({ cdnFallback: true })`. A UI library should not fetch executable code
  from a third-party host by default. A strict CSP (`connect-src 'self'`) blocked
  it regardless; prefer copying the binary into `public/`.
- **Card accepts `variant="outline"`**, the spelling every other component uses.
  `"outlined"` remains valid indefinitely.

### Fixed: Form values — the write path was inert (P0)

`setValue()`, Vue `v-model`, Svelte `bind:value` and Angular's reactive-forms
`writeValue` all target a `value` attribute that **Select, Autocomplete,
DatePicker and RadioGroup never observed** — their state lives under
`selected`. So every one of those writes silently did nothing, and `getValue()`
returned `''` even after the user had picked an option. `CxFormElement` carried
a comment promising per-component overrides that were never written.

- `value` is now a true alias of each element's real state attribute, live on
  read, so one code path serves the raw element, all four frameworks, and the
  imperative API.
- **`<Checkbox>`: `el.checked = false` used to CHECK the box.** The setter sent
  `''`, which is the attribute spelling of "present with no value" and
  normalises to `'checked'` — and `el.checked = false` is exactly what Angular's
  ControlValueAccessor writes to clear a field.
- **`<RadioGroup>` submitted the wrong value.** The form value was read from the
  first `<input>` in the group rather than the checked one, and user selections
  never updated the element's own state.
- **`<Switch>`** had no `checked` accessor (Checkbox did), so Angular's CVA was
  writing to an inert expando property.
- **`<Slider>`** never recorded drag results, so `getValue()` reported the last
  programmatic value and any unrelated re-render snapped the thumb back.
- **`<SearchBar>`** declared `getValue`/`setValue`/`clear` on every framework's
  ref type while implementing none of them — it extends `CxElement`, not
  `CxFormElement`, so it inherited nothing and the calls threw.

New gate — **Check 40: every `IMPERATIVE_METHODS` entry must exist on the
element** (own, inherited, or installed). That is what caught SearchBar, and it
closes the general "typed ref promises a method that throws at runtime" class.

### Fixed: Packaging — the CSS was never published

- **`tokens.css`, `tokens-shadow.css`, `cx-utilities.css`, `syntax.css` and the
  self-hosted fonts now ship.** `package.json` `files` did not include `dist/`,
  so a published install contained exactly three CSS files (two brand themes
  and the Tailwind input). Zero-config mode was unaffected because it inlines
  CSS from `generated/styles.js` — but every documented URL-mode and SSR
  workflow told consumers to `cp node_modules/@colletdev/core/dist/tokens.css`
  from a path that did not exist. Also exported as `@colletdev/core/dist/*` so
  bundlers can resolve it. Unpacked size 2.9 MB → 3.8 MB.
- Removed the stale, unreferenced `dist/utilities.css` (superseded by
  `cx-utilities.css`).

### Fixed: Localization shipped complete, and completely unreachable

- **`setStrings()` / `stringKeys()` were dead code.** `crates/core/src/i18n.rs`,
  the WASM exports (`cx_set_strings`, `cx_string_keys`) and
  `packages/core/src/i18n.js` all shipped and all worked — but *nothing imported
  the JS module*. It was absent from `generated/index.js`, absent from the
  `exports` map, and its two internal hooks had no caller: `flushPendingStrings()`
  was never invoked, so any override queued before WASM resolved was dropped
  permanently, and the re-render registry was never populated, so an override
  applied after mount would not have repainted anything. A consumer could not
  reach the feature by any import path. Now:
  - `setStrings` and `stringKeys` are exported from the package entry point, and
    from a new `@colletdev/core/i18n` subpath.
  - `init({ strings })` applies overrides **before the first render**, so
    components paint in the target language rather than flashing English.
  - Overrides queued before WASM is ready are replayed on load — including in
    `lazy: true` mode, where `init()` returns long before WASM resolves.
  - Every connected component re-renders on a string change, making a runtime
    locale switch visible without a reload. The registry lives in `runtime.js`
    (not `i18n.js`) so the module graph stays acyclic.
  - Scope today is honest and documented: **3 keys, DatePicker only**. The
    mechanism is complete; coverage is not.
  - Covered by `packages/demo/tests/i18n.spec.ts` (5 tests).

### Fixed: The documentation site did not boot

- **All 60 pages of `frust-docs/` rendered blank.** The site serves a vendored
  copy of `packages/core`, synced by an *enumerated list of files*. `runtime.js`
  later gained `import './slots.js'` and `index.js` gained
  `import '../src/i18n.js'`; neither new module was in the list. A single 404 on
  a static ES import aborts the whole module graph, so `init()` never ran, no
  `<cx-*>` element upgraded, and the FOUC rule held every one of them at
  `opacity: 0` — a blank site with nothing in the page explaining why. The sync
  now copies the entire `src/` tree and **verifies every relative import
  resolves**, failing the build if one does not. Same defect class, same fix
  shape, as the docs-sync glob above.
- **The docs site fetched a 404ing JetBrains Mono from jsDelivr** on every page
  load, so every code block fell back to the system monospace. Now served from
  the vendored `dist/fonts/` the package publishes, with `local()` first — two
  fewer cross-origin requests per page.
- Added a **Guides** section to the docs-site navigation, present on all 60
  component pages (21 of which are hand-authored and were patched directly), and
  a first-class [Slot Projection](frust-docs/guides/slot-projection.html) page
  with a live, runnable demo.
- Covered by `packages/demo/tests/docs-site.spec.ts` (4 tests), which serves
  `frust-docs/` and asserts components actually upgrade — a behavioural check,
  not a file list, because a file list is the same enumeration that caused the bug.

### Fixed: Documentation that shipped stale

- **The build synced 6 of 11 reference docs.** `packages/docs/` is the copy
  distributed by `npx @collet/docs init`, and the sync step was a hand-written
  list of `cp` lines that never grew with the directory — so
  `form-integration.md`, `browser-support.md`, `messages.md` and `ssr.md`
  drifted silently. Replaced with a glob plus a post-sync `cmp` that fails the
  build on any mismatch.
- **`messages.md` was wrong about the Chat family API** — it showed
  `<MessagePart>` taking children (the element has no slots; content is a
  prop), `<Chat>` taking message components as children (it takes `turns`),
  `ActivityGroup status="running"` (the enum is `pending | done | error`), and
  `parts` as `string[]` (it is `MessageGroupPart[]`). Verified against the
  manifest and corrected.
- `ssr.md` pointed at `generated/*.css` for assets that live in `dist/`;
  `browser-support.md` listed Safari `99+` for `CSS.highlights` (it shipped in
  17.2).

### Fixed: `useFormControl` was non-functional

- The hook returned `onCxChange` / `onCxBlur` / `value` — **prop names no
  wrapper accepts** (all 60 use `onInput`/`onChange`/`onBlur`, and the value
  prop is `value` on only 4 of 13 form components). Spreading the result into a
  field silently did nothing, and TypeScript could not catch it because JSX
  spreads are not checked for unknown props. Rewritten as
  `useFormControl(Component, options)` → `{ render, ref }`: the value type
  follows the component, bound props are `Omit`ted so they cannot be
  overridden, and a non-form component fails to compile. Now exported from the
  package barrel as well as the subpath — the docs had always told consumers to
  import it from `@colletdev/react`, where it did not exist.

### Fixed: Rust-native (iced) icon fidelity

- Autocomplete, Popover and Treemap rendered raw glyph literals (`×`, `✕`, `→`)
  instead of resolving through the shared icon map, so they disagreed in weight
  and baseline with the same affordances on the web. The parity gate reported
  only the FIRST violation per run, so fixing one merely revealed the next; it
  now reports all of them.

### Fixed: Cross-browser

- **Autofilled inputs are themed in Firefox.** The override shipped
  `:-webkit-autofill` only — a Blink/WebKit-only selector that Firefox discards
  — so Firefox users saw the browser's own opaque autofill background in both
  light and dark, while a comment in the stylesheet claimed the override was
  "cross-browser". The standard `:autofill` now ships alongside it as a
  **separate rule block**: a mixed selector list is discarded wholesale by
  whichever engine does not recognise one of the two.

### Fixed: Test determinism (no product change)

- **Accessibility audits wait for the page to settle.** axe-core blends
  foreground colour by opacity, so auditing a component mid-entrance-animation
  measured a washed-out colour and reported contrast failures that did not
  exist. Audits now wait for `document.fonts.ready`, for design tokens to
  resolve, and for running animations to finish.
- **`color-contrast` is disabled on Firefox only.** Collet authors colour in
  `oklch()`. Chromium and WebKit serialize `getComputedStyle().color` back as
  `rgb()`; Firefox returns the `oklch()` string, which axe-core cannot parse —
  so it derived a bogus foreground. Measured on cx-dialog's title: axe claimed
  3.08:1 where the element's real computed colour was `oklch(0.27 0.003 90)` at
  opacity 1 on white, roughly 13:1. Contrast remains fully enforced on Chromium
  and WebKit, and every other WCAG rule still runs on Firefox.
- **Violation reports name the offending element.** Reporting only
  `{id, description, count}` made a failure nearly unactionable.
- **The leak sweep declares its Chromium-only constraint.** It reads listener
  counts via CDP, which errors on Firefox/WebKit; whether the runner surfaced
  that as a failure or absorbed it depended on worker scheduling. The coverage
  gap is now explicit in the report: **listener-teardown regressions are
  verified on Chromium only.**

### Added: A falsifiability harness

- **`node scripts/verify-regressions.mjs`** re-introduces each fixed defect,
  runs the specific gate that should catch it, and requires that gate to FAIL —
  then restores and requires it to pass. A fix whose guard stays green when
  reverted is reported as UNGUARDED, which is a finding, not a pass.
- Its first run was **7/13**. Two of the six failures were real gate
  weaknesses in the checks added earlier in this release:
  - the publish gate's regex matched `## [0.5.0-withheld]` when looking for
    `0.5.0`, so a prerelease or typo'd heading satisfied it;
  - parity check 10c asserted only that the structured-prop comparator
    *existed* — it passed with zero call sites, and still passed when only one
    of two assignment sites was bypassed. It now requires every element
    assignment to be guarded.
- Now **13/13 PROVEN**.

### Documented

- **A known-good Content Security Policy.** The shipped bundle contains zero
  `eval()` and zero `new Function()`, so Collet runs with **no `'unsafe-eval'`** —
  only the far narrower `'wasm-unsafe-eval'`. Verified, and now published as a
  copy-pasteable header with notes on why each directive is needed.
- **The 150 ms action-event cooldown**, which was undocumented. `cx-click`,
  `cx-close`, `cx-action`, `cx-dismiss` and `cx-navigate` are debounced per
  element; state-change events are not; elements with an `href` are exempt.
- **Plain-CSS theming is now the primary path** in the docs — a complete brand
  identity is ~25 custom properties and no tooling — with the token compiler
  presented as the systematic option rather than the entry point.
- **Page-level scrolling guidance:** use native document scroll, which
  `tokens.css` already themes via `scrollbar-color`. `cx-scrollbar` /
  `cx-scroll-area` are for panes. A nested JS scroller costs scroll restoration,
  hash anchors, find-in-page and mobile URL-bar collapse.
- **Slot projection, as a first-class concept** rather than a prop-table
  footnote — in `core.md` and as a dedicated docs-site guide with a live demo.
  `TableCell.slot`, `TableRow.detail_slot`, `TabItem.content_slot` and
  `AccordionItem.panel_slot` make Rust emit a `<slot name>`; a matching
  `slot="NAME"` on a **direct child** of the host projects your own live element
  into it, keeping its listeners, identity and framework state. Documents why it
  exists (an HTML string cannot carry an event listener), the exact contract, the
  five rules, the dev-mode diagnostic for an unmatched name (the platform
  otherwise renders it as nothing, silently), and the per-framework spellings.
- **The canonical boolean-attribute rule:** absent → the declared default (22
  fields default to `true`, so absent is *not* false); exactly `"false"` → false;
  any other present value → true, including the legacy `disabled="disabled"`
  spelling. Both the JS runtime and Rust's `coerce_bool` implement this table,
  and SSR emits `attr="false"` so a server-painted component does not flip on
  upgrade.
- **`@colletdev/core` has zero runtime dependencies**, and the published CSS +
  self-hosted fonts are now documented as a supported entry path, not just an
  internal build artifact.
- **`init()` is safe to call more than once**, including alongside a framework
  provider — with the failure mode it replaced spelled out.
- **Declarative Shadow DOM has a higher browser floor than the rest of the
  library** — Firefox **123**, not 101 — and degrades to a normal client render
  below it. Also documented: `innerHTML` does **not** parse
  `<template shadowrootmode>`; client-side injection of server-rendered DSD needs
  `setHTMLUnsafe()` (Chrome 124+, Safari 17.4+, Firefox 148+).
- **`llms.txt` now carries the three rules agents get wrong** — booleans, slot
  projection, and localization — in the preamble that ships at all 10 package
  roots, where an agent reads it before any component page.

### Added: Gates that make these defect classes structurally impossible

The existing 8,625 parity checks compared *wrappers against the manifest* — and
stayed green while the element source and the manifest disagreed, then
faithfully propagated the manifest's wrong answer into all eleven targets. Four
new check families guard the boundary that actually drifted (now **9,315**
checks):

- **36 — Manifest ⇔ element events:** every `_emit('cx-…')` in element source
  must be declared. Verified by deleting the `cx-toggle` declaration: the build
  goes red.
- **37 — Manifest ⇔ Rust slots:** every `<slot name="…">` in `render.rs` must be
  declared, which forces the wrapper prop to be a framework node.
- **38 — One prop name, one type:** a prop name may not have conflicting
  structural shapes across components, with an explicit, justified allowlist for
  the intentional exceptions (`expanded`, `selected`, `value`).
- **39 — Boolean round-trip:** any intercepted boolean attribute must read an
  explicit `false` as off. This is the check that found the `disabled={false}`
  defect above.
- **49 — Vue attribute + slot transport:** an explicit `false` must leave
  `buildColletAttrs` as a boolean rather than the string `'false'` (the two look
  identical in a diff and behave oppositely on `readonly`/`disabled`/`open`),
  and every one of the 60 wrappers must forward light-DOM children. Verified by
  reverting each half: the first goes red on 1 check, the second on 60.
- **51 — Declared export subpaths are built:** every non-wildcard `exports`
  entry must map to a source file that the package's `tsconfig` actually
  compiles. This is the check that found `@colletdev/vue/nuxt` pointing at a
  file no clean build produced.
- Checks 10c and 8 were strengthened rather than relaxed: the structured-prop
  value guard and each framework's callback surface are now asserted directly.
  10c additionally pins that Vue detaches structured props from its reactivity
  before assignment, because the element's identity fast-path is correct for
  the other three frameworks and wrong for Vue's in-place mutation.
- **`publish-packages.sh` refuses to publish a version with no CHANGELOG entry**,
  or with package copies out of sync with the root — the gap that produced this
  very backfill.

---

## [0.5.1] — 2026-08-25

Published to npm on 2026-08-25. Contents verified by unpacking the registry
tarball rather than assuming the branch matched:

- `tokens.css` defaults wrapped in `@layer collet`, so an ordinary consumer
  selector wins without `!important`.
- CSS is published for the first time: `dist/tokens.css`,
  `dist/tokens-shadow.css`, `dist/cx-utilities.css`, `dist/syntax.css`,
  `dist/fonts/`.

Known to be ABSENT from this published artifact despite appearing on the branch
at the time: slot projection, localization, and the removal of the `tailwindcss`
runtime dependency. See [Unreleased].

## [0.5.0] — 2026-07-23

First release qualified for production on the web. Native targets are gated as
experimental and must not be shipped to production.

### Added

- **Rust-native rendering backends.** `collet-iced` (60 widgets), plus GPUI and
  Slint ports over the shared `ViewNode` IR, and a complete native manifest.
- **Declarative Shadow DOM / SSR parity** — `@colletdev/core/server` gained a
  typed `ServerComponentConfigMap` covering all 60 components, camelCase
  normalization, name validation, and attribute/URL escaping.
- **`llms.txt` / `llms-full.txt`** at every package root — complete
  machine-readable API reference for all 60 components.
- **Release infrastructure:** OIDC trusted publishing, docs/artifact drift gate,
  a no-`any` gate over web wrapper types, publint + ATTW over packed tarballs,
  and a WASM compressed-size budget.

### Changed

- **Every per-component wrapper is under 1 KB gzip** across React, Vue, Svelte
  and Angular, via shared per-framework runtimes.
- **Honest target tiering.** Web Custom Elements and the four framework wrappers
  are **GA**. Iced, GPUI, Slint, iOS, Android and React Native are
  **experimental** — the iOS/Android platform bridges are not built, so
  on-device rendering does not work there yet.
- Dark-palette contrast redesign across the design system.
- All wrapper packages pass `publint` and `are-the-types-wrong`.

### Fixed

- **Angular: recursive self-instantiation (P0).** Wrappers emitted the Custom
  Element tag inside their own template, which Angular matched against the
  component's own selector — roughly 1,700 nested instances per element. Fixed
  with the host-attach pattern; parity check 11b prevents recurrence.
- **Vue binding (P0)** — structured and boolean props were routed through DOM
  property binding, where the serialized string `'false'` is truthy.
- **Accessibility:** WCAG AA muted-text contrast, ARIA roles no longer reflected
  onto nameless composite hosts, pagination ellipsis is a real `<li>`,
  `role="group"` on named scroll viewports, dark-mode checkbox/radio legibility.
- **Table:** working 10k-row virtual scroll; single-select rows use native radios.
- **Lifecycle:** teardown hook and per-element leak cleanup across all 60
  components.
- **Chat family:** composed submit, declarative turns, height, markdown.
- `init()` in URL mode adopts the Shadow DOM token subset rather than the full
  `tokens.css`, so an explicit `data-theme` is no longer overridden by
  `prefers-color-scheme`.

---

## [0.4.38] — 2026-03-23

### Added: Native React Native Rendering — @colletdev/react-native

- **True native rendering for all 58 components** — components render as real React Native views (View, Text, Pressable, TextInput, Modal, Switch, ScrollView, FlatList) via Rust FFI. Works with ScrollView, KeyboardAvoidingView, SafeAreaView, and react-navigation. No WebView.
- **Architecture:** Props → JSON config → NativeModules.ColletNative (TurboModule) → Rust `cx_render_native()` → ViewNode JSON → TypeScript ViewNodeRenderer → native RN views.
- **58 render_native.rs implementations** with 438 Rust tests covering all states, variants, themes, and accessibility.
- **native-api dispatcher** expanded to all 58 components (3,550 LOC, 33 tests).
- **TypeScript adapter:** ViewNodeRenderer (recursive), oklch→hex color conversion, layout/style/a11y mappers.
- **TurboModule bridges:** iOS (ColletNativeModule.swift + .m) and Android (ColletNativeModule.kt + Package.kt).

### Added: WebView Package Split — @colletdev/react-native-webview

- **Separate package for WebView-based rendering** — pixel-perfect web rendering via shared WebView bridge. Use for full-page Collet experiences (AI chat, dashboards, data tables) where 100% visual parity matters.
- **Same 58 components, same API** — different rendering engine. Codegen: `scripts/generate-react-native-webview.mjs`.
- **Peer dep: react-native-webview>=13.13.4** (not required by the native package).

### Changed: 8 Packages

- Collet now ships **8 npm packages**: core, react, react-native (native views), react-native-webview (WebView bridge), vue, svelte, angular, docs.
- `@colletdev/react-native` no longer requires `react-native-webview` as a peer dependency.
- Build pipeline, publish script, finalize-delivery, and CHANGELOG sync updated for 8 packages.
- Parity checks: 2,189 passing across all frameworks.

---

## [0.4.13] — 2026-03-23

### Added: React Native Package — @colletdev/react-native

- **All 58 components available in React Native** via a shared WebView bridge architecture. `<ColletProvider>` creates a single managed WebView that hosts the actual Custom Elements — same rendering, same behavior, zero drift from the web package. Each component has a typed React Native wrapper with the same props API as `@colletdev/react`. Events flow via postMessage bridge (callback props). Imperative methods are async (Promise-based via bridge calls). Slots accept HTML strings. Accessibility roles mapped per component.
- **Codegen pipeline:** `node scripts/generate-react-native.mjs` reads the same `custom-elements.json` manifest and `component-config.mjs` config as all other framework generators — single source of truth for all 5 frameworks.
- **Parity checks:** 3 new checks (25-27) verify RN wrapper coverage, event callbacks, and imperative methods. 2,189 total checks passing.
- **Package:** `@colletdev/react-native@0.4.12` — peer deps: `react>=18`, `react-native>=0.72`, `react-native-webview>=13`.

### Fixed: Boolean Default-True Attributes — All 4 Framework Wrappers

- **`showInfo={false}` and other default-true booleans now work correctly** — 22 boolean attributes across the component library default to `true` in the WASM adapter (e.g. `show-info`, `dismissible`, `show-prev-next`, `copy-button`, `traffic-lights`, `loop-mode`, `motion-blur`, `tooltips`, `show-value`, `show-values`, `show-icon`, `show-action-button`, `hoverable`, `custom-cursor`, `line-numbers`, `backdrop`). Previously, all 4 framework wrappers (React, Vue, Svelte, Angular) only set the HTML attribute when the prop was truthy and omitted it when `false` — making `false` indistinguishable from "not passed," so the WASM default (`true`) always won. Now all wrappers send `attr="false"` when the prop is explicitly `false`, which the CE's `attributeChangedCallback` correctly interprets. Components affected: Pagination, Alert, Toast, CodeBlock, Carousel, Chat, ChatInput, SpeedDial, Slider, Treemap, Table, TopBar, Sidebar.

### Fixed: Pagination CE — Structural Alignment

- **Pagination Custom Element now matches the established hand-authored CE pattern** — added `_isInitialized` guard and moved `super.connectedCallback()` to end of initialization, matching all 12 other hand-authored CEs (accordion, tabs, collapsible, dialog, drawer, sidebar, top-bar, date-picker, split-button, profile-menu, speed-dial, scrollbar). Prevents duplicate event listeners on DOM reparenting and ensures event delegation is wired before the base class triggers the first render.

### Improved: Pagination E2E Test

- **Strengthened click assertion** — the Playwright test for pagination click events now asserts `result.fired === true` and `result.detail.page === 2` instead of the weak `typeof changed === 'boolean'` which accepted both success and failure.

### Changed

- **Parity checks:** 2,188 (up from 2,184 — new boolean attribute + controlled mode checks).

---

## [0.4.10] — 2026-03-20

### Fixed: Pagination — Interactive Events

- **`cx-pagination` now emits `cx-change` with `PageDetail`** — previously, pagination rendered clickable page buttons but never dispatched any events when clicked, making the component presentation-only and unusable for interactive pagination. The Custom Element now delegates clicks on `[data-page]` buttons and emits `cx-change` with `{ page: number, page_size: number }` detail. Internal state updates automatically (uncontrolled mode), or consumers can drive `current-page` from event data (controlled mode). Same delegation pattern as `cx-table`'s internal pagination and `cx-tabs`.
- **All 4 framework wrappers updated** — React (`onChange` prop with `PageDetail`), Vue (`@cx-change` emit), Svelte (event forwarding), Angular (`@Output() cxChange`). Manifest `events` array populated. 2,184 parity checks pass (6 new checks for pagination events).
- **test.html event names fixed** — pagination listener changed from nonexistent `cx-page-change` to `cx-change`; table listener changed to `cx-page` (matching its actual event name).

---

## [0.4.9] — 2026-03-20

### Fixed: Build Reliability — Tailwind Scanner Root Cause + CSS Generation Audit

- **Automated Tailwind class extraction** — new `scripts/extract-tailwind-classes.mjs` replaces the hand-maintained `tailwind-safelist.html`. The scanner extracts ~6,700 Tailwind utility classes from 447 Rust source files and enumerates ~6,200 design system combinatorial classes (spacing x breakpoints, typography, radius, shadows). Runs automatically as Step 2.5 in `build-packages.sh` before Tailwind compilation. Root cause: Tailwind v4 content scanner cannot parse Rust raw strings with `{}` format interpolation, causing missed classes at runtime (mobile bar invisible in 0.3.9, tree view indent in 0.3.11).
- **CSS generation scope fix** — removed double-brace escaping (`{{`/`}}`) from Tree View, Command Palette, and Tag Input CSS rules in `generate_component_motion_css()` in `tokens.rs`. These components used raw string concatenation (not `format!()`), so literal braces were doubled unnecessarily — producing `{{` in the CSS output which browsers silently rejected.
- **Svelte subpath exports** — `@colletdev/svelte/markdown`, `@colletdev/svelte/markdown-stream`, and `@colletdev/svelte/types` added to `package.json` exports map.

---

## [0.4.8] — 2026-03-20

### Fixed: Production Hardening — Next.js, Nuxt 3, Angular, Delivery Pipeline

- **`"use client"` directive on all React wrappers** — all 63 React files (50 generated + 13 hand-authored) now include `"use client"` as the first line. Without this, Next.js App Router treats Custom Element wrappers as server components, causing `addEventListener` failures at runtime. The directive is emitted by `generate-react.mjs` and survives TypeScript compilation into `dist/`.
- **React subpath exports** — `@colletdev/react/markdown`, `@colletdev/react/markdown-stream`, `@colletdev/react/form-control`, and `@colletdev/react/types` are now importable as standalone subpaths. Previously, hooks and types were only accessible through the barrel export.
- **Nuxt 3 module** — `@colletdev/vue/nuxt` provides zero-config Nuxt 3 integration. Add to `nuxt.config.ts` modules array. Auto-registers `cx-*` Custom Elements (suppresses Vue warnings), auto-imports `useMarkdown`/`useMarkdownStream`/`useFormValidation` composables, and initializes WASM via a client-side plugin.
- **Vue subpath exports** — `@colletdev/vue/markdown`, `@colletdev/vue/markdown-stream`, and `@colletdev/vue/types` are now importable as standalone subpaths.
- **Angular compiled output** — `@colletdev/angular` now ships pre-compiled ES2022 JS + `.d.ts` type declarations (via `dist/`). Previously shipped raw `.ts` files, requiring consumers to have matching TypeScript configuration and adding compilation overhead. Build step added to `build-packages.sh`.
- **Angular subpath exports** — `@colletdev/angular/types` is now importable directly.
- **Svelte subpath exports** — `@colletdev/svelte/markdown`, `@colletdev/svelte/markdown-stream`, and `@colletdev/svelte/types` are now importable as standalone subpaths.
- **CHANGELOG sync to npm packages** — root `CHANGELOG.md` is now re-synced to all 6 `packages/*/CHANGELOG.md` after the entry is written (Phase 6i in `/finalize-delivery`), then TGZ is re-packed. Previously, `build-packages.sh` Step 8 copied a stale CHANGELOG during Phase 4, before the entry was written in Phase 6. Consumers installing 0.4.7 from npm received a CHANGELOG with no 0.4.7 entry.
- **New component delivery checklist** — `/component` Phase 6b now validates standalone CE behavior: inline behavior (no `_behaviors/*.js` dependency), no SVG sprite `<use>` across Shadow DOM, CSS scope verification, focus ring coverage, and `composed: true` on dispatched events. This prevents the Treemap-class fix chain (0.4.2-0.4.6, 5 fixes in 1 day).

---

## [0.4.7] — 2026-03-20

### Fixed: Scrollbar Layout — Default Height + Framework Wrapper DX

- **Scrollbar default height changed from `h-64` to `fill`** — `<cx-scrollbar>` now fills its parent container automatically via `flex-1 min-h-0`, working naturally in flex, grid, and block layouts without requiring an explicit `height` prop. The host element already had `display: flex; flex-direction: column; min-height: 0`, but the internal container forced a fixed 256px height. Pass `height="h-64"` (or any Tailwind class) for fixed-height use cases.
- **Svelte `elements.d.ts` type qualification** — complex prop types (`AccordionItem[]`, `SelectOption[]`, `TreemapNode[]`, etc.) in the `SvelteHTMLElements` augmentation now use `import('./types.js').TypeName` instead of bare type references. Fixes TypeScript errors for Svelte consumers importing `@colletdev/svelte/elements`.
- **Svelte `bind:value` for Select, Autocomplete, DatePicker, RadioGroup** — the V_MODEL writeback handler assigned to an undeclared `value` variable, breaking two-way binding. The `value` prop is now declared in the Props interface with `$bindable()` and included in `$props()` destructuring.
- **Angular `CxMessagePart` memory leak** — event listener for `cx-stream-end` was added in `ngAfterViewInit` with an anonymous handler and no cleanup. Added `OnDestroy`, named handler reference, and `_cleanup` array matching the pattern used by all other Angular wrappers.

### Improved: WASM Optimization Re-enabled

- **wasm-opt (binaryen v126)** integrated into `build-packages.sh` — components WASM 930KB to 840KB (−90KB), markdown WASM 225KB to 209KB (−16KB). Previously disabled (0.2.43–0.2.47) due to RuntimeError crashes with Rust 2024 edition output; validated with 306 Playwright E2E tests across 3 browsers.

---

## [0.4.6] — 2026-03-20

### Fixed: Focus Ring Consistency — Treemap Breadcrumb + Standalone Listbox

- **Treemap breadcrumb buttons** missing centralized `FocusStrategy::Standard` focus ring — keyboard navigation showed no visible focus indicator on breadcrumb items. Added `.focus_ring(FocusStrategy::Standard)` to `resolve_breadcrumb_item_styles()`.
- **Standalone listbox container** (non-embedded mode) missing focus ring — when listbox is used outside a Select dropdown and receives `tabindex="0"` for keyboard access, no focus indicator appeared. Added `.focus_ring(FocusStrategy::Standard)` to `resolve_container_styles()` when `embedded == false`.
- Full audit of all 60 components confirmed 30/60 correctly use the centralized `FocusStrategy` system, remaining 30 are non-interactive or use architectural alternatives (virtual focus via `data-focused`, CSS-based inset outlines for `overflow:hidden` containers).

---

## [0.4.5] — 2026-03-20

### Fixed: Treemap CSS — Rules Trapped Inside Reduced-Motion Media Query

- **Root cause:** The entire treemap CSS section (~70 rules — hover brightness, cursor:pointer, drill arrow opacity:0/1 transition, tooltip transition, focus ring, breadcrumb scroll, responsive breakpoints) was accidentally nested inside `@media (prefers-reduced-motion: reduce)` in `generate_component_motion_css()`. Under normal motion preferences, treemap received zero component-specific CSS.
- **Symptoms:** Drill arrow always visible (opacity:1 instead of 0), no cursor:pointer on zoomable cells, no hover brightness lift, no focus ring, no breadcrumb horizontal scroll, no responsive aspect-ratio breakpoints.
- **Fix:** Closed the reduced-motion block before the treemap section in `tokens.rs`. Treemap rules now apply unconditionally. Treemap's own reduced-motion overrides remain in a dedicated `@media (prefers-reduced-motion: reduce)` block.
- Diagnostic verified: `drillOpacity: "0"`, `cellCursor: "pointer"`, `treemapRules: 18` (was 4), spring transition on drill arrow active.

### Fixed: Missing Type Re-exports Across All Frameworks

- **TreemapNode, TreemapChangeDetail, TreemapSelectDetail, TreemapHoverDetail** — not importable from `@colletdev/react`, `@colletdev/vue`, `@colletdev/svelte`, `@colletdev/angular`. Now re-exported from all framework index files.
- **CxTreeNode, TreeSelectDetail, TreeChangeDetail** (TreeView) — same fix.
- **CxCommandItem, CxCommandGroup, CommandSelectDetail** (CommandPalette) — same fix.
- **CxTagData, TagInputChangeDetail, TagDismissDetail** (TagInput) — same fix.
- **ChatSubmitDetail, ScrollEndDetail, ClearDetail, FilterDetail, ResizeDetail, ScrollDetail** — event detail types missing from Vue/Svelte/Angular indices (React already had them). Now exported from all 4.
- Parity checker updated: 34 prop types + 36 event detail types enforced across all frameworks (was 29 + 21).

---

## [0.4.4] — 2026-03-20

### Fixed: Treemap CE — Tooltip, Overlay, Drill Arrow

- **Tooltip never dismisses:** Changed `pointerenter`/`pointerleave` (capture phase) to `pointerover`/`pointerout` — the former are non-bubbling and don't reliably fire for Shadow DOM descendants
- **Hover overlay invisible:** Added `background:oklch(1 0 0 / 0.12)` to the cell overlay `<div>` — previously rendered with no background, transparent even when toggled visible
- **Drill arrow not visible:** Replaced SVG sprite `<use href="#icon-arrow-right">` with inline SVG path — `<use>` can't reference `<symbol>` IDs across the Shadow DOM boundary
- Removed unused `Icon` import from render.rs

---

## [0.4.3] — 2026-03-20

### Fixed: Treemap Production Readiness

**Self-Contained Custom Element**
- Treemap CE now embeds all behavior inline (zoom animation, keyboard navigation, hover tooltip, label visibility, imperative methods) — no dependency on external `_behaviors/treemap.js` module
- Previously, npm consumers got a non-interactive treemap: click, keyboard, zoom, and imperative methods were silent no-ops because the behavior JS was only available in the SSR gallery
- All 530 lines of behavior (D3-style zoom animation, 2D spatial keyboard nav, ResizeObserver label visibility, pointer/focus tooltip, imperative API) inlined directly in the CE class
- Events (`cx-change`, `cx-select`, `cx-hover`) now dispatch via `_emit()` with `composed: true` — cross Shadow DOM correctly

**New Prop: `fit`**
- `fit="content"` (default): `aspect-ratio: 5/3; min-height: 200px` — backward-compatible
- `fit="fill"`: `height: 100%` — fills parent container height, solves fixed aspect-ratio breaking height-constrained layouts
- Full stack: Rust `TreemapFit` enum → WASM adapter → CE attribute → React/Vue/Svelte/Angular wrappers
- JSDoc: `Grid sizing mode: 'content' (default, 5:3 aspect ratio) or 'fill' (fills parent container height).`

**Technical Details**
- Shadow DOM dual containment check on focusout (invariant 20)
- Proper cleanup: ResizeObserver disconnect + window scroll listener removal in `disconnectedCallback`
- Zoom-out animation finds previous root's bounds in new layout for smooth reverse interpolation
- `prefers-reduced-motion` respected — instant swap, no animation

---

## [0.4.2] — 2026-03-20

### New Component: Treemap

**Zoomable hierarchical data visualization** — squarified layout with D3-quality zoom animation, keyboard navigation, and full npm pipeline integration.

**Core**
- Squarified treemap algorithm (Bruls et al.) in Rust with JS port for client-side zoom
- Recursive `TreemapNode` data model: `{ id, label, value, intent?, children?, disabled? }`
- 12 category color CSS vars (`--cx-treemap-color-0` through `--cx-treemap-color-11`) — overridable via `:root`
- Per-node semantic coloring via `intent` prop (neutral, primary, info, success, warning, danger)
- Virtualization: nodes below `minCellArea` (default 0.001) aggregated into "Other" cell
- `showValues` toggle for formatted value display (K/M/B compact notation)
- Sharp and Rounded shape variants

**Zoom & Navigation**
- D3-style zoom animation: old cells expand/shrink, new cells interpolate in, 750ms cubicInOut easing
- Breadcrumb bar with click-to-navigate, always-visible Root button
- `currentRoot` prop for controlled zoom state
- Respects `prefers-reduced-motion` (instant swap, no animation)

**Interaction**
- 2D spatial keyboard navigation (Arrow keys find nearest cell by direction)
- Enter to zoom into drillable cells, Escape to zoom out
- Home/End jump to first/last cell
- Drill-down arrow icon (Icon::ArrowRight) with spring easing on hover/focus
- `position:fixed` tooltip on drillable cells — styled to match Tooltip component (InverseSurface/InverseText, elevation shadow)
- Tooltip follows cell on hover and keyboard focus ("click to explore" / "Enter to explore")

**Events & Imperative API**
- `cx-change` — zoom level changed: `{ nodeId, path, depth }`
- `cx-select` — leaf cell clicked: `{ id, label, value }`
- `cx-hover` — cell hover enter/leave: `{ id, label, value, hasChildren, rect, enter }`
- `zoomTo(nodeId)`, `zoomOut()`, `resetZoom()` imperative methods via ref

**CSS Parts** (10 — most granular of any component)
- `base`, `grid`, `breadcrumb`, `breadcrumb-item`, `cell`, `cell-label`, `cell-value`, `cell-overlay`, `cell-drill`, `tooltip`

**npm Distribution**
- Custom Element: `<cx-treemap>`
- React: `<Treemap>` with typed `onTreemapChange`, `onTreemapSelect`, `onTreemapHover` callbacks
- Vue: `<Treemap>` with `v-on:cx-change`, `v-on:cx-select`, `v-on:cx-hover`
- Svelte: `<Treemap>` with `oncxchange`, `oncxselect`, `oncxhover`
- Angular: `CxTreemap` with `(cxChange)`, `(cxSelect)`, `(cxHover)` outputs
- TypeScript interfaces: `TreemapNode`, `TreemapChangeDetail`, `TreemapSelectDetail`, `TreemapHoverDetail`

**Accessibility**
- `role="region"` container with `aria-label`
- `role="button"` cells with `aria-label="{label}: {value}"`
- Roving tabindex (first non-disabled cell = 0, rest = -1)
- `KeyboardPattern::Treemap` — Arrow keys, Enter, Escape, Home, End
- Focus ring (`outline-offset: -2px`) inset to avoid overflow:hidden clipping
- Breadcrumb navigation with `aria-current="location"` on current segment

**Gallery:** 9 sections — basic flat, deep hierarchy (5 levels), shape variants, rounded + deep zoom, intent coloring, intent + hierarchy, large dataset (100+ nodes), pre-zoomed state, without values

**Tests:** 40+ Rust tests, 7 snapshots, 12+ layout algorithm tests

### Cross-Cutting Fix: JSON State Serialization

- Fixed `state_script.rs` using `html::escape()` for JSON strings inside `<script type="application/json">` blocks — browsers do NOT HTML-decode script content, so `R&D` became `R&amp;D` in the parsed JSON
- Replaced with proper `json_escape()`: handles `"`, `\`, `<` (prevents `</script>` injection), control characters
- **Affects 7 components:** tabs, autocomplete, select, accordion, toggle-group, radio-group, date-picker

### Codegen: Imperative Method Generation

- Auto-generated Custom Elements now support imperative methods via behavior module's `methods` object
- `generate-elements.mjs` generates method stubs on the CE class that delegate to `__cx._behaviors[name].methods`
- First consumer: Treemap (`zoomTo`, `zoomOut`, `resetZoom`)

### ScrollArea & Scrollbar DX Overhaul

**Viewport height fix**
- Removed fragile `height: var(--cx-viewport-height, auto)` CSS var fallback from viewport — flexbox layout (`flex-1 min-h-0`) now correctly constrains height without consumer workarounds

**Overscroll containment**
- Added `overscroll-behavior: contain` to scrollbar viewport — prevents scroll chaining to parent containers when scrolling hits top/bottom

**RTL support**
- Migrated all scrollbar positioning from physical (`right-0`, `left-0`, `pr-*`) to logical CSS properties (`end-0`, `start-0`, `pe-*`) — scrollbars now mirror correctly in RTL layouts

**Both-axis corner overlap**
- Vertical and horizontal tracks in `axis="both"` mode no longer overlap at the corner — each track stops short with size-aware gap classes

**Sentinel reliability**
- IntersectionObserver sentinel increased from 1px to 8px (`h-2`) — fixes unreliable "at bottom" detection that caused auto-scroll and new-content indicators to misfire

**Dynamic content observer**
- ResizeObserver now observes ALL slotted elements (not just the first) and properly unobserves old elements on `slotchange` — fixes stale thumb sizing when content is dynamically replaced
- `recalculate()` imperative method added to both `<cx-scroll-area>` and `<cx-scrollbar>` — call after programmatic content changes to force thumb recalculation

**Typed `autoScroll` prop**
- `autoScroll` prop now typed as `'off' | 'bottom-stick'` (was `string`) across all 4 framework wrappers — IDE autocomplete shows valid values

**Tailwind safelist**
- Added corner gap classes (`bottom-1` through `bottom-3`) and RTL scrollbar classes (`me-1` through `me-3`, `pe-1.5` through `pe-3.5`) to prevent Tailwind v4 scanner misses

### Flavour Dark Mode & DX

**Automatic dark mode adaptation**
- Texture layers now invert (`filter: invert()`) and switch blend mode (`multiply` → `soft-light`) in dark mode via CSS custom properties — textures that crushed dark surfaces to black now preserve visibility
- Vignette opacity automatically reduces in dark mode (1.0 → 0.5) to prevent over-darkening
- Gloss preset highlight colors dim in dark mode via `--cx-flavour-gloss-start`/`--cx-flavour-gloss-mid` CSS vars

**Animation refinement**
- Hue-rotate reduced from 360° to 30° with `alternate` direction — preserves color-intentional presets (Blueprint blue no longer drifts to orange)
- Layer pulse opacity cycles between 1.0 and 0.6 with staggered delays per layer

**Customizable CSS vars**
- `--cx-flavour-texture-invert` (0 or 1), `--cx-flavour-texture-blend` (multiply/soft-light), `--cx-flavour-vignette-opacity` (0–1), `--cx-flavour-gloss-start`/`--cx-flavour-gloss-mid` (rgba), `--cx-flavour-anim-duration` (default 8s)

**Shadow DOM notes**
- Added `SHADOW_DOM_NOTES` entry for `<cx-flavour>` — all 4 framework wrappers now include JSDoc warning about the `position: relative` parent requirement

---

## [0.4.1] — 2026-03-19

### Production Hardening

**WASM load resilience**
- `fetchWasmValidated()` now retries 3x with exponential backoff (200/400/800ms + jitter) on server errors (5xx) and network failures
- MIME fallback: wrong `Content-Type` triggers `WebAssembly.compile` validation — valid WASM bytes pass despite misconfigured servers (S3, nginx defaults)
- Lazy mode catches unhandled WASM load rejections instead of surfacing as uncaught promise errors

**FOUC prevention**
- Changed from `display:none` to `opacity:0` with 2-second safety timeout
- Elements are now accessible to screen readers while loading (layout preserved)
- If JS/WASM fails to load, content is revealed after 2s instead of being permanently hidden

**Error events**
- `cx-load-error` event on `document` when WASM crashes fatally or fails to load
- `cx-render-error` event on individual elements when rendering fails (bubbles, composed)
- Production: failed components show `<slot></slot>` fallback (light DOM children visible)
- Dev mode: keeps the red error box with stack trace

**Controlled input focus**
- Value-only bypass: `value` attribute changes on rendered form elements apply directly to inner `<input>`/`<textarea>` DOM property without WASM re-render — preserves cursor position, undo history, and mobile keyboard
- IME composition guard: re-renders deferred during `compositionstart` to `compositionend` — prevents CJK/multilingual input destruction

**Browser support**
- Console warning when Constructable Stylesheets unavailable: `[cx] This browser does not support Constructable Stylesheets.` with polyfill instructions
- Minimum: Chrome 73+, Firefox 101+, Safari 16.4+, Edge 79+

### Documentation

- **`browser-support.md`** — Full browser matrix, polyfill table (`construct-style-sheets-polyfill`, `element-internals-polyfill`), server MIME config (nginx, Apache, Vercel, Netlify), error event reference
- **`ssr.md`** — Server-side rendering recipes for Next.js App Router, Nuxt 3, SvelteKit, Remix, and Astro with Declarative Shadow DOM, plus Workbox service worker and streaming SSR patterns

---

## [0.4.0] — 2026-03-19

### Breaking — CSS class rename (RTL migration)

All semantic spacing classes inside component Shadow DOM changed from physical to logical CSS properties. If you target Collet's internal classes via `::part()` or CSS custom properties, this may affect you:

| Before (physical) | After (logical) |
|---|---|
| `text-left` | `text-start` |
| `text-right` | `text-end` |
| `ml-*`, `mr-*` | `ms-*`, `me-*` |
| `pl-*`, `pr-*` | `ps-*`, `pe-*` |
| `border-l-*` | `border-s-*` |

**Who is affected:** Only consumers who override Collet's internal CSS classes in custom stylesheets. If you use Collet components with props only (the normal path), no action needed — components render correctly in both LTR and RTL.

**Not changed:** Scrollbar track positioning, absolute offsets, animation keyframes, separator borders, and other intentionally physical layout classes remain unchanged.

### Added
- **i18n / RTL support on all 57 Custom Elements** — set `dir="rtl"` on any `<cx-*>` element or an ancestor to get mirrored layouts. The `dir` attribute is now observed and propagated into Shadow DOM. Inherits from the nearest ancestor `[dir]` element when not set directly.
  ```html
  <!-- On a single component -->
  <cx-button dir="rtl" label="שלח"></cx-button>

  <!-- Or on a container — all children inherit -->
  <div dir="rtl">
    <cx-text-input label="שם" placeholder="הזן שם"></cx-text-input>
    <cx-select label="מדינה" :options="countries"></cx-select>
  </div>
  ```
- **Virtual scrolling for Table and Listbox** — pass a `virtual-scroll` config to render large datasets without DOM bloat. The runtime uses IntersectionObserver-based sentinels with rAF-batched updates.
  ```html
  <!-- Table: 10,000 rows, 50 visible, 36px row height -->
  <cx-table :virtual-scroll='{"totalCount":10000,"renderCount":50,"startIndex":0,"itemHeight":36}'
            :columns="cols" :rows="visibleRows">
  </cx-table>

  <!-- Listbox: 5,000 options -->
  <cx-listbox :virtual-config='{"totalCount":5000,"renderCount":50,"startIndex":0}'
              :options="visibleOptions">
  </cx-listbox>
  ```
  Emits `cx-virtual-scroll` event with new window parameters on scroll. Configurable `overscan` (default 5) for buffer items above/below viewport.
- **100% JSDoc coverage** — all 646 props across all 57 components now show inline descriptions in IDE autocomplete for React, Vue, Svelte, and Angular wrappers. New parity check (Check 24) prevents regression.
- **Form library integration recipes** — 8 copy-pasteable recipes in `@colletdev/docs` (`form-integration.md`): react-hook-form, Formik, VeeValidate, Vuelidate, SvelteKit Superforms, Angular Reactive Forms, Angular Template-driven Forms, plain HTML `<form>`.
- **Vue `useFormValidation` composable** — bridges VeeValidate's `useField()` to Collet's `error`/`hint` props:
  ```vue
  <script setup>
  import { TextInput, useFormValidation } from '@colletdev/vue';
  const email = useFormValidation('email', yup.string().email().required());
  </script>
  <template>
    <TextInput label="Email" v-bind="email.props" />
  </template>
  ```
- **100% E2E test coverage** — 57/57 components now have dedicated Playwright specs with axe-core WCAG 2a/2aa auditing (37 new specs). 3 additional RTL-specific specs.

### Changed
- **`Side::Start` / `Side::End`** — new logical direction variants added to the design system spacing module, replacing physical `Left`/`Right` for semantic spacing.
- **WASM binary sizes** — Components: 819 KB, Markdown: 209 KB (lazy-loaded).

### Fixed
- **Vue `MarkdownStreamRef` export error** — removed non-existent type re-export from Vue barrel index that caused `tsc` compilation failure in consumer projects.

---

## [0.3.12] — 2026-03-18

### Fixed
- **Shadow DOM dark mode broken for all theme-dependent CSS custom properties:** `:host` rules in `tokens-shadow.css` (the adopted stylesheet) were overriding inherited CSS custom property values from the document `:root`. This blocked the cascade for ALL theme-adaptive values — not just ToggleGroup, but also CodeBlock opacity, Prose language badge, Texture blend mode, and pressed-state tints. Every component using these tokens rendered with light-mode values regardless of `html[data-theme="dark"]`.
  - **Root cause:** CSS inheritance: properties set on `:host` take precedence over values inherited from `:root` through the shadow boundary. Dark mode selectors like `html[data-theme="dark"]` can't cross into Shadow DOM stylesheets.
  - **Fix:** Removed all `:host` declarations for theme-dependent values. Defined theme-adaptive tokens exclusively on `:root` / `html[data-theme="dark"]` in the document head, letting them cascade into Shadow DOM via CSS custom property inheritance. Zero `:host` selectors remain in `tokens-shadow.css`.
  - **New CSS custom properties:** `--cx-pressed-tint`, `--cx-pressed-text`, `--cx-toggle-hover-brightness`, `--cx-code-muted-opacity`, `--cx-code-gutter-opacity`, `--cx-code-hover-muted-opacity`, `--cx-prose-lang-opacity`, `--cx-texture-default-opacity`, `--cx-texture-blend-mode` — all theme-adaptive via `:root` cascade.
- **ToggleGroup filled variant hover brightness in dark mode:** Replaced duplicated `html[data-theme="dark"]` / `:host([data-theme="dark"])` selector pairs with a single `filter: brightness(var(--cx-toggle-hover-brightness))` rule. Light mode: `1.65`, dark mode: `1.25`.
- **Badge Sharp variant missing from gallery:** `BadgeShape::Sharp` was fully implemented in Rust/WASM/types but had no demo in the gallery Shapes section. Added Sharp and Primary Sharp badge examples.

### Changed
- **tokens-shadow.css regenerated:** Zero `:host` selectors (except in comments). All theme-dependent values now cascade from document `:root` through the shadow boundary.
- **CodeBlock opacity classes use CSS vars:** `.cx-code-muted` → `opacity: var(--cx-code-muted-opacity)`, `.cx-code-gutter` → `opacity: var(--cx-code-gutter-opacity)`.
- **Prose language badge uses CSS var:** `opacity: var(--cx-prose-lang-opacity)`.
- **Texture component uses CSS vars:** `opacity: var(--cx-texture-default-opacity)`, `mix-blend-mode: var(--cx-texture-blend-mode)`.

---

## [0.3.11] — 2026-03-18

### Added
- **TagInput component** — tokenized input with tags, keyboard management (Backspace to remove last tag), `maxTags` limit, duplicate prevention. 3 variants (Outline/Filled/Ghost), 3 shapes (Sharp/Rounded/Pill). Form-associated (`<form>` submission). Events: `cx-change`, `cx-dismiss`, `cx-input`. Full npm pipeline: WASM adapter, Custom Element (`<cx-tag-input>`), React/Vue/Svelte/Angular wrappers with typed props and events.
- **CommandPalette component** — `Ctrl+K` / `Cmd+K` searchable command launcher. Grouped items with keyboard navigation (Up/Down/Enter/Escape), `autofocus` search input, `role="combobox"` + `role="listbox"` pattern, empty state as `role="status"`. Events: `cx-select`, `cx-close`, `cx-input`. Full npm pipeline: WASM adapter, Custom Element (`<cx-command-palette>`), React/Vue/Svelte/Angular wrappers.
- **TreeView component** — hierarchical tree with WAI-ARIA tree pattern (`role="tree"` / `role="treeitem"` / `role="group"`). Roving tabindex, expand/collapse with chevron rotation, 3 selection modes (None/Single/Multiple with checkboxes). 3 variants (Default/Outline/Compact). Responsive depth-aware indentation: mobile `pl-2` per level (capped at `pl-8`), desktop `sm:pl-4` per level (capped at `sm:pl-20`) — optimized for 320px viewports. Events: `cx-select`, `cx-change`. Full npm pipeline.
- **`KeyboardPattern::Tree`** — new WAI-ARIA keyboard pattern variant for tree navigation (Up/Down siblings, Left/Right expand/collapse, Home/End, type-ahead).

### Fixed
- **Badge Sharp shape variant missing** — added `Sharp` to `BadgeShape` enum for square-cornered badges. Registered in WASM adapter field values and npm codegen.
- **TreeView focus ring invisible for keyboard users** — focus ring was on inner `<div>` but `tabindex` lived on `<li>` (roving tabindex). Moved `FocusStrategy::Standard` to `<li role="treeitem">` via new `resolve_treeitem_styles()`.
- **CommandPalette WCAG violations** — added `autofocus` to search input, `aria-selected="false"` on all `role="option"` items, changed empty state from `role="option" aria-disabled="true"` to `role="status"`.
- **CommandPalette search wrapper missing input focus micro-interaction** — added `MicroInteraction::input_focus()` to search wrapper styles.

---

## [0.3.10] — 2026-03-18

### Fixed
- **Vue/Svelte/Angular mobile slots broken (string instead of slot projection):** TopBar `mobileLeading`/`mobileTrailing` and Sidebar `mobileActions` were typed as `string` in Vue, Svelte, and Angular — only React was updated in 0.3.8. Event handlers on mobile bar action buttons (theme toggles, notifications) didn't work. Now all 4 frameworks use Shadow DOM slot projection via named slots (`mobile-leading`, `mobile-trailing`). **Vue:** use `<template #mobile-leading>` / `<template #mobile-trailing>`. **Svelte:** use `{#snippet mobileLeading()}...{/snippet}`. **Angular:** use `<div slot="mobile-trailing">`.
- **20 additional Tailwind classes missing from shadow stylesheet:** File upload drag states (`data-[drag=accept]:brightness-110`, `data-[drag=reject]:outline-dashed`), tooltip hover/focus animations (`group-hover/tooltip:scale-100`, `group-hover/tooltip:translate-y-0`), carousel nav positioning (`sm:left-2`, `md:right-4`), and responsive density classes (`lg:gap-5`, `xl:gap-6`) were all missing. Root cause: same Tailwind v4 scanner limitation with Rust raw strings. All added to `tailwind-safelist.html`.
- **Generator skip lists:** TopBar and Sidebar added to `CUSTOM_VUE_FILES`, `CUSTOM_SVELTE_FILES`, and `CUSTOM_ANGULAR_FILES` to prevent hand-authored mobile slot implementations from being overwritten by `build-packages.sh`.

---

## [0.3.9] — 2026-03-18

### Fixed
- **Mobile bar invisible at all viewports:** The WASM-rendered mobile bar uses `hidden max-md:flex`, but the Tailwind v4 content scanner failed to extract `max-md:flex` from Rust raw strings with `{}` interpolation. The compiled Shadow DOM stylesheet contained `.max-md\:block` and `.max-md\:hidden` but was missing `.max-md\:flex{display:flex}`, so the mobile bar stayed `display:none` at every viewport width. Fixed by adding `max-md:flex` and `max-md:hidden` to `packages/core/tailwind-safelist.html`.

---

## [0.3.8] — 2026-03-18

### Fixed
- **TopBar `mobileLeading`/`mobileTrailing` — string → ReactNode:** These props were typed as `string`, meaning React event handlers (theme toggles, notification buttons) couldn't be attached. Now typed as `React.ReactNode` and rendered via Shadow DOM slot projection (`<slot name="mobile-leading">`, `<slot name="mobile-trailing">`). Light DOM children keep their React event handlers intact. **Migration:** Replace raw HTML strings with JSX — `mobileLeading={<MyBrand />}` instead of `mobileLeading="<span>Brand</span>"`.
- **Sidebar `mobileActions` — string → ReactNode:** Same fix as TopBar. The mobile bar trailing zone now accepts `React.ReactNode` via `<slot name="mobile-trailing">`. Event handlers on action buttons (search, notifications) work correctly.
- **TopBar WASM slot rendering:** `render_mobile_bar()` in Rust was missing the `if self.slotted` branch for mobile-leading and mobile-trailing zones. Added `<slot name="mobile-leading">` and `<slot name="mobile-trailing">` fallback rendering when the Custom Element sets `slotted=true`.
- **`ColletProvider` missing from barrel export:** `packages/react/generated/index.ts` lost the `ColletProvider` and `useCollet` exports on every `build-packages.sh` run because the generator template didn't include them. Fixed in both the output file and `generate-react.mjs` template. Consumers can now `import { ColletProvider, useCollet } from '@colletdev/react'` reliably.
- **Gallery: TopBar/Sidebar mobile bar invisible at desktop:** The mobile bar uses `hidden max-md:flex` (invisible above 768px). Added forced-visible preview demos in both TopBar and Sidebar gallery sections so the mobile bar is visible at any viewport width.

### Changed
- **Vue/Svelte/Angular wrappers:** `mobileLeading`, `mobileTrailing` (TopBar) and `mobileActions` (Sidebar) updated from string attributes to slot-projected content across all 4 framework wrappers. Consistent API surface.

---

## [0.3.7] — 2026-03-18

### Added
- **`engines` field on all packages:** All 6 package.json files now declare `"engines": { "node": ">=18.0.0", "npm": ">=9.0.0" }`. Prevents silent failures on older tooling.
- **Flavour `animate` implementation:** The `animate` boolean prop on `<cx-flavour>` now works. Implemented via a lazily-created shadow DOM `CSSStyleSheet` with `hue-rotate` and `opacity` pulse keyframes. Respects `prefers-reduced-motion`. Controlled via `--cx-flavour-anim-duration` CSS custom property.
- **20 per-component Playwright tests:** `packages/demo/tests/components/` with specs for 20 interactive components (button, text-input, checkbox, select, autocomplete, date-picker, dialog, drawer, tabs, accordion, menu, tooltip, search-bar, file-upload, slider, table, popover, sidebar, speed-dial, split-button). Each includes keyboard accessibility and axe-core WCAG 2a/2aa audits. Shared `_a11y.ts` helper injects axe-core 4.10.2 via CDN.
- **20 missing frust-docs component pages:** All 54 component pages now documented. Sidebar updated across all existing pages.
- **TopBar mobile bar redesign:** Replaced the floating hamburger button with a full-width mobile bar strip (`max-md:flex`). Three zones: `mobile-leading` (hamburger + brand), spacer, `mobile-trailing` (action buttons). New props: `mobileLeading`, `mobileTrailing`. Desktop bar unchanged, hidden on mobile via `max-md:hidden`.
- **Sidebar mobile bar redesign:** Same three-sibling pattern as TopBar. Full-width mobile bar with hamburger + brand + action slots. New prop: `mobileActions` for trailing action buttons in the mobile bar. Desktop sidebar unchanged.
- **`::part()` on all TopBar zones:** `part="bar"` (desktop nav), `part="leading"`, `part="content"`, `part="trailing"`, `part="mobile-bar"`, `part="mobile-leading"`, `part="mobile-trailing"`.
- **`::part()` on all Sidebar zones:** `part="base"` (desktop wrapper), `part="header"`, `part="nav"`, `part="footer"`, `part="mobile-bar"`, `part="mobile-leading"`, `part="mobile-trailing"`.
- **`ColletProvider` React component:** `<ColletProvider mode="light" brand="acme" locale="en">` — wraps init(), CxTheme, and React context in a single component. Exports `useCollet()` hook for child access to configuration. Replaces manual init/theme/locale wiring.
- **`toggle()` imperative method:** Added to `cx-dialog`, `cx-drawer`, `cx-sidebar`, `cx-top-bar` Custom Elements and React wrapper refs. Eliminates `useState` for open/close toggle patterns.
- **`--cx-viewport-height` CSS custom property:** ScrollArea viewport div now reads `height: var(--cx-viewport-height, auto)`. Consumers set `cx-scroll-area { --cx-viewport-height: 400px; }` to control viewport height through Shadow DOM without inspecting internals.
- **`::part()` documentation in JSDoc:** React wrapper generator now auto-emits `@csspart` annotations from the `COMPONENT_PARTS` config for all 54 components. No more DevTools spelunking.

### Fixed
- **Menu/ProfileMenu event `detail.id` bug:** Menu items rendered by WASM now include `data-item-id` attribute. Previously, `cx-action` events fell back to `textContent.trim()` as the item ID — two items with identical labels were indistinguishable. The `detail.id` now matches the input data's `id` field for all menu item types (standard, checkbox, radio).
- **CI: full Playwright suite:** CI was running only `smoke.spec.ts` while 217+ tests existed. Updated `enforce.yml` to run full `npx playwright test` across Chromium, Firefox, and WebKit.
- **Build pipeline fail-fast:** `gen_tokens`, `gen_shadow_tokens`, and `gen_syntax_css` cargo binary calls now exit with explicit error on failure instead of silently producing empty files.
- **`_props` mutation convention documented:** Added JSDoc in `runtime.js` explaining `_setProp()` (triggers re-render) vs `this._props.key = val` (JS-initiated, no re-render).

---

## [0.3.6] — 2026-03-17

### Fixed
- **Flavour host positioning (Custom Element):** `<cx-flavour>` was unusable as a framework component because the host element had only `display: block` — no `position` set. The inner shadow DOM div (`position: absolute; inset: 0`) therefore escaped the host and latched onto a random positioned ancestor in the light DOM (often the viewport), filling the wrong container or appearing invisible. Root cause: the gallery works with SSR plain HTML where `.cx-flavour` is a real div inside a `position: relative` container; the Custom Element wraps this in Shadow DOM but the host had no containing block. Fix: `_hostSheet` on `CxFlavour` now sets `:host { display: block; position: absolute; inset: 0; pointer-events: none; overflow: hidden; }`. **Usage:** parent container must have `position: relative` (or `absolute`/`fixed`/`sticky`).
- **`::part(panel)` missing on DatePicker, Select, Autocomplete floating panels:** Consumers could not style the floating calendar or dropdown panel from outside the Shadow DOM. `Menu`, `Popover`, `ProfileMenu`, and `SplitButton` already had `part="panel"`. Added `part="panel"` to `cx-date-picker`, `cx-select`, and `cx-autocomplete` floating panels for consistency. Usage: `cx-date-picker::part(panel) { ... }`.

---

## [0.3.5] — 2026-03-17

### Fixed
- **Floating panel offset inside Dialog/Drawer (all 7 components):** `cx-date-picker`, `cx-select`, `cx-autocomplete`, `cx-popover`, `cx-menu`, `cx-profile-menu`, `cx-split-button` panels opened ~100-150px above their trigger inside dialogs and drawers. Root cause: `_applyFloatingPosition()` read `trigger.getBoundingClientRect()` BEFORE setting `panel.style.position = 'fixed'`. Callers un-hide the panel (`display:none` → `display:block`) before calling the method so `offsetWidth` is accurate — but this puts the panel in normal flow. The `getBoundingClientRect()` call forces layout, the panel's natural height (~260px calendar, ~200px dropdown) overflows the dialog body's `overflow-y-auto` container, the container scrolls, and the trigger shifts upward. The captured coordinates are wrong. Fix: set `position:fixed` as the FIRST operation, before any layout-triggering reads. The panel is immediately out of flow, the scroll container never overflows, and `getBoundingClientRect()` returns the trigger's true viewport position.

---

## [0.3.4] — 2026-03-17

### Fixed
- **Floating panel sub-pixel gap (all 7 components):** `getBoundingClientRect()` returns fractional pixel values — used directly as CSS values, the browser snaps the panel to a different pixel boundary than the trigger. Fix: `Math.round()` on all positional values, `Math.floor()` on `maxHeight`.

---

## [0.3.3] — 2026-03-17

### Added
- **Sage brand (`@colletdev/core/brands/sage.css`):** Muted sage-green brand identity. Light and dark modes, hue 155-158°, max chroma 0.040 — refined and subdued, not garish.
- **Dusk brand (`@colletdev/core/brands/dusk.css`):** Warm lavender-purple brand identity with Instrument Serif heading font. Light and dark modes, hue 305-308°, max chroma 0.038.
- **Brand CSS file distribution:** `"./brands/*": "./brands/*"` export map added to `@colletdev/core`. Brands are loaded with `registerBrandCSS('sage', '/brands/sage.css')` or via `init({ brandsUrl: '/brands/' })`.
- **`removeBrand(name)`:** New API in `@colletdev/core/brand` that removes a programmatically-registered brand definition. Returns `true` if the brand existed, `false` otherwise.
- **Brand gallery:** `/components/brand` gallery page demonstrating brand switching, layering, and API reference.

### Fixed
- **Floating panels inside Dialog/Drawer (all 7 components):** `cx-date-picker`, `cx-select`, `cx-autocomplete`, `cx-popover`, `cx-menu`, `cx-profile-menu`, `cx-split-button` calendar/dropdown panels rendered hundreds of pixels outside the dialog. Root cause: `fill: 'forwards'` left `transform: scale(1) translateY(0)` permanently on the dialog panel element after the entrance animation completed. Per CSS spec, any `transform` on an ancestor creates a new containing block for `position: fixed` descendants, so `_positionFloatingFixed()` coordinates were relative to the dialog panel origin, not the viewport. Fix: cancel the animation fill on `finish` — `enterAnim.addEventListener('finish', () => enterAnim.cancel(), { once: true })` — so the panel has no transform in the DOM when floating children open.
- **Dark mode elevation hierarchy:** Surface (0.050 lightness) was darker than Background (0.185), inverting the elevation stack. Corrected to a proper ascending hierarchy: Background 0.145 → Surface 0.185 → SurfaceRaised 0.215 → SurfaceOverlay 0.250. All with zero chroma (true neutral grey, not brownish).
- **Sidebar state on hard refresh (0.3.2):** After WASM rendered HTML with `data-sidebar-state="narrow"`, the `--sidebar-width` CSS custom property was not set to match, causing visual/logical state divergence. Fixed by syncing CSS vars immediately after `_injectHtml()` in `_doRender()`.
- **`cx-toggle` event undocumented (0.3.2):** Sidebar emitted `cx-toggle` with `{ state: 'expanded' | 'narrow' }` detail but the event was not wired in any framework wrapper and had no type definition. Now fully documented across React (`onToggle`), Vue, Svelte, Angular, and all framework `types.ts` files. `SidebarToggleDetail` interface added to component-config.mjs.

---

## [0.3.2] — 2026-03-17

### Added
- **Brand Trait system:** Named brand identities that swap the entire visual language with one attribute. `<cx-theme brand="acme">` applies a complete set of CSS custom property overrides across 7 token categories (colors, fonts, radius, spacing, shadows, duration, easing). Two registration paths: JS object via `createBrand()` or CSS file via `{brandsUrl}/{name}.css`.
- **`@colletdev/core/brand` export:** New module with `createBrand()`, `registerBrandCSS()`, `loadBrand()`, `getBrand()`, `listBrands()`, `validateBrand()`, `setBrandsUrl()`. Full TypeScript types for all 72 tokenizable properties across 7 categories.
- **Brand defaults:** Brands can declare default density/radius/mode/scale — applied unless the consumer explicitly overrides them via attribute.
- **Brand dark mode:** Brands can define separate dark-mode color palettes. Automatically applied when `mode="dark"` is active.
- **`brandsUrl` init option:** `init({ brandsUrl: '/brands/' })` enables automatic CSS-based brand loading. Brand files fetched on-demand from `{brandsUrl}/{name}.css`, cached permanently.
- **Brand attribute on all framework wrappers:** React, Vue, Svelte, Angular `<CxTheme>` wrappers all accept the `brand` prop.
- **Parity check 23:** Verifies brand attribute presence on `<cx-theme>` across all 4 framework wrappers.
- **10 Playwright E2E brand tests:** Brand application, layering (density/accent), defaults, nesting, runtime switching, dark mode, programmatic API, CSS file loading.

### Fixed
- **Accordion/Collapsible hover regression (since 0.3.0):** The `--mod-cx-accordion-bg` and `--mod-cx-collapsible-bg` inline style overrides (added in the 0.3.0 theming overhaul) set `background-color: var(--mod-cx-accordion-bg, transparent)` on trigger buttons. This inline style beat the `[data-accordion-trigger]:hover { background-color: var(--cx-color-secondary) }` rules in tokens.css, making hover feedback invisible. Fix: removed the `background-color` override — hover/active backgrounds are exclusively handled by component motion CSS rules, which now apply correctly.
- **File Upload invisible background (since 0.3.0):** `add_override("background-color", ..., "inherit")` replaced the actual background color with `inherit`, making the dropzone invisible against the page background. Fix: switched to `wrap_override` which preserves the original color value as fallback — e.g., Filled variant now produces `background-color: var(--mod-cx-file-upload-bg, var(--cx-color-inverse-surface))` instead of `background-color: var(--mod-cx-file-upload-bg, inherit)`.

---

## [0.3.1] — 2026-03-17

### Fixed
- **Toggle group border-radius on connected buttons:** `--mod-cx-button-radius` inline shorthand overrode the longhand `border-radius: 0` rules that reset inner corners on connected buttons. All toggle group CSS rules now use `!important` to beat inline overrides. Affects both horizontal and vertical orientations with rounded/pill shapes.
- **Floating panels clipped inside Dialog/Drawer:** `overflow-hidden` on the panel `<div>` clipped `position: fixed` floating children (Select, DatePicker, Popover) because `showModal()` top layer + entrance animation `transform` creates a containing block. Changed to `overflow-visible` on all Dialog and Drawer panel variants. Inner flex wrapper handles scroll containment via `overflow-y-auto`.
- **React 19 JSX.IntrinsicElements type augmentation:** `declare global { namespace JSX }` no longer works in React 19 — JSX namespace moved to `React.JSX`. Updated to `declare module 'react' { namespace JSX }` in both the generator and generated output.
- **Demo pages unprefixed CSS vars:** Fixed 38 `var(--color-*` references in LoginPage.tsx and DashboardPage.tsx to use the new `--cx-` prefix.

### Added
- **Migration guide:** `packages/docs/migration-0.3.md` — complete 0.2.x → 0.3.0 migration guide covering CSS custom property prefix change, CLI one-liner, verification grep, and new features overview.

---

## [0.3.0] — 2026-03-17

### BREAKING CHANGES
- **CSS custom property prefix:** All 194 design token CSS variables now use `--cx-` prefix to eliminate namespace collisions with consumer Tailwind setups. `--color-primary` → `--cx-color-primary`, `--duration-fast` → `--cx-duration-fast`, `--space-4` → `--cx-space-4`, etc. **Migration:** find-replace `var(--color-` → `var(--cx-color-`, `var(--duration-` → `var(--cx-duration-`, `var(--ease-` → `var(--cx-ease-`, `var(--space-` → `var(--cx-space-`, `var(--radius-` → `var(--cx-radius-`, `var(--shadow-` → `var(--cx-shadow-`, `var(--font-` → `var(--cx-font-` across your custom CSS. Internal component rendering is unaffected.

### Added
- **`<cx-theme>` declarative theme element:** Scoped theming without WASM or Shadow DOM. Attributes: `mode` (light/dark/auto), `density` (compact/default/spacious), `radius` (none/sm/md/lg/full), `accent` (CSS color), `scale` (font size %). Nesting works via CSS cascade. Framework wrappers included.
- **`--mod-cx-*` per-component override API:** Every component exposes CSS override variables (Spectrum pattern). Set `--mod-cx-button-bg: purple` to override button background without reaching into internals. 54 components, ~180 override variables total. Falls back to design token defaults when unset.
- **`@colletdev/core/tailwind.css` Tailwind v4 preset:** `@import "@colletdev/core/tailwind.css"` provides native Tailwind utilities mapped to Collet tokens: `bg-cx-primary`, `text-cx-surface`, `p-cx-md`, `rounded-cx-lg`, `shadow-cx-md`, `font-cx-sans`, etc. 26 colors + 16 spacing + 8 radius + 5 shadow + 3 font tokens.
- **`:state()` custom pseudo-class:** Component states exposed via `CustomStateSet` (ElementInternals). Enables CSS hooks like `cx-switch:state(checked)`, `cx-dialog:state(open)`, `cx-button:state(disabled)`. 16 interactive components with state reflection. Feature-detected (Chrome 90+, Firefox 126+, Safari 17.4+).
- **Density system:** `<cx-theme density="compact">` reduces spacing globally (4px steps). Compact/default/spacious presets. Font sizes and icon sizes preserved (no accessibility regressions).
- **Accent color override:** `<cx-theme accent="#6366f1">` overrides the primary color family. Derives hover/active states via `color-mix()` in oklch.

---

## [0.2.58] — 2026-03-17

Re-release of 0.2.57 with corrected version number (0.2.57 was already published before the DX audit landed). No code changes — identical to 0.2.57 content below.

---

## [0.2.57] — 2026-03-17

### Added
- **Wrapper & Codegen DX Audit (all frameworks):** Comprehensive audit across 6 dimensions (convenience, performance, styling, safety, complexity, testing) with 15 issues identified and resolved.
- **Vue event listener cleanup (C1):** All Vue wrappers with events now have `onUnmounted()` cleanup — prevents listener leaks on component teardown.
- **Vue/Svelte complex prop batching (C2):** Vue `watch` and Svelte `$effect` now apply JSON diff guards on complex attributes, avoiding redundant `attributeChangedCallback` invocations.
- **Android Kotlin enum codegen (C4):** `generate-android.mjs` now auto-generates Kotlin enums from component-config.mjs — previously required manual `dev.collet.types`.
- **iOS form event callbacks (C5):** `generate-ios.mjs` now emits input, change, focus, and blur callbacks — previously only onClick/onDismiss.
- **Angular typed ref interfaces (C8):** Angular wrappers expose typed `ElementRef` interfaces (like React's `ButtonRef extends HTMLElement`) — replaces `nativeElement as any`.
- **Vue `useMarkdown()` composable (C9):** Reactive markdown rendering for Vue 3 consumers.
- **Svelte markdown helpers (C9):** `renderMarkdown()` and `useMarkdownStream()` for Svelte 5 consumers.
- **React `useFormControl()` hook (C10):** Form library integration hook for controlled form elements.
- **WeakRef floating panel auto-cleanup (C11):** `disconnectedCallback` now uses `WeakRef`-based tracking for floating panel cleanup — eliminates leak risk from manual `__cxFloatingCleanup`.
- **Parity checks expanded:** 1,878 → 1,958 automated checks across 20 categories (was 12). New checks: Vue event cleanup (#19), floating panel cleanup (#20), config import consistency (#12 extended to all 4 generators), structural event forwarding (#8 upgraded from string-match).
- **102 Playwright E2E tests:** 52 CE runtime tests (registration, Shadow DOM, attributes, events, ARIA, form association, imperative methods, cleanup, FOUC, tokens, semantic HTML), 31 React wrapper tests (prop forwarding, events, slots, rendering, modals, toast, switch), 19 init pipeline + smoke tests.
- **Shadow DOM convention notes (25 components):** `SHADOW_DOM_NOTES` map in `component-config.mjs` emits IDE-surfaced JSDoc blocks on interface/class definitions across all 4 framework generators. Covers: JSON prop patterns (Menu entries, Table columns), custom event semantics (cx-input vs native input), slot projection rules, form integration caveats (native `<form onSubmit>` won't catch Shadow DOM events), and imperative method availability.
- **8 missing typed props in hand-authored wrappers:** TopBar (`groups`, `separatorsAfter`, `drawerSize`) and MessagePart (`slotted`, `attachmentName`, `attachmentSize`, `attachmentType`, `attachmentThumbnail`) now have full prop coverage with JSON diff guards.

### Fixed
- **Angular `[collet]` selector attribute (C3):** Documented required attribute and simplified selector usage.
- **Parity check 12 gap (C7):** Now verifies config source imports for all 4 frameworks (was React-only).
- **Event forwarding check false-positives (C14):** Check 8 upgraded from string-match to structural AST validation (addEventListener/emit/Output pattern detection).

---

## [0.2.56] — 2026-03-17

### Fixed
- **Stale `peerDependencies` in wrapper packages:** `@colletdev/react`, `@colletdev/vue`, `@colletdev/svelte`, and `@colletdev/angular` all declared `"@colletdev/core": "0.2.54"` as a peer dependency in 0.2.55, causing npm peer dependency warnings in consumer apps. Root cause: `build-packages.sh` never synced peer deps on version bumps — they were set manually once and never updated. **Fix:** Step 1c in `build-packages.sh` now auto-syncs `@colletdev/core` peer dep from core's `package.json` on every build. `publish-packages.sh` Check 3b blocks publish if any peer dep is out of sync.

---

## [0.2.55] — 2026-03-17

### Fixed
- **`::backdrop` opaque in consumer apps (dialog, drawer, sidebar, top-bar):** All four dialog-using Custom Elements adopted the `::backdrop { background: transparent }` override into Shadow DOM only. But `::backdrop` renders in the browser's top layer (document scope) — Shadow DOM stylesheets cannot reach it. In the gallery this was masked by `tokens.css` being loaded at document level. In consumer apps without that global stylesheet, the native semi-opaque backdrop blocked the JS-animated blur overlay from being visible. **Fix:** each CE now injects a one-time `document.adoptedStyleSheets` entry that makes `::backdrop` transparent, independent of any external CSS.
- **Floating panel misaligned on first open (all 7 floating CEs):** `_positionFloatingFixed()` ran while panels were `display:none` (Tailwind `hidden` class), so `offsetWidth` returned 0. Dropdowns appeared misaligned on first open but corrected on window resize. **Fix:** three-phase open sequence — unhide with `opacity:0` (has layout), position (reads real dimensions), then reveal. Affected: ProfileMenu, Menu, SplitButton, Select, Autocomplete, Popover, DatePicker.

### Added
- **TopBar mobile drawer parity with Sidebar:** TopBar now accepts `groups` (NavGroup[]) and `separators-after` (number[]) for structured mobile navigation — same types and rendering as Sidebar. Drawer defaults to left side panel (was top). Manifest: 612 → 614 attributes.
- **Declarative `open` prop for TopBar/Sidebar mobile drawers:** Both CEs support the `open` HTML attribute with an `_autoOpen` queue that defers opening until after WASM render. Works with React controlled state (`open={isOpen}`). Includes `isOpen` read-only property and `open()`/`close()` imperative API.

---

## [0.2.52] — 2026-03-16

### Fixed
- **Toggle group selected indicator border-radius:** The Custom Element's `#positionIndicator` was missing the `borderRadius` copy from the pressed button's computed style. The selected item's background was always rectangular, ignoring the container's rounded/pill corners for first/last items. Also added `data-ready` attribute after initial positioning to enable Spring easing transitions on subsequent selections — matching the gallery behavior module.
- **7 missing icon parse entries:** `book-open`, `wrench`, `pen-line`, `file`, `image`, `file-spreadsheet`, and `user-plus` existed in the `Icon` enum (69 variants) but had no `parse_icon()` mapping in the WASM layer. These icons were unreachable from Custom Elements despite being registered in the Rust design system.

### Added
- **Custom icon support:** Icon props (`icon-leading`, `icon-trailing`, `icon-only`, `icon`, `prefix-icon`, `suffix-icon`) now accept raw SVG HTML in addition to built-in icon names. Pass `<svg viewBox="0 0 24 24" ...>...</svg>` and it gets wrapped in Collet's icon container with proper sizing, `currentColor` color inheritance, flex centering, and `aria-hidden="true"`. Works on Button, FAB, Alert, Toast, SplitButton, and TextInput.
- **`<cx-icon>` utility Custom Element:** Standalone wrapper for custom icons that applies Collet's icon traits. Supports `size` (xs/sm/md/lg/xl) and `label` (for semantic icons visible to screen readers) attributes.
- **`CxIconName` TypeScript type:** Union of all 69 valid built-in icon strings with IDE autocomplete. `CxIconProp` type accepts both `CxIconName` and custom SVG strings.

---

## [0.2.51] — 2026-03-16

### Fixed
- **Floating panel positioning for menus:** `cx-profile-menu`, `cx-menu`, and `cx-split-button` were using `matchWidth: true` which forced the dropdown panel to the trigger's pixel width (32-48px for avatars/icon buttons). Menus now let CSS (`MenuWidth` / `w-max`) control panel width. Profile menu uses `alignEnd` to right-align against the trigger, preventing overflow when placed near the right viewport edge.

### Added
- **`alignEnd` positioning option:** New option for `_positionFloatingFixed()` — right-aligns the panel's right edge to the trigger's right edge. Used by `cx-profile-menu` where the avatar is typically near the right viewport edge.
- **`autoUpdate` scroll/resize repositioning:** All 7 floating components (`cx-profile-menu`, `cx-menu`, `cx-split-button`, `cx-select`, `cx-autocomplete`, `cx-popover`, `cx-date-picker`) now reposition automatically on scroll and window resize while open. Listeners are capture-phase (catches nested scroll containers) and auto-cleaned on close.
- **TopBar mobile drawer:** Hand-authored Custom Element with full drawer open/close, overlay animation, backdrop click, Escape key, scroll locking, and focus return. The auto-generated CE (33 lines) relied on `dialog.js` in document scope, which cannot find the `<dialog>` inside Shadow DOM — the hamburger button was completely non-functional. Now matches the Sidebar pattern. Adds `open()` / `close()` imperative API, `cx-close` and `cx-navigate` events across all 4 framework wrappers.

---

## [0.2.49] — 2026-03-16

### Fixed
- **MessagePart markdown crash — actual root cause:** The component WASM was compiled WITHOUT the `markdown` feature (`default-features = false` in wasm-api/Cargo.toml). When `markdown: true` reached the Rust builder, the `#[cfg(not(feature = "markdown"))]` branch ran instead of the real markdown path — passing through raw data that the renderer couldn't handle. The JS-side interception (stripping the markdown key, re-rendering via separate WASM) was a workaround that never reliably worked across all code paths and browser environments. **Fix:** enabled `features = ["markdown"]` on the components dependency so the WASM includes pulldown-cmark and renders GFM markdown internally. Removed all JS-side markdown interception from `message_part.js` and `chat-builders.js`. Binary size: 654 KB → 846 KB raw (209 KB → 288 KB gzipped).

### Changed
- **`wasm-opt` removed from build pipeline:** Both WASM binaries now ship straight from `wasm-pack` without post-processing. The aggressive `-Oz --converge` flags were producing incorrect WASM in earlier versions.
- **MessagePart `_doRender` simplified:** The JS Custom Element now passes props straight through to WASM with zero interception. No markdown key stripping, no JS-side rendering, no `onMarkdownReady` re-render queue. The separate markdown WASM (`wasm-api-markdown`) is still available for streaming and the consumer `renderMarkdown()` API.

## [0.2.47] — 2026-03-16

### Fixed
- **WASM `RuntimeError: unreachable` crash in consumer environments:** Replaced `lol_alloc::LeakingPageAllocator` (minimal bump-pointer allocator) with `dlmalloc` (standard WASM allocator). `lol_alloc` has no error recovery — any allocation failure immediately traps with `unreachable`. `dlmalloc` handles memory management properly with fallbacks. Binary size: 582 KB → 626 KB (+44 KB, 8% increase).
- **`wasm-opt` producing incorrect WASM:** Downgraded from `-Oz --converge` (aggressive iterative optimization) to `-O1` (safe optimizations only). The aggressive passes could produce invalid WASM when combined with Rust 2024 edition features (bulk-memory, nontrapping-float-to-int).

### Changed
- **WASM panic strategy reverted:** `panic = "unwind"` (added in 0.2.45 as a diagnostic) reverted to `panic = "abort"`. The unwind change didn't help (the crash was in the allocator, not Rust panics) and added WASM exception handling instructions that `wasm-opt` may not handle correctly.

## [0.2.46] — 2026-03-16

### Fixed
- **MessagePart `markdown: false` WASM allocator crash (re-release):** 0.2.45 shipped with the defensive fixes (Vite content comparison, version guard, chat-builders health check) but WITHOUT the actual root cause fix. This release includes the key change: `message_part.js` and `chat-builders.js` now **delete** the `markdown` key entirely instead of setting it to `false`. Absent keys and explicit `false` are semantically identical to `JsConfig.bool()`, but explicit `false` triggers `handle_alloc_error` → `unreachable` in some browser WASM runtimes.

## [0.2.45] — 2026-03-16

### Fixed
- **Stale WASM binary after `npm update`:** The Vite plugin compared WASM binaries by file size only. A rebuilt binary with the same byte length (e.g., only the embedded version string changed) was silently skipped, leaving the old binary in `public/`. New JS glue + old WASM binary caused `RuntimeError: unreachable` that killed ALL component rendering via the `_wasmHealthy` crash guard. The plugin now compares binary content with `Buffer.equals()`.
- **Version mismatch silently ignored:** `init()` logged a console warning on JS/WASM version mismatch but continued rendering, letting the first `RuntimeError` cascade into a full crash. Now calls `disableWasm()` and throws a clear error with fix instructions before any component renders.
- **Chat crash cascade:** `chat-builders.js` (`addUserMessage`, `addAssistantMessage`, etc.) only checked `isWasmReady()` but not `isWasmHealthy()`. After a first component crash set `_wasmHealthy = false`, the chat builders still called into the corrupted WASM instance, producing a second `RuntimeError` that crashed the React error boundary. Now checks health before every WASM call.
- **MessagePart `markdown: false` WASM crash:** Passing `markdown: false` as an explicit key in the config object crashed the WASM allocator (`handle_alloc_error`) in browser environments. The `message_part.js` CE and `chat-builders.js` now delete the `markdown` key entirely instead of setting it to `false` — absent keys are handled identically by `JsConfig.bool()` (returns `false` for both `undefined` and explicit `false`).

### Changed
- **WASM panic strategy:** Release profile changed from `panic = "abort"` to `panic = "unwind"`. Future panics produce actual error messages instead of bare `unreachable` traps. Binary size impact: +41 bytes.

## [0.2.44] — 2026-03-16

### Fixed
- **WASM binary rebuild:** The 0.2.43 npm package shipped a stale WASM binary from the 0.2.42 build cycle — never rebuilt after the JS-side markdown fix was applied. The binary reported version `0.2.42` while the JS layer expected `0.2.43`, and the `_wasmHealthy` crash guard disabled ALL component rendering on first `RuntimeError`. This release rebuilds both WASM binaries (components + markdown) from current source with correct `wasm-opt --enable-bulk-memory --enable-nontrapping-float-to-int` flags and matching `CX_NPM_VERSION=0.2.44`.
- **MessagePart markdown rendering:** Consumers using `<MessagePart markdown content="..." />` no longer crash the entire Collet library. The JS-side fix (stripping `markdown` flag before WASM call) was already correct in 0.2.43 source but the stale binary negated it.

### Changed
- **Release process:** WASM binaries MUST be rebuilt via `bash scripts/build-packages.sh` before every npm publish. The 0.2.43 release skipped this step, shipping a binary from a previous build cycle. The build script already reads `CX_NPM_VERSION` from `packages/core/package.json` — the gap was in the release checklist, not the tooling.

## [0.2.43] — 2026-03-16

### Added
- **iOS distribution package (`packages/ios/`):** Swift Package (SPM) with ColletView recursive renderer, ColletTheme (light/dark/system), oklch-to-SwiftUI Color conversion, VoiceOver accessibility mapping, 69 SF Symbol icons, and CxButton + CxBadge wrappers with graceful degradation to native SwiftUI.
- **Android distribution package (`packages/android/`):** Kotlin Compose library (AAR) with ColletView recursive renderer, Material 3 theme integration, oklch-to-Compose Color conversion, TalkBack semantics mapping, 69 Material icons, and CxButton + CxBadge wrappers with `remember` memoization and `@Preview` annotations.
- **Native codegen scripts:** `generate-ios.mjs` and `generate-android.mjs` read `native-manifest.json` + `component-config.mjs` to generate typed platform wrappers — same propagation pattern as web framework generators.
- **Native parity checks (13-18):** Wrapper count, prop parity with web, accessibility mapping completeness, icon mapping completeness, config source consistency. Total automated checks: 2,062.
- **`gen_native_manifest` binary:** Rust binary in `native-api` that outputs JSON manifest of available native components and icons, consumed by codegen scripts.
- **Native pipeline in `build-packages.sh`:** Steps 9-12 gated by `--native` flag — manifest generation, XCFramework build, JNI library build, codegen, parity verification.

### Fixed
- **MessagePart markdown crash:** `_doRender()` passed `markdown: true` to the component WASM, which no longer includes pulldown-cmark (removed in 0.2.42). The WASM panicked on the unsupported flag. Fixed by stripping `markdown` before calling `wasmFn()` in both render paths — the JS-side markdown WASM handles all markdown processing.

### Changed
- **`build-packages.sh` hardened:** Replaced `|| true` silent-failure guards in native build steps with proper pre-flight checks and clear warning messages.
- **`/finalize-delivery` updated:** Phase 4g for native pipeline, native rows in summary table, native parity in quality audit.

## [0.2.42] — 2026-03-16

### Fixed
- **MessagePart WASM crash (root cause):** `wasm-opt` was stripping bulk memory operations (`memory.copy`) from the WASM binary, replacing them with `unreachable` traps. MessagePart — with the largest config struct (22+ fields) — was the first component to crash, but the corruption affected all components with large structs. Fixed by passing `--enable-bulk-memory --enable-nontrapping-float-to-int` to wasm-opt. All components now render correctly.
- **WASM binary size reduction:** Removed unnecessary `pulldown-cmark` dependency from the WASM binary. The `components` crate was pulling `core` with default features (which includes markdown), even though the WASM build disables markdown. Fixed by setting `default-features = false` on the `core` dependency in `components/Cargo.toml`.

### Changed
- **WASM binary rebuilt:** Fresh binary with correct wasm-opt post-processing. Previous 0.2.40/0.2.41 binaries were identical and contained corrupted instructions.

## [0.2.41] — 2026-03-16

### Fixed
- **False positive required-attr warnings in React:** `[cx-button] Missing required attribute(s): label` no longer fires for icon-only buttons. Added `_requiredUnless` conditional logic — `label` is not required when `icon-only` is set. Double-rAF timing ensures React concurrent mode has finished setting props. Applied to button (label unless icon-only) and chat-input (label unless aria-label).
- **Snapshot test failures:** Feature-gated `syntax-highlight` snapshot tests in `code_block` and `message_part` that failed on CI without the feature enabled.

## [0.2.40] — 2026-03-16

### Fixed
- **WASM crash guard — prevents browser tab lock:** When the WASM binary crashes (`RuntimeError: unreachable`), components now fail gracefully instead of producing 14,000+ errors in an infinite loop. Three layers of defense: (1) global `_wasmHealthy` flag — first WASM RuntimeError disables all rendering; (2) per-element `__renderFails` counter — stops retrying after 3 failures; (3) MessagePart `onMarkdownReady` callback guards against re-render loops after failure. Clear error message points to fix (clear bundler cache).
- **Early WASM health check in `init()`:** `cx_version()` is called as a canary during init. If the WASM binary is corrupt or incompatible, `init()` returns immediately without registering any components — preventing silent cascading failures.
- **MessagePart re-render loop:** When main WASM is broken but markdown WASM loads, `onMarkdownReady` no longer re-queues renders for elements that have already failed.

## [0.2.39] — 2026-03-16

### Added
- **Table auto-pagination:** Set `page-size` attribute alone and the table handles pagination internally — no need to manage `current_page` or `total_items`. The CE tracks page state, slices rows, and renders the pagination bar automatically. For full control, the `pagination` prop still works as before. New `goToPage(n)` imperative method for programmatic navigation.
- **WASM version mismatch guard:** `init()` now compares the JS package version against the WASM binary version at startup. When Vite or another bundler serves a cached JS module from version X but the WASM binary is from version Y, the mismatch previously caused a cryptic `RuntimeError: unreachable`. Now it logs a clear error with fix instructions (clear bundler cache). Version is embedded at build time via `CX_NPM_VERSION` env var.

## [0.2.38] — 2026-03-16

### Fixed
- **Required prop dev warnings:** `init({ dev: true })` now warns when required attributes (e.g., `label` on form controls, `title` on dialogs) are missing on hand-authored Custom Elements. Added `static _requiredAttrs` to all 14 hand-authored CEs (select, autocomplete, date-picker, dialog, drawer, table, tabs, text-input, checkbox, radio-group, switch, slider, search-bar, chat-input). Deferred via `requestAnimationFrame` to avoid false positives during framework hydration.

## [0.2.37] — 2026-03-16

### Fixed
- **RadioGroup `options` prop missing from wrappers:** The codegen field parser couldn't handle multiline `cfg.array("options")` in the Rust source. RadioGroup now has a typed `options: RadioOption[]` prop in React, Vue, Svelte, and Angular — no more `ref.current.setAttribute` workaround needed.
- **Table/Select/Accordion performance with object props:** React wrappers now batch all complex props into a single `useLayoutEffect` with JSON diff guards. Previously, each structured prop had its own effect — a sort click changing rows, sorts, and pagination would fire 3 separate effects. Now it's one effect per commit with per-prop diffing. Angular wrappers also diff before `setAttribute` in `ngOnChanges`. Consumers no longer need to memoize structured props.

## [0.2.36] — 2026-03-16

### Fixed
- **WASM 404 with Vite pre-bundling:** Vite plugin now intercepts WASM requests by filename suffix instead of exact path, serving the binary directly from the package regardless of Vite's module URL rewriting. Also handles `wasm_api_markdown_bg.wasm`.
- **Select/Autocomplete empty on first render (SSR + frameworks):** First render now defers by one `requestAnimationFrame` so React `useLayoutEffect`, Vue watchers, and Angular `ngOnChanges` have time to set attributes before WASM renders. Subsequent renders still use `queueMicrotask` (no perceptible delay).
- **Required prop dev warnings:** `init({ dev: true })` now warns when required attributes (e.g., `label` on form controls, `title` on dialogs) are missing. Deferred via `requestAnimationFrame` to avoid false positives during framework hydration. Covers 15 components.
- **Vite `optimizeDeps` config:** Plugin auto-configures `optimizeDeps.include` for `@colletdev/core` and excludes the WASM glue module to prevent re-optimization loops.

## [0.2.35] — 2026-03-16

### Added
- **Dev-mode runtime warnings:** `init({ dev: true })` enables zero-cost runtime checks that warn on unknown attributes (typos, casing mistakes) and malformed JSON for structured data props. Auto-detects localhost/Vite DEV when `dev` option is omitted. Guarded behind a boolean — zero overhead in production.
- **58 complex field props:** Structured data props (columns, rows, items, groups, entries, slides, steps, turns, etc.) now flow through all 4 framework wrappers. Previously silently ignored because the manifest filter excluded `type === 'object'` fields. Affects 23 components across React, Vue, Svelte, and Angular.
- **`::part()` CSS Parts:** All 54 components expose semantic `::part()` selectors for Shadow DOM styling. `COMPONENT_PARTS` centralized map in `component-config.mjs`. `cssParts` field added to `custom-elements.json` manifest. Auto-generated CSS Parts section in component docs.
- **DSD/Static Safe flags:** 30 components marked as Declarative Shadow DOM safe (work without JS). `STATIC_SAFE` set in `component-config.mjs`. `staticSafe` boolean in manifest. "DSD safe" / "Requires JS" indicator in component docs.
- **Composition recipes:** 6 copy-paste recipes in `packages/docs/recipes.md` — Login Form, Settings Page, Data Table with Filters, Chat Interface, Dashboard Layout, Notification Center. React-first with Vue/Svelte/Angular equivalents.
- **Props vs Refs guide:** Decision matrix in `core.md` — when to use declarative props vs imperative ref methods. Framework-specific examples (React useRef, Vue useTemplateRef, Svelte bind:this, Angular @ViewChild).

### Fixed
- **Dead code cleanup:** Removed 89 lines of dead complex-field handling code from `generate-elements.mjs` that was unreachable due to the manifest filter bug.

### Internal
- Manifest attributes increased from 573 to 609 (58 complex field props now included).
- Parity checks: 1,863 passing (12 categories).
- `generate-skill-docs.mjs` now imports `COMPONENT_PARTS` and `STATIC_SAFE` from shared config.

## [0.2.34] — 2026-03-16

### Changed
- **WASM binary split:** Markdown renderer extracted into a separate lazy-loaded WASM binary. Initial load reduced from 758 KB → 582 KB raw (294 → 211 KB gzip, 231 → 163 KB brotli — **27% smaller**). Markdown WASM (201 KB raw, 86 KB gzip) loads on-demand when first markdown rendering is needed. Apps that don't use markdown never download it.
- **New crate:** `crates/wasm-api-markdown/` — standalone cdylib for GFM markdown rendering via pulldown-cmark.
- **`@colletdev/core/markdown`:** Now lazy-loads its own WASM binary instead of depending on the main component WASM. New exports: `loadMarkdownWasm()`, `isMarkdownWasmReady()`, `onMarkdownReady()`. Existing `renderMarkdown()` and `renderMarkdownSync()` work unchanged.
- **`@colletdev/core/server`:** `createRenderer()` now accepts `markdownWasmPath` option. Both WASM binaries are loaded — component rendering and markdown rendering are independent.
- **Build pipeline:** `build-packages.sh` Step 2b builds the markdown WASM separately with wasm-opt.

### Internal
- `wasm-api` no longer has a `markdown` feature — the crate is always markdown-free.
- `message_part.js` pre-renders markdown on the JS side before passing to WASM, with automatic re-render when the markdown WASM loads.
- `chat-builders.js` uses `renderMarkdownSync()` from the new markdown module.
- Stale `<cx-markdown>` Custom Element removed (was broken — routed through non-existent dispatcher arm).
- Parity checks: 1,827 passing (12 categories).

## [0.2.33] — 2026-03-15

### Changed
- **WASM binary:** 25.9% smaller (1,023 KB → 758 KB raw, 294 KB gzip, 231 KB brotli). Removed `serde` + `serde-wasm-bindgen` from the WASM compilation — all 54 adapters now use zero-copy `JsConfig` (`js_sys::Reflect`) for JS→Rust data passing. No API changes for consumers.
- **@colletdev/core TGZ:** 516 KB → 444 KB (-14%) due to smaller WASM binary.

### Fixed
- **MessagePart CE:** Added missing `slotted`, `attachment-name`, `attachment-size`, `attachment-type`, `attachment-thumbnail` to `observedAttributes` + `_booleanAttrs`/`_numericAttrs`.

### Internal
- Codegen parser (`generate-elements.mjs`) rewritten to extract component attributes from `JsConfig` method calls instead of serde struct definitions. Uses brace-depth function body extraction + word-boundary regex to prevent nested helper fields from leaking into the manifest.
- Parity checks updated: 1,835 passing (12 categories).

## [0.2.32] — 2026-03-15

### Fixed
- **Tooltip:** Remove dead CSS-only visibility/animation classes in Custom Element context. When consumed as `<cx-tooltip>`, the JS portal always manages show/hide — the `group-hover/tooltip:visible` and `group-has-[:focus-visible]/tooltip:*` classes were dead code inflating the CSS bundle. SSR/gallery rendering is unaffected.
- **Release process:** Added this changelog to address the gap in release documentation.

## [0.2.31] — 2026-03-15

### Fixed
- **Switch:** Decoupled JS Custom Element from Rust class naming. Removed hardcoded `THUMB_TRANSLATE` map from `switch.js`; Rust now emits `data-thumb-distance="Xrem"` on the thumb element and JS reads from the DOM. Prevents drift when size mappings change in Rust.
- **PKG_VERSION:** Synced `generated/index.js` version constant with `package.json` after pipeline rebuild.

## [0.2.30] — 2026-03-15

### Fixed
- **Pipeline:** Rebuilt after version bump to ensure `generated/index.js` reflects correct `PKG_VERSION`.

## [0.2.29] — 2026-03-15

### Fixed
- **Runtime:** Added missing `isWasmReady` import in `generated/index.js` — calling `isWasmReady()` in the dev-warning block threw `ReferenceError` at startup.
- **Runtime:** Render error indicator for failed WASM renders (red outline + error text in Shadow DOM).
- **Runtime:** Scroll lock guard prevents body scroll when dialogs/drawers are open.
- **Codegen:** Fixed method return values in generated framework wrappers.
- **Codegen:** Fixed Vue `v-model` null handling and Angular merged CVA listener.
- **Codegen template:** Added `isWasmReady` to the import template in `generate-elements.mjs` so future rebuilds include it.

## [0.2.28] — 2026-03-14

### Added
- **Chat DX:** Submitting prop (submit button spinner, textarea stays active), getValue/setValue on Chat + ChatInput, streaming turn API (`startStreamingTurn`/`appendTokens`/`endStream`), `removeMessage`/`updateMessage` by ID.
- **MessagePart:** Hand-authored Svelte, Vue, and Angular wrappers with full streaming API support.
- **DX:** Vue `v-model` support, Svelte `bind:value`, JSDoc improvements, `getValue`/`setValue` across form components.

### Fixed
- **Chat:** Queue imperative methods (`addUserMessage`, etc.) before WASM loads — prevents `ReferenceError` when framework wrappers call methods during mount.
- **Tooltip:** Portal motion, arrow rendering, and arrow prop control.
- **Spinner:** Morph variant — three separated shapes with wave animation.
- **Chat DX:** WASM glue binding, activity-group toggle, GroupPart types, declarative messages.

## [0.2.23] — 2026-03-13

### Added
- **Chat:** Turnkey message composition API with full visual control.
- **Chat:** File attachment system — upload, preview, drag-drop.

### Fixed
- **MessagePart:** Delegate code-block rendering to CodeBlock component.
- **Messages:** ARIA role rename + typed wrappers + interactive demo polish.
- **Chat/ScrollArea:** Fix broken layout + rewrite gallery with enterprise composition.

## [0.2.18] — 2026-03-12

### Added
- **ScrollArea, Chat:** Premium composite components.
- **Flavour:** Visual texture component.

### Fixed
- **MessageBubble:** Prevent text overflow — add `min-w-0` + `break-words`.
- **Performance:** Minify CSS + JSON in npm bundle — 138 KB raw / 32 KB gzip saved.

## [0.2.13] — 2026-03-11

### Fixed
- **DX:** Harden npm pipeline — shared config, 1,743 parity checks, CE attribute validation.
- **Events:** Add typed `onSubmit`/`onInput`/`onClear` to all framework wrappers.

## [0.2.9] — 2026-03-10

### Added
- **TopBar:** Complete delivery pipeline — responsive nav bar component.

### Fixed
- **TopBar:** Gallery uses real Collet components + filled variant contrast.
- **CodeBlock:** Always-dark surface for syntax highlighting, CSS custom properties for dark mode contrast.
