Component definition
A catalog component is a pair: def.ts, a framework-agnostic definition
the model reads, and impl.tsx, the React code that renders it. The
definition is all the model knows about a component: that it exists, its props
and its events. An entry points into it.
interface ComponentDefinition {
name: string; // catalog name the model emits — PascalCase, unique
description: string; // prose the model reads to decide when to use it
props?: StandardSchemaV1 & StandardJSONSchemaV1; // the props object; leave it out when there are none
callbacks?: Record<string, StandardSchemaV1 & StandardJSONSchemaV1>; // event → payload
hidden?: boolean; // kept out of the model's prompt, still renderable
}props and each callback are schemas that implement both
Standard Schema and
Standard JSON Schema : Zod 4.2+,
Valibot or ArkType (the examples use Zod). The prompt prints the JSON Schema as
a TypeScript-like signature; the implementation gets the inferred type.
import z from "zod";
import { createComponentDefinition } from "@uicast/core";
export const BadgeDef = createComponentDefinition({
name: "Badge",
description:
"A small status indicator badge for labels, counts, or tags. Use it for " +
"status (active, pending), categories, counts, or any short label.",
props: z.strictObject({
text: z.string().optional().meta({ description: "The badge text content" }),
variant: z
.enum(["neutral", "info", "warning", "danger"])
.default("neutral")
.meta({ description: "Visual style: neutral, info, warning, danger" }),
}),
});name
What the model writes in an entry’s component field.
PascalCase and unique in the catalog: two defs sharing a name throw
Duplicate component name: "Badge".
description
The model reads it to decide when to use the component. It is printed word for
word on the - Badge — … line below.
props — the contract with the model
The model sees whatever the schema holds. Omit props for a component that
takes none. getComponentsPartialPrompt turns the def above into:
- Badge — A small status indicator badge for labels, counts, or tags. Use it for status (active, pending), categories, counts, or any short label.
Props:
- text?: string — The badge text content
- variant?: "neutral" | "info" | "warning" | "danger" = "neutral" — Visual style: neutral, info, warning, dangerDescribe every field with .meta, or it prints as a bare type.
.default("neutral") makes variant optional and prints as = "neutral".
Props are parsed through the schema before render sees them, so an
omitted variant arrives as "neutral".
An entry’s props must produce that shape:
{ "key": "order-status", "component": "Badge",
"props": { "expr": "({ text: scopes.root.order.status, variant: scopes.root.order.status === 'paid' ? 'neutral' : 'danger' })" } }Use z.strictObject, so a prop the model invented fails the element into its
error slot instead of being dropped silently.
Never a children prop. It is the entry field that lists child keys, so
declaring it throws on creation. Name the prop for what it holds (text,
title, label); nested elements reach the implementation as React
children.
callbacks — event payloads
An optional map from event name to payload schema: the shape of evt in that
callback’s steps.
createComponentDefinition({
// …
callbacks: {
onChange: z.strictObject({
value: z.string().meta({ description: "The current value" }),
}),
onClear: z.null().meta({ description: "Clear button clicked" }),
onKeyDown: keyboardEventSchema,
},
});With keyboardEventSchema as a shared event, the prompt
prints:
Event handlers:
- onChange(evt)
- value: string — The current value
- onClear() — Clear button clicked
- onKeyDown(evt: KeyboardEvent)The model can then write:
{ "key": "search", "component": "SearchInput",
"props": { "expr": "({ value: scopes.root.q })" },
"callbacks": { "onChange": [{ "set": "scopes.root.q", "expr": "evt.value" }] } }A callback with no payload is z.null(), printed as a call with no argument:
Event handlers:
- onPress() — Fires when pressedThe implementation calls it as onPress().
Shared events
A payload many components fire, like onClick’s, would print under each one.
A JSON Schema $id prints it once:
// src/uicast-catalog/events/keyboard.ts
export const keyboardEventSchema = z
.object({
key: z.string().meta({ description: 'The key value (e.g. "Enter", "a")' }),
repeat: z.boolean().meta({ description: "Whether the key is auto-repeating" }),
})
.meta({ $id: "KeyboardEvent", description: "Fires on a key press" });
// in every def that fires it
createComponentDefinition({
// …
callbacks: { onKeyDown: keyboardEventSchema },
});getComponentsPartialPrompt finds the $id on its own, and each handler
becomes one line, onKeyDown(evt: KeyboardEvent):
## Common Events
- KeyboardEvent — Fires on a key press
- key: string — The key value (e.g. "Enter", "a")
- repeat: boolean — Whether the key is auto-repeatingA payload without a $id prints inline under each handler. Two different
payloads with the same $id throw at prompt assembly.
The implementation still passes that exact payload, not the React event:
createComponentImplementation({
// …
render: ({ onKeyDown }) => (
<input onKeyDown={(e) => onKeyDown({ key: e.key, repeat: e.repeat })} />
),
});hidden — keep a def out of the prompt
hidden: true keeps a def out of the prompt; an entry that names it still
renders.
Registration
Register each component twice: the def for the prompt, the impl for the renderer.
import { getComponentsPartialPrompt } from "@uicast/core/prompt";
import { EntriesRenderer, RendererProvider } from "@uicast/react";
import { defs } from "@uicast/shadcn-catalog/layout/defs";
import { impls } from "@uicast/shadcn-catalog/layout/impls";
import { BadgeDef } from "./badge/def";
import { BadgeImpl } from "./badge/impl";
const definitions = [...defs, BadgeDef];
const implementations = [...impls, BadgeImpl];
getComponentsPartialPrompt({ definitions });
<RendererProvider evaluator={evaluator} implementations={implementations}>
<EntriesRenderer entries={entries} />
</RendererProvider>;With only the def, the model emits a Badge that renders the
error slot with reason: "unknown-component". With only
the impl, the model never learns the component exists.