Components, pages and screens
There is no CSS build, no node in the build and no JavaScript framework. The UI is five Go packages, each a layer that the one above composes, and three shells that compose the top layer and write nothing of their own.
| Package | What it is |
|---|---|
Two themes as struct literals — the surfaces, text, borders, accent, focus,
four statuses and the sidebar palette — rendered to |
|
An immutable |
|
Forty-one functions of the form |
|
|
|
A document as a value. |
|
The seven generated pages of a resource — list, detail, create and edit
forms, the two writes and delete — as pure renderers of an |
Screens derive from schemas
rest.Spec.Mount registers an httpx.Resource beside the five API routes: the
names, the path, the two permissions, the immutable fields, the schema, and the
five operations bound to the entity’s type as closures that ask the same
authorizer the routes do. The admin module reads that register and calls
screens.Mount for every resource it finds.
Everything on those pages comes from the schema. A select exists because the
struct says enum; a field is required because it says validate:"required",
absent from the form because it is read-only, absent from the list because it
says ui:"hide:list", shown read-only because the Spec named it Immutable.
Adding an entity to an application adds seven screens and no code; adding a
field adds it to every screen in the same commit as the API.
The cost is that a generated screen is generic. It cannot know that an identifier is a person, so it renders the identifier and says there is no picker for it. When that matters, the answer is a hand-written screen for that interaction — the reference application has five, and each says why — not a tag that makes the generator cleverer. This is ADR 0007.
Four shells, one abstraction
The public site is the smallest: two public routes, a bar with the site’s
title, tagline, logo and navigation from the site settings, an article from
the content module’s renderer, the tenant’s theme and colour pinned per
document, and no controller of its own. It is
modules/web, composed only where no storefront
claims the root. The admin module is composition only: one Chrome, one Frame (sidebar,
header, footer, the confirm dialog), one Navigation value computed once from
what the application serves, five hand-written pages, and screens.Mount per
resource. A client’s storefront is the same shape with a bar where the admin
has a sidebar, its own class lists handed to ui.Compose as an Extra, and a
pinned theme because a shop is the brand’s colour. The native shell is the
third: it reads screens.Describe’s JSON at `GET /api/v1/admin/resources and
generates its screens by the same rules. See The native shell.
The browser
htmx is vendored and minified, and it is the only third-party byte the browser
runs. Beside it are four controllers of a few dozen lines: a theme that must
survive a reload, a validation error that must not cost a page, a destructive
action that must be confirmed, and a sign-in form that posts to a JSON route.
The controllers read the shell’s sign-in path off <html> and name no route.
Every HTML response carries a content security policy whose script-src is
this origin plus a per-request nonce; the one inline script, which applies a
stored theme before first paint, carries it. style-src keeps
'unsafe-inline' because components emit style attributes for a column’s
width, which a nonce cannot cover; ARCHITECTURE.md says so rather than leave
it to be found.
The component gallery at /admin/_gallery renders every component once, from
the package’s own exported list — the same list the tests render to prove the
class closure — so the page and the test cannot disagree about what exists.
The design export
Each gallery entry is an Example: an explicit identity such as
pk-ui.component.button/primary, the typed props and slots the constructor
was called with, and a node that renders through that constructor.
ui.Export projects a palette, a list of examples
and any stylesheet additions into one JSON snapshot: the tokens of each theme,
every glyph as SVG with its source and licence, every example with its
property values, their JSON Schema, its slot names and its rendered HTML, the
composed stylesheet, and a content hash. A product calls the same function with
its own design.Pair and examples; nothing registers a second catalog.
go run ./tools/designexport
go run ./tools/designexport --example pk-ui.component.button/primary
tools/designexport writes the reference
snapshot to standard output. --example selects one entry, and --props
applies a property patch read from standard input through
Example.WithProps, so a design tool can ask the Go renderer for a variation
instead of restating the component in another language. The patch is exact:
public field names and types only, no unknown or transport fields.
tools/designexport/openpencil
turns the snapshot’s tokens and icons into native variables and linked icon
components in a .fig file, and carries the pinned OpenPencil SDK corrections
its conformance tests need. Its README states what is supported and what is
not: converting components, pages and flows, and the hosted editor build, are
unfinished, and its dependency audit is an open gate. CI runs that suite beside
make check.