Brick definitions are the single source of truth for every runtime. They live in bricks/<name>/ and are registered in bricks/index.ts.
33 bricks · 63 parts
| Brick | Parts |
|---|---|
alert |
Alert |
badge |
Badge |
block |
Block |
box |
Box |
breadcrumb |
Breadcrumb |
button |
Button |
card |
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter |
checkbox |
Checkbox |
container |
Container |
dialog |
Dialog |
disclosure |
Disclosure, Summary |
form |
Form, FormItem, FormDescription, FormMessage |
form/controls |
Fieldset, Legend, DataList, DataOption, Output, Meter, Progress |
grid |
Grid, GridCol |
group |
Group |
icon |
Icon |
iconbadge |
IconBadge |
image |
Image, Picture, Source |
inline |
Inline |
input |
Input |
label |
Label |
linebreak |
Break |
link |
Link |
list |
List, ListItem |
radio |
Radio |
select |
Select, SelectOption, OptGroup |
separator |
Separator |
stack |
Stack |
switch |
Switch |
table |
Table, TableCaption, TableHead, TableBody, TableFoot, TableRow, TableHeadCell, TableCell, TableColGroup, TableCol |
text |
Text |
textarea |
Textarea |
title |
Title |
List at any time:
bun src/infrastructure/cli.ts listbricks/button/
button.def.ts # definition (required)
button.variants.json # CVA-style class recipe (if brick uses variants)
button.data.json # showcase fixtures for parity tests (optional)
Definitions import from ../_dsl, which re-exports IR constructors and prop factories:
| Factory | Prop |
|---|---|
pVariant(), pSize() |
CVA-bound string props |
pClass() |
Caller Tailwind utilities |
pStr(name), pBool(name), pInt(name) |
Scalar props |
pAttrs() |
Rest/attrs escape hatch |
pChildren() |
Children slot |
pPass(name) |
Passthrough attribute forward |
controlPassthroughProps() |
Common form control forwards |
Render tree builders: el, text, slot, when, forEach, fwd, attrClass, attrExpr, attrBool, attrStatic, attrRest.
// bricks/button/button.def.ts
export default brick({
id: "ui.button",
dir: "button",
recipes: { button: { file: "button.variants.json", recipe: buttonVariants } },
parts: [{
name: "Button",
recipeId: "button",
asChild: true, // React-only; other runtimes use ButtonClasses()
classes: {
recipe: { variant: "Variant", size: "Size" },
state: [{ test: prop("Disabled"), classes: "pointer-events-none opacity-50" }],
},
props: [
pVariant(), pSize(), pClass(), pStr("Type"), pStr("Href"),
pBool("Disabled"), ...controlPassthroughProps(), pAttrs(), pChildren(),
],
// Optional Href → <a> with button classes; else <button>.
render: when(isSet(prop("Href")), [el("a", …)], [el("button", …)]),
}],
});CVA-style JSON consumed by all runtimes:
{
"id": "ui.button",
"base": "inline-flex …",
"keys": ["variant", "size"],
"defaults": { "variant": "default", "size": "default" },
"byKey": {
"variant": { "default": "…", "ghost": "…" },
"size": { "default": "…", "sm": "…" }
}
}- Go embeds via
//go:embedin<brick>_gen.go. - TS runtimes import JSON and derive literal-union types in
<brick>.shared.ts. - PHP compiles recipes into constants in
Classes.php.
Optional colocated fixtures become parity test cases automatically:
{
"id": "ui.button",
"showcase": {
"variant.ghost": {
"props": { "Variant": "ghost", "Size": "sm" }
}
}
}tests/support/fixtures.ts loads bricks/<dir>/<stem>.data.json and expands each showcase entry into a test case (defaults + each fixture).
validateRegistry enforces before emission:
- Recipe integrity and key alignment
- All expression
prop()references exist on the part - Passthrough props are not used in logic (only forwarded)
- Slot/children symmetry
- Tag group membership for dynamic tags
- No duplicate brick IDs or part names
Failures throw DefinitionError with a descriptive message.
- Create
bricks/<name>/<name>.def.tsand colocate<name>.variants.json(and optional<name>.data.json). - Register the import in
bricks/index.ts. - Run
bun run check. - Run
bun test— parity across all runtimes is asserted automatically; no per-runtime test authoring needed.
Svelte and Vue require one file per part (SFC constraint):
Card.svelte,CardHeader.svelte, …
Go Templ and React colocate all parts in one file per brick:
card.templ,card.tsx
Latte and Twig always emit one template per part:
Card.latte,CardHeader.latte, …
Some bricks declare typed records for list-driven rendering:
recordTypes: [{
name: "SelectOptionItem",
fields: [
{ name: "Value", type: "string" },
{ name: "Label", type: "string" },
],
}],
props: [
{ name: "Options", type: "items", cva: false, itemsOf: "SelectOptionItem" },
],
render: el("select", [...], [
forEach("Options", [ el("option", [...], [text(item("Label"))]) ]),
slot(),
]),forEach and branched roots affect PHP template eligibility — see PHP complex parts.
Uniform contract improvements verified across all runtimes:
- Empty string attributes are omitted (
name="",class=""never emitted). IconBadgedeprecated legacy props dropped from generated API.- React
<textarea>maps content todefaultValue(SSR output identical). - Rest/
Attrsspread is always last — caller wins deterministically. Title/CardTitleReact-onlyH1–H6wrappers not generated; useas={1..6}.