@exponential_app/eui
v0.21.0
Published
genUI: the core A2UI catalog (components, macros, model descriptions, token names), the A2UI basic catalog map, the TypeScript reference reducer, the runtime theme format with the built-in themes, and the theme builder.
Maintainers
Readme
@exponential_app/eui — the genUI catalog
The data half of genUI (VAPP-84): the core catalog of
components an A2UI surface may use, the A2UI basic catalog vendored and
mapped onto it, the macro table that turns composite components into
natives, the model-facing descriptions and prompt, the token names
themes must define, the runtime theme format with the four built-in
themes and the theme builder (VAPP-92), codegen for TypeScript, Swift, Kotlin
and Rust, and the fixtures every renderer suite locks against. No renderer
lives here (React: VAPP-87, Solid: FEED-128, the web component
<exponential-surface>: FEED-130, the Rust core: VAPP-86, painters:
VAPP-88/89/90).
It also defines the host API every renderer shares (VAPP-91: transport,
functions, bindings, negotiation, policy, declarative packages, the
Exponential connector) and packages the conformance suite. The workspace
package.json stays "private": true with main on the sources; the npm
package is staged and published by release/ on eui-v* tags. Depends on
nothing from the Exponential app.
Layout
| path | what |
|---|---|
| catalog/core.catalog.json | THE source: ids, enums, shared shapes, functions (the basic 14 + the 19 core ones, functions.core), the built-in glyphs (builtinIcons), 89 components (87 offered + the hidden TreeGuides part and Unknown placeholder; round 4 removed the round-3 aliases; EXP-1259 added SearchList, EXP-1324 Trace + TraceItem, EXP-1280 ChoiceTiles, NEW-77 VoiceWave, VAPP-162 MessageBubble, CallSurface, Plan, QrCode, QrScanner) with props + descriptions |
| catalog/macros.json | the declarative expansion table, one template per macro component |
| catalog/basic-map.json | A2UI basic → core: components (+ named transforms), icons, functions |
| catalog/style.json | the Box style whitelist (VAPP-4), one source for TS / schema / natives |
| catalog/tokens.json | the token NAMES ($color.primary, $spacing.md, $breakpoint.md, $ease.standard, …); values come from a theme |
| catalog/strings.json | round 1: the renderers' built-in UI copy (ids + English defaults), overridable per surface, referenced as $string.<id> |
| catalog/locale.json | round 1: week start by region, likely regions (CLDR), deprecated language aliases, the RTL languages, the glyphs mirrored under RTL |
| catalog/code.json | round 1: CodeBlock's built-in tokenizer (rules + per-language specs) |
| catalog/a11y.json | round 1: the machine-readable role vocabulary; per component its role, notes and keyboard expectations; the rules; the host commands (focus, announce, scrollIntoView). Macro parts carry role/states/name via $a11y (macros.json) |
| catalog/layout.json | round 2: the shared layout numbers (window threshold + overscan, field and media intrinsic sizes, Resizable step/min/hit, the tree-guide column/radius/bridge) |
| catalog/recipes.json | the recipe contract: interaction states (+ invalid, dragover), the recipe key whitelist (+ motion, transform, per-side borders), every native's parts + when props (macro parts come from macros.json) |
| catalog/host.json | the host API contract (VAPP-91): message kinds, ops, error codes, function decisions, URL schemes, MCP carrier, package format |
| catalog/core.schema.json | generated: the catalog as JSON Schema in A2UI's catalog shape |
| catalog/theme.schema.json | generated: a theme file as JSON Schema |
| themes/ | the built-in themes: neutral (stock shadcn, the root), exponential (GENERATED: design-tokens values + exponential.recipes.json), playful (the test theme), orange (the brand reference: exponential with an orange primary) |
| builder/ | the theme builder page (dev:builder / build:builder); ui.exponential.at mounts it (VAPP-93) |
| src/ | the TS reference implementation (below) |
| generated/*.{swift,kt,rs} | generated constants for the three native targets; *Themes* = the built-ins RESOLVED, embedded as JSON; catalog.descriptions.generated.rs = the catalog's text for a Rust host behind the crate's descriptions feature (VAPP-106) |
| generated/core.runtime.json | VAPP-106: the RUNTIME catalog src/catalog.ts loads (the source minus every description, example and $comment): what a renderer needs, what the Rust core embeds |
| generated/style.runtime.json | VAPP-160: catalog/style.json minus every $comment (the spec prose): what src/ imports, so no bundle ships the comments |
| generated/core.descriptions.json | VAPP-106: the rest of the source, keyed like the catalog; describeCatalog() (src/catalog.described.ts) attaches it for the prompt, the schema emitter and the docs |
| docs/components.generated.json | generated: one doc page per component as data (ui.exponential.at renders it, VAPP-93) |
| docs/themes.generated.json | generated: the token vocabulary with every built-in's values, the recipe contract per component |
| fixtures/ | the contract (below) |
| docs/round-1-contract.md | the renderer-hardening round: every contract change with notes per renderer |
| docs/round-2-contract.md | round 2: Resizable, sticky, backdrop blur, animations, the Formatter, sections + scrollToIndex, the 53 gpui-vs-web decisions; a checklist per renderer |
| docs/round-3-contract.md | round 3 (VAPP-102): ONE Row / Section / Chip / Segmented / Menu, the core-computed tree guides (src/tree-guides.ts, fixtures/tree-guides.json), the 16 names round 4 removed, the lite prune |
| docs/round-5-contract.md | round 5 (VAPP-162): MessageBubble, CallSurface, Plan, QrCode (the core's Nayuki port, fixtures/qr-code.json), QrScanner (host-owned camera), Segmented orientation (the rail) + option.count, VoiceWave bars, Button hold, TraceItem done/skipped, the serif family; a checklist per renderer |
| src/host/ | the host API reference: router, decoders, policy, sources, packages, the ExponentialHost runtime + transports |
| src/connector/ | the Exponential connector (MCP OAuth + exp: sources over MCP) and createAppHost |
| conformance/ | the conformance suite: manifest.json (generated), report.schema.json, the runner guide; round 1's real-font harness beside it (run.ts, dump.ts, compare.ts, fonts.json); the shared web runner (browser-suites.ts) and the web-parity check (parity.ts: Solid and the web component against React) |
| release/ | the npm build + staging scripts and the release runbook (all registries) |
| vendor/a2ui/ | A2UI v0.9 schemas, byte-pinned (see its README) |
| vendor/json-render/ | attribution for the borrowed description wording |
The catalog
Canonical names are shadcn's; the issue's table decides what is native
(a renderer paints it) and what is a macro (expanded into natives before
painting). Every component has one sentence, every prop one sentence; both
are what the model reads. Components carry variant and size where
shadcn does (default|secondary|outline|ghost|destructive|link,
sm|default|lg|icon); what a variant looks like is the theme's recipe, keyed
by the recipe: {macro, part, props} every expanded node carries.
Ids: CORE_CATALOG_ID = https://ui.exponential.at/catalogs/core/v1,
CORE_LITE_CATALOG_ID (the subset without overlays, media and Chart),
A2UI_BASIC_CATALOG_ID; SUPPORTED_CATALOG_IDS is what a client advertises.
Wire form = A2UI's: a flat list of {id, component, …props, children: [ids] |
{componentId, path}, slots?: {name: id}, on?: {event: Action}, style?}.
Normalized form (what painters get) = UiNode: {id, component, props,
style?, on?, children: [UiNode], slots?, template?, recipe?}.
Themes (VAPP-92)
A theme is data, never code: one JSON file (catalog/theme.schema.json)
every renderer loads at runtime, safe to fetch from a URL or an app package.
Three levels of customisation, cheapest first: tokens (colours per mode,
radius, spacing, type, control heights, shadows, border widths, motion, the press / popover-enter scale),
recipes (per component part × when selector a small style object), and a
painter override through the extension API (VAPP-91) that must keep the
measure contract (src/geometry.ts, fixtures/control-geometry.json).
{
"id": "brand", "name": "Brand", "extends": "neutral", // override only what changes
"modes": { "light": { "color": { "primary": "#2563eb" }, "shadow": { "sm": [{ "x": 0, "y": 1, "blur": 2, "spread": 0, "color": "#0000000d" }] } }, "dark": { … } },
"tokens": { "spacing": { "md": 12 }, "radius": { … }, "type": { "size": { … }, "lineHeight": { … }, "weight": { … }, "family": { "sans": "Inter" } }, "control": { … }, "opacity": { … }, "border": { … }, "motion": { … } },
"fonts": { "Inter": { "fallback": "ui-sans-serif, system-ui", "weights": [400, 500, 600, 700], "source": "host" } },
"recipes": {
"Button": { "root": [
{ "style": { "borderRadius": "$radius.full" } }, // the part's base
{ "when": { "variant": "outline", "state": "hover" }, "style": { "backgroundColor": "$color.accent" } }
] }
}
}- Colours are
#rrggbb[aa]only; the builder's importer converts oklch/hsl/rgb (importShadcnCss). Shadows live undermodes(they differ in the dark); every other group undertokens. Fonts are referenced by family; the host registers the files per platform (fontssays what to fall back to). - Recipes:
recipes.<Component>.<part>= a list of rules; a rule applies when everywhenentry matches (state= every listed state is active; any other key = the recipe prop equals the value or is in the list). Rules merge by SPECIFICITY (one point perwhencondition; ties in source order, later wins — VAPP-90, the order the web's CSS gives them), so a child theme's appended base rule never shadows its parent'schecked/focus/variantrules; a child theme's rules come after its parent's. The keys a rule may set arecatalog/recipes.jsonkeys(the Box visual subset + padding/gap/size +native); values may be token references. Parts andwhenprops per component:recipeParts()(natives fromrecipes.json, macros from their templates: everypart, the macro'srecipeProps+$recipekeys). States: hover, pressed, focus, disabled, checked, open, selected, invalid, dragover. - Precedence a painter applies (
resolveNodeStyle): the native's own recipe (Text/root for its variant) < the node's style (the macro template's structure + the author's style) < the macro part's recipe (Badge/label) < the style keys written ON the macro node (VAPP-163: the expander lists them, sorted, in the root'srecipe.authored; condition blocks count as one key each; only the author's keys count, a macro node an outer template drew passes on just its root'sauthored). Macro templates state only structure, spacing, radius and control heights; every colour and border is a recipe, so a theme restyles what a template drew. - Surface settings (round 1):
modelight | dark | system (resolveMode),densitycompact | default | comfortable (applyDensityscales$control.*and$spacing.*by the theme's$density.*), high contrast (applyContrastmerges the theme'scontrastoverlays),$breakpoint.*for style conditions and responsive props,$ease.*for transitions;shadownone → xl is the elevation scale. - Loading:
validateTheme(json)→ issues with a path and a readable message (unknown token, key outside the whitelist, unknown part…);loadTheme(json, {themes})flattens theextendschain into aResolvedTheme(every token present, recipes merged) or throws ONEThemeErrorlisting every issue;tryLoadThemenever throws. A root theme must give every token name a value. Built-ins:builtinTheme(id),BUILTIN_THEMES,DEFAULT_THEME_ID(exponential). - Resolving:
resolveRecipe(theme, {component, part, props, states}, mode)→ concrete values (hex, px, shadow layers, family);resolveStyleValuesfor a Box style;styleToCssfor web painters. - Built-ins:
neutral= stock shadcn light + dark and the full recipe set (the root every other theme extends);exponential= the zinc glass look, GENERATED frompackages/design-tokens/tokens.json+ the app'sstyles.csspalettes +themes/exponential.recipes.jsonso the app and the SDK cannot drift;playful= the deliberately different test theme (pill buttons, no card borders, underlined tabs, Nunito) the conformance suite renders on every painter;orange= the brand-coloured reference theme,extends: exponentialwith only the colours changed (primary, ring, the accent pair, the chart scale), the smallest brand theme there is. The natives get all four RESOLVED ingenerated/ExponentialUIThemes.generated.*(parse once at startup). - Builder (
builder/,bun run --filter @exponential_app/eui dev:builder→ :4180): pick a base, edit tokens and recipes with the recipe sheet and the kitchen sink previewed live, import a shadcnglobals.css/ tweakcn export or a theme JSON, export the smallestextendstheme (diffTheme). The preview is the real React renderer (builder/preview.tsxon@exponential_app/eui-react, VAPP-87): what the builder shows is what a host gets.
The reference implementation (src/)
- Round 1 (
docs/round-1-contract.md):dynamic.ts(the BIND pass:resolveDynamic,bindTreewith props along the schema and DATA props verbatim, per-row Table slot cells viabindRowSlot,runActionwith thesetwrite-back),strings.ts,locale.ts,code.ts(CodeBlock tokenizer),chart.ts(extents, nice ticks),a11y.ts, and instyle.tsthe condition resolver (resolveConditions,activeBreakpoint);theme.tsgainsresolveMode,applyDensity,applyContrast. reduceSurface(components, {catalogId, extensions?})— flat list → one normalized tree, basic components mapped, macros expanded, every unknown component theUnknownplaceholder (never an error), prop issues listed.reduceNesteddoes the same for the nested authoring form the fixtures use.expandMacros(tree)— the macro table applied until every node is native. The template language is documented inmacros.json's$commentand implemented inexpr.ts(pure, line-by-line mirrorable in Rust).validateProps(def, props)— the mini schema (types.tsPropSchema).- Round 4 (VAPP-103,
fixtures/round4-contract.json):validateNode(node)(the reducer runs it on every authored node: the node's style, every function its props /visible/ actions call — a catalog function or a namespaced host function,isCallableName— and a Table's slot columns); colours in a node are$color.*tokens only; the core functionfilter{items, query?, fields?, where?};withOwnWrites(an action's context resolves AFTER the component's own write, given explicitly by prop) andsubmitClosesOverlay(a valid Form submit closes the nearest overlay around it when that is a dismissible Dialog/Drawer);themeOrDefault(never throws: the default theme + the issues); a theme must name$schema=THEME_SCHEMA_ID; an action's function is itsfunctionCall. validateStyle/create/props— the VAPP-4app-cssover the whitelist.defineExtension(def)— an extension catalog: own id,extendsthe core, components (+ macro templates), no core name shadowed.catalogPrompt({extensions?, lite?, terse?})— the compact system prompt.coreSchema(iconNames)/extensionSchema(ext, iconNames)— JSON Schema.
Motion (@exponential_app/eui/motion, EXP-1584)
The Motion component's engine is the Rust crate eui-motion (Lottie, the
dialect, the display list, the clock, state machines); its WASM build is
committed in wasm/motion/ (scripts/build-motion-wasm.sh, ≤ 480 KB raw, about 200 KB gz,
SOURCE_HASH gates drift). @exponential_app/eui/motion = loadMotionWasm
(browsers, lazy); @exponential_app/eui/motion/server = motionWasmSync +
motionPropsProblems (servers: validate a Motion node's source). The shared
Canvas2D painter + player is src/web/primitives/motion*.ts. The model guide
is the catalog's Motion.layers description (eval/motion/guide.md, scored by
eval/motion/); the format is docs/spec/motion.md.
The web layer (@exponential_app/eui/web)
FEED-128: the FRAMEWORK-FREE half of a web renderer, so React and Solid compile byte-identical stylesheets and share one bind pass. src/web/: theme-css.ts (compileTheme: a resolved theme → one scoped sheet; globalsCss(theme, {light?, dark?}): the theme as UNSCOPED shadcn globals with literal values, :root + .dark, for an app that is not genUI), box-css.ts (the per-node sheet), base-css.ts (BASE_CSS, layer xui-base), data.ts (JSON pointers, bindings, client functions, bindTree), urls.ts + media.ts (URL/media policy, the bounded blob cache, fetchLimited), host-types.ts (HostPlugin<Icon, Markdown>, the events), and the natives' pure helpers (coerce, table, dates, inputs = HostOwnedController, markdown = the parser to a tree, form = the field registry, list, listbox). Nothing in it imports a UI framework.
FEED-131: @exponential_app/eui/web/primitives (src/web/primitives/) is the framework-free half of the shadcn primitive set: one module per primitive holding its Tailwind class recipes (cva recipes keep their names, buttonVariants, alertVariants, codeBlockVariants; plain strings are one frozen <MODULE>_CLASSES object keyed by part, with conditional fragments as keyed entries; the named constants MENU_*_CLASS, SEGMENTED_*, GLASS_CARD_CLASS, FIELD_CHROME_CLASS, MOBILE_ARMS stay) plus the pure helpers (cn, getInitials, middleTruncate, avatarHueIndex, ringGeometry, choiceToggle, isTypeaheadPortalInteraction, …). The React set and its Solid twin import ONE source of class strings, and recipes.test.ts fails if either renderer's file of the same name carries a class literal. A Tailwind build that paints them scans this directory (@source).
The DOM contract every web renderer keeps: a .xui-surface.<scope> root with data-xui-mode; data-xui-id (placed nodes and parts), data-xui-part="<Component>/<part>", recipe props as data-r-* (natives) / data-m-* (macro parts), forced states as data-xs; xui-<Component>-<part> part classes (partClass); and the cascade order LAYERS (xui-base < xui-recipe < xui-node < xui-part).
The host API (VAPP-91)
A surface needs exactly this from its host, the same shape on all four
platforms (TS here, the Rust core's host module, the Swift and Kotlin
painters through the facade); catalog/host.json is the contract and
fixtures/host-{transport,policy,router}.json lock it everywhere.
- Transport: messages in = the four A2UI v0.9 messages plus two
extensions,
applyTemplate {surfaceId, templateId, packageId?, data?}andbindDataModel {surfaceId, path, source}; client messages out = A2UI's{version, action: {name, surfaceId, sourceComponentId, timestamp, context, payload?}}or{version, error: {code, surfaceId, message, path?}}. Adapters:MemoryTransport,JsonlStreamTransport,SseTransport,WebSocketTransport,McpTransport(A2UI resources withapplication/json+a2uiin a tool result; actions back as tools/calla2ui_event), all on the shared decoders (JsonlDecoder,SseDecoder,messagesFromMcpResult). - Router:
HostRouter.route(message)→ opscreate | components | data | bind | delete | send(pure; errorsUNSUPPORTED_CATALOG,SURFACE_NOT_FOUND,INVALID_MESSAGE,TEMPLATE_NOT_FOUNDgo back assend). - Functions: an
on.<event>{functionCall: {call, args}}(A2UI's key) to a non-built-in name runs the host's registered function after the gate:decideFunction(policy, name, registered)→allow | ask | deny | not_found(deny wins, then allow, then ask, thendefault;harness.*prefixes),ask= the host'sonFunctionCallconsent hook; a package surface'sfunctionslist narrows it (combineDecisions). The catalog's functions (the basic 14 + the 15 core ones) are built in. - Bindings:
sourceURIs<scheme>:<name>?k=v(parseSource); a host registers a resolver per scheme (subscribe(source, emit) → cancel), each emit lands at the bound path. - Negotiation:
supportedCatalogIds(extensionIds)= core, core lite, A2UI basic, then the registered extensions;clientCapabilities. - Links:
Link.href,Button.hrefandRow.hrefopen through the host'sopenUrl, and a host's optionaltextLinks(text)hook turns parts of aRowsubtitle into tappable spans ([{text, href?}]; genUI knows nothing of what they mean).catalog/host.jsonurlsis the contract. - Markdown hooks (
catalog/host.jsonmarkdown, all optional): atextLinkssegment'schip: {kind: issue|mention|entity, label, icon?}paints the Chip look inline in a Markdown's text;imageSize(src)gives a block image its aspect before it loads;onImagePress(src)makes block images pressable;attachment(href)paints a one-link paragraph as the Video, AudioPlayer or FileUpload file row look. Locked byfixtures/markdown-host.jsonon every renderer. - Policy (every renderer, every href and src):
decideUrl(schemeshttps http mailto tel;schemes: ["https"]= https only; optional host allowlist; relative urls againstbaseUrl) foropenUrl,Link, markdown links and FileUpload file urls (safeHref; denied = plain text);mediaRequest(url, {baseUrl, rules, schemes, hosts, allowPrivate})= the image loader's url + headers (auth for/api/attachments), null when the media policy (defaulthttps http data,fileonly when listed) denies it;MEDIA_LIMITS(20 MiB, 30 s, 32 Mpx across every frame, read bymediaImageWithinLimitsbefore decoding: header or SVG size ×imageFrames; an unreadable raster is refused) bound every image load, and the React video/audio whole-fetch fallback too (VAPP-107). Private hosts are denied by default (VAPP-107,isPrivateHost):localhost,*.localhost,*.local, 0/8, 10/8, 127/8, 169.254/16, 172.16/12, 192.168/16,::,::1, fe80::/10, fc00::/7 and IPv4-mapped private addresses, for http(s) hrefs and media alike, reasonprivate; a host opts names back in withallowPrivate: ["localhost"](*.suffix, or*for all), which ahostsallowlist does not do. Literals only: no DNS lookup (a public name that resolves privately is the network's job). The threat model: https://exponential.at/docs/sdk/genui/reference/security/. - Runtime:
new ExponentialHost({transport, functions, sources, extensions, packages, policy}):connect(),receive(message),surface(id)(aSurfaceStore),action(...),callFunction(...),openUrl(url),mediaRequest(src),paintError({surfaceId, componentId, message})(a renderer'sonPaintError→ ONE A2UIRENDER_FAILEDerror per surface + component + message),status/hasTransport/unsupportedCatalog(thehost_offlinestate and the catalog-update banner),issues+onIssue(invalid packages, the errors it answered, an unusablecreateSurface.theme; a package inpackagesthat fails validation throwsPackageError). A surface'screateSurface.theme(a built-in id or a theme JSON) lands onstore.theme. React paints it with<HostSurface host surfaceId>(the surface's theme wins overtheme). Stream transports end on a clean EOF (resumableor an SSEretry:reconnects) and reconnect after errors. - Declarative apps (VAPP-82): a package
{id, name, version, catalogId, templates: {<id>: {components, data?, bindings?}}, functions?, theme?, icon?}(validatePackage);createAppHost({package, …})runs one in any host. Against an Exponential instance,ExponentialConnectordoes the MCP OAuth grant (discovery, dynamic registration, PKCE S256, consent;authorizeUrl()returns{url, verifier, state, clientId}: keep all three for the callback,exchange(code, verifier, clientId), so a connector rebuilt after the redirect never registers a second client) and servesexp:issues|boards|teams|membersover the instance's MCP tools (sources(), polled) plusexponential.mcp(functions()). Hosted apps (the peer-link transport, VAPP-10) embed through the sameTransportinterface. - The package format, version 2 (VAPP-161,
docs/package-format.md: the prose spec other projects link to):formatVersion; the neutralrecords:<kind>?querysource +records.upsert/records.deleteover a host-providedRecordStore(MemoryRecordStore+recordsHostare the reference);tools(MCP servers and scripts,toolConsent= the consent card the host must show);extensions(declared, versioned); the package FILE'sauthor+ Ed25519signatureover RFC 8785 canonical JSON (signPackageFile/verifyPackageFile).formatVersion: 5is required; no older format is read. The host contract is version 3 (catalog/host.jsonversion).
The Exponential app is a host like any other: web apps/web/src/lib/exponential-ui-host.tsx
(harness.* functions, exp: over the Electric collections, the consent
card, the app extension's painters from @exp/ui, the Devices package
packages/ui/exponential-ui/templates/devices.json on /exponential-ui-devices).
Samples outside the workspace: samples/exponential-ui/ (a local A2UI
server, a third-party theme, one extension component, six hosts).
Conformance
conformance/README.md: 25 suites (version 2); a renderer is conformant
when bun run --filter @exponential_app/eui conformance:check <report> says
so. All six renderers and the core run it in CI (exponential-ui.yml);
the Solid renderer and the web component also pass the web-parity check
against React.
Fixtures (the contract; the Rust core replays them with these test names)
| file | locks |
|---|---|
| catalog-components.json | every component × its example × every enum value × both booleans |
| catalog-macros.json | every macro case → the expanded tree, byte for byte; round 1 adds a bound:<prop> case per bindable macro prop and a responsive:<prop> case per responsive one |
| bind-time.json | round 1: every bound macro case × three data models (sample, falsy twin, path missing) → the tree after the bind pass + every set press; extra = Table slot cells bound per row (rowSlots), literal data rows, a bound accessible name, the $set collision issue, string page data |
| style-conditions.json | round 1: style × surface/state context → the flattened style (media in source order, then states) |
| code-tokens.json | round 1: source × language → CodeBlock tokens per line |
| catalog-basic-map.json | hand-written A2UI basic surfaces → expected trees + issues |
| catalog-extension.json | an example extension (native + macro + enum) and its cases |
| kitchen-sink.json / .expanded.json | every visible component once, the VAPP-4 layout cases kept, round 1's responsive section (1→2→3 column grid, collapsing sidebar, hidden-on-narrow nodes, state blocks) and bound macros; view id exponential-ui-kitchen-sink |
| specimens.json | generated (VAPP-93): one surface per component (its example, up to two enum props and two booleans, captioned; an overlay once, open) + exponential-ui-demo from the hand-written demo-surface.json; id = the view id ui.exponential.at shows four shots of |
| prompt-budget.json | the prompt's size on record (full / lite / terse) and the budget |
| motion-frames.json | EXP-1602: Motion display lists (JSON + packed) per Lottie case and frame; every painter's pixel reference is conformance/motion/parity/ |
| motion-validate.json | EXP-1602: the engine's refusals and warnings (expressions, the limits.json › motion limits) |
| motion-playback.json | EXP-1604: the shared playback clock (props × elapsed ms → frame) |
| motion-state.json | EXP-1605: dotLottie state machines (steps → state, entered, inputs) |
| theme-resolved.json | every built-in theme resolved: what a native loader must produce from the same files |
| theme-recipes.json | theme × component part × recipe props → visuals per mode and state (one case per distinct look) |
| theme-extends.json | extends cases (the acceptance case: neutral + primary + button radius) → chain + probe styles |
| theme-invalid.json | bad themes → the issues they must raise (paths ×4, never a crash) |
| control-geometry.json | theme × control × props → the box a painter or override must measure to |
| host-transport.json / host-policy.json / host-router.json | the host API (VAPP-91): decoders, the gate / urls / media / sources / negotiation, router flows → ops |
| host-package.json | the package format (VAPP-161): records queries and values, tool consent, extension compatibility, canonical JSON, signatures (RFC 8032 test key) |
| qr-code.json | VAPP-162: the QR matrix (the port of Project Nayuki's generator) bit for bit |
| voice-wave.json | NEW-77: the VoiceWave geometry and the child rule; VAPP-162 bars |
| pointer.json | EXP-1385: a landed pointer write reads back (- appends and reads the last element) |
| format.json | round 2: the format functions through the English fallback Formatter (= Intl en-US; UTC, and zoned at a fixed offset) + what a text prop shows for a bound number/boolean |
| template-items.json | round 2: item keys (#<index> for missing/duplicate), Table row keys, accumulated (escaped) instance suffixes, template nodes lifted out of the tree, cycles reported |
| text-direction.json | round 2: per-node direction (direction on any node) and the physical text alignment |
| resizable.json | round 2: Resizable sizes: normalize, drag, keys (rtl), extents, px → percent |
| virtual-list.json | round 2: the one-axis window, scrollToIndex, sections, the sticky header |
| animations.json | round 2: timing + sampled frames of every animation per built-in theme, reduced motion, the CSS @keyframes |
| bench-list.json | round 2: the 100,000-row List every renderer benches (numbers in each renderer's README) |
| round4-contract.json | round 4 (VAPP-103): authored trees → the validator's issues; filter args → items; an Input/Switch change → the data + the action context after the own write; a Form submit → which Dialog/Drawer closes |
| conformance-known.json | the gpui ratchet + round 2's 53 divergence decisions (causes, rules) |
Commands
bun run --filter @exponential_app/eui generate # idempotent; generate.test.ts gates drift
bun run --filter @exponential_app/eui test
bun run --filter @exponential_app/eui typecheckChange a component = edit core.catalog.json (and macros.json for a macro),
run generate, commit everything it rewrote. The icon vocabulary is read from
packages/icons/icons.json at generate time; the exponential theme reads
packages/design-tokens/tokens.json and packages/ui/src/styles.css at
generate time too (never at runtime).
bun run --filter @exponential_app/eui dev:builder # the theme builder on :4180
bun run --filter @exponential_app/eui build:builder # bundles builder/dist (ignored)
bun run --filter @exponential_app/eui size # VAPP-106: bundle + binary sizes vs release/size-budget.json (release/README.md)Entries (VAPP-106)
The root @exponential_app/eui is what a renderer needs (the runtime catalog,
macros, the reducer, the validator, bind-time, formatting, themes, the host
API). What only a server or a tool needs is an entry of its own, so a
browser never downloads it: @exponential_app/eui/prompt (catalogPrompt,
estimateTokens, PROMPT_RULES; it attaches the catalog's descriptions and
examples with describeCatalog() first), ./schema (coreSchema,
extensionSchema, with the vendored A2UI basic catalog), ./conformance
(conformanceManifest, checkReport, with every fixture bundled),
./host (the host API alone). The entries share chunks; sizes are on
record in release/sizes.json.
Extensions
The Exponential app's extension (IssueRow, SessionRow, IssueChip, …) lives with
the app at packages/ui/exponential-ui/extension.json and is validated by
@exp/ui's tests through defineExtension; it is the first user of the
mechanism and not part of this package.
Anchored and hosted popups (round 7)
Popover and Menu take side + align (start | center | end: top-start,
top-end, bottom-start…) and anchor (a node id: the overlay opens against that
node, which becomes its trigger). A host may show them OUTSIDE the surface
(VAPP-281): the core's hostedPopups setting places them unclipped against the
anchor with no flip and no shift (anchorPosition, overlay-geometry.json
hosted), web hosts set HostPlugin.popupHost. See docs/round-7-contract.md.
