Adding a component
npm run new:component -- ribbonThe scaffold creates src/components/ribbon.ts and its test file, and registers the component. The contract tests then fail until every placeholder is filled in, so npm test tells you what's left.
The definition
import type { ComponentDef } from "sayshape";
export const ribbon: ComponentDef = {
type: "ribbon",
category: "content",
description: "A full-width colored strip with a short notice.",
text: { description: "The notice.", required: true },
props: {
tone: {
type: "enum",
values: ["accent", "good", "warn", "bad"],
default: "accent",
description: "Color role.",
},
},
example: `ribbon "Free shipping this week" tone=good`,
layout(box) {
box.h = 32;
},
paint(box, ctx) {
const p = ctx.props(box.node);
const color = ctx.theme.colors[p.oneOf("tone", ["accent", "good", "warn", "bad"] as const, "accent")];
const { g } = ctx;
ctx.roundRect(box.x, box.y, box.w, box.h, ctx.theme.radius.sm);
g.fillStyle = color;
g.fill();
g.font = ctx.font(600, 13);
g.fillStyle = ctx.theme.colors.onAccent;
g.textAlign = "center";
g.textBaseline = "middle";
g.fillText(p.text(), box.x + box.w / 2, box.y + box.h / 2 + 1, box.w - 16);
},
};Fill in everything a model needs: a plain description of when to use it, quoted text if it takes any, every prop with its type, description and default or required, children and parents if relevant, and a short example that parses with no diagnostics. Interactive components register hit regions while painting; components that write a bind= value use ctx.set(p.raw("bind"), value) so paths like t.done work inside each. src/components/input.ts, navigation.ts and map/index.ts are good references.
Tests
Add parsing tests with good and bad props, layout at narrow and wide widths, and interaction tests with render(). If it binds, add it to the table in test/write-data.test.ts. The contract tests already check documentation, layout bounds, theme-only colors and keyboard reach.
How documentation stays in sync
There's one source for each component, its ComponentDef. Everything else is generated from it:
| Output | Generated by | Checked by |
|---|---|---|
| Parser validation | The registry, at runtime | Unit and contract tests |
| The model guide | languageGuide(), at runtime | test/prompt.test.ts and a fixture of the full guide |
| The README's component reference | npm run docs:components | npm run docs:check in CI |
| This website's component reference | npm run site:docs, before every site build | test/site.test.ts parses every example |
So after changing a definition, run npm run docs:components for the README; the website regenerates its pages on its next build. Never edit the generated reference by hand: edit the definition.