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+, Valibot or ArkType (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" }), }), 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 pressed

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

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

Last updated on