One brick definition → six identical runtimes.
Spec-driven codegen for UI8Kit ui/ primitives. Define props, variants, and a render tree once; emit idiomatic Templ, React, Svelte, Vue, Latte, and Twig with the same DOM, design tokens, and ARIA contract — verified by parity tests, not by review.
| Fact | Value |
|---|---|
| Inventory | 33 bricks · 63 parts · 6 runtimes |
| PHP templates | 60 / 63 parts (3 skipped by structural predicate) |
| License | MIT |
| Runtime | Output | Composition |
|---|---|---|
| Go Templ | ui/<brick>/<brick>.templ + _gen.go |
SSR, { children... }, Attrs |
| React 19 | ui/<brick>/<brick>.tsx |
forwardRef, asChild / Slot |
| Svelte 5 | ui/<brick>/<Part>.svelte |
runes, snippets, <svelte:element> |
| Vue 3 | ui/<brick>/<Part>.vue |
<script setup>, slots, <component :is> |
| Latte | ui/<brick>/<Part>.latte |
typed {parameters}, n:attr |
| Twig | ui/<brick>/<Part>.html.twig |
ui8kit_attr_str, include with |
Plus a static HTML export via bun run build:html → examples/html/.
All runtimes share the same colocated *.variants.json (CVA-style recipes). Attribute logic lives in a small typed expression IR and is printed idiomatically per target.
PHP note: Latte/Twig skip
breadcrumb/Breadcrumb,select/Select, andicon/Icon(loops over typed items or branched roots). Class helpers inphp/UI8Kit/Classes.phpare still emitted for every part. Details: docs/php-complex-parts.md.
Hand-writing .templ + .tsx (and then Svelte, Vue, Latte, Twig) multiplies drift. This engine inverts the workflow:
- Brick definitions (
bricks/<name>/<name>.def.ts) — single source of truth - Canonical renderer (
src/domain/render.ts) — executable DOM spec - Emitters — one printer per runtime
- Parity tests — SSR every generated component and assert normalized DOM equals the canonical output
Identity across runtimes is a property of the test suite, not a convention.
Prerequisites: Bun. Go 1.25+ and PHP 8.1+ / Composer are optional (parity suites skip when missing).
bun install
bun run check # validate all brick definitions
bun run generate # emit all six runtimes → generated/
bun test # domain units + DOM parity
bun run verify # check + typecheck + tests (CI equivalent)Seven welcome screens (generated primitives + shadcn tokens) live in examples/:
bun run generate && cd examples && bun install && bun run build:css
bun run dev:templ # :8080
bun run dev:react # :5173
bun run dev:svelte # :5174
bun run dev:vue # :5175
bun run dev:latte # :5176
bun run dev:twig # :5177
bun run build:html # → examples/html/bun src/infrastructure/cli.ts generate \
--out generated \
--go-module github.com/ui8kit/ui \
--runtimes templ,react,svelte,vue,latte,twigAlso available: bun run generate:latte-bundle for a Latte-only package — see docs/latte-bundle.md.
generated/
go.mod composer.json
cmd/parity/ # Go + PHP render harnesses
php/UI8Kit/ # Rt.php, Classes.php, TwigExtension.php
utils/ # shared runtime support (Go + TS)
ui/
index.ts | index.svelte.ts | index.vue.ts
button/
button.variants.json # shared CVA recipe
button.shared.ts # TS types + classes helpers
button.templ | button.tsx | Button.svelte | Button.vue
Button.latte | Button.html.twig
Consume: Templ → templ generate && go build · TS → Vite (@vitejs/plugin-vue, @sveltejs/vite-plugin-svelte) · PHP → composer install in generated/ with a loader rooted at generated/ui.
Peer deps: react / svelte / vue + clsx + tailwind-merge; PHP: latte/latte ^3 or twig/twig ^3.
bricks/ # 33 definitions + variants.json + fixtures
src/
domain/ # pure model — expr IR, render, validate (zero I/O)
emitters/ # one printer per runtime
application/ # generate pipeline
infrastructure/ # CLI
runtime/ # support files copied into generated/utils
tests/ # domain units + cross-runtime parity
examples/ # seven welcome previews
docs/ # full documentation
| Principle | How it shows up |
|---|---|
| Single source of truth | Brick defs are data, not templates |
| Closed IR | 13 expression kinds · 5 node kinds — exhaustive per emitter |
| Open emitters | New runtime = new printer, model unchanged |
| Fail fast | Validation before any emit |
| Parity as gate | Canonical renderer is the spec; tests enforce it |
Deep dive: docs/architecture.md.
Decided once, applied everywhere (docs/contract.md):
| Concern | Rule |
|---|---|
| Prop naming | Canonical Go PascalCase → React / Svelte / Vue native names |
| Variants | Shared *.variants.json; TS literal unions; Go embedded Variants |
| Class merge | Recipe → variants → base → fragments → state → caller (cn / Cn) |
| Children | Templ / React / Svelte snippets / Vue slots — or none |
| Root tags | Same TagGroup allow-lists in every runtime |
| Booleans vs ARIA | Native bools present-or-absent; ARIA always "true" / "false" |
| Rest / Attrs | Spread last — caller wins |
| Behavior | Presentation-only; client hooks via @ui8kit/aria at app level |
// bricks/button/button.def.ts (abridged)
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"), pBool("Disabled"),
...controlPassthroughProps(), pAttrs(), pChildren()],
render: el("button", [
attrExpr("type", oneOfOr(prop("Type"), ["submit", "reset"], "button")),
attrClass(),
attrBool("disabled", prop("Disabled")),
fwd("id", "ID"), fwd("aria-label", "AriaLabel"),
attrRest(),
], [slot()]),
}],
});- Create
bricks/<name>/<name>.def.ts+<name>.variants.json(+ optional.data.jsonfixtures) - Register in
bricks/index.ts bun run check→bun test
Authoring guide: docs/bricks.md.
In: ui/ primitives — 33 bricks, 63 parts (Tiers A–E), including multi-part composites (card, table, form, select, breadcrumb).
Out (later phase): behavior-driven components/ (Sheet, Tabs, Popover, Combobox, Menu, Toast) that need @ui8kit/aria. The IR and emitters are ready once that contract is specified.
Deliberate deviations from upstream hand-written pairs (empty attrs omitted, rest spread last, etc.): docs/contract.md.
| Doc | Topic |
|---|---|
| Overview | Goals, runtimes, scope |
| Getting started | Install, generate, consume |
| Architecture | Domain, IR, emitters, pipeline |
| Bricks | Authoring & validation |
| Runtimes | Per-runtime notes |
| Contract | Cross-runtime rules |
| Testing & CI | Parity harnesses |
| Examples | Local previews |
| Latte bundle | Latte-only package |
MIT © UI8Kit