a2ui-catalog
v1.0.0
Published
Reusable A2UI component catalog, renderer, and runtime — drop into any React project
Readme
@a2ui/catalog
Standalone A2UI component catalog and renderer. Drop into any React + Tailwind project.
Install
# from npm (once published)
npm install @a2ui/catalog
# or local path during development
npm install file:../path/to/packages/a2ui-catalogPeer dependencies your project must have:
npm install react lucide-react tailwind-merge clsx zustandUsage in a new project
1. Create your store
Your store must satisfy the A2UIStoreSlice shape:
// src/store/a2uiStore.ts
import { create } from "zustand";
import { setPointer, validateMessage } from "@a2ui/catalog";
import type { A2UIStoreSlice, SurfaceState, A2UIMessage, A2UIComponent } from "@a2ui/catalog";
export const useA2UIStore = create<A2UIStoreSlice>((set, get) => ({
surfaces: {},
canvasError: null,
setError: (msg) => set({ canvasError: msg }),
patchData: (surfaceId, path, value) => {
const s = get().surfaces[surfaceId];
if (!s) return;
const next = setPointer(structuredClone(s.dataModel), path, value) as Record<string, unknown>;
set({ surfaces: { ...get().surfaces, [surfaceId]: { ...s, dataModel: next } } });
},
applyMessage: (raw) => {
const msg = raw as Record<string, unknown>;
try { validateMessage(msg as A2UIMessage); } catch (e) {
set({ canvasError: e instanceof Error ? e.message : String(e) });
return;
}
if ("createSurface" in msg) {
const body = msg.createSurface as { surfaceId: string; components?: A2UIComponent[]; dataModel?: Record<string, unknown> };
const components: Record<string, A2UIComponent> = {};
for (const c of body.components || []) components[c.id] = c;
set({ surfaces: { [body.surfaceId]: { catalog: "AppCatalog", components, dataModel: body.dataModel || {}, status: "creating" } } });
}
if ("updateComponents" in msg) {
const body = msg.updateComponents as { surfaceId: string; components: A2UIComponent[] };
const s = get().surfaces[body.surfaceId];
if (!s) return;
const components = { ...s.components };
for (const c of body.components) components[c.id] = c;
set({ surfaces: { ...get().surfaces, [body.surfaceId]: { ...s, components, status: "ready" } } });
}
if ("updateDataModel" in msg) {
const body = msg.updateDataModel as { surfaceId: string; path?: string; value: unknown };
const s = get().surfaces[body.surfaceId];
if (!s) return;
const next = setPointer(s.dataModel, body.path || "/", body.value) as Record<string, unknown>;
set({ surfaces: { ...get().surfaces, [body.surfaceId]: { ...s, dataModel: next, status: "ready" } } });
}
if ("deleteSurface" in msg) {
const { surfaceId } = msg.deleteSurface as { surfaceId: string };
const surfaces = { ...get().surfaces };
delete surfaces[surfaceId];
set({ surfaces });
}
},
}));2. Render
// src/App.tsx
import { A2UIRenderer } from "@a2ui/catalog";
import { useA2UIStore } from "./store/a2uiStore";
export default function App() {
return (
<A2UIRenderer
surfaceId="main"
useStore={useA2UIStore}
apiBase="http://localhost:8000" // your backend URL
/>
);
}3. Feed it SSE messages
const es = new EventSource("http://localhost:8000/api/stream");
es.onmessage = (e) => {
const msg = JSON.parse(e.data);
useA2UIStore.getState().applyMessage(msg);
};That's it. The renderer handles everything else.
What's in the package
| Export | What it is |
|---|---|
| A2UIRenderer | The main renderer component |
| A2UIStoreSlice | TypeScript type your store must implement |
| AppCatalog | The component registry map |
| validateMessage | Validates incoming A2UI messages |
| getPointer / setPointer | JSON Pointer utilities |
| All components | ItemCard, MetricCard, Chart, WeatherCard, etc. |
Adding your own components
// extend the catalog after import
import { AppCatalog } from "@a2ui/catalog";
import { MyCustomCard } from "./MyCustomCard";
AppCatalog["MyCustomCard"] = { component: MyCustomCard };