Pipeline Apps

App layout and authoring previews

Compile declarative templates and scoped CSS into portable app components, then review real desktop and mobile rendering.

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 &amp; and numeric references are accepted.

SourceNative behavior
div, section, header, footer, aside, article, main, nav, ul, ol, figureSemantic box containers
h1–h4, p, span, strong, em, small, li, label, figcaption, blockquoteText leaves; inline boxes when they hold elements or several interpolations
img, textarea, a href="/page"image, multiline input, navigation
Existing portable component namesExisting typed components
:prop="$state.path"Read binding to local state
$item.path, $item, $index inside list templatesRead the current record/index
{{ $state.path }} in textInterpolation; any number per element, mixed with inline elements
@click="action", @submit="action", @change="action"Declared button/form/select actions
v-if="$state.ready", v-showRender only when the bound value is boolean true
v-if="!$state.ready", v-elseRender 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 containersAccessible 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:

TokensValues
--app-space-1 … --app-space-84, 8, 12, 16, 24, 32, 48, 64 px
--app-text-xs … --app-text-3xl0.75rem to a fluid 2.75rem heading size
--app-radius-sm, --app-radius, --app-radius-lg, --app-radius-fullDerived from radius
--app-shadow-sm, --app-shadow, --app-shadow-lgElevation
--app-accent-soft, --app-accent-strong, --app-on-accentAccent tints and text on accent
--app-surface-sunken, --app-surface-raised, --app-border-strongSurface and border variants
--app-heading-font, --app-mono-font, --app-ease, --app-durationType 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.

Copyright © 2026