# @adia-ai/web-components

Vanilla web components + A2UI runtime for AdiaUI. **95 light-DOM custom
elements** + **56 composable traits**, a reactive core (signals + tagged-template renderer), form-associated form controls, and integration into the A2UI generation pipeline.

> This package ships UI atoms only. Composite shells (admin / chat / editor / simple / theme) live in [`@adia-ai/web-modules`](../web-modules). The generation pipeline lives in [`@adia-ai/a2ui-compose`](../a2ui/compose); the pattern corpus in [`@adia-ai/a2ui-corpus`](../a2ui/corpus); the MCP server in [`@adia-ai/a2ui-mcp`](../a2ui/mcp).
>
> **Consumer guide:** [`USAGE.md`](./USAGE.md) — property reactivity, event contract, form participation, lifecycle, registration, TypeScript. Start here if you're integrating AdiaUI into an app.
>
> **Live demos** for every primitive at [`https://ui-kit.exe.xyz/site/components/<name>`](https://ui-kit.exe.xyz/site/) — `<name>` is the tag without the `-ui` suffix (e.g. [`button`](https://ui-kit.exe.xyz/site/components/button), [`select`](https://ui-kit.exe.xyz/site/components/select), [`slider`](https://ui-kit.exe.xyz/site/components/slider)).

## Install

```bash
npm install @adia-ai/web-components
```

For composite shells (admin / chat / editor / simple / theme clusters), pair with [`@adia-ai/web-modules`](../web-modules):

```bash
npm install @adia-ai/web-components @adia-ai/web-modules
```

### Using with AI agents (Claude Code / Cursor / Windsurf / Claude Desktop)

When composing UI with an AI coding agent, load the **`adia-ui-kit` skill** in the agent's harness. It encodes:

- 95-primitive catalog (which `*-ui` tag does what + when to use it)
- Composition patterns (admin shell, dashboards, forms, chat, editor surfaces)
- Pre-flight manifest gate (catches "imports without declared deps" before authoring)
- Plan-Execute-Verify discipline (render the result in a real demo to verify)

The skill lives at [`.agents/skills/adia-ui-kit/`](https://github.com/adiahealth/gen-ui-kit/tree/main/.agents/skills/adia-ui-kit) in the repo; activation phrases include "build a page with AdiaUI", "compose this UI", "wire these components", "use AdiaUI primitives to compose…", and "what's the pattern for an admin app / dashboard / form / chat surface". Without the skill, an agent works against this README; with the skill, it has the reasoning ladder + catalog + recipes for every primitive.

## CDN — no bundler (CodePen, marketing pages, static HTML)

Since **v0.6.30**, this package ships pre-flattened + minified bundles under `dist/` that work via jsdelivr/unpkg with zero build tooling. Drop these into any HTML page:

```html
<!-- CSS: all primitives, tokens, resets (443 KB raw / ~50 KB gzipped) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@adia-ai/web-components@0.6/dist/web-components.min.css">

<!-- JS: registers all 95 primitives (~250 KB gzipped via Brotli) -->
<script type="module" src="https://cdn.jsdelivr.net/npm/@adia-ai/web-components@0.6/dist/web-components.min.js"></script>
```

The `@0.6` range tracks the latest `0.6.x` patch automatically (won't jump to a breaking `0.7`). For reproducible builds, pin an exact version instead — e.g. `@adia-ai/web-components@0.6.45/dist/web-components.min.css`.

For composite shells, add the corresponding bundle from `@adia-ai/web-modules` — see [its README](../web-modules/#cdn-no-bundler--codepen-marketing-pages-static-html) or the [CDN usage guide](https://ui-kit.exe.xyz/site/cdn-usage). The kitchen-sink path is `@adia-ai/web-modules/dist/everything.min.js` (all primitives + all 4 shells; ~190 KB gzipped) — one tag for CodePen demos.

**Pick ONE bundle path.** Mixing (e.g. `everything.min.js` + a separate `web-components.min.js`) causes `customElements.define` to throw "name already defined" on dup-load. The choice tree:

- HTML-first / CodePen / marketing → `@adia-ai/web-modules/everything` (one tag covers everything)
- Primitives only, no shells → `@adia-ai/web-components/js/bundled`
- Vite/webpack/esbuild → existing root export (`import '@adia-ai/web-components'`); the `*/bundled` exports are additive — bundler consumers see no change.

## Quick start

ESM-only — bundlers (Vite, esbuild, webpack 5+, Rollup) resolve `import` for both `.js` and `.css`; in plain HTML use `<script type="module">` + `<link rel="stylesheet">`.

```js
import '@adia-ai/web-components';          // registers every *-ui tag (95 primitives)
import '@adia-ai/web-components/css';      // every primitive's CSS (one stylesheet)
```

Or per-component for tree-shaking-conscious bundles:

```js
import '@adia-ai/web-components/components/button';        // <button-ui> only
import '@adia-ai/web-components/components/button.css';    // its CSS
```

Then use the tags in markup:

```html
<card-ui>
  <header>
    <span slot="icon"><avatar-ui icon="user"></avatar-ui></span>
    <span slot="heading"><text-ui strong>Hello</text-ui></span>
  </header>
  <section>Composition works out of the box — no framework.</section>
</card-ui>
```

The package `sideEffects` entry keeps the import above from being
tree-shaken; importing `index.js` side-effect-registers every component.

## Layout

```
web-components/
├── core/           — Reactivity + base classes
│   ├── signals.js       signal() / computed() / effect() / batch() / untracked()
│   ├── template.js      tagged templates: html, css, repeat, stamp
│   ├── element.js       UIElement — light-DOM reactive base
│   ├── form.js          UIFormElement — form-associated + ElementInternals + validation
│   ├── provider.js      global provider registration + router-ui
│   ├── anchor.js        popover + anchor-positioning
│   ├── markdown.js      lightweight markdown renderer
│   ├── transport.js     SSE / streaming helpers for LLM adapters
│   └── data-stream.js   `data-stream-*` attribute trait (HTTP/SSE/WS,
│                        signal-backed, refcounted shared transports)
│
├── components/     — 95 *-ui custom elements (primitives)
│   └── <tag>/
│       ├── <tag>.js       class definition (extends UIElement / UIFormElement)
│       ├── <tag>.css      @scope(tag-ui) two-block: tokens + styles
│       ├── <tag>.yaml     authoring contract (props, slots, events, examples)
│       └── <tag>.a2ui.json  generated — do NOT edit
│
│  Composite shells (admin-shell, chat-shell, editor-shell, simple-shell,
│  theme-panel, gen-root, a2ui-root) ship in the sibling
│  `@adia-ai/web-modules` package — see ADR-0012 for the three-tier
│  rationale.
│
├── traits/         — 56 composable behaviors via defineTrait() + the
│                     <traits-host> wrapper for raw-HTML declarative
│                     composition. Generated catalog at _catalog.json
│                     drives the MCP get_traits tool + per-trait demo
│                     pages. Full contract in docs/specs/traits.md.
│
├── a2ui/           — deprecation shim for one release
│   └── index.js            Re-exports @adia-ai/a2ui-runtime with a
│                           one-time console.warn. Removed in 0.1.0.
│                           All actual A2UI runtime code (renderer,
│                           registry, streams, surface manifest,
│                           wiring, dockables, controllers) lives in
│                           `@adia-ai/a2ui-runtime` at packages/a2ui/runtime/.
│
└── styles/         — Global tokens and CSS layering
    ├── tokens.css              all --a-* design tokens
    ├── colors/                 primitives / semantics / scrims
    ├── typography.css, space.css, radius.css, shadow.css
    └── themes/*.css            8 themes (ocean, forest, sunset, …)
```

Build + dev utilities (including `build-a2ui-data.mjs`, `qa-training.mjs`,
`a2ui-to-html.cjs`, `mcp-call.cjs`, `mcp-pipeline.cjs`, `screenshot.cjs`)
live at the repo-root `scripts/` directory rather than inside this
package — they span the monorepo (MCP server, a2ui-corpus data,
component catalog) and aren't scoped to web-components alone.

## Using a primitive (consumer guide)

The complete consumer surface — property reactivity, event contract, form participation, lifecycle, registration, TypeScript — lives in [**`USAGE.md`**](./USAGE.md). Start there if you're integrating AdiaUI into an app rather than authoring a new primitive.

Quick reference:

```js
// Property reactivity — every prop is signal-backed
slider.value = 75;            // triggers re-render

// Event contract — CustomEvent with typed detail (since v0.4.5)
slider.addEventListener('change', (e) => console.log(e.detail.value));

// Form participation — 15 primitives extend UIFormElement
new FormData(document.getElementById('settings'));  // includes every <input-ui>, <select-ui>, etc.

// Lifecycle — UIElement provides connected/render/updated/disconnected hooks
class MyWidget extends UIElement {
  connected() { /* set up */ }
  render() { /* update DOM */ }
  disconnected() { /* tear down */ }
}

// Registration — conflict-safe via defineIfFree
import { defineIfFree } from '@adia-ai/web-components/core';
defineIfFree('my-widget', MyWidget);
```

TypeScript users: types ship in the package since v0.4.6. `import type { UISlider, SliderChangeEvent } from '@adia-ai/web-components'` for hand-typed handlers; `HTMLElementTagNameMap` is augmented so `document.querySelector('slider-ui')` returns `UISlider`.

## Authoring a primitive

If you're contributing a new primitive (rather than consuming one), this is for you. Every component is a single-file class extending `UIElement` (or `UIFormElement` for form fields). All styling lives in a sibling `.css` file using **two-block `@scope`**:

```css
@scope (button-ui) {
  :where(:scope) {
    /* Tokens only — zero specificity, parent overrides win */
    --button-bg:     var(--a-accent-bg);
    --button-fg:     var(--a-accent-fg);
    --button-radius: var(--a-radius);
  }
  :scope {
    /* Styles — consume component tokens only, never global directly */
    background: var(--button-bg);
    color:      var(--button-fg);
    border-radius: var(--button-radius);
  }
  :scope[variant="danger"] {
    --button-bg: var(--a-danger-bg);   /* variants override tokens, not styles */
  }
}
```

Authoring rules (enforced by `ui-audit-coherence`):

1. **No raw colors** in component CSS — every color goes through a token.
2. **Variants change tokens, modes change layout.** A selector that sets
   `display`, `padding`, `flex`, `grid`, etc. is a *mode* and needs a
   Sanctioned Mode Attributes entry in the contract doc.
3. **Boolean props default to `false`.** If the expected default is "on",
   flip the name (`closable` → `permanent`).
4. **Event listeners in `connected()` have matching `removeEventListener`
   in `disconnected()`.** Handlers are stable `#field` arrows, never
   inline.
5. **Light DOM only.** No `::part()`, `::slotted()`, shadow roots. Use
   attribute selectors on children: `:scope > [slot="icon"]`.

### `static template` is a closure — import what it references

The `static template` property is a regular JavaScript closure (typically a tagged template literal — `() => html` followed by the template body). Any signal, variable, or function it references must be **lexically in scope** at the file where the class is defined. This is JavaScript scoping, not anything AdiaUI-specific — but it surprises authors coming from frameworks where templates magically receive props.

```js
// ❌ WRONG — ReferenceError at runtime: minL is not defined
//   (no import; the template function can't see minL just because
//    it's exported elsewhere in the monorepo)
class MyPanel extends UIElement {
  static template = () => html`<div>L: ${minL.value}</div>`;
}

// ✅ RIGHT — import the signal locally
import { minL } from './state.js';

class MyPanel extends UIElement {
  static template = () => html`<div>L: ${minL.value}</div>`;
}
```

The error surfaces in the browser console as `ReferenceError: minL is not defined` at first render — not at module load, not at `tsc --noEmit`. **The trace points at the template function body**, not at the missing import, which is the disorienting part.

If your primitive needs external reactive state that varies per-instance, **expose it as a property** rather than reaching for module-scoped signals:

```js
// ✅ Best — per-instance reactive prop
class MyPanel extends UIElement {
  static properties = {
    minL: { type: Number, default: 0 },
  };
  static template = el => html`<div>L: ${el.minL}</div>`;
}

// Consumer:
<my-panel .minL=${minL}></my-panel>  // signal binds reactively per ADR-template-binding
```

The `el` parameter is the element instance — every signal-backed property is reactively read.

Full authoring contract: [`docs/specs/component-token-contract.md`](../../docs/specs/component-token-contract.md).
The `adia-ui-authoring` skill encodes the 20 non-negotiable rules.

## Card-n / drawer-ui composition parity

Both accept bare `<header>` / `<section>` / `<footer>` tags OR explicit
`[slot="header|body|footer"]`. Both activate a 3-column header grid when
any direct `[slot="icon|heading|description|action"]` child is present
(`:has(> …)` — direct-child only). Drawer supports multi-section bodies
with sticky header/footer. See the drawer component page for worked
examples.

## A2UI runtime

```javascript
import { A2UIRenderer } from '@adia-ai/a2ui-runtime';
// (The old `@adia-ai/web-components/a2ui` subpath still resolves in 0.0.4
// via a deprecation shim that prints a console.warn; removed in 0.1.0.)

const renderer = new A2UIRenderer({ target: document.getElementById('canvas') });
renderer.apply({
  type: 'updateComponents',
  components: [
    { id: 'root', component: 'Card', children: ['hdr', 'body'] },
    { id: 'hdr',  component: 'Header', slots: { heading: 'Generated UI' } },
    { id: 'body', component: 'Section', children: ['btn'] },
    { id: 'btn',  component: 'Button', attrs: { variant: 'primary' }, content: 'Click' },
  ],
});
```

Accepts the four A2UI message kinds: `updateComponents`,
`updateDataModel`, `wireComponents`, `createSurface`. The registry
normalizes LLM-emitted aliases (e.g. `Carousel` → `swiper-ui`) so generated
output is robust to name drift.

## Data streaming via `data-stream-*` attributes

Any element with a settable `.data` property — chart-ui, table-ui,
heatmap-ui, stat-ui, list-ui, etc. — can be fed from a backing
source via attributes alone. No per-component opt-in:

```html
<!-- HTTP one-shot fetch, JSON -->
<chart-ui type="area" x="month" y="revenue"
          data-stream-src="/api/revenue?range=3m"
          data-stream-path="data"></chart-ui>

<!-- HTTP polling every 5s -->
<table-ui sortable striped
          data-stream-src="/api/orders"
          data-stream-interval="5000"></table-ui>

<!-- Server-Sent Events, append on each message -->
<heatmap-ui type="matrix" rows="7" cols="52"
            data-stream-src="/sse/activity"
            data-stream-mode="sse"
            data-stream-merge="append"></heatmap-ui>

<!-- Spread a multi-property response onto the element -->
<stat-ui data-stream-src="/api/kpi"
         data-stream-target="*"></stat-ui>
```

Modes: HTTP (one-shot or polling), `sse` (`EventSource`), `ws`
(`WebSocket`). Formats: `json` (default), `csv`, `tsv`, `jsonl`,
`text` — auto-detected from URL extension or content-type. Two
elements with attribute-identical streams share one transport
(refcounted, signal-backed); explicit `data-stream-id` lets
unrelated configs share intentionally. Programmatic access via
the `streams` registry export from `core/data-stream.js`.

Implementation: `core/data-stream.js` (~360 lines). Full
attribute table + live demos:
[`/site/components/chart#data-stream`](./components/chart/chart.html).

## Build

```bash
npm run build:components    # regenerate all .a2ui.json from YAML
```

The build also writes `packages/a2ui/corpus/catalog-a2ui_0_9.json` and
`catalog-a2ui_0_9_rules.txt` — the flat-file catalog the MCP server and
generation engine consume.

## Themes, density, scale

```html
<div data-theme="ocean" density="compact" size="sm">
  …all descendants re-theme / re-densify / re-scale automatically…
</div>
```

- `[data-theme]` — 8 themes: `default`, `ocean`, `forest`, `sunset`,
  `lavender`, `rose`, `slate`, `midnight`
- `[density]` — `compact` (0.85×) · `spacious` (1.15×)
- `[size]` — `sm`|`md`|`lg` shifts the entire typescale + component
  dimensions
- `[radius]` — `sharp` (0) · `rounded` (1) · `round` (2)

Each is a CSS-variable override; no class toggles, no re-imports.

## Dependency direction

```
a2ui-compose  ──reads──>  a2ui-corpus  ←─reads──  web-components
a2ui-mcp      ──reads──>  a2ui-compose, a2ui-corpus
```

Web-components never imports from a2ui-compose or a2ui-mcp. The A2UI renderer
consumes a protocol, not a generator — anything that emits valid A2UI
messages drives it.

## License

MIT
