Skip to Content
Component definition

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.

src/uicast-catalog/badge/def.ts
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, danger

Describe 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 pressed

The 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-repeating

A 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.

Last updated on