Skip to main content
ess-ui/1 reference | ESSEvery construct of an ess-ui/1 document, generated from the schema.ESSreferenceessreferenceadopterdevelopertry-spec-driven-developmentreference

ess-ui/1 reference

Every construct of an ess-ui/1 document: what it is for, its properties, the short forms it accepts, an example and the checks that apply to it. This page is generated from schemas/ui/ess-ui.schema.yaml by ess ui docs; change the schema, not the page.

Foundations​

How types, expressions, short forms and nesting work in every construct below.

Type rule​

Types are YAML structure, never strings to be parsed.

A field's type is one of three things. (1) A lowercase primitive name from primitives. (2) A constructor map from constructors. (3) A capitalized bare name: a construct of this schema (Page, Reads, State …) or, inside an app document, an entry of the document's types map or a type of the ESS model (PartnerId, Money). Names that must resolve at validation time (a page, an overlay, a view, a command) use {ref: kind}. required, default and note are siblings of type, never part of it.

Primitives

PrimitiveMeaning
stringtext
integerwhole number
numberdecimal number
booleantrue or false
durationa length of time; pattern: "^[0-9]+(ms|s|m|h)$"; example: 30s
timestampan instant
datea calendar date
timea time of day
jsonany JSON value
expra binding expression (see expressions); a value, never a type constructor
namea node name; matches NodePath.syntax.segment_pattern

Constructors

ConstructorFormExampleNotes
list{list: T}{list: PartnerId, unique: true}options: {unique: {type: boolean, default: false, effect: no_duplicate_elements, note: a set}}
map{map: {key: K, value: V}}{map: {key: StepId, value: {record: {x: number, "y": number}}}}
optional{optional: T}{optional: UserId}
enum{enum: [a, b]}{enum: [light, dark]}
one_of{one_of: [T1, T2]}{one_of: [duration, expr]}
record{record: {field: T}}{record: {amount: number, currency: {enum: [EUR, USD, GBP]}}}
ref{ref: kind}{ref: view}view, command, event and actor resolve in the ESS model; the others in the document; kinds: [shell, page, section, overlay, channel, state, page_kind, composite_kind, widget, tone_map, view, command, event, actor]
const{const: value}{const: ess-ui/1}

Expressions​

Binding expressions — the values of fields typed expr.

Expressions connect a node to state and data. They are short paths plus a few operators; a renderer evaluates them, a validator resolves every path. A string that does not parse, or that joins words with an operator but reads no path or function form (Limit in cents, not yet sent), is literal text, including a widget argument substituted into an expr position.

FormMeaning
state.<name>page, section or overlay state
params.<name>path params of the page, or values passed to an overlay
args.<name>a widget parameter, inside the widget's body
row.<field>the current row of a collection, record or board
selection.<field>the selected row(s) of the section
draft.<field>the unsaved input of the enclosing form
section.<name>.selectionanother section's selection
channel.<name>.<field>a named field of a channel
shell.<name>shell state
url.query.<name>a query parameter (guards only)
actor.<field>the signed-in actor: user_id, account_id, roles, signed_in, may(<command>)
widget.<field>the widget row inside a board
same_as(<section>)the params of another section's read
matches(params)true when a live row satisfies the section's read params
operators==, !=, in [..], not, and, or, literals

Short forms​

Abbreviated forms and special values, each defined by what it accepts and what it expands to.

Validation: a short form is checked against its accepts before it expands; one that accepts does not admit is refused with its path

Placeholders in an expansion

PlaceholderMeaning
$valuethe value written ($value.<field> is one of its fields)
$keythe map key the value sits under
$pagethe enclosing page
$actionthe enclosing action
$itemone value of an enum type

Operators in an expansion

OperatorMeaning
exprthe expanded value is this expression string
first_presentthe first listed source that has a value
each_value_of_enum_typeone entry per value of the named enum type, shaped by as; the document's types are read first, and a name they do not declare is an enum of the model the document is loaded with (--model), whose entries send each variant's wire spelling and show its display name
remove_inheritedthe inherited entry of that name is removed during page kind merge
merge_underthe named overlay is copied and the local props are merged over it

A placeholder that resolves to nothing: an entry of expands_to whose placeholder resolves to nothing is dropped

Inheritance

How a page, or a page kind, is merged over the kind it names.

layout: {merge: replace}
maps:
merge: deep
key_order: [inherited, then_new]
conflict_winner: page
null_value: remove_inherited
named_lists:
match_by: name
order: [inherited_in_their_order, then_new_in_page_order]
remove: "{name: <n>, remove: true}"
different_component: page_entry_replaces
other_lists: {merge: replace}

Layers​

What may nest inside what.

LayerContainsNote
shellpageapplication frame: regions, preload, guards, navigation
pagesection, overlayone route; owns page state and layout
sectioncomposite, widget, primitivea region with its own read and lifecycle
compositecomposite, widget, primitiveone member of the composite union
widgetcomposite, widget, primitivean app-defined composite; may not contain itself, directly or indirectly
primitivenothingone of 9 renderer-neutral leaves

The document​

Root, naming, node addressing and shared types.

Document​

The root of a ess-ui/1 file describing one application.

One document describes one frontend. It names the ESS model every view, command and event resolves against, the placement profile that decides where state lives, the shells, navigation, reusable widgets and pages. Names are dot-separated and follow navigation, not files: page partners.list of app portal is portal.partners.list, and its route is derived from the name. Old routes are kept as page aliases.

Properties

PropertyTypeRequiredDefaultNote
formatexactly ess-ui/1yesformat marker
appnameyesroot of every fully qualified name
titlestringhuman title of the application
modelstringyesthe ESS system that views, commands and events resolve against
actorone of: from_session | anonymousfrom_sessionwhose grants decide what is visible
placement_profileone of: thin | fat | hybridyesdefault state placement for the whole document
placement_defaultsmap of StateClass → Storeoverrides entries of the profile's default table
shellsmap of name → Shellyesapplication frames
navigationNavigationyesmenu structure and home page
page_kindsmap of name → PageKindapp-defined page templates
widgetsmap of name → Widgetapp-defined composites usable wherever a composite kind is
typesmap of name → Typenamed value types
pagesmap of name → Pageyesevery route of the app
channelsmap of name → Channellive data sources
fixturesFixtureIndexsample data so renderers run without a backend
unmappedlist of stringdocument-level gaps found by a retrofit
tokensTokensdesign tokens, merged over the built-in table
themesmap of name → Themenamed looks, each the tokens it overrides
themeThemeChoicewhich theme is shown and the shell state that chooses it
tone_mapsmap of name → ToneMapvalue-to-tone maps, named by tone_by.tones

Example

format: ess-ui/1
app: portal
title: Partner portal
model: partners.platform
actor: from_session
placement_profile: hybrid
placement_defaults: {draft: session_storage, preference: server}
fixtures: {dir: fixtures, index: fixtures/index.yaml}
navigation: {home: overview, sections: [{name: home, label: Home, pages: [overview]}]}
shells: {app: {regions: {main: {kind: page_outlet}}}}
pages:
overview:
kind: dashboard_page
title: Overview
sections: [{name: board, component: board, reads: {view: dashboards.Mine}}]

Type​

A named value type declared once in the document's types map.

Use types for value shapes several states, params or filters share — money, a time window, an enum of stages. ESS entity ids (PartnerId, UserId) are not redeclared; they resolve in the model.

Properties

PropertyTypeRequiredDefaultNote
(value)one of: string | map of name → jsona primitive name, a constructor map, or a capitalized named type

Example

Money: {record: {amount: number, currency: {enum: [EUR, USD, GBP]}}}
DealStage: {enum: [lead, qualified, proposal, won, lost]}
TimeWindow:
record:
from: timestamp
to: timestamp
preset: {enum: [this_month, last_30d, this_quarter, custom]}

Checks

CheckSubjectRuleSeverity
types_structural**.type{must_match: type_rule, forbidden: string_holding_a_type_expression}error

NodePath​

The stable address of any node — shell region, page, section, overlay, composite, widget instance or primitive.

Every node has a name unique among its siblings: map entries are named by their key, list entries carry name (or a derived default). A node's path is the chain of container keys and names from the document root, joined with /. Page names keep their dots, since / is the separator. Paths never contain list indices. Rows of a collection are addressed by their row key under rows/. Editors address a node by path to insert, replace or remove it; validators report every finding by path.

Properties

PropertyTypeRequiredDefaultNote
(value)stringa path; see syntax

Syntax

separator: /
segment_pattern: "^[A-Za-z0-9_.-]+$"
forbidden: [list_indices, empty_segment]
grammar:
- {segment: container_key, then: child_name, note: e.g. pages/partners.list}
- segment: field_key
then: none
note: "a single-valued child such as header or reads: pages/partners.list/header"
containers:
- container:
- shells
- page_kinds
- widgets
- types
- pages
- channels
- regions
- overlays
- state
- params
- board_widgets
child_name: map_key
note: maps; order carries no meaning
- {container: [columns, inputs, fields], child_name: {first_present: [name, field]}}
- container:
- row_actions
- bulk_actions
- actions
- item_actions
- node_actions
- edge_actions
- alternatives
child_name: {first_present: [name, derived_action_name]}
- container:
- sections
- item
- choices
- parts
- metrics
- toolbar
- guards
- tabs
- groups
- children
- body
- navigation_sections
child_name: name
note: lists of named nodes; order is meaningful
- {container: preload_views, child_name: view}
- container: rows
child_name: {row_key: {first_present: [live.match, id]}}
note: runtime rows of a collection
widget_instances: {expanded_body_path: <instance path>/body/<node name>}
uniqueness: {scope: siblings, severity: error}
stability: "inserting, removing or reordering a sibling changes no other node's path"

Examples

shells/app/regions/nav, pages/partners.list/sections/list/columns/tier, pages/partners.list/sections/list/row_actions/edit, pages/partners.list/sections/list/item/card/body/tier, pages/partners.list/sections/list/rows/pt-003/item/card, pages/tickets.detail/sections/reply/children/send, pages/partners.detail/layout/columns/side, widgets/partner_card/body/title

Example

pages/partners.list/sections/list/item/card/body/tier

Shell and navigation​

The application frame and how pages are reached.

Shell​

An application frame with regions, preloads, guards and shell-wide state.

A shell is what stays on screen while pages change: navigation, page outlet, overlay slot, notifications. Use one shell per distinct frame (the signed-in app, the sign-in screens). Guards run before any page of the shell opens; preloads warm views many pages need, without blocking navigation unless the policy says so.

Properties

PropertyTypeRequiredDefaultNote
regionsmap of name → Regionyesnamed areas of the frame
preloadPreloadviews read when the shell starts
guardslist of Guardchecks run before each page of the shell opens
statemap of name → Stateshell-wide state: nav expanded, theme, credentials
overlaysmap of name → overlayoverlays reachable from every page
sourcelist of stringtraceability to retrofitted files

Example

regions:
nav: {kind: navigation, props: {collapsible: true, search: {over: [label, synonyms]}}}
main: {kind: page_outlet}
overlay: {kind: overlay_outlet, props: {one_at_a_time: true}}
notify: {kind: notifications}
state:
nav_expanded: {type: boolean, class: preference, default: true}
credentials:
type: {record: {access_token: string, refresh_token: string}}
class: credential
sensitive: true
store: server_session
pinned: true
preload:
policy: in_background
except_on: [auth.sign_in]
views: [{view: session.Me}, {view: tags.All}]
guards: [{name: signed_in, when: not actor.signed_in, then: {redirect: auth.sign_in}}]

Checks

CheckSubjectRuleSeverity
page_outletshells.* a page renders in{must: have_a_region_of_kind_page_outlet}error

Region​

One named area of a shell.

Regions say what a shell area is for, not how it looks. A renderer maps navigation to a sidebar, a TUI pane or a command palette; overlay_outlet is where drawers and dialogs appear; assistant hosts a chat helper.

Properties

PropertyTypeRequiredDefaultNote
kindone of: navigation | page_outlet | overlay_outlet | notifications | assistant | account_menuyesrole of the region
propsmap of name → jsonkind-specific options: collapsible, search, one_at_a_time, actions
visibleexpr"true"shows the region only when true

Example

kind: account_menu
props:
actions:
- {name: switch_org, opens: switch_org, label: Switch organization}
- {name: sign_out, does: session.SignOut, label: Sign out}

Preload​

Views a shell reads before or alongside the first page.

Preloading replaces route guards that block navigation until many stores are filled. in_background starts the reads and lets the page render at once; before_first_page blocks and should be rare.

Properties

PropertyTypeRequiredDefaultNote
viewslist of record { view: name of an ESS view, params: optional map of name → expr, endpoint: optional string }views to read; each is named by its view
policyone of: before_first_page | in_background | on_demandin_backgroundwhether the first page waits
except_onlist of name of a Pagepages that skip the preload

Example

policy: in_background
except_on: [auth.sign_in]
views: [{view: session.Me}, {view: tags.All}]

Guard​

A condition checked before a page opens, with a redirect, refusal or state change.

Guards express sign-in requirements and link-borne state. Evaluated in order; the first whose when holds decides. Business permissions belong in actor grants.

Properties

PropertyTypeRequiredDefaultNote
namenameyesnode name among the shell's guards
whenexpryescondition over actor, url and page
thenone of: record { redirect: name of a Page, params: optional map of name → expr } | record { refuse: string } | record { set: map of expr → expr }yesredirect, refuse with a message, or set shell state

Example

name: invite_link
when: url.query.invite
then: {set: {shell.invite_token: url.query.invite}}

Checks

CheckSubjectRuleSeverity
page_refs**.navigate.to, **.to.to, pages.*.switch_to[], shells.*.guards[].then.redirect{must_resolve: {ref: page}}error

The menu — sections of pages, the home page, hidden routes and search.

Navigation lists every page exactly once: in a section, or in hidden when it is reached by link or redirect only. With visibility: by_actor_grants a page appears when the actor may read the views its eager sections read, so role lists disappear from UI code.

Properties

PropertyTypeRequiredDefaultNote
homename of a Pageyespage opened at the app root
sectionslist of NavSectionyesmenu groups in display order
hiddenlist of name of a Pagerouted pages not shown in the menu
visibilityone of: by_actor_grants | by_rolesby_actor_grantshow the menu decides what to show
searchrecord { over: list of (one of: label | synonyms) }menu search fields
restrictionslist of record { when: expr, sections: list of name, unmapped: optional list of string }when a condition holds only these sections show

Example

home: overview
visibility: by_actor_grants
search: {over: [label, synonyms]}
sections:
- name: sales
label: Sales
icon: briefcase
pages: [partners.list, deals.list, deals.pipeline]
hidden: [partners.detail, auth.sign_in]
restrictions: [{when: "actor.roles == {partner}", sections: [home, billing, support]}]

Checks

CheckSubjectRuleSeverity
nav_resolvesnavigation.sections[].pages[], navigation.hidden[], navigation.home{must_resolve: {ref: page}}error

One group of the menu.

A section is a heading with pages under it — a fixed list, or entries generated from a view.

Properties

PropertyTypeRequiredDefaultNote
namenameyesnode name
labelstringheading text
iconstringsemantic icon name mapped by the renderer
pagesone of: list of name of a Page | DynamicNavEntriesyesfixed pages or entries from a view

Example

name: billing
label: Billing
icon: receipt
pages: [invoices.list]

DynamicNavEntries​

Menu entries generated from the rows of a view.

Use when the menu depends on data, such as one entry per saved view.

Properties

PropertyTypeRequiredDefaultNote
from_viewname of an ESS viewyesone entry per row
pagename of a Pageyespage each entry opens
paramnameyespage param filled from the row id
labelexprrow.labelentry text
synonymslist of stringextra search words
filterexprrows kept when the predicate is true

Example

from_view: saved_views.Mine
page: deals.saved
param: view_id
label: row.label
synonyms: [filter, view]

How a page appears in the menu, declared on the page as nav.

The label and search synonyms of a page, plus the role list a retrofitted app used.

Properties

PropertyTypeRequiredDefaultNote
labelstring$page.titlemenu text
synonymslist of stringextra words the menu search matches
roles_todaylist of stringtraceability only; visibility comes from grants

You may also write

WhereYou writeIt means
labelnothing: leave it out$page.title

Example

label: Partners
synonyms: [resellers, accounts]

Pages and sections​

Routes, layouts, page templates, and the section as the unit of loading.

Page​

One route — its state, layout, header, sections and overlays.

A page owns the state a link should reproduce (filters, paging, selection) and composes sections, each loading on its own. Start from a page kind and declare only what differs. layout arranges sections renderer-neutrally. Use switch_to for sibling pages shown as a view switch; switching keeps every current param the target page declares, by name, so sibling views of one record stay on that record. actor names the ESS actor the page is built for: checked against the model, every command the page sends (its sections, header and overlays) must be in that actor's may, as the served surface would refuse it otherwise. A page without actor is checked as before; renderers do not read it.

Properties

PropertyTypeRequiredDefaultNote
kindname of a PageKindyestemplate the page starts from
shellname of a Shell{first_present: [shells.app, only_shell]}frame the page renders in
titlestringheader title
navNavEntrymenu label and synonyms
paramsmap of name → Typepath parameters (always in the URL)
aliaseslist of stringlegacy route paths
switch_tolist of name of a Pagesibling pages offered in the header
visibleexprextra condition beyond grants, such as a feature flag
actorname of an ESS actorthe ESS actor whose grants bind every command the page sends; with a model, each must be granted to it
layoutPageLayoutstackhow sections are arranged
statemap of name → optional Statepage state
headerheadertitle, total, actions, live status
sectionslist of Sectionyesregions of the page, named, in declaration order; merged with the kind's by name
overlaysmap of name → optional overlaydrawers and dialogs of the page (unordered)
profileone of: thin | fatplacement for this page in a hybrid document
sourcelist of stringfiles the page was retrofitted from
unmappedlist of stringgaps the retrofit could not resolve

You may also write

WhereYou writeIt means
state.* | overlays.* | header.*exactly null{remove_inherited: $key}
sections[]record { name: name, remove: exactly true }{remove_inherited: $value.name}
shellnothing: leave it out{first_present: [shells.app, only_shell]}

Inheritance

from: kind
merge: shorthands.inheritance
conflict_winner: page
applies_to: [state, sections, overlays, header, layout]

Example

kind: detail_page
title: Partner
params: {id: PartnerId}
layout:
columns:
- {name: side, sections: [summary], width: narrow}
- {name: main, sections: [deals], width: wide}
header: {actions: [{name: edit, opens: edit, label: Edit}]}
sections:
- name: summary
component: record
reads: {view: partners.ById, params: {id: params.id}}
fields: [name, {field: tier, as: badge}]
- name: deals
component: collection
reads: {view: deals.ByPartner, params: {partner_id: params.id}}
columns: [title, {field: stage, as: badge}]
overlays: {edit: {kind: drawer, component: form, same_as: partners.list.edit}}

Checks

CheckSubjectRuleSeverity
page_reachablepages.*{must_be_in: ["navigation.sections[].pages[]", "navigation.hidden[]", "navigation.sections[].pages.page"]}error
page_refs**.navigate.to, **.to.to, pages.*.switch_to[], shells.*.guards[].then.redirect{must_resolve: {ref: page}}error
shell_refspages.*.shell{must_resolve: {ref: shell}}error

PageLayout​

A renderer-neutral arrangement of a page's sections.

stack shows sections one below the other in declaration order. columns splits the page into named columns, each listing its sections. areas names grid cells row by row and places sections into areas; an area spanning several cells repeats its name. Renderers without columns or areas fall back to a stack in section declaration order.

Properties

PropertyTypeRequiredDefaultNote
stackexactly stacksections top to bottom in declaration order
columnslist of record { name: name, sections: list of name of a Section, width: optional (one of: narrow | normal | wide) }named columns left to right
areasrecord { grid: list of list of name, place: map of name → list of name of a Section }grid of area names and the sections placed in each

You may also write

WhereYou writeIt means
the value itselfnothing: leave it outstack

Form

one_of:
- {const: stack}
- record:
columns:
list:
record:
name: name
sections: {list: {ref: section}}
width: {optional: {enum: [narrow, normal, wide]}}
- record:
areas:
record:
grid: {list: {list: name}}
place: {map: {key: name, value: {list: {ref: section}}}}

Default

stack

Degrades

no_columns: stack
no_areas: stack

Example

areas:
grid: [[kpis, kpis], [pipeline, feed], [board, board]]
place: {kpis: [kpis], pipeline: [pipeline], feed: [feed], board: [board]}

Checks

CheckSubjectRuleSeverity
section_refs**.header.total, **.header.filters, **.depends_on, pages.*.layout.columns[].sections[], pages.*.layout.areas.place.*[]{must_resolve_in: [page.sections]}error
layout_completepages.*.layout{must: place_every_section_once}warning

PageKind​

A page template that fills the same sections, state, header and overlays.

Most admin pages share one grammar: header with title and total, filters, a collection, a create/edit drawer and a delete confirm. A page kind captures it once. Built-in kinds are listed in builtins; a document adds its own under page_kinds with extends.

Properties

PropertyTypeRequiredDefaultNote
extendsname of a PageKindkind to start from
purposestringone line on when to use it
statemap of name → Statestate every page of the kind has
sectionslist of Sectionsections every page of the kind has, named, in order
overlaysmap of name → overlayoverlays every page of the kind has
headerheaderdefault header
layoutPageLayoutdefault layout

Builtins

list_page:
purpose: "rows of one entity with search, filters, paging and row actions"
state:
search: {type: string, class: page_state, store: url, pinned: true}
page: {type: integer, class: page_state, store: url, pinned: true, default: 1}
size: {type: integer, class: page_state, store: url, pinned: true, default: 25}
sort:
type: {optional: {record: {by: string, dir: {enum: [asc, desc]}}}}
class: page_state
store: url
pinned: true
sections:
- name: filters
component: filter_bar
binds: [state.search]
search: {binds: state.search}
- {name: list, component: collection}
header: {title: from_page, total: list, filters: filters}
report_page:
purpose: read-only rows over a time window with export and optional live inserts
state:
window:
type:
record:
from: timestamp
to: timestamp
preset: {enum: [this_month, last_30d, this_quarter, custom]}
class: page_state
store: url
pinned: true
default: {preset: last_30d}
page: {type: integer, class: page_state, store: url, pinned: true, default: 1}
size: {type: integer, class: page_state, store: url, pinned: true, default: 50}
sections:
- name: filters
component: filter_bar
binds: [state.window]
window: {binds: state.window}
- {name: list, component: collection}
header: {title: from_page, total: list, filters: filters}
detail_page:
purpose: one record and its related sections
sections: [{name: summary, component: record}]
settings_page:
purpose: one settings record edited in place
sections: [{name: settings, component: form}]
dashboard_page:
purpose: a board of widgets each with its own read or channel
sections: [{name: board, component: board}]
editor_page:
purpose: a graph or document edited as a whole
sections: [{name: editor, component: graph_editor}]
form_page:
purpose: a standalone form such as sign-in
sections: [{name: form, component: form}]
static_page: {purpose: fixed content without reads, sections: [{name: body, component: record}]}

Example

extends: list_page
purpose: rows of one entity with a drawer to create and edit and a confirm to delete
state: {tags: {type: {list: Tag}, class: page_state, store: url, pinned: true}}
overlays:
create: {kind: drawer, component: form}
edit: {kind: drawer, component: form}
delete: {kind: dialog, component: confirm, confirm_label: Delete}
header:
title: from_page
total: list
filters: filters
actions: [{name: create, opens: create, label: New}]

Section​

A region of a page with one read and its own loading lifecycle.

The section, not the page, is the unit of loading. Each has at most one reads, its own states and optional live updates; a slow section never blocks its siblings. Use load: on_visible for partial loading and depends_on when a section needs another section's selection. component names a member of the composite union or a widget; its props are written inline beside the section's own fields, and a key that is neither is refused. children adds widgets or primitives rendered with the section (a caption, a button). title is the heading a reader sees, which tells two sections over the same view apart.

Properties

PropertyTypeRequiredDefaultNote
namenameyesnode name among the page's sections
componentone of: name of a Composite | name of a Widgetyeswhat the section renders
titlestringheading shown above the section; absent, none
readsReadsthe section's data; one per section; it is the composite's own reads
liveLivehow channel events change the rows
loadone of: eager | on_visible | on_demandeagerwhen the read starts
depends_onname of a Sectionsibling whose selection the params use; loads after it
visibleexprshows the section only when true
statesSectionStateshow each lifecycle state renders
statemap of name → Statestate local to the section
childrenlist of Nodeextra named nodes rendered after the composite
profileone of: thin | fatplacement for this section in a hybrid document
degradesDegradesfallbacks for renderers lacking a capability
unmappedlist of stringgaps found by a retrofit

Lifecycle

states: [idle, loading, ready, empty, refreshing, stale, failed, forbidden]
transitions:
- {from: idle, to: loading, "on": mounted and dependencies satisfied}
- {from: loading, to: ready, "on": the read returned rows}
- {from: loading, to: empty, "on": the read returned zero rows}
- {from: loading, to: failed, "on": refusal or transport error}
- {from: loading, to: forbidden, "on": the actor lacks the grant for the view}
- from: [ready, empty]
to: refreshing
"on": "params changed, a command outcome touched the view, or a poll tick"
- {from: refreshing, to: [ready, empty, failed], "on": the read finished}
- {from: ready, to: stale, "on": the feeding channel is down past stale_after}
- {from: stale, to: refreshing, "on": the channel reconnected}
- {from: failed, to: loading, "on": retry}
invariants:
- {id: first_paint, rule: shell and page skeleton render before any read}
- {id: stale_marked, rule: "a stale value is marked stale, never shown as current"}

Example

name: list
component: collection
reads:
view: activity.Page
params: {from: state.window.from, to: state.window.to, types: state.types}
paging: cursor
live:
channel: activity
effect: insert_top
only_if: matches(params)
when_paged_away: count_new
states:
loading: skeleton
empty: {message: Nothing happened in this window}
stale: {mark: both}

Checks

CheckSubjectRuleSeverity
section_refs**.header.total, **.header.filters, **.depends_on, pages.*.layout.columns[].sections[], pages.*.layout.areas.place.*[]{must_resolve_in: [page.sections]}error
widget_expands**.component naming a widget{must: [resolve_widget, required_args_present, args_match_param_types, no_recursion], then: check_expanded_body}error

SectionStates​

How a section renders each lifecycle state.

Declare what the user sees while loading, when there is nothing, when the read failed, when access is denied, and when live data went stale.

Properties

PropertyTypeRequiredDefaultNote
loadingone of: skeleton | spinner | noneskeletonfirst load
refreshingone of: keep_rows | skeletonkeep_rowslater loads
emptyrecord { message: string, action: optional Action }ready with no rows
failedrecord { message: optional string, retry: boolean }{retry: true}read refused or transport error
forbiddenrecord { message: optional string }actor lacks the grant
stalerecord { mark: (one of: badge | dim | both) }{mark: badge}live channel down

Example

loading: skeleton
empty: {message: No partners yet, action: {name: create, opens: create, label: New partner}}

Composites, widgets and primitives​

The composite union, header and overlay, app-defined widgets, the 9 primitives, and renderer fallbacks.

collection​

Rows of a view with columns, sorting, paging, selection and row actions.

Use for any list, table or card grid. Columns are fields of the rows; as picks a semantic rendering. Row actions run commands bound to the row or open overlays. item nests nodes per row (widgets, metrics, primitives); expand shows detail inline; group_by and reorder give a kanban. A TUI renders it as a scrollable list.

Properties

PropertyTypeRequiredDefaultNote
readsReadsthe rows
columnsone of: list of Field | record { binds: expr, all: list of Field } | stringfixed columns, user-selectable columns, or an UNMAPPED string
sortrecord { by: name, dir: optional (one of: asc | desc), allowed: optional list of name, mode: optional (one of: server | client) }default and allowed sort
styleone of: table | cards | list | treetablepresentation hint
selectionone of: none | single | multiple | record { mode: (one of: single | multiple), enabled: expr }nonerow selection, optionally only in a mode
row_actionslist of Actionactions per row
bulk_actionslist of Actionactions on the selection
actionslist of Actionactions on the whole collection
expandNodedetail shown when a row expands
itemlist of Nodenested named nodes per row, in order
reorderrecord { does: name of an ESS command, endpoint: optional string }drag to reorder, saved by a command
group_bynamefield rows are grouped under
group_orderlist of stringorder groups are shown in; values not listed follow in the order they first appear
show_empty_groupsbooleanfalsea heading for every group_order value, even one no row falls under

Example

component: collection
style: cards
selection: multiple
reads:
view: partners.Page
params: {q: state.search, tier: state.tier}
paging: server
debounce: 300ms
columns:
- {field: name, sortable: true}
- {field: tier, as: badge}
- {field: revenue_ytd, as: number, sortable: true}
item: [{name: card, component: partner_card, args: {partner: row}}]
row_actions:
- {name: open, navigate: {to: partners.detail, params: {id: row.id}}, label: Open}
- {name: delete, opens: delete, as: icon}

record​

One row shown as labelled, read-only fields.

Use for detail views, KPI panels and read-only summaries.

Properties

PropertyTypeRequiredDefaultNote
readsReadsthe row
fieldslist of Fieldfields to show
tabslist of Tabfields split into tabs
itemlist of Nodenested named nodes such as metrics or widgets, in order
actionslist of Actionactions on the record

Example

component: record
reads: {view: partners.ById, params: {id: params.id}}
fields: [name, {field: tier, as: badge}, website, {field: created_at, as: date}]
item: [{name: revenue, component: money, args: {value: row.revenue_ytd}}]

form​

Inputs bound to one ESS command's input.

A form is a command's input made editable. Field types, required-ness, validation and error messages come from the command; the form chooses order, grouping and widgets. A refusal is shown on the form as the command's typed error. Settings pages use groups that save on change; typed editors use variant_by to generate one form per type.

Properties

PropertyTypeRequiredDefaultNote
doesname of an ESS commandyescommand the form submits
loadsReadsinitial values for edit forms
fieldslist of Field{from: does.input_fields, order: declaration}inputs shown
groupslist of FormGroupindependently saved groups
tabslist of Tabfields split into tabs
partslist of Nodenested named nodes inside the form, in order
actionslist of Actionextra actions next to submit
resultNodeshows the command's outcome
recordNoderead-only fields above the inputs
submitrecord { label: optional string, closes: optional boolean, auto: optional boolean }submit button; auto submits on open
draftStatethe unsaved input (class draft)
saveone of: on_submit | on_change | on_bluron_submitwhen the command runs
variant_byrecord { field: name, forms: map of string → name }one generated form per value of a field
endpointstringtraceability

Example

component: form
does: deals.CreateDeal
tabs:
- {name: basics, label: Basics, fields: [title, {field: value, as: number}]}
- {name: schedule, label: Schedule, fields: [{field: close_date, as: date}]}

choice​

Pick one or many values from a view or a fixed list.

One composite replaces dropdowns, tag pickers, tree selects and checkbox lists. style is a hint; a TUI renders all styles as a checklist popup.

Properties

PropertyTypeRequiredDefaultNote
readsReadsoptions from a view
optionsone of: list of record { value: json, label: string } | list of string | namefixed options, or a named enum type: one of the document's types, else, with --model, an enum of the model by its qualified name, the name below the system, or the last segments only one enum ends with
valuenamethe row field of reads each option sends, by its wire name (the key its rows carry; --model names the wire name of a field written by its model name); a row without it offers no option. Absent: the read's key, else the row field named like the form field the choice picks for, else the identity of the entity the view projects (--model), else id
labelnamethe row field of reads each option shows, by its wire name; absent, or missing or null in a row: the row's label, else its name, else the value
bindsexprstate the value is written to
multiplebooleanfalsemany values
styleone of: dropdown | tags | tree | grouped | radio | segmented | checklistdropdownpresentation hint
creatablerecord { does: name of an ESS command }typed values create new options
visibleexprshows the choice only when true
notestringauthor remark

You may also write

WhereYou writeIt means
options[]string{value: $value, label: $value}
optionsname{each_value_of_enum_type: $value, as: {value: $item, label: $item}}

Example

component: choice
multiple: true
options: DealStage

filter_bar​

Search, choices, inputs and a time window bound to page state.

A filter bar writes page state; sections read it in their params. Because that state lives in the URL, a filtered view is a link.

Properties

PropertyTypeRequiredDefaultNote
bindslist of expryesstate the bar writes
searchrecord { binds: expr, placeholder: optional string }free-text search
windowrecord { binds: expr, time: optional boolean }date or date-time range
choiceslist of Nodechoice composites, named, in order
inputslist of Fieldfree inputs each bound to state with binds
actionslist of Actionactions next to the filters
resetbooleanfalseoffers a reset-all button

You may also write

WhereYou writeIt means
choices[]record { name: name, remove: exactly true }{remove_inherited: $value.name}

Example

component: filter_bar
binds: [state.search, state.stage, state.min_value]
search: {binds: state.search}
choices: [{name: stage, component: choice, multiple: true, options: DealStage}]
inputs: [{field: min_value, as: number, binds: state.min_value}]

Page title, count, primary actions, view switch, filters and live status.

Every page has one header, placed by position (header) rather than by component, so it is not a member of the composite union. total names the section whose total is shown; live lists channels whose connection state is shown, so a stale page is visibly stale, and never applies events. title_from shows a field of the record a section of the page holds (its first row, with its live changes applied) in place of title, which stays the text shown until the record holds the field. A header metric takes its own live, as any nested composite with reads does.

Properties

PropertyTypeRequiredDefaultNote
titleone of: string | exactly from_pageheader title
title_fromrecord { section: name of a Section, field: name }a field of the first row of a section that reads, shown as the title once present; title until then
totalname of a Sectionsection whose total is shown
actionslist of Actionprimary actions
switchlist of namepages from switch_to, or modes of this page
filtersname of a Sectionfilter bar rendered in the header
livelist of name of a Channelchannels whose lifecycle is shown
metricslist of Nodeheadline metrics, named, in order
helprecord { text: optional string, link: optional string }help text or link

You may also write

WhereYou writeIt means
titleexactly from_page$page.title

Example

live: [tickets]
actions: [{name: create, opens: create, label: New ticket}]

Checks

CheckSubjectRuleSeverity
channel_refs**.live.channel, **.header.live[], channel.<name> inside expr{must_resolve: {ref: channel}}error
section_refs**.header.total, **.header.filters, **.depends_on, pages.*.layout.columns[].sections[], pages.*.layout.areas.place.*[]{must_resolve_in: [page.sections]}error

overlay​

A drawer, dialog, fullscreen pane or popover shown on demand.

Overlays hold forms, confirms and detail views opened by actions. They belong to a page or a shell. An overlay is a frame around one member of the composite union (or a widget): the frame fields below sit beside the member's own props, and a key that is neither is refused. same_as reuses another overlay's definition, addressed as <page>.<overlay>; local props override.

Properties

PropertyTypeRequiredDefaultNote
kindone of: drawer | dialog | fullscreen | popoveryespresentation hint; a TUI uses a full-screen pane
componentone of: name of a Composite | name of a Widgetyeswhat the overlay renders
titlestringoverlay title; a confirm shows it as its question
paramsmap of name → exprvalues passed by the opener
statemap of name → Stateoverlay-local state
visibleexprshows the overlay only when true
degradesDegradesfallbacks for renderers lacking a capability
same_asname of an overlayreuse another overlay; local props override
unmappedlist of stringgaps found by a retrofit

You may also write

WhereYou writeIt means
same_asname of an overlay{merge_under: $value}

Example

kind: drawer
component: record
title: Invoice
reads: {view: invoices.ById, params: {id: params.id}}
fields: [number, partner, {field: status, as: badge}, {field: due, as: date}]
actions: [{name: pdf, export: {reads: invoices.Pdf, as: zip}, label: Download PDF}]

Checks

CheckSubjectRuleSeverity
same_as_resolves**.same_as{must_resolve: {ref: overlay}}error
widget_expands**.component naming a widget{must: [resolve_widget, required_args_present, args_match_param_types, no_recursion], then: check_expanded_body}error

confirm​

A yes/no step before a command, listing references and consequences.

Use before destructive commands, inside an overlay whose title is the question. references shows what still uses the record; input asks the user to type a value; alternatives offers a softer action.

Properties

PropertyTypeRequiredDefaultNote
bodystringexplanation
doesname of an ESS commandcommand run on confirm, unless the action that opened the confirm runs the same command
referencesname of an ESS viewthe used-by view shown first
consequenceslist of stringwhat the command will do
confirm_labelstringDeleteconfirm button text
dangerbooleantruestyles the confirm as destructive
inputrecord { label: string, must_equal: optional expr }type-to-confirm
alternativeslist of Actionother actions offered
endpointstringtraceability

Example

kind: dialog
component: confirm
title: Close ticket
does: tickets.CloseTicket
input: {label: Type CLOSE to confirm, must_equal: CLOSE}
alternatives: [{name: snooze, does: tickets.SnoozeTicket, label: Snooze for a day}]

metric​

One number from a view or channel, with a window and a stale mark.

For KPI tiles and per-row live numbers. With from a metric reads a channel field; when the channel is stale the number is marked stale, not shown as current.

Properties

PropertyTypeRequiredDefaultNote
readsReadsvalue from a view
fromexprvalue from a channel field
matchnamerow field that picks a keyed value
windowdurationtime window the value covers
formatone of: number | duration | percent | bytesnumberdisplay format
labelstringcaption
aggregateone of: count | sum | min | max | avgcomputed over every row of the read instead of read from one; needs reads
fieldnamerow field aggregate reads; required for every aggregate but count

Example

component: metric
from: channel.metrics.overdue_invoices
label: Overdue invoices

chart​

A series view drawn as a chart.

A renderer without charts shows the series as a table (degrade no_charts).

Properties

PropertyTypeRequiredDefaultNote
readsReadsyesthe series
chartone of: line | bar | doughnut | pie | polar | single_number | list | table | record { binds: expr, options: list of string }yeschart kind, fixed or chosen by data
xnamex-axis field
serieslist of namey fields

Example

component: chart
chart: bar
reads: {view: deals.StageTotals, params: {window: state.window}}
x: stage
series: [count, value]
degrades: {no_charts: table}

board​

A user-arranged grid of widgets, persisted.

A board reads a dashboard record; each row places a widget whose kind is chosen by widget_by. A renderer without free layout stacks them in layout order.

Properties

PropertyTypeRequiredDefaultNote
readsReadsyesthe dashboard record
widget_bynamerow field choosing the widget kind
widgetsmap of name → Nodenode per widget kind
item_actionslist of Actionactions on each placed widget
layoutrecord { persisted_by: name of an ESS command, editable_by: optional name of an ESS command, state: optional expr }how the arrangement is saved

Example

component: board
reads: {view: dashboards.Mine}
widget_by: type
widgets:
win_rate: {component: metric, reads: {view: deals.WinRate}, format: percent, label: Win rate}
layout: {persisted_by: dashboards.SaveLayout, state: state.layout}
degrades: {no_free_layout: stack}

graph_editor​

Nodes and edges of a model, editable on a canvas.

For flow and workflow editors. Nodes open an overlay to edit; edges carry actions such as inserting a step. reads holds the nodes, and the edges too unless edges.reads names a view of their own; nodes.key is the field edge endpoints name and nodes.label the field a node shows. A renderer without a canvas falls back to a collection of nodes.

Properties

PropertyTypeRequiredDefaultNote
readsReadsyesthe nodes, and the edges unless they have their own read
nodesrecord { key: optional name, label: optional name, kind_by: optional name, opens: optional name of an overlay }node key (default id), label and kind fields, and edit overlay
edgesrecord { reads: optional Reads, from: name, to: name, kind_by: optional name }edge read, endpoint fields (node keys) and kind
node_actionslist of Actioncontext menu of a node
edge_actionslist of Actioncontext menu of an edge
toolbarlist of Nodenamed nodes above the canvas, left to right

Example

component: graph_editor
reads: {view: workflows.Graph}
nodes: {kind_by: step_type, opens: edit_step}
edges: {from: from_step, to: to_step, kind_by: outcome}
edge_actions: [{name: insert, does: workflows.InsertStep, label: Insert step}]
toolbar: [{name: zoom, primitive: input, as: number, binds: state.zoom}]
degrades: {no_graph_editor: collection}

rich_text​

Text with expression completion or markup.

For message templates with {{expression}} completion, markup and JSON bodies.

Properties

PropertyTypeRequiredDefaultNote
completesname of an ESS viewview of completable expressions
syntaxone of: plain | expression | ssml | json | curlexpressionlanguage of the text
bindsexprdraft field the text is written to

Example

component: rich_text
completes: templates.Expressions
syntax: expression
binds: draft.body

references​

The "used by" list of a record.

Shown before destructive commands and in usage tabs so users see what depends on a record.

Properties

PropertyTypeRequiredDefaultNote
readsReadsyesthe referencing records
columnslist of Field[label, type]columns shown
navigatesbooleantruerows link to the referencing record

Example

component: references
reads: {view: partners.UsedBy, params: {id: params.id}}

Widget​

An app-defined composite with typed parameters, usable wherever a composite kind is.

Declare a widget under the document's widgets when the same arrangement of composites and primitives recurs — a partner card, a money value, a toned status. Its params are typed; its body is a list of named nodes that may read args.<param>. A widget is expanded at its use site and then checked like a built-in. A widget may not contain itself.

Properties

PropertyTypeRequiredDefaultNote
summarystringyesone line shown in pickers and docs
docstringlonger description for authors
paramsmap of name → record { type: Type, required: optional boolean, default: optional json, note: string }typed parameters
arrangeone of: row | column | gridcolumnhow body nodes are arranged
bodylist of Nodeyesnamed nodes; each may use args

Example

summary: "A partner as a card with logo, tier and revenue."
params: {partner: {type: Partner, required: true, note: the partner row}}
arrange: column
body:
- {name: logo, primitive: image, src: args.partner.logo_url, alt: Partner logo, fit: contain}
- {name: title, primitive: text, text: args.partner.name, style: heading}
- name: tier
component: status_badge
args:
status: args.partner.tier
tones: {registered: neutral, silver: info, gold: warning, platinum: success}
- {name: revenue, component: money, args: {value: args.partner.revenue_ytd}}
- name: open
primitive: link
text: Open
to: {to: partners.detail, params: {id: args.partner.id}}

WidgetInstance​

A use of a widget, with arguments.

Written like a composite with component naming the widget and args supplying its params. Required params must be given; each arg must match the param type.

Properties

PropertyTypeRequiredDefaultNote
componentname of a Widgetyesthe widget used
argsmap of name → jsonone entry per param: an expression, or a literal such as a map
namenamenode name, required inside lists
visibleexprshows the instance only when true
bodylist of Nodewritten by expansion, never by an author: the widget body with args substituted

Expansion

- {step: substitute, detail: args.<param> in the body is replaced by the bound expression}
- step: validate
detail: the expanded nodes are checked like built-ins; findings are reported at <instance path>/body/<node name>
- step: bound
detail: the expanded bodies of all uses in a document hold at most 100000 YAML values; the outermost use that passes the limit is refused (widget_expands)

Example

component: status_badge
args: {status: row.stage, tones: deal_stage}

Checks

CheckSubjectRuleSeverity
widget_expands**.component naming a widget{must: [resolve_widget, required_args_present, args_match_param_types, no_recursion], then: check_expanded_body}error

Node​

Anything that can appear inside a section, composite or widget — a composite, a widget instance or a primitive.

Wherever a nested element is allowed (item, children, body, parts, expand, choices, metrics, toolbar), a Node is expected. In lists every node carries name; in the one map of nodes (board widgets) the key is its name. A component naming a member of the composite union is a composite; any other component names a widget.

Properties

PropertyTypeRequiredDefaultNote
(value)one of: Composite | WidgetInstance | Primitiveexactly one of component or primitive

Exactly one of

component, primitive

Example

name: send
primitive: button
label: Send
tone: primary
action: {name: send, does: tickets.PostReply}

Field​

One input of a form or one column of a collection.

Columns and form inputs are fields. as is a semantic widget, not a component; a bare name is enough when the defaults fit.

Properties

PropertyTypeRequiredDefaultNote
fieldnameyesfield of the command input or view row
namename{from: field}node name
labelstring{from: model.field_label}caption
asone of: text | number | secret | toggle | choice | tags | time | date | date_range | duration | file | rich_text | json | list | map | cron | color | badge | audio | link | metrictextsemantic widget
choiceNodethe choice composite for as: choice
sortablebooleanfalsecolumn can sort
visibleexprshows the field only when true
bindsexprbind to UI state instead of the command input
label_fromrecord { view: name of an ESS view, field: name, key: optional name }show a field of a related view instead of the value: the row of view whose key (default id) equals the value; the view is read once, without params
notestringauthor remark

You may also write

WhereYou writeIt means
the value itselfstring{field: $value, name: $value}
namenothing: leave it out{first_present: [field]}

Example

field: tier
as: choice
choice: {component: choice, options: PartnerTier}

Tab​

One tab of a form, record or overlay.

A tab holds fields, or a nested node, or an action such as opening another overlay.

Properties

PropertyTypeRequiredDefaultNote
namenameyesnode name
labelstringtab text
visibleexprshows the tab only when true
fieldsone of: list of Field | stringfields in the tab, or an UNMAPPED string
formone of: Node | Actionnested node or an action

Example

name: schedule
label: Schedule
fields: [{field: close_date, as: date}]

FormGroup​

A group of settings fields saved on its own.

Settings pages save each group independently, often on change.

Properties

PropertyTypeRequiredDefaultNote
namenameyesnode name
labelstringgroup heading
fieldslist of Fieldyesfields in the group
saveone of: with_form | on_changewith_formwhen the group saves
doesname of an ESS commandcommand for this group if not the form's
actionslist of Actionextra actions such as rotating a key
endpointstringtraceability

Example

name: profile
label: Profile
save: on_change
fields: [name, website, {field: logo, as: file}]

Composite​

The composite union — one type whose member is chosen by component.

Every composite kind is a member of one union discriminated by component; the member's props follow inline beside the fields below, and a key the member does not declare is refused, naming the node's path. A composite written where a nested node is expected, a section and an overlay all hold one member. header and overlay are composites placed by position, so they are not members. A component that names no member names a widget. The bare-name shorthand accepts a member only: a widget is always written {component: <widget>, args: …}. A nested composite with its own reads (a tab's node, a header metric, a record's item) takes live as a section does: the channel's events change its own rows while it is shown. A node that is not shown (an inactive tab, a collapsed expand, a node whose visible is false) applies no event and reads its view again when it is shown. live on a node that reads nothing is refused, and so is when_paged_away.

Properties

PropertyTypeRequiredDefaultNote
componentname of a Compositeyeskind of the nested composite
namenamenode name, required inside lists
statemap of name → Statestate local to this composite
visibleexprshows the composite only when true
degradesDegradesfallbacks for this composite
liveLivehow channel events change the composite's own reads while it is shown; only on a member with reads, without when_paged_away
unmappedlist of stringgaps found by a retrofit

You may also write

WhereYou writeIt means
the value itselfname of a Composite{component: $value}

Union

tag: component
members:
- collection
- record
- form
- choice
- filter_bar
- confirm
- metric
- chart
- board
- graph_editor
- rich_text
- references
other_tag_values: {ref: widget}

Example

component: metric
from: channel.metrics.open_deals
label: Open deals

Checks

CheckSubjectRuleSeverity
widget_expands**.component naming a widget{must: [resolve_widget, required_args_present, args_match_param_types, no_recursion], then: check_expanded_body}error

Degrades​

Fallbacks for renderers that lack a capability.

A renderer profile lists the capabilities it lacks. A construct declares a fallback per capability; otherwise the capability table's first fallback applies, and refuse stops generation.

Properties

PropertyTypeRequiredDefaultNote
(capability)one of: no_free_layout | no_charts | no_graph_editor | no_rich_text | no_drawer | no_drag | no_audio | no_file_upload | no_live | no_iframe | no_columns | no_areaskey of the degrades map; the value is a fallback name

Rule

when: {target_lacks: $capability, construct_uses: $capability}
use: [construct.degrades.$capability, "capabilities.$capability.fallbacks[0]"]
if_none: refuse

Capabilities

no_free_layout: {fallbacks: [stack], applies_to: [board]}
no_charts: {fallbacks: [table, metric], applies_to: [chart, board]}
no_graph_editor: {fallbacks: [collection, refuse], applies_to: [graph_editor]}
no_rich_text: {fallbacks: [plain], applies_to: [rich_text, form]}
no_drawer: {fallbacks: [dialog, fullscreen], applies_to: [overlay]}
no_drag: {fallbacks: [move_buttons], applies_to: [collection, board, graph_editor]}
no_audio: {fallbacks: [link], applies_to: [collection, record]}
no_file_upload: {fallbacks: [refuse], applies_to: [form, Action]}
no_live: {fallbacks: [poll, refuse], applies_to: [Live, Channel]}
no_iframe: {fallbacks: [link], applies_to: [board, record]}
no_columns: {fallbacks: [stack], applies_to: [PageLayout], order: section_declaration}
no_areas: {fallbacks: [stack], applies_to: [PageLayout], order: section_declaration}

Example

no_graph_editor: collection
no_drag: move_buttons

Primitive​

The closed set of nine renderer-neutral leaves.

Composites are the normal level of a spec. Primitives exist for the small pieces inside widgets, item templates and section children: a caption, a toned badge, a button, an image. Every primitive has primitive (its kind) and name; the kinds and their props are the constructs text, badge, icon, button, link, input, toggle, image and divider.

Properties

PropertyTypeRequiredDefaultNote
primitiveone of: text | badge | icon | button | link | input | toggle | image | divideryeskind of primitive
namenamenode name, required inside lists
visibleexprshows the primitive only when true
statemap of name → Statestate local to the primitive
degradesDegradesfallbacks for renderers lacking a capability

Tone

enum: [neutral, info, success, warning, danger]

Example

name: hint
primitive: text
text: "Tip: drop a CSV here to import partners"
style: caption

Checks

CheckSubjectRuleSeverity
primitive_props**.primitive{must: props_of_kind_only}error

text​

A run of text, literal or from a field, with a style and a value format.

Use for captions, headings and formatted values inside widgets and templates.

Properties

PropertyTypeRequiredDefaultNote
textexprliteral or expression
fieldnamerow field shown
styleone of: body | caption | heading | monobodytypographic role
formatone of: plain | number | currency | percent | date | time | durationplainvalue formatting
currencyexprcurrency code for format currency

Exactly one of

text, field

Example

name: amount
primitive: text
text: args.value.amount
format: currency
currency: args.value.currency

badge​

A short value in a toned pill.

For statuses and tiers. tone is fixed; tone_by picks a tone from the value, through a map written out (map) or the name of an entry of the document's tone_maps (tones), exactly one of the two. The loader resolves tones to its map, so a renderer only reads map. A value the map does not name takes tone. What a tone looks like is the tone's entry of Tokens.tone.

Properties

PropertyTypeRequiredDefaultNote
textexprliteral or expression
fieldnamerow field shown
toneone of: neutral | info | success | warning | dangerneutralfixed tone, and the tone of a value tone_by does not map
tone_byrecord { value: expr, map: optional map of string → (one of: neutral | info | success | warning | danger), tones: optional name of a ToneMap }tone per value: map written out, or tones naming an entry of tone_maps; exactly one of the two

Exactly one of

text, field

Example

name: badge
primitive: badge
text: args.status
tone_by: {value: args.status, tones: args.tones}

Checks

CheckSubjectRuleSeverity
tone_map_refs**.tone_by.tones{must_resolve: {ref: tone_map}}error

icon​

A semantic icon with an accessible label.

The renderer maps the semantic name to its icon set; a TUI shows a glyph or the label. Its tone is fixed (tone) or picked from a value (tone_by), exactly as on a badge, so a state shown as an icon follows the same tone map as the badge showing it elsewhere.

Properties

PropertyTypeRequiredDefaultNote
iconstringyessemantic icon name
toneone of: neutral | info | success | warning | dangerneutralcolour role, and the tone of a value tone_by does not map
tone_byrecord { value: expr, map: optional map of string → (one of: neutral | info | success | warning | danger), tones: optional name of a ToneMap }tone per value: map written out, or tones naming an entry of tone_maps; exactly one of the two
labelstringyesaccessible text

Example

name: typing
primitive: icon
icon: typing
tone: info
label: Someone is typing
visible: channel.ticket_chat.typing

Checks

CheckSubjectRuleSeverity
tone_map_refs**.tone_by.tones{must_resolve: {ref: tone_map}}error

button​

A button that runs one action.

Use inside widgets and section children; page-level actions belong in the header. Its emphasis (tone) has no token group of its own: a renderer draws it from color tokens by the fixed table under Emphasis, the values the React renderer uses today, so a theme that overrides accent restyles every primary button. none draws nothing.

Properties

PropertyTypeRequiredDefaultNote
labelstringyesbutton text
actionActionyeswhat the button does
toneone of: primary | secondary | danger | ghostsecondaryemphasis
iconstringoptional semantic icon

Emphasis

primary: {fill: accent, text: on_accent, border: accent}
secondary: {fill: surface, text: text, border: line}
danger: {fill: danger, text: on_accent, border: danger}
ghost: {fill: none, text: text, border: none}

Example

name: send
primitive: button
label: Send
tone: primary
action: {name: send, does: tickets.PostReply}

Text that navigates to a page or an external address.

Prefer to (a page of this app); href is for external addresses.

Properties

PropertyTypeRequiredDefaultNote
textexpryeslink text
torecord { to: name of a Page, params: optional map of name → expr }page to open
hrefexprexternal address

Exactly one of

to, href

Example

name: open
primitive: link
text: Open
to: {to: partners.detail, params: {id: args.partner.id}}

Checks

CheckSubjectRuleSeverity
page_refs**.navigate.to, **.to.to, pages.*.switch_to[], shells.*.guards[].then.redirect{must_resolve: {ref: page}}error

input​

A single free input bound to state or a draft field.

For inputs outside a form's field list — a settings search, an inline value.

Properties

PropertyTypeRequiredDefaultNote
asone of: text | number | search | date | time | secrettextinput type
bindsexpryesstate or draft field written
placeholderstringhint text

Example

name: api_help
primitive: input
as: search
placeholder: Search settings
binds: state.settings_search

toggle​

An on/off switch bound to state, or running an action.

Bind to UI state with binds, or run a command with action.

Properties

PropertyTypeRequiredDefaultNote
labelstringyesswitch text
bindsexprboolean written
actionActioncommand run on change

Exactly one of

binds, action

Example

name: api_toggle
primitive: toggle
label: Allow API access
binds: draft.api_enabled

image​

An image with required alternative text.

A renderer without images (a TUI) shows the alt text.

Properties

PropertyTypeRequiredDefaultNote
srcexpryesimage address
altstringyesalternative text
fitone of: cover | containcontainscaling

Example

name: logo
primitive: image
src: args.partner.logo_url
alt: Partner logo
fit: contain

divider​

A visual separator.

Separates groups of nodes inside widgets and children.

Properties

PropertyTypeRequiredDefaultNote
orientationone of: horizontal | verticalhorizontaldirection

Example

name: rule
primitive: divider

Reads, actions and fixtures​

How the UI reads ESS views, runs ESS commands, and runs without a backend.

Reads​

The ESS view a section or composite reads — or, while designing, a named placeholder backed by a fixture.

Every piece of data on screen comes from an ESS view. Params bind page state; paging says who pages. While a screen is designed before its model exists, write placeholder with a view name and a fixture file instead of view; renderers read the fixture, validators report the placeholder as a warning until it is bound. key names the field that identifies a row when it is not id, so row paths and row actions address the rows of a view keyed by another field, with or without a channel. endpoint and derived are traceability for retrofits.

Properties

PropertyTypeRequiredDefaultNote
viewname of an ESS viewESS view name
placeholdernamea view name not yet bound to the model
fixturestringfixture file answering the placeholder
keynamethe field that identifies a row: rows, row paths and row actions are keyed by it, and live.match defaults to it; absent, the section's live.match, else id
paramsmap of name → exprview params bound to state
filterexprClient row predicate after live effects and before local paging; never sent to the server, never authorization. Only listing reads and choices, with client or none paging.
pagingone of: server | client | cursor | append | nonenonewho pages
debouncedurationcoalesce param changes before reading
refreshone of: duration | exprpoll interval
cacherecord { store: Store, ttl: optional duration }{store: {resolve: {class: view_cache}}}where the result is cached
endpointstringtraceability to the HTTP call replaced
derivedstringtraceability when data is computed client-side today

You may also write

WhereYou writeIt means
the value itselfname of an ESS view{view: $value}

Exactly one of

view, placeholder

Constraints

- {when: {paging: client}, requires: {view.bounded: true}, else: refuse}
- {when: {placeholder: present}, requires: {fixture: present}, else: refuse}
- {when: {filter: present}, requires: {paging: {one_of: [client, none]}}, else: refuse}

Example

placeholder: forecast.Quarterly
fixture: fixtures/forecast.yaml

Checks

CheckSubjectRuleSeverity
unbound_placeholder**.reads.placeholder{must: bind_to_view}warning
fixture_per_view**.reads.view{must_be_in: [fixtures.views, fixtures.derived]}warning
filter_exprreads.filter and dynamic menu filter{must: parse_as_boolean_predicate}error
filter_rootsreads.filter and dynamic menu filter{must: read_only_row_params_state_shell_args_without_calls}error
filter_pathsreads.filter and dynamic menu filter{must: resolve_declared_paths}error
filter_placereads.filter{must: belong_to_listing_or_choice}error
filter_pagingreads.filter{must: use_client_or_none_paging}error
filter_over_paramreads.filter{must: prefer_existing_server_parameter}warning

Action​

One user-triggered effect: run a command (does), open an overlay, navigate, export, upload, copy or set UI state.

does names an ESS command; its input is bound from the row, selection or state, and its typed errors are shown where the action happened. There is no reload to declare: the command's outcome names the entities it changed, and every section reading a view over them is patched or re-read. Visibility follows actor grants; visible adds a UI condition.

Properties

PropertyTypeRequiredDefaultNote
namenamenode name among sibling actions; derived when absent, except for a copy action, whose expression is no name
doesname of an ESS commandESS command to run
bindmap of name → exprcommand input from row, selection or state
opensname of an overlayoverlay to open
navigaterecord { to: name of a Page, params: optional map of name → expr }page to go to
exportrecord { reads: name of an ESS view, as: (one of: csv | json | zip | png), params: optional expr }download a view
uploadrecord { accept: list of string, does: name of an ESS command }file picker feeding a command
copyexprvalue copied to the clipboard
setsmap of expr → (one of: expr | exactly toggle)UI state change only, never business data
labelstringbutton or menu text
asone of: button | icon | toggle | choice | menu_item | linkbuttonpresentation hint
choiceNodeoptions for as choice
loadsReadscurrent value for a header toggle or choice
confirmone of: name of an overlay | record { title: string, show: optional expr, confirm_label: optional string }confirm first; confirming runs this action, whether or not the confirm declares does
optimisticbooleanfalseapply the expected outcome at once and revert on refusal; requires: {does.outcome: unique_for_input}
bulkbooleanfalseapplies to the collection's selection
visibleexprUI condition beyond grants
endpointstringtraceability

You may also write

WhereYou writeIt means
sets.*exactly toggle{expr: not $key}
confirmrecord { title: string, show: optional expr, confirm_label: optional string }{overlay: {kind: dialog, component: confirm, title: $value.title, does: $action.does, confirm_label: $value.confirm_label}, show: $value.show}
namenothing: leave it out{first_present: [opens, does.last_segment_snake_case, navigate.to, export.reads, upload.does, sets.first_key]}

Exactly one of

does, opens, navigate, export, upload, copy, sets

Example

- name: move
does: deals.MoveDeal
bind: {deal_id: row.id}
as: choice
choice: {component: choice, options: DealStage}
optimistic: true
- {name: import, upload: {accept: [text/csv], does: partners.ImportPartners}, label: Import CSV}
- name: export
export: {reads: invoices.Page, as: csv, params: same_as(list)}
label: Export CSV
- name: remind
does: invoices.SendReminder
bind: {invoice_id: row.id}
visible: row.status == overdue
confirm: {title: Send payment reminder}

Checks

CheckSubjectRuleSeverity
opens_resolves**.opens{must_resolve_in: [page.overlays, page.kind.overlays, shell.overlays]}error
page_refs**.navigate.to, **.to.to, pages.*.switch_to[], shells.*.guards[].then.redirect{must_resolve: {ref: page}}error
filter_exportexport.params{must: not_claim_to_copy_client_filter}warning

FixtureIndex​

Sample data per view and event scripts per channel, so renderers run without a backend.

In fixture mode a renderer answers every read from a fixture file and plays each channel's script in a loop, honouring delivery and coalesce. A file holds one view or several; derived answers ById views from a list fixture. Placeholders name their fixture on the read itself.

Properties

PropertyTypeRequiredDefaultNote
dirstringfixture directory relative to the document
indexstringfile holding views, derived and scripts
viewsmap of name of an ESS view → stringview to fixture file
derivedmap of name of an ESS view → (one of: record { by_id_from: name of an ESS view, key: name } | record { same_as: name of an ESS view })views answered from another fixture
scriptsmap of name of a Channel → stringchannel to event script file

Script entry

type:
one_of:
- {record: {at: duration, event: {ref: event}, payload: json}}
- record:
at: duration
lifecycle: {enum: [connecting, loading, live, reconnecting, stale, closed]}
payload: {optional: json}
note: "an event to deliver, or a connection lifecycle change to simulate"

View file

type:
one_of:
- record:
view: {ref: view}
total: {optional: integer}
by_params: {optional: json}
rows: {list: json}
- record:
views:
map:
key: {ref: view}
value:
record:
total: {optional: integer}
by_params: {optional: json}
rows: {list: json}
note: "one view per file, or several; by_params records which params the rows answer"

Example

dir: fixtures
views: {partners.Page: portal.yaml, deals.Page: portal.yaml}
derived: {partners.ById: {by_id_from: partners.Page, key: id}}
scripts: {activity: scripts/activity.yaml}

Live data​

Channels and how sections apply their events.

Channel​

A live source of ESS events or a live view, with delivery and resume semantics.

A channel says what it carries and in which direction; the renderer picks the transport. delivery: latest_value may drop intermediate values (metrics); every_event may not (an activity feed). resume says what happens after a reconnect. The connection lifecycle is UI state, and a stale value is marked. Scope is decided by the ESS model.

Properties

PropertyTypeRequiredDefaultNote
carriesone of: record { events: list of name of an ESS event } | record { view: name of an ESS view }yesevents (deltas) or a live view (replacing value)
directionone of: server_to_client | bothyesboth when the client also sends commands on it
sendslist of name of an ESS commandcommands sent over the channel
deliveryone of: latest_value | every_eventyeswhether values may be dropped
resumeone of: from_last_seen | refetchyesbehaviour after reconnect
scopeone of: account | user | session | paramaccountwhich events reach the actor
sessionrecord { per: name }one channel instance per value
lifecyclelist of (one of: connecting | loading | live | reconnecting | stale | closed)connection states shown to the user
reconnectrecord { backoff: record { from: duration, to: duration } }reconnect backoff
stale_afterdurationdown time after which fed sections are stale
bufferStateclass channel_buffer: unapplied events and last-seen cursor
fieldsmap of name → stringnamed fields readable as channel.<name>.<field>
transport_todaystringtraceability only; never read by a renderer
sourcelist of stringtraceability
unmappedlist of stringgaps found by a retrofit

Transport choice

- declared: direction server_to_client
renderer_picks: "SSE, or long polling where SSE is unavailable"
- {declared: direction both, renderer_picks: WebSocket}
- {declared: no channel and a read with refresh, renderer_picks: polling}

Example

carries: {events: [tickets.ReplyPosted, tickets.TypingStarted]}
direction: both
sends: [tickets.PostReply, tickets.StartTyping]
delivery: every_event
resume: from_last_seen
scope: param
session: {per: ticket_id}
buffer: {type: {list: ChatEvent}, class: channel_buffer}

Checks

CheckSubjectRuleSeverity
script_per_channelchannels.*{must_be_in: [fixtures.scripts]}warning

Live​

How a section, or a nested composite that reads, applies a channel's events to its rows.

effect decides what an event does: patch a row, insert or patch, insert at the top of a feed, remove, replace, or re-read. only_if: matches(params) drops live rows outside the section's filters; when_paged_away: count_new shows "12 new" instead of shifting rows. Every effect acts on rows, whatever shows them: a record shows the first row, a metric takes its value or aggregate from the rows, a list shows them all.

Properties

PropertyTypeRequiredDefaultNote
channelname of a Channelyeschannel to consume
onlist of name of an ESS eventsubset of the channel's events
effectone of: patch_row | insert_or_patch | insert_top | remove_row | replace | refetchyeswhat an event does to the rows
matchnamerow identity field; absent, the read's key, else id
only_ifexprdrop events that fail the condition
coalescedurationbatch bursts into one render
when_paged_awayone of: count_new | ignore | insertcount_newbehaviour when the reader is not on page one
max_rowsintegercap for insert_top feeds

Example

channel: tickets
effect: insert_or_patch
only_if: matches(params)
when_paged_away: count_new
coalesce: 500ms

Checks

CheckSubjectRuleSeverity
channel_refs**.live.channel, **.header.live[], channel.<name> inside expr{must_resolve: {ref: channel}}error

State placement​

Where every piece of UI state lives, and how a profile moves it.

State​

One piece of UI state, its class and where it is stored.

Every piece of state — filters, a selection, an open drawer, a draft, a cached view, a channel buffer, a preference, a token — is declared with a class. Its store is explicit or resolved from the placement profile, so one document deploys as a thin or a fat client. pinned keeps a placement under any profile; sensitive forbids browser storage and the URL.

Properties

PropertyTypeRequiredDefaultNote
typeTypeyesstructural type of the value
classStateClassyeswhat kind of state this is
storeStore{resolve: PlacementProfile.resolution}explicit placement
pinnedbooleanfalseno profile may move it
defaultjsoninitial value
scopeone of: tab | device | session | user | account | string{by_store: {memory: tab, url: tab, session_storage: tab, local_storage: device, server_session: session, server: user}}sharing (string only for an UNMAPPED marker)
sensitivebooleanfalseforbids url, session_storage and local_storage
ttldurationexpiry
clear_onlist of (one of: signout | account_switch | submit | close)[signout, account_switch]events that clear it
fallbackrecord { when: expr, store: Store }second placement used when the condition holds
endpointstringtraceability for store server
notestringauthor remark

Example

layout:
type: {list: {record: {widget: string, x: integer, "y": integer, w: integer, h: integer}}}
class: preference
store: server
scope: user
fallback: {when: not actor.may(dashboards.SaveLayout), store: local_storage}

Checks

CheckSubjectRuleSeverity
state_resolves**.state.*{must: resolve_to_store, refusals: PlacementProfile.resolution.refusals}error

StateClass​

The kind of a state — the key the placement profile uses.

A profile maps each class to a default store; the class also decides what reload and sign-out do.

Properties

PropertyTypeRequiredDefaultNote
(value)one of: page_state | selection | component_state | draft | view_cache | channel_buffer | preference | credentialone of the classes below

Meaning

page_state: "filters, search, paging, sort, mode — what a link should reproduce"
selection: rows or a record the user picked; drives dependent sections and overlays
component_state: "open/closed, expanded rows, active tab, scroll, tickers"
draft: unsaved form input
view_cache: "the last result of a read (rows, total, cursor)"
channel_buffer: "events received but not applied, plus the last-seen cursor"
preference: "per-user choices that outlive a page (theme, nav expanded, columns, layouts, positions)"
credential: tokens held for the actor; always sensitive

Example

type: {list: PartnerId, unique: true}
class: selection

Store​

Where a state lives, with its durability, sharing, and reload/reconnect behaviour.

Six stores from most local to authoritative. memory is lost on unmount; url travels with links; session_storage survives a reload in one tab; local_storage survives restarts on one device; server_session lives on the server for one login session (for example in a key-value store) and is shared by its tabs; server is an ESS entity or view, durable and changed only by commands.

Properties

PropertyTypeRequiredDefaultNote
(value)one of: memory | url | session_storage | local_storage | server_session | serverone of the stores below

Meaning

memory:
durability: "lost on unmount, reload and tab close"
sharing: one component instance in one tab
on_reload: gone; sections re-read and drafts are lost
on_reconnect: kept; the channel's resume decides replay or re-read
url:
durability: "survives reload, back/forward and bookmarks"
sharing: whoever holds the link
on_reload: restored exactly
on_reconnect: unaffected
constraints:
serializable: true
max_bytes_per_page: 2048
allows_sensitive: false
sent_with_each_request_in_thin: true
session_storage:
durability: survives reload; lost when the tab closes; a duplicated tab gets a copy
sharing: one tab
on_reload: restored before the first read
on_reconnect: unaffected
constraints:
allows_sensitive: false
max_bytes_per_app: 1048576
key: [origin, actor.user_id, actor.account_id]
local_storage:
durability: survives reload and browser restart until the profile is cleared or clear_on fires
sharing: "one device (browser profile), all its tabs, synced by the storage event"
on_reload: restored before first paint
on_reconnect: unaffected
constraints: {allows_sensitive: false, key: [origin, actor.user_id, actor.account_id]}
server_session:
durability: "survives reload, tab close and reconnect; lost on sign-out, account switch or session expiry"
sharing: every tab of the same login session; not other devices or users
on_reload: the server restores it and sends the rendered view model
on_reconnect: restored; buffered events let the server replay what the client missed
constraints: {bounded_per_session: true, authoritative: false}
server:
durability: durable
sharing: by scope — user (a preference entity) or account (a shared record); other sessions see changes through channels
on_reload: re-read
on_reconnect: re-read or replayed by the channel's resume
constraints: {writes_via: ess_command}

Matrix

- store: memory
survives_reload: false
survives_tab_close: false
survives_signout: false
across_tabs: false
across_devices: false
across_users: false
- store: url
survives_reload: true
survives_tab_close: via_link
survives_signout: via_link
across_tabs: via_link
across_devices: via_link
across_users: via_link
- store: session_storage
survives_reload: true
survives_tab_close: false
survives_signout: false
across_tabs: false
across_devices: false
across_users: false
- store: local_storage
survives_reload: true
survives_tab_close: true
survives_signout: unless_cleared
across_tabs: true
across_devices: false
across_users: false
- store: server_session
survives_reload: true
survives_tab_close: true
survives_signout: false
across_tabs: true
across_devices: false
across_users: false
- store: server
survives_reload: true
survives_tab_close: true
survives_signout: true
across_tabs: true
across_devices: true
across_users: when_scope_account

Example

type: {enum: [light, dark]}
class: preference
store: local_storage
pinned: true
default: light

PlacementProfile​

A default store per state class — thin, fat or hybrid.

The profile is how one document deploys as a thin or a fat client. thin keeps everything but the URL on the server: a presenter executes reads, holds drafts and applies channel events, and the client paints view models. fat keeps UI state in the browser and talks to ESS views, commands and channels directly. hybrid lets each page or section pick.

Properties

PropertyTypeRequiredDefaultNote
ideastringone line on the deployment shape
readsstringwho executes reads
channelsstringwho applies channel events
defaultsmap of StateClass → Storedefault store per class

Profiles

thin:
idea: the server holds all state except what the URL carries; the client renders view models and forwards input
reads: a server-side presenter
channels: "the server subscribes, applies events to the session's view cache and pushes rendered patches"
defaults:
page_state: url
selection: server_session
component_state: server_session
draft: server_session
view_cache: server_session
channel_buffer: server_session
preference: server
credential: server_session
fat:
idea: the client holds and computes UI state; the server is only the ESS model
reads: "the client, against ESS views"
channels: the client subscribes and applies events itself
defaults:
page_state: url
selection: memory
component_state: memory
draft: session_storage
view_cache: memory
channel_buffer: memory
preference: local_storage
credential: memory
hybrid:
idea: each page or section picks thin or fat with profile
reads: per section
channels: "server-applied when every fed section is thin, else delivered to the client"
defaults: {}
unresolved: refuse

Resolution

order:
- {step: explicit_store, source: state.store}
- {step: pinned, source: state.store, when: {state.pinned: true}, effect: profile_ignored}
- {step: section_profile, source: "profiles[section.profile].defaults[state.class]"}
- {step: page_profile, source: "profiles[page.profile].defaults[state.class]"}
- {step: document_defaults, source: "document.placement_defaults[state.class]"}
- step: profile_defaults
source: "profiles[document.placement_profile].defaults[state.class]"
refusals:
- when: {state.sensitive: true, store: [url, session_storage, local_storage]}
result: refuse
- {when: {state.class: credential, store_not_in: [memory, server_session]}, result: refuse}
- {when: {store: url, value_serializable: false}, result: refuse}
- {when: {store: url, value_bytes_over: 2048}, result: refuse}
- {when: {renderer.thin_only: true, resolved_by_profile: fat}, result: refuse}

Reload and reconnect

reload:
memory: the section re-enters loading; a renderer warns on unload while a memory draft is dirty
url: page state restored; sections re-read with the same params
session_storage: restored before the first read; drafts refill their forms
local_storage: "restored before first paint; a cached view paints stale-marked, then refreshes"
server_session: the server renders the page from session state
server: re-read
reconnect:
memory: "kept; resume refetch re-reads, from_last_seen replays from the buffer cursor"
server_session: "the server replays buffered events or re-reads, then pushes one patch"
server: unaffected

Example

placement_profile: hybrid
placement_defaults:
page_state: url
selection: memory
draft: session_storage
preference: server
credential: server_session
pages:
overview: {kind: dashboard_page, profile: fat}
invoices.list: {kind: report_page, profile: thin}

Style tokens and themes​

What the roles a document names look like: design tokens, themes, the theme preference and tone maps.

Tokens​

Design tokens — the values a renderer draws a document's roles with, in five groups.

A document says what a node means, never how it looks: a badge has a tone, a text a style, a button an emphasis. Tokens say what those roles look like. Each group is a map of name to value. The built-in table below is merged under a document's tokens: group by group and name by name, so a document without tokens: looks as it always did and one that names three colors changes three. A node never names a token: a tone reaches tone, a text style reaches type, a button's emphasis reaches color through the table on button, and space and radius are read by the renderer's own stylesheet. Inside tone, a value is the bare name of a color. A type entry replaces the entry of its name whole, and a field it leaves out falls back to body, then to the built-in body. The values follow the grammar below; quote a color, since an unquoted # starts a YAML comment. A terminal draws no color: it draws a tone as the emphasis below, which adds no text and never uses reversed video, the mark of the cursor, and it reads no space, type or radius.

Properties

PropertyTypeRequiredDefaultNote
colormap of name → stringcolor literals by name; see the grammar
spacemap of name → stringlengths by name, read by the renderer's stylesheet
radiusmap of name → stringlengths by name, read by the renderer's stylesheet
typemap of (one of: body | caption | heading | mono) → record { family: optional string, size: optional string, weight: optional integer }family, size and weight per text style
tonemap of (one of: neutral | info | success | warning | danger) → record { text: name, fill: name }the color names a tone's text and fill are drawn with

Grammar

color: "`#rgb`, `#rrggbb` or `#rrggbbaa` in hexadecimal digits, or `rgb(r g b / a)` with r, g and b whole numbers from 0 to 255 and a a decimal from 0 to 1; no `hsl()`, no named colors"
length: "`0`, or a decimal followed by `px`, `rem` or `em`"
weight: a whole number from 100 to 900 in steps of 100

Builtins

color:
bg: "#f7f7f8"
surface: "#ffffff"
text: "#1c1d21"
muted: "#676a73"
line: "#dcdde1"
accent: "#2f5bd3"
info: "#2f7fd3"
success: "#2e8a4f"
warning: "#b7791f"
danger: "#c0392b"
danger_fill: "#f6d5d1"
success_fill: "#d7f0df"
warning_fill: "#f7e7c6"
info_fill: "#d9e4fb"
focus: "#eef2fd"
on_accent: "#fff"
backdrop: rgb(0 0 0 / 0.3)
space: {xs: 0.25rem, sm: 0.5rem, md: 1rem, lg: 1.5rem}
radius: {sm: 4px, md: 6px, lg: 8px, pill: 999px}
type:
body: {family: "system-ui, sans-serif"}
caption: {size: 0.8rem}
heading: {size: 1.05rem, weight: 600}
mono: {family: "ui-monospace, monospace"}
tone:
neutral: {text: text, fill: line}
info: {text: info, fill: line}
success: {text: success, fill: line}
warning: {text: warning, fill: line}
danger: {text: danger, fill: danger_fill}

Terminal emphasis

neutral: bold
info: italic
success: bold
warning: underlined
danger: bold and underlined

Example

color: {surface: "#1f2024", text: "#ecedf0", line: "#34363c", danger_fill: "#4a2420"}

Checks

CheckSubjectRuleSeverity
types_structural**.type{must_match: type_rule, forbidden: string_holding_a_type_expression}error
token_valuestokens.color.*, tokens.space.*, tokens.radius.*, tokens.type.*{must_match: Tokens.grammar}error
token_namestokens.type.*, tokens.tone.*{must_be_in: [text.style, Primitive.tone]}error
token_refstokens.tone.*{must_resolve_in: [Tokens.builtins.color, tokens.color]}error

Theme​

A named look — the tokens it overrides.

A theme holds only the values that differ, in the groups and names of tokens:, and is the built-in table, then tokens:, then its own overrides; a theme without overrides is the base values. Every theme therefore defines every token. A theme may override any group: one that overrides space alone is a denser look. One theme is shown at a time and themes do not combine, so a look both dark and dense is a theme of its own. An override may name only a token the built-in table or tokens: declares.

Properties

PropertyTypeRequiredDefaultNote
(value)Tokensthe values that differ from the base, by group and name

Example

space: {xs: 0.125rem, sm: 0.25rem, md: 0.5rem, lg: 0.75rem}

Checks

CheckSubjectRuleSeverity
theme_tokensthemes.*{must: [override_a_declared_token, match_Tokens.grammar, tone_names_a_color_of_the_theme]}error

ThemeChoice​

Which theme is shown, and the shell state the user chooses it with.

default names the theme shown. chosen_by names shell state, shell.<name>: a state of class preference whose type is an enum, each variant naming a theme. A renderer shows the theme that state holds, and default where the shell declares no such state (a sign-in shell) or it holds no value. The user changes it like any state, with an action that sets it; where it is stored, how long it lives and who shares it are the state's own store, scope and pinned. To keep one choice across shells, each declares a state of the same name with scope: user in local_storage. A theme no variant names is not offered. A terminal never reads the choice.

Properties

PropertyTypeRequiredDefaultNote
defaultnameyesthe theme shown where no state chooses one
chosen_byexprshell.<name>: a preference state whose enum variants name themes

Example

default: light
chosen_by: shell.theme

Checks

CheckSubjectRuleSeverity
theme_choicetheme{must: [default_names_a_theme, themes_declared, chosen_by_names_a_shell_preference_enum_whose_variants_and_default_name_themes]}error

ToneMap​

A value-to-tone map, declared once and named by every tone_by that colors by it.

A status written by hand at every badge can be colored two ways in two places. A tone map names it once: tone_by: {value: row.stage, tones: deal_stage} on a badge or an icon is resolved by the loader to that map. A widget parameter that carries a map name is typed {ref: tone_map}. The chain is value to tone (tone_maps), tone to text and fill (Tokens.tone), and those to color values (Tokens.color, per theme).

Properties

PropertyTypeRequiredDefaultNote
(value)map of string → (one of: neutral | info | success | warning | danger)tone per value

Example

lead: neutral
qualified: info
proposal: warning
won: success
lost: danger

Checks

CheckSubjectRuleSeverity
tone_map_unusedtone_maps.*{must: be_named_by_a_tone_by}warning

Checks​

What a validator checks in a document, and the construct each check applies to. Every finding names the failing node by its NodePath.

CheckApplies toSubjectRuleSeverity
names_uniqueevery nodeevery node{must: unique_name_among_siblings}error
nav_resolvesNavigationnavigation.sections[].pages[], navigation.hidden[], navigation.home{must_resolve: {ref: page}}error
nav_uniqueevery nodenavigation.sections[].pages[]{must: list_each_page_once}error
page_reachablePagepages.*{must_be_in: ["navigation.sections[].pages[]", "navigation.hidden[]", "navigation.sections[].pages.page"]}error
opens_resolvesAction**.opens{must_resolve_in: [page.overlays, page.kind.overlays, shell.overlays]}error
same_as_resolvesoverlay**.same_as{must_resolve: {ref: overlay}}error
page_refsAction, link, Page, Guard**.navigate.to, **.to.to, pages.*.switch_to[], shells.*.guards[].then.redirect{must_resolve: {ref: page}}error
shell_refsPagepages.*.shell{must_resolve: {ref: shell}}error
page_outletShellshells.* a page renders in{must: have_a_region_of_kind_page_outlet}error
channel_refsLive, header**.live.channel, **.header.live[], channel.<name> inside expr{must_resolve: {ref: channel}}error
section_refsheader, Section, PageLayout**.header.total, **.header.filters, **.depends_on, pages.*.layout.columns[].sections[], pages.*.layout.areas.place.*[]{must_resolve_in: [page.sections]}error
layout_completePageLayoutpages.*.layout{must: place_every_section_once}warning
widget_expandsSection, overlay, WidgetInstance, Composite**.component naming a widget{must: [resolve_widget, required_args_present, args_match_param_types, no_recursion], then: check_expanded_body}error
primitive_propsPrimitive**.primitive{must: props_of_kind_only}error
unbound_placeholderReads**.reads.placeholder{must: bind_to_view}warning
fixture_per_viewReads**.reads.view{must_be_in: [fixtures.views, fixtures.derived]}warning
script_per_channelChannelchannels.*{must_be_in: [fixtures.scripts]}warning
state_resolvesState**.state.*{must: resolve_to_store, refusals: PlacementProfile.resolution.refusals}error
types_structuralType, Tokens**.type{must_match: type_rule, forbidden: string_holding_a_type_expression}error
unmapped_reportedevery node**{matches: unmapped_marker.pattern, action: report}warning
token_valuesTokenstokens.color.*, tokens.space.*, tokens.radius.*, tokens.type.*{must_match: Tokens.grammar}error
token_namesTokenstokens.type.*, tokens.tone.*{must_be_in: [text.style, Primitive.tone]}error
token_refsTokenstokens.tone.*{must_resolve_in: [Tokens.builtins.color, tokens.color]}error
theme_tokensThemethemes.*{must: [override_a_declared_token, match_Tokens.grammar, tone_names_a_color_of_the_theme]}error
theme_choiceThemeChoicetheme{must: [default_names_a_theme, themes_declared, chosen_by_names_a_shell_preference_enum_whose_variants_and_default_name_themes]}error
tone_map_refsbadge, icon**.tone_by.tones{must_resolve: {ref: tone_map}}error
tone_map_unusedToneMaptone_maps.*{must: be_named_by_a_tone_by}warning
filter_exprReadsreads.filter and dynamic menu filter{must: parse_as_boolean_predicate}error
filter_rootsReadsreads.filter and dynamic menu filter{must: read_only_row_params_state_shell_args_without_calls}error
filter_pathsReadsreads.filter and dynamic menu filter{must: resolve_declared_paths}error
filter_placeReadsreads.filter{must: belong_to_listing_or_choice}error
filter_pagingReadsreads.filter{must: use_client_or_none_paging}error
filter_over_paramReadsreads.filter{must: prefer_existing_server_parameter}warning
filter_exportActionexport.params{must: not_claim_to_copy_client_filter}warning
account_menu_keysevery nodeshells.*.regions.*.props.actions[] of an account_menu{must_be_in: [name, label, opens, does, sets, navigate]}error

Retrofits and lowering​

How a document records what a retrofit could not determine, where it came from, and what each construct becomes in the ESS model.

Unmapped marker​

How a retrofit marks a value it could not determine.

example: {endpoint: "UNMAPPED: the submit handler is not wired to an API call"}
pattern: "^UNMAPPED: .+$"
accepted_by:
types: [string, expr, name, duration, "{enum: …}", "{ref: …}"]
note: "a field of any other type (boolean, a number, a record, a list or a map) refuses the marker"
at_reads: a marker written as a `reads` or `loads` is no read at all; the node records it in its `unmapped` list
validator: {action: report, severity: warning}
renderer: {action: treat_as_absent}

Traceability​

KeyCarried
endpointon Reads, Action, forms and overlays — the HTTP call of a retrofitted app
sourceon Page, Shell and Channel — files the construct was read from
transport_todayon Channel — topic or URL used today
roles_todayon NavEntry — role list used today
unmappedevery value matching unmapped_marker.pattern, and every unmapped list entry

Lowering​

ConstructLowers to
reads (view, params)ESS view with params, filter and paging
reads placeholdernothing until bound; reported as a warning
doesESS command; the form is its input, refusals are its typed errors
visibility and shown actionsactor grants
live and channel eventsESS events published by the model's components
channel delivery and resumecomponent reach and binding delivery guarantees; streaming delivery is new
state in store serveran ESS entity (a preference entity for class preference) and its view
state in store server_sessiona presentation domain in ESS (an entity per session) or host session storage
state in memory, url, session_storage, local_storagenothing; renderer-owned
shell, pages, layouts, sections, composites, widgets, primitives, states, degradesUiIr (new)

Quick reference​

Every construct with its one-line summary, chapter by chapter.

ConstructChapterSummary
DocumentThe documentThe root of a ess-ui/1 file describing one application.
TypeThe documentA named value type declared once in the document's types map.
NodePathThe documentThe stable address of any node — shell region, page, section, overlay, composite, widget instance or primitive.
ShellShell and navigationAn application frame with regions, preloads, guards and shell-wide state.
RegionShell and navigationOne named area of a shell.
PreloadShell and navigationViews a shell reads before or alongside the first page.
GuardShell and navigationA condition checked before a page opens, with a redirect, refusal or state change.
NavigationShell and navigationThe menu — sections of pages, the home page, hidden routes and search.
NavSectionShell and navigationOne group of the menu.
DynamicNavEntriesShell and navigationMenu entries generated from the rows of a view.
NavEntryShell and navigationHow a page appears in the menu, declared on the page as nav.
PagePages and sectionsOne route — its state, layout, header, sections and overlays.
PageLayoutPages and sectionsA renderer-neutral arrangement of a page's sections.
PageKindPages and sectionsA page template that fills the same sections, state, header and overlays.
SectionPages and sectionsA region of a page with one read and its own loading lifecycle.
SectionStatesPages and sectionsHow a section renders each lifecycle state.
collectionComposites, widgets and primitivesRows of a view with columns, sorting, paging, selection and row actions.
recordComposites, widgets and primitivesOne row shown as labelled, read-only fields.
formComposites, widgets and primitivesInputs bound to one ESS command's input.
choiceComposites, widgets and primitivesPick one or many values from a view or a fixed list.
filter_barComposites, widgets and primitivesSearch, choices, inputs and a time window bound to page state.
headerComposites, widgets and primitivesPage title, count, primary actions, view switch, filters and live status.
overlayComposites, widgets and primitivesA drawer, dialog, fullscreen pane or popover shown on demand.
confirmComposites, widgets and primitivesA yes/no step before a command, listing references and consequences.
metricComposites, widgets and primitivesOne number from a view or channel, with a window and a stale mark.
chartComposites, widgets and primitivesA series view drawn as a chart.
boardComposites, widgets and primitivesA user-arranged grid of widgets, persisted.
graph_editorComposites, widgets and primitivesNodes and edges of a model, editable on a canvas.
rich_textComposites, widgets and primitivesText with expression completion or markup.
referencesComposites, widgets and primitivesThe "used by" list of a record.
WidgetComposites, widgets and primitivesAn app-defined composite with typed parameters, usable wherever a composite kind is.
WidgetInstanceComposites, widgets and primitivesA use of a widget, with arguments.
NodeComposites, widgets and primitivesAnything that can appear inside a section, composite or widget — a composite, a widget instance or a primitive.
FieldComposites, widgets and primitivesOne input of a form or one column of a collection.
TabComposites, widgets and primitivesOne tab of a form, record or overlay.
FormGroupComposites, widgets and primitivesA group of settings fields saved on its own.
CompositeComposites, widgets and primitivesThe composite union — one type whose member is chosen by component.
DegradesComposites, widgets and primitivesFallbacks for renderers that lack a capability.
PrimitiveComposites, widgets and primitivesThe closed set of nine renderer-neutral leaves.
textComposites, widgets and primitivesA run of text, literal or from a field, with a style and a value format.
badgeComposites, widgets and primitivesA short value in a toned pill.
iconComposites, widgets and primitivesA semantic icon with an accessible label.
buttonComposites, widgets and primitivesA button that runs one action.
linkComposites, widgets and primitivesText that navigates to a page or an external address.
inputComposites, widgets and primitivesA single free input bound to state or a draft field.
toggleComposites, widgets and primitivesAn on/off switch bound to state, or running an action.
imageComposites, widgets and primitivesAn image with required alternative text.
dividerComposites, widgets and primitivesA visual separator.
ReadsReads, actions and fixturesThe ESS view a section or composite reads — or, while designing, a named placeholder backed by a fixture.
ActionReads, actions and fixturesOne user-triggered effect: run a command (does), open an overlay, navigate, export, upload, copy or set UI state.
FixtureIndexReads, actions and fixturesSample data per view and event scripts per channel, so renderers run without a backend.
ChannelLive dataA live source of ESS events or a live view, with delivery and resume semantics.
LiveLive dataHow a section, or a nested composite that reads, applies a channel's events to its rows.
StateState placementOne piece of UI state, its class and where it is stored.
StateClassState placementThe kind of a state — the key the placement profile uses.
StoreState placementWhere a state lives, with its durability, sharing, and reload/reconnect behaviour.
PlacementProfileState placementA default store per state class — thin, fat or hybrid.
TokensStyle tokens and themesDesign tokens — the values a renderer draws a document's roles with, in five groups.
ThemeStyle tokens and themesA named look — the tokens it overrides.
ThemeChoiceStyle tokens and themesWhich theme is shown, and the shell state the user chooses it with.
ToneMapStyle tokens and themesA value-to-tone map, declared once and named by every tone_by that colors by it.