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

FieldNotes
idPattern ^ui:[a-z][a-z0-9-]+$. Directory name = id.slice(3).
nameHuman-readable, ≤ 30 chars.
categoryOne of commerce | support | scheduling | forms | feedback | logistics | identity | content.
versionSemver. Bump on every breaking schema/preset change.
protocolVersionCurrently "v0.9".
lifecycleexperimental | stable | deprecated. New components ship as stable only after axe + visreg are green.
descriptionOne sentence, ≤ 140 chars.
peerCatalogsDefault ["https://a2ui.org/specification/v0_9/basic_catalog.json"].
dataModelSchemaInline JSON Schema for the dataModel. Subset: type/required/properties/items/enum/const/minimum/maximum/pattern/oneOf/additionalProperties.
actionPolicy.allowAction names dispatched without confirmation.
actionPolicy.confirmAction names that go through the confirm dialog.
screenshots[{ state, viewport }] — drives toHaveScreenshot baseline names.
agentPromptPrompt fragment teaching an LLM when to emit this surface.
i18n.keysReserved locale keys. v1 ships English only via strings.en.
filesEverything copied at install time except manifest.json (the build script adds that).
integrity.sha256Stub "0".repeat(64) — overwritten by build:ui-catalog.

Quality gates (stable tier)

  • All frames pass validateA2UIFrame (every variant in A2UIv09Frame).
  • Every preset seed produces a dataModel that validates against dataModelSchema.
  • Every action emitted in preset.ts appears in actionPolicy.allow ∪ actionPolicy.confirm AND in actions.ts.
  • axe-core: zero WCAG 2.2 AA violations on the preview surface.
  • Playwright toHaveScreenshot baseline committed.
  • README.md and CLAUDE.md cover 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 (not confirm).
  • Major — required field added, action moved from allow to confirm (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.

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 publish is reserved; no implementation).
  • Sigstore-style signing.
  • i18n string contribution (keys are reserved; only English strings ship).
  • A2UI protocol versions other than v0.9.
Source: docs/guides/contributing-ui-catalog.md