App layout and authoring previews
App layout is authored independently of stored entity schemas. The engine owns typed actions, permissions, synchronization, durable records and server business rules. The presentation compiler gives authors flex/grid composition, typography, spacing, themes and responsive containers within that existing contract.
Author a page
Keep actions, entities, pipelines and capabilities in app.toml. A page declares
exactly one of the existing root tree, an embedded template, or a relative
ui file. Referenced files are embedded into the authored snapshot before
signing, so publication can reproduce the tree without the author's filesystem.
[app]
# Other identity/version fields remain required.
minimum_runtime_version = "1.2.0"
[[views]]
id = "main"
name = "Main"
initial_page = "home"
[views.theme]
accent = "#176b61"
background = "#f4f3ee"
surface = "#ffffff"
text = "#203633"
muted = "#667873"
border = "#dbe1da"
font = "Inter, system-ui, sans-serif"
[[views.pages]]
id = "home"
route = "/"
title = "Committees"
sites = ["web"]
ui = "ui/home.vue"
<template>
<section class="workspace">
<header><h1>Committee workspace</h1></header>
<form aria-label="Choose committee">
<select name="committee_id" label="Committee"
:options="$state.committees" :value="$state.selected_id"
option-value="id" option-label="name" @change="choose_committee" />
</form>
<section v-if="$state.has_proposals" class="proposals" aria-label="Proposals">
<article v-for="proposal in $state.proposals" :key="proposal.id" class="proposal">
<h2>{{ proposal.title }}</h2>
<p>{{ proposal.summary }}</p>
<footer>Due {{ proposal.deadline }} · <strong>{{ proposal.cost }}</strong></footer>
</article>
</section>
<p v-else class="empty">No proposals yet.</p>
</section>
</template>
<style scoped>
.workspace { display: grid; gap: var(--app-space-5); padding: var(--app-space-5); }
.proposals { display: grid; gap: var(--app-space-4); }
.proposal { padding: var(--app-space-5); border: 1px solid var(--app-border); border-radius: var(--app-radius-lg); }
.proposal p { color: var(--app-muted); }
.proposal footer strong { font-variant-numeric: tabular-nums; }
@media (min-width: 900px) {
.workspace { grid-template-columns: 270px minmax(0, 1fr); }
}
</style>
choose_committee must be a declared action. A select stores the chosen scalar
identity in action_input.values.committee_id and carries the complete option
record in action_input.item. String and numeric identities retain their types.
Use entity hydration to populate options; people choose record names, not raw
foreign keys. A select belongs inside a form even when its change action is
independent of submission. Options are bounded to 500 unique identities.
Supported template subset
This is a declarative source format, not a Vue runtime. Quote attribute values
and close every element, including void controls (<input ... />). Bare boolean
attributes such as required, disabled and submit mean true. XML character
references such as & and numeric references are accepted.
| Source | Native behavior |
|---|---|
div, section, header, footer, aside, article, main, nav, ul, ol, figure | Semantic box containers |
h1–h4, p, span, strong, em, small, li, label, figcaption, blockquote | Text leaves; inline boxes when they hold elements or several interpolations |
img, textarea, a href="/page" | image, multiline input, navigation |
| Existing portable component names | Existing typed components |
:prop="$state.path" | Read binding to local state |
$item.path, $item, $index inside list templates | Read the current record/index |
{{ $state.path }} in text | Interpolation; any number per element, mixed with inline elements |
@click="action", @submit="action", @change="action" | Declared button/form/select actions |
v-if="$state.ready", v-show | Render only when the bound value is boolean true |
v-if="!$state.ready", v-else | Render unless the value is true; v-else negates the previous sibling's condition |
v-for="(row, i) in $state.rows" | A plain list whose rows join the parent's flex/grid; row.x reads $item.x |
aria-label="..." on containers | Accessible name for a landmark or region |
tab-label="Overview" | Label a direct child of tabs |
Rich text works as it does on a web page:
<p>Due {{ row.due }} · <strong>{{ row.budget }}</strong></p> compiles into
inline spans that keep the authored spacing. Labels of buttons, badges and links
take one interpolation without surrounding text. v-for renders up to 100 rows
per page. A loop's v-if filters rows when it reads the loop alias. When it
reads other state, it guards the whole loop. A nested loop reads only its own
alias. :key is accepted and ignored.
No scripts, imports, arbitrary expressions, event handlers, transforms, DTDs or
processing instructions execute through this source. v-else-if, v-model,
v-html, :class and :style are rejected with a hint. Derive display state
in action flows and choose between variants with v-if/v-else. There is one
root and optionally one <style scoped> block. Sources are capped at 128 KiB;
trees retain the 256-node/12-depth limits. Compile errors name the page, line
and column.
The additional interactions are select, badge, details and dialog.
Badges require a label; disclosures require a label and wrap ordinary children.
Dialogs require title and trigger-label, wrap ordinary children, use the
browser's modal focus behavior, close with Escape and restore trigger focus.
Forms and action controls stay disabled while their own submission is pending.
The engine remains responsible for eligibility, validation and transaction
semantics; a disabled button or hidden node cannot enforce a business rule.
Scoped presentation
CSS compiles to optional ComponentNode.presentation.rules, never a global
stylesheet. The template is static, so the compiler resolves selectors against
it at build time. Use element names, .class, #id, *, :root, compounds
such as h2.title or .card.featured, descendant selectors (.card h3) and
child selectors (.card > header). The compiler applies standard specificity
and source order, and inline style wins. A state pseudo-class applies to the
selected element: :hover, :focus-visible, :disabled, :focus-within,
:active, :first-child, :last-child and :nth-child(odd|even). Ancestor
states such as .card:hover .title, sibling combinators, attribute selectors,
!important, @keyframes, @import and @font-face are rejected.
Width queries are measured against the app surface's available width, so they
work beside platform navigation or inside another host. @container and
@media are equivalent. Both accept (min-width: N), (max-width: N),
(width >= N)/(width <= N) and and combinations in px, rem or em, for
240–3200 pixels. Mobile-first and desktop-first CSS both work. Media features
other than width are rejected; the host already honors reduced motion. Choose
minmax(0, 1fr) for flexible tracks and check actual content overflow.
Shared controls expose stable ::part(control), label, panel, trigger,
body, title and item hooks where relevant. For example, style
.proposals::part(item) to change a list row wrapper. The property allowlist
covers flex, grid (including grid-template-areas/grid-area), place-*,
logical sizing and spacing, typography (text-wrap, font-variant-numeric,
line-clamp, quoted font names), colors (color-mix(), oklch()), borders,
shadows, filters, backdrop-filter, transforms and transitions. Authored
custom properties (--gap: 24px) inherit like ordinary CSS variables. The
animation property names host keyframes: app-fade-in, app-rise,
app-scale-in, app-slide-in, app-pulse, app-spin and app-shimmer.
Values reject URLs, executable functions, CSS escapes, fixed positioning and
unbounded stacking levels. Strings are limited to names, spaces and dots. Each
node has at most 32 rules, 48 declarations per rule and 16 KiB of declaration
text.
Theme tokens are accent, background, surface, text, muted, border,
danger, success, warning, font and radius. Runtime 1.2.0 adds
on_accent, accent_soft, heading_font, mono_font and shadow. They
become app-local --app-* variables (on_accent becomes --app-on-accent).
The host derives a shared scale from them. Use the scale instead of choosing a
new value on every page:
| Tokens | Values |
|---|---|
--app-space-1 … --app-space-8 | 4, 8, 12, 16, 24, 32, 48, 64 px |
--app-text-xs … --app-text-3xl | 0.75rem to a fluid 2.75rem heading size |
--app-radius-sm, --app-radius, --app-radius-lg, --app-radius-full | Derived from radius |
--app-shadow-sm, --app-shadow, --app-shadow-lg | Elevation |
--app-accent-soft, --app-accent-strong, --app-on-accent | Accent tints and text on accent |
--app-surface-sunken, --app-surface-raised, --app-border-strong | Surface and border variants |
--app-heading-font, --app-mono-font, --app-ease, --app-duration | Type families and motion |
Headings, paragraphs, buttons, inputs, badges, cards, list rows and empty states already use this scale, with hover, focus and pressed states. Use contrast appropriate for the chosen background. Themes grant no font downloads or asset/network permissions; name fonts the viewer already has, with fallbacks.
TOML authoring automatically declares the presentation_v1 host feature.
Native manifest authors must declare it explicitly on the view. Both publication
and hosts reject unknown features. Presentation requires runtime 1.1.0 or
newer. Page-style authoring and the wider vocabulary require runtime 1.2.0.
That covers expanded properties, functions, strings, states and max-width
queries, plus inline text tags, plain lists, negated visibility, container
labels and the newer theme tokens. Validation names the exact runtime to
declare. Old releases omit these optional fields and keep their serialization.
Selector features compile to the same per-element rules, so they also work at
runtime 1.1.0. Deploy compatible publication validation and host rendering
before upgrading an installed app to use a feature.
Review before publication
Open /apps/preview from Apps → Preview layouts. Governance, operations,
collection and studio examples show different workflows. The studio example is
written entirely in page-style markup and CSS. Edit embedded TOML, open the
compiled manifest JSON produced by rac app inspect --json, or open app-release.json from
rac app build. A source containing external ui references must first be
expanded by the CLI. Apply realistic fixture JSON and inspect populated, empty,
loading and error states at desktop and mobile sizes.
The preview uses the publication compiler and installed renderer, with only local-state/navigation action ports. It does not execute entity writes, capability requests or live data views. It cannot prove persistence, server-side voting rules, export behavior or release authenticity.
Start with a clear primary task and a recognizable visual hierarchy. Compose navigation, context and primary content intentionally. Choose names for entity relationships, put secondary forms in a dialog or disclosure, and provide useful state messages. Inspect screenshots yourself; structural validity does not assess visual quality. Exercise keyboard focus, dialog dismissal, labels, the primary action and long content, then verify persistence in the installed app through the ordinary release lifecycle.
The existing runtime-app.spec.ts browser gate renders all four examples on
desktop and mobile, retains state screenshots, checks responsive columns and
overflow, and exercises named selection and dialog focus. Its screenshots are
review artifacts rather than proof that every authored design looks good.