Skip to content

Repository files navigation

UI8Kit Codegen

One brick definition → seven identical runtimes.

Spec-driven codegen for UI8Kit ui/ primitives. Define props, variants, and a render tree once; emit idiomatic Templ, React, Svelte, Vue, SolidJS, Latte, and Twig with the same DOM, design tokens, and ARIA contract — verified by parity tests, not by review.

FactValue
Inventory33 bricks · 63 parts · 7 runtimes
PHP templates59 / 63 parts (4 skipped by structural predicate)
LicenseMIT

Runtimes

RuntimeOutputComposition
Go Templui/<brick>/<brick>.templ + _gen.goSSR, { children... }, Attrs
React 19ui/<brick>/<brick>.tsxforwardRef, asChild / Slot
Svelte 5ui/<brick>/<Part>.svelterunes, snippets, <svelte:element>
Vue 3ui/<brick>/<Part>.vue<script setup>, slots, <component :is>
SolidJSui/<brick>/<brick>.solid.tsxsplitProps, Dynamic, *Classes()
Latteui/<brick>/<Part>.lattetyped {parameters}, n:attr
Twigui/<brick>/<Part>.html.twigui8kit_attr_str, include with

Plus a static HTML export via bun run build:htmlexamples/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, and icon/Icon (loops over typed items or branched roots). Class helpers in php/UI8Kit/Classes.php are still emitted for every part. Details: docs/php-complex-parts.md.


Why codegen?

Hand-writing .templ + .tsx (and then Svelte, Vue, Solid, Latte, Twig) multiplies drift. This engine inverts the workflow:

  1. Brick definitions (bricks/<name>/<name>.def.ts) — single source of truth
  2. Canonical renderer (src/domain/render.ts) — executable DOM spec
  3. Emitters — one printer per runtime
  4. 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.


Quick start

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 seven runtimes → generated/
bun test# domain units + DOM parity
bun run verify # check + typecheck + tests (CI equivalent)

Local previews

Eight 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 dev:solid # :5178
bun run build:html # → examples/html/

CLI

bun src/infrastructure/cli.ts generate \
--out generated \
--go-module github.com/ui8kit/ui \
--runtimes templ,react,svelte,vue,solid,latte,twig

Also available: bun run generate:latte-bundle for a Latte-only package — see docs/latte-bundle.md.


Generated layout

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 | index.solid.ts
button/
button.variants.json # shared CVA recipe
button.shared.ts # TS types + classes helpers
button.templ | button.tsx | button.solid.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, vite-plugin-solid) · PHP → composer install in generated/ with a loader rooted at generated/ui.

Peer deps: react / svelte / vue / solid-js + clsx + tailwind-merge; PHP: latte/latte ^3 or twig/twig ^3.


Architecture

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/ # eight welcome previews
docs/ # full documentation
PrincipleHow it shows up
Single source of truthBrick defs are data, not templates
Closed IR13 expression kinds · 5 node kinds — exhaustive per emitter
Open emittersNew runtime = new printer, model unchanged
Fail fastValidation before any emit
Parity as gateCanonical renderer is the spec; tests enforce it

Deep dive: docs/architecture.md.


Cross-runtime contract

Decided once, applied everywhere (docs/contract.md):

ConcernRule
Prop namingCanonical Go PascalCase → React / Svelte / Vue native names
VariantsShared *.variants.json; TS literal unions; Go embedded Variants
Class mergeRecipe → variants → base → fragments → state → caller (cn / Cn)
ChildrenTempl / React / Svelte snippets / Vue slots — or none
Root tagsSame TagGroup allow-lists in every runtime
Booleans vs ARIANative bools present-or-absent; ARIA always "true" / "false"
Rest / AttrsSpread last — caller wins
BehaviorPresentation-only; client hooks via @ui8kit/aria at app level

Brick definition

// bricks/button/button.def.ts (abridged)exportdefaultbrick({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 ButtonClassesclasses: {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()],// Href set → <a>; otherwise <button>. asChild / ButtonClasses remain for exotic roots.render: when(isSet(prop("Href")),[el("a",)],[el("button",)]),}],});

Adding a brick

  1. Create bricks/<name>/<name>.def.ts + <name>.variants.json (+ optional .data.json fixtures)
  2. Register in bricks/index.ts
  3. bun run checkbun test

Authoring guide: docs/bricks.md.


Scope

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.


Documentation

DocTopic
OverviewGoals, runtimes, scope
Getting startedInstall, generate, consume
ArchitectureDomain, IR, emitters, pipeline
BricksAuthoring & validation
RuntimesPer-runtime notes
ContractCross-runtime rules
Testing & CIParity harnesses
ExamplesLocal previews
Latte bundleLatte-only package

MIT © UI8Kit

About

Spec-driven codegen engine for UI8Kit ui/ primitives. One typed render contract per brick — six generated runtimes (plus a static HTML export) with an identical DOM, design, and ARIA contract

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages