Your own components
The planner chooses, for each source, one of the components whose manifest accepts that source's data. The library ships seventeen. When none says what you want, add your own: the planner treats them exactly like the library's.
Generate one
pnpm web4kit add component quote-card --shape record --what "A short quote from a guest, set large"This writes two files and registers the component:
web4/components/quote-card.tsx: the manifest, the props schema, and the markup.web4/components/quote-card.test.ts: validates the manifest and renders sample data.web4/components/index.ts: the import and the list entry are added at the// web4kit:marker lines. Keep those lines.
Reload the page. Every record source whose fields cover the component's required roles now offers quote-card in its component question. The X-ray shows which component was chosen and by whom. A new component changes the question for those sources, so their calibration becomes stale. They're ungated in dev until you run pnpm calibrate.
The command refuses invalid names (use kebab-case) and ids that already exist, including the library's. It also refuses when a file is already there or the markers are missing. In each case it changes nothing.
The manifest is the contract
export const quoteCard = defineComponent({
manifest: {
id: "quote-card",
what: "A short quote from a guest, set large",
accepts: [{ shape: "record", requires: ["title"] }],
affordances: ["highlight"],
footprint: {
mobile: { colSpan: 12, rowSpan: 1 },
tablet: { colSpan: 12, rowSpan: 1 },
desktop: { colSpan: 6, rowSpan: 1 },
},
mediaHeavy: false,
},
props: z.object({ title: z.string(), body: z.string().optional() }),
toProps: (data, binding) => bindRecord(data, binding),
render: (p, ctx) => <Card>…</Card>,
});whatis a prompt. The model reads it literally when choosing a component for a source. Say what the component shows and when it's the right choice ("a short quote, set large"), not how it's built. Editing it later makes the sources offered this component stale, like editing a source'swhat.acceptsdecides where it's offered.shapeis the data shape (list,record,media-list,schedule,geo,timeseries,graph).requiresnames the roles a source must bind in itsfields. A source without atitlerole never sees a component that requires it. When there are several compatible components,rank(lower first) picks the default.footprintis per device. It sets columns out of 12 and rows. The layout solver uses it, and the renderer gives the block exactly that space, so design for it at every device.mediaHeavycomponents need afallback. Visitors with Save-Data on get the fallback component instead.
Props and fail-soft rendering
toProps maps the source's data through the plan's role → field binding. bindRecord and bindList from @web4kit/react do the usual cases. The canonical shapes (schedule, geo, timeseries, graph) arrive already in their schema, so use ScheduleDataSchema and its siblings from @web4kit/manifest as props. Props are validated before every render. If validation fails, or the render throws, the block falls back to the source's default component. Neither the page nor the plan breaks, and the X-ray shows the fallback and why.
Use the library's primitives (Card, Heading, Badge) and design tokens (bg-card, text-muted-foreground, …) so your components match the rest of the page in both themes.