Version 0.4.28 · Published to npm. Scaffold with bun create broapp my-app. View on npm

Broapp
GitHub

Components

Three layers draw an Autoapp page, and one theme reaches all three. This is what each layer is for, what may be added to it, and how anything added is proved. The token table itself is in design.md; the authoring checklist a model follows is "Adding a component" in the common rules.

Three layers

The renderer — broapp-autoapp/react, drawing a declarative view specification. Six kinds: page, section, text, table, form and button, plus the strips the framework owns (approvals, runs, workflow drafts). Plain CSS in view.css, one .autoapp- class per part, no Tailwind, no component library, no generated browser code. It is pinned: an AI engineer reshapes an application by proposing a different view specification, so the page's script hash does not move and its policy stays pinned. Adding a kind here is a framework decision, not an application's.

The panel — broapp-ai-elements/ui, the AI chat. Vendored shadcn/ui source on Radix primitives, compiled Tailwind, scoped to .broapp-chat and its neighbours. It is vendored rather than depended on because a page has one stylesheet and one policy, and a registry that pulled its own assets could not live inside either. Every vendored file names its upstream version in its first line and marks local edits LOCAL / END LOCAL; a test holds both.

The application — its own src/ui/, which draws the frame around the renderer and composes the panel. React, plain CSS, whatever it likes, as long as the build's rules hold: no off-origin URL, one stylesheet, one script.

One theme

The application's palette is seven properties on its own :root:

--bg --surface --border --text --text-muted --accent --accent-contrast

The panel reads those seven and maps them onto shadcn's names in src/ui/tailwind.css. Since prompt g every colour token the renderer reads follows one of the same seven by meaning — --autoapp-surface is declared var(--surface, #ffffff), and so on — so an application sets its palette once and both vocabularies take it. The reads column of AUTOAPP_TOKENS is the mapping; there is no second copy of it anywhere.

Precedence, in the order it resolves:

  1. The renderer's defaults are on :where(:root), at zero specificity.
  2. One of the seven, on the application's :root, reaches every token that

follows it.

  1. An --autoapp-* token set directly on :root beats what it would have

followed. The panel never reads an --autoapp-* property, so an override is how an application makes the renderer and the panel differ on purpose.

  1. .broapp-tokens carries the panel's mapping. The panel's own scopes include

it, and every portalled component carries it too: a select's content, a dropdown menu, a tooltip, a hover card and a dialog are all rendered at the end of the document, outside the panel, and without that class every colour in them resolves to nothing. That was a real fault, found by the harness below on the day it was written, and it is now the rule.

A token whose meaning has no place in a palette — a notice, a warning, an error, a code block's ground, a button's own grey — follows nothing and keeps its default until an application sets it.

Two audiences, two instructions

A person applying a brand's style guide and a model adding a component are told different things in different places, and neither file repeats the other.

  • The engineer reads spec.reference topic theme: the token list

generated from the table, what each token follows, the precedence above, and "Applying a style guide" — map by role not by name, the seven first, and what a guide cannot become (a marketing component is not a kind; a face the page does not ship will not load; a light-only guide sets color-scheme: light).

  • The engineer also reads topic design: how to decide what a page is made

of and every word on it, distilled so that every line names a kind or a property it can set. What it carries, what it leaves out and where it comes from is in design-guidance.md.

  • A model or contributor adding a component reads "Adding a component" in

prompts/autoapp/00-common-rules.md: where it may live, what it may import, typed props and controlled state, keyboard and focus and accessibility, the states it has to draw, tokens only, and the four places it has to appear before it is done — the reference topic, the gallery, the harness, the tests.

One set of controls

A control means the same thing wherever it is drawn: in a form the renderer draws, in AiSettings, in a launcher panel, in a component nobody has written yet. The vocabulary is small, and a new component uses it before inventing.

  • A field is a label above a control. The label takes font-size-small and

weight-strong. "Required", "Optional" or "Saved" sits at the right end of the label's line in font-size-caption, never in brackets inside the label. A hint goes under the control, in font-size-caption and the muted colour.

  • One height, one corner, one border for a field, a select, a read-only

value row and a button beside them: radius-sm, border-width, and a height they share, so a row of them sits on one line.

  • Three weights of action. One primary per section: the accent, and full

width when it ends a form. Secondary: bordered, the button ground. Quiet: text only, inside a row or a label line (Replace, Remove, Refresh). Destructive is the error colour on a secondary shape, says so under it, and asks once more.

  • A true-or-false setting is a switch (role="switch") at the right of its

label, with the consequence as a hint. A checkbox is for choosing from a list.

  • A section is a title (font-size-heading), one muted sentence that adds

something, then fields. Sections in one panel are divided by a rule, not boxed: a card inside a card is always wrong, and AiSettings is already one.

  • A panel over the page opens with a title, a rule under it and a close

button in the top right corner, by way of one shared header component. The launcher's is PanelHeader; Log, Knowledge, Backlog and Settings all use it.

  • Focus is the focus-ring tokens on every control, icons included.

ai.css holds the reference implementation. Its lengths are --ai-* properties that follow the --autoapp-* control tokens and fall back to their defaults, so the settings panel and a rendered form on one page are one set, and a preset that changes the tokens changes both. This is the one place a layer outside the renderer reads --autoapp-*, and it reads lengths only: colour still comes from the seven. tests/autoapp-theme.test.ts holds each fallback to the table, and holds every launcher panel to the shared header.

How it grows

  • A renderer kind is added in packages/broapp-autoapp: the view schema,

the renderer, view.css in tokens only, the views reference topic, the gallery, and a specification test. No new dependency; the renderer has none for drawing.

  • A panel component is vendored from the registry with its version in the

first line, or composed from what is already vendored. cva for a component with real variants, cn() for merging a caller's classes, a Radix primitive where focus management or a portal is the hard part — and none of the three anywhere else.

  • An application component is the application's own, and the one place

ordinary React and ordinary CSS need no justification.

Nothing crosses: the renderer does not import Tailwind, the panel does not read --autoapp-*, and an application's stylesheet does not write rules for .autoapp- or .broapp-chat classes.

How it is proved

Text first, because it is cheap. tests/autoapp-theme.test.ts holds the table, the generated stylesheet, the renderer's stylesheet, the starter, Notes, the launcher and both presets to each other: no literal length, colour, weight, tracking or family in view.css; every fallback the table's default; every reads one of the seven.

Then a browser, because agreement between vocabularies cannot be read off a string. bun run theme-check builds the page with the real build under three themes — the starter's palette, the quiet preset, and a hand-written override — opens each under light, dark and no-preference, opens the panel's select so its portal is exercised, and reads computed styles: the colours, corner and family of the renderer's button and of the select's content, whether the content still sits in the panel's scope, and the contrast of every text/background pair against .:.broapp-tmp/theme-check.md. Nine combinations, one line per measurement in .broapp-tmp/theme-check.md. A failure is a named combination and property, not a screenshot. tests/autoapp-theme-browser.test.ts runs the same rules and skips itself where Chromium is missing; CI installs Chromium in one job.

What it does not cover yet: one ordinary control and one portalled component, both in a page built for the purpose. Not the approvals strip, not a table under a long word, not a real bridge, not a screenshot anybody compares. Every new component adds its combination, which is what makes that list shrink.

Edit this page on GitHub