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

design

Two themes as struct literals — the surfaces, text, borders, accent, focus, four statuses and the sidebar palette — rendered to --pk-color-* custom properties. design.Pair is the two an installation ships; a client’s colours are one value.

ui/style

An immutable ClassList builder and the rule emitter. A class list names semantic roles; a role is a custom property written in terms of a token; so utility rules are theme-independent and switching theme is one attribute on <html>.

ui/components

Forty-one functions of the form Button(ButtonProps) g.Node. Each declares the class lists it can emit, and a test proves every declared class resolves to a rule and no rendered class is undeclared. Accessibility is by construction: labels are associated, icons hidden from assistive technology, states carry their ARIA attributes.

ui

ui.Compose(theme, extra…​) folds the tokens, the roles, a small base layer, the components' lists and a consumer’s own lists and rules into one Sheet — bytes and their fingerprint — computed once at mount and served from memory. ui.Assets is the tree a shell serves: the sheet, the gallery sheet and the controllers.

ui/page

A document as a value. Chrome is what every page of a shell shares — brand, asset prefix, stylesheet, controllers, sign-in path, pinned theme. Request is what a render may know about the caller, read once. View is what a handler returns. Frame arranges a body. page.Serve is the one adapter between a handler and the router: it reads the request, frames the view, turns a redirect into a redirect and a refusal into a fault page.

ui/screens

The seven generated pages of a resource — list, detail, create and edit forms, the two writes and delete — as pure renderers of an httpx.Resource and what a handler read, plus Mount, which puts them behind page.Serve for any shell. screens.Describe publishes the same knowledge as JSON.

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.