Contributing an A2UI catalog component
This guide is for adding a new component to the first-party A2UI catalog. External publishing (separate registry, signing, review queue) is reserved for v2.
TL;DR
# 1. Create the source directory.
mkdir -p a2ui-catalog/<name>/tests
# 2. Author the nine files. Use a2ui-catalog/eta-promise/ as the template.
# - manifest.json, schema.json, preset.ts, actions.ts, component.tsx,
# mock.ts, i18n.ts, README.md, CLAUDE.md, tests/unit.test.tsx
# 3. Build the registry to validate + compute integrity.
pnpm build:ui-catalog
# 4. Run the contract test.
pnpm vitest run a2ui-catalog/<name>
# 5. Wire it into the showcase gallery.
# Add an import + entry to apps/showcase/src/lib/catalogIndex.ts.
# 6. Run the e2e + a11y + visreg spec.
pnpm --filter showcase exec playwright test --grep @a2ui-catalog --update-snapshots
The contract
Every component manifest must validate against schemas/ui-catalog-manifest.schema.json. The build script (pnpm build:ui-catalog) runs the validator in --check mode in CI.
Required fields
| Field | Notes |
|---|---|
id | Pattern ^ui:[a-z][a-z0-9-]+$. Directory name = id.slice(3). |
name | Human-readable, ≤ 30 chars. |
category | One of commerce | support | scheduling | forms | feedback | logistics | identity | content. |
version | Semver. Bump on every breaking schema/preset change. |
protocolVersion | Currently "v0.9". |
lifecycle | experimental | stable | deprecated. New components ship as stable only after axe + visreg are green. |
description | One sentence, ≤ 140 chars. |
peerCatalogs | Default ["https://a2ui.org/specification/v0_9/basic_catalog.json"]. |
dataModelSchema | Inline JSON Schema for the dataModel. Subset: type/required/properties/items/enum/const/minimum/maximum/pattern/oneOf/additionalProperties. |
actionPolicy.allow | Action names dispatched without confirmation. |
actionPolicy.confirm | Action names that go through the confirm dialog. |
screenshots | [{ state, viewport }] — drives toHaveScreenshot baseline names. |
agentPrompt | Prompt fragment teaching an LLM when to emit this surface. |
i18n.keys | Reserved locale keys. v1 ships English only via strings.en. |
files | Everything copied at install time except manifest.json (the build script adds that). |
integrity.sha256 | Stub "0".repeat(64) — overwritten by build:ui-catalog. |
Quality gates (stable tier)
- All frames pass
validateA2UIFrame(every variant inA2UIv09Frame). - Every preset seed produces a dataModel that validates against
dataModelSchema. - Every action emitted in
preset.tsappears inactionPolicy.allow ∪ actionPolicy.confirmAND inactions.ts. - axe-core: zero WCAG 2.2 AA violations on the preview surface.
- Playwright
toHaveScreenshotbaseline committed. README.mdandCLAUDE.mdcover install, action table, and "don't break the contract" notes.
The shared test harness at a2ui-catalog/_shared/catalogTestHarness.ts handles contracts 1–3 in vitest. Contracts 4–6 are enforced by the Playwright suite at apps/showcase/tests/a2ui-catalog/<name>.spec.ts.
Experimental tier
For components that need primitives outside basicCatalog (image annotation, maps, native tables), set requiresCustomPrimitives: true and lifecycle: "experimental". The A2UI envelope stays minimal (data + actions); a host React renderer in component.tsx carries the visual weight. You may waive the axe stable bar so long as the README documents the gap.
Semver bump rules
- Patch — bug fixes in
preset.ts,component.tsx, or docs. dataModel + action allowlist unchanged. - Minor — new optional dataModel fields, new actions in
allow(notconfirm). - Major — required field added, action moved from
allowtoconfirm(tighter), removed action, breaking schema change.
Bumping the version forces gecx ui:doctor to report installed copies as outdated until the user runs gecx add ui:<name> again.
Adding to the showcase gallery
Edit apps/showcase/src/lib/catalogIndex.ts:
import { createMyComponentScenario } from '../../../../a2ui-catalog/my-component/mock';
export const COMPONENT_MODULES = {
// ...
'ui:my-component': { createScenario: createMyComponentScenario },
};
The dynamic preview route at apps/showcase/src/app/components/[slug]/page.tsx picks it up automatically.
When to invent vs. extend
If your use case is 80% covered by an existing component, send a PR that extends the existing component (add an optional field, a new action) rather than fork. The catalog is small on purpose.
Out of scope for v1
- External publishing (
gecx publishis reserved; no implementation). - Sigstore-style signing.
- i18n string contribution (keys are reserved; only English strings ship).
- A2UI protocol versions other than v0.9.
docs/guides/contributing-ui-catalog.md