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+,
ArkType, or Valibot through toStandardJsonSchema() from
@valibot/to-json-schema (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" }),
}),
onKeyDown: keyboardEventSchema,
},
});With keyboardEventSchema as a shared event, the prompt
prints:
Event handlers:
- onChange(evt)
- value: string — The current value
- 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.
An id, as on any shared type, 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 an 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.