The catalog
This page describes the shipped behaviour of defineCatalog — what registration rejects, how a
hint addresses a field, and what an editing surface reads a block's fields through. The
declarations behind it are generated from the source into
the API reference. Why the catalog
exists apart from the registry is
Catalog and registry are separate; why
hints sit beside the schema rather than inside it is
Editing hints live beside the schema, not inside it.
defineCatalog
defineCatalog returns its argument after
checking everything checkable at registration, because a bad hint or bad defaults are silent at
every later point. Derived from
packages/core/src/defineCatalog.test.ts and the demo's
examples/demo/src/nubbin/catalog.ts:
import { defineCatalog } from "@nubbin/core";
import { z } from "zod";
const liveBandSchema = z.object({
label: z.string(),
items: z.array(z.object({ text: z.string(), at: z.string() })),
});
export const catalog = defineCatalog({
LiveBand: {
schema: liveBandSchema,
defaults: { label: "On now", items: [] },
ui: { fields: { items: { data: { revalidate: 60 } } } },
},
});
It throws on:
| Rejected | Why |
|---|---|
A ui.fields key naming a path the schema does not define | An unresolvable hint is invisible at runtime — the inspector falls back to a default treatment and renders something plausible. The error names the block, every bad path, and the paths the schema does define |
defaults that fail the entry's own schema | Defaults are what a freshly dropped block renders with, so invalid defaults produce a block that is broken the instant it is placed |
A data hint on a path containing [] | A hole resolves to one value, and an array-member path names every member, so it has no single target. Such a path may still carry a hint; only a data one is refused |
Two data hints on one block whose paths nest, such as cta and cta.label | Two holes writing into one value have no defined order of application — see A data hint addresses a path, not a top-level key |
Hint paths are resolved by reading the schema through the Standard JSON Schema converter the
schema itself exposes (~standard.jsonSchema, Standard Schema spec 1.1) — the package calls
no validator function and imports no validator. Two consequences, both thrown at
registration:
- a schema that does not expose the converter is rejected when its entry carries
ui.fields; - a field JSON Schema cannot express is rejected rather than degraded, because the converter
runs with
unrepresentable: "throw".
defaults are validated with the schema's own ~standard.validate, which must be
synchronous — an async validator is refused with an error.
An entry carrying only schema is stored as-is; both checks run only when the fields they
check are present.
Catalog and CatalogEntry
Keys of a Catalog are block names — the same
names the registry resolves. A
CatalogEntry is serializable data only:
schema, hints, defaults, docs, and never a component. That is
the catalog/registry split — an editing surface and CI read the catalog; rendering needs the
registry.
schema is typed unknown rather than StandardSchemaV1 because the capability actually
required — the JSON Schema converter — is narrower than the validation interface, and the
runtime check happens either way.
BlockUi and FieldHint
BlockUi.fields is keyed by schema path in
dotted form, with [] addressing array members:
title, cta.label, items[].icon. Every key must resolve against the entry's schema —
see the registration checks above. Why paths rather than a mapped type, and how a control is
chosen from a FieldHint, are argued in
api.md.
FieldHintData
A data hint says how a field's value
resolves at render. An absent data hint means static: the value freezes
into the artifact's props at compile. A hint means the value is left out and resolved by the
consumer's resolver instead, cached for revalidate seconds. The two states and the reasoning are
Data lifecycle is per field; what the
compiler does with the hint is on the compile page.
SchemaAdapter, FieldNode and FieldKind
SchemaAdapter is the contract for
reading a schema's field structure: describe returns
one FieldNode per addressable path, in the
same dotted form hint keys use, each carrying the
FieldKind an inspector renders it as. The
schema root itself has no path, so the result is exactly the set of paths a hint may target.
zodAdapter
zodAdapter is
the shipped implementation — the adapter defineCatalog resolves hint paths through,
exported so an editing surface can describe a block's fields without the block's component.
It is named for the reference validator but reads any schema exposing the converter; why the
converter beat validator-internal traversal is argued in api.md. Duplicate
paths — a union whose branches share a field — are reported once, keeping the first kind
seen.