@adia-ai/web-components
v0.4.3
Published
AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.
Readme
@adia-ai/web-components
Vanilla web components + A2UI runtime for AdiaUI. 80 light-DOM custom elements, a reactive core, a trait system, and a renderer that turns A2UI protocol messages into live DOM.
This package ships UI atoms only. The generation pipeline lives in
@adia-ai/a2ui-compose; the pattern corpus in@adia-ai/a2ui-corpus; the MCP server in@adia-ai/a2ui-mcp.
Install
npm install @adia-ai/web-componentsFor composite shells (admin / chat / editor / simple / theme clusters), pair with @adia-ai/web-modules:
npm install @adia-ai/web-components @adia-ai/web-modulesQuick start
<link rel="stylesheet" href="node_modules/@adia-ai/web-components/index.css" />
<script type="module">
import '@adia-ai/web-components'; // registers every *-ui tag
</script>
<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 AdiaElement — light-DOM reactive base
│ ├── form.js AdiaFormElement — form-associated + 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/ — 80 *-ui custom elements (primitives)
│ └── <tag>/
│ ├── <tag>.js class definition (extends AdiaElement)
│ ├── <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 elements (app-shell, app-nav*, adia-chat, adia-editor,
│ gen-ui, a2ui-root) ship in the sibling `@adia-ai/web-modules`
│ package as of 0.0.29 — see ADR-0012 for the three-tier rationale.
│
├── traits/ — 41 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.
Component contract
Every component is a single-file class extending AdiaElement (or
AdiaFormElement for form fields). All styling lives in a sibling .css
file using two-block @scope:
@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):
- No raw colors in component CSS — every color goes through a token.
- 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. - Boolean props default to
false. If the expected default is "on", flip the name (closable→permanent). - Event listeners in
connected()have matchingremoveEventListenerindisconnected(). Handlers are stable#fieldarrows, never inline. - Light DOM only. No
::part(),::slotted(), shadow roots. Use attribute selectors on children::scope > [slot="icon"].
Full authoring contract: docs/specs/component-token-contract.md.
The adia-ui-author 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
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:
<!-- 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.
Build
npm run build:components # regenerate all .a2ui.json from YAMLThe 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
<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|lgshifts 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-corpusWeb-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
