Presentation and localization
Presentation attributes preserve the underlying typed value and wire encoding. They tell Studio and clients how to validate, edit, localize, summarize, or chart the value. How a value is arranged and styled on a device belongs to the package's Display Template; the attributes that only change that are compiled into it.
Source documentation
Extraction syntax is only part of the contract. Follow the schema documentation invariants for every package, function, type, parameter, field, member, variant, and List half. A public function must additionally follow the developer function-comment invariants so its discovery description is complete, written in active voice, and explicit about who performs every action.
TypeScript uses leading /// package comments and JSDoc on declarations:
/// Weather monitor
/// Helps people review recent conditions measured at one weather station.
/**
* Weather measurements
* Lists recent readings from the selected weather station.
* ----
* Weather measurement
* Records one observation made by the selected weather station.
*/
samples: Sample[];
Rust uses inner rustdoc for the package and outer rustdoc for declarations:
//! Weather monitor
//! Helps people review recent conditions measured at one weather station.
/// Weather measurements
/// Lists recent readings from the selected weather station.
/// ----
/// Weather measurement
/// Records one observation made by the selected weather station.
pub samples: Vec<Sample>,
The first line is the display name. Remaining ordinary text is the description. For a List, at least four hyphens separate container documentation above from item documentation below. Put the divider above custom document properties that belong to the item. Plain code comments are ignored by document extraction.
Codebots always author at least one natural label line and one complete
description line for every authored node. Put custom [PropertyName] blocks
after that ordinary text and before the first TypeScript @Libertas... tag.
Use [DefaultText] when a useful initial String value, Endpoint server name,
or function task name differs from its ordinary localized fallback. Use
[NullText] and [SetText] when a Nullable choice needs its
own branch labels. The full
standard and custom property contract, including reserved names, List
ownership, localization and initialization, is in the
schema documentation invariants.
AI tool-call descriptions
The Hub appends the tool-call value-representation hints to AI tool parameter and response descriptions, from the effective schema attributes, including inherited attributes, and built-in date/time types, and then the field's declared answer labels. AI Import uses the same list. Authored descriptions remain intact. App authors and CodeBots do not repeat this guidance in source comments. Keep comments focused on domain meaning, scope, consequences, and any App-specific limits. This does not remove the requirement for a natural label and complete description on every authored node.
These hints describe the tool JSON representation. They do not change App
storage or protocol encoding, supply missing values or dynamic choices, or
execute copying or calculations. Display Template presentation never reaches
AI descriptions: no template rule, format pattern, label, or title
changes what a tool or AI Import description says.
Which requests become tools, and how their parameters and values are formed,
including the null rule, is owned by AI tool calls.
Display Template
Every App package has exactly one Display Template, version 0.1: the presentation layer that decides how the values its schema describes are drawn on a given device. The schema owns what the App accepts, sends, computes, protects, or means; the template owns only how the same value is arranged and styled. A template can make a correct App look worse, never make it incorrect. This section owns the template's file, compiled form, cascade, conditions, catalog, and behavior hints. The Libertas Schema Path owns its selector grammar and publication owns its identity and delivery.
-
**Mandatory.** Every package has a template, including a TypesOnly package.
A package with no presentation attributes and no
display.jsoncompiles to the empty template. The empty template is valid, has a digest and a documents-manifest entry, and renders exactly the platform default. - Sole runtime owner of presentation. Clients read promotion and
flattening, titles, folding, inline choices, SuppressLabel,
UIIncludeDescription, the
Formatdisplay pattern, inline media size, and chart appearance only from the template. Promotion, flattening, labels, and titles are authored only as template rules. The other source attributes remain the authoring convenience: Studio validates them, compiles every occurrence into a template rule at the lowest specificity, and removes them from the final schema. Each attribute's section below keeps its authoring text and meaning; the template is where that meaning reaches a client. - **Not part of the schema.** The template is absent from the schema JSON, its revision digest, and the App version. A template change is a document-only revision: it never rebuilds the App or allocates an App version.
-
**Opt-in arrangement.** `rows`, one row per child, is the default layout.
Every other layout applies only where a rule asks for it. A
tableor table-arrangedsummarywhose columns do not fit, and amaster-detailbelow the pane threshold, fall back under the width-based layout. -
**Presentation only, a layer apart from business logic.** Template rules are
not on the same level as the App's business logic and never reach it. No
rule, condition, or device state changes what a field stores, its validation
rules,
CopyFromorCopyTo, calculation requests, encoding, or the AI Import and AI Search contracts; none makes a field Hidden or read-only or changes a user's submitted payload. No rule reaches an AI description. A rule that withholds a row, such aswhen_single_choice, leaves the value and every rule that reads it exactly as if the row were shown. The pane arrangement may load or refresh a leading page through a declared pure navigation Request. A condition may change style, layout, or fold state only. Hidden, ReadOnly, and every other schema rule apply before and regardless of the template. - **Forward tolerant.** A client that meets a token value, layout, hint, or member it does not know uses the platform default for that item and applies the rest of the rule, so a newer template never breaks an older client.
Template boundary
An attribute belongs in the schema when changing it would change what the App accepts, sends, computes, protects, or means. It belongs in the template when changing it would only change how the same value is drawn on one device. Apply three tests in order:
- **Does the server or the wire depend on it?** Encoding, defaults, nullability, ranges, uniqueness, validation, copies, calculations, action sets, access, and identity are schema.
- **Does it state what the value is or means?** Money and currency, units, dates, formatted text, labels, help text, and which fields a chart encodes are schema; meaning is content, localized with the strings.
- **Would a different choice be right on another screen size?** Promotion, flattening, folding, inline choices versus a chooser, an inline description, density, arrangement, and chart appearance are template, with a source attribute, where one exists, as the default rule.
| Schema only | Template (a source attribute, where one exists, is the default rule) |
|---|---|
| Hidden, ReadOnly, Fixed, Default, Nullable, Size, Number ranges, Unique, Unordered | promote with use_parent_name, and flatten with name_row |
| ValidationRules, RangeMin, RangeMax, EnumSource, ConstrainedBy, ActionControl | Foldable and initially_open |
| CopyFrom, CopyTo, CalculationRequest, Link, LinkOnly, RequestConfirmation | InlineChoices |
| NextRequest, NextResponse, PrevRequest, WorkflowRequest, DefaultRequest, ExcludeUI | SuppressLabel, UIIncludeDescription |
| AccessPrivilege, CacheIdentity | Format display pattern and precision, never the value or currency |
| Money, PhysicalUnit, DateOnly, FormattedText, display names, descriptions | Chart scale type, guide position, palette, and preview chart sizing |
| MimeTypes | Inline media size |
| Chart channel bindings: which field is x, y, color, or tooltip | Template-only concepts: layout, density, emphasis, label, title, button_labels, action_controls, when_single_choice, device conditions |
Unordered and ExcludeUI look presentational, but the wire and the protocol
depend on them, so they stay in the schema. Format looks semantic, but
Money already carries the currency and the precision that matter to the
value, so the display pattern moves. label and title look like
schema labels, but the node's display name stays its meaning in the schema;
they change only what a person sees, so they are template. Studio enforces the table: a rule
member that is not in the catalog, including every
schema-only attribute, is rejected.
Display Template file
The source of truth is display.json at the project root, beside
Cargo.toml or package.json, under version control. There is one file per
project; device variance is expressed inside it as conditions, never as a
second template for a locale or platform. A missing file is valid and compiles
to a template holding only the rules derived from source attributes. Studio's
editor writes this file in place through the same validated writer as a hand
edit, never a second copy. The Hub stores only the compiled, published copy
and never accepts a template Studio did not compile from source. For the
conversational loop of hand-editing display.json and watching it live in
Studio, see the Studio MCP live-preview guide.
{
"DisplayVersion": 1,
"rules": [
{ "select": "^<Hub.topics>[]", "layout": "rows", "style": { "emphasis": "primary" } },
{ "select": "^<Hub>",
"right_pane": ["EditWages", "EditInterest"],
"detail": ["^<Hub.told>", "^<Hub.topics>[]"], "refresh": "OpenHub" },
{ "select": "^<Review.return_lines>", "layout": "table",
"columns": [{ "value": "label", "header": "RETURN_LINE" }, "amount"] },
{ "select": "^<IncomeDocumentItem>", "layout": "card", "when": { "form": "phone" } },
{ "select": "^<Answer>", "inline": true },
{ "select": "^<Business>.expenses", "fold": { "initially_open": false } },
{ "select": "$.estimate:pending", "style": { "emphasis": "muted" } },
{ "select": "^<TopicRow>:condition([status] is NeedsAttention)", "style": { "color": "error", "icon": "warning" } },
{ "select": "^<Employee>", "layout": "split", "direction": "row",
"regions": [
{ "fields": ["photo"], "ratio": 1, "style": { "align": "center" } },
{ "fields": ["name", "date_of_birth", "employee_id"], "ratio": 2, "layout": "form-grid", "columns": 2 }
] }
]
}
The example is schematic: its names stand for an App's own variants, types,
and fields. DisplayVersion is the JSON format version, like a document's
DocumentVersion; version 0.1 of the template is DisplayVersion 1, and the
number never chooses a winner in convergence. rules is an ordered array.
A rule is one JSON object with these members:
| Member | Required | Meaning |
|---|---|---|
select |
Yes | One selector, optionally ending in a pseudo-state or a :condition(...) data condition. |
when |
No | A presentation condition on the pane's size class and orientation, the device's form, pointer, or theme, or edit/view mode. Without it the rule applies everywhere. |
layout |
No | One catalog layout, with the layout-specific members its row names. |
right_pane, refresh, detail |
No | Leading Response relationships, its pure loader, and optional linked-node highlighting under master-detail panes. |
style |
No | An object of style tokens. |
label |
No | Replaces the node's displayed name and keeps its value, on a scalar field too (labels and titles): { "field": "path" } takes the text from a String or FormattedText field below the node, or below the struct that holds a scalar field, and a string names a fixed StringResources key. |
title |
No | Replaces the node's whole row, label and value, with its text, in the same two forms; never on a scalar, whose row is its value (labels and titles). |
button_labels |
No | Localized captions for existing protocol-page controls, by slot: go_back, next, close, actions (button labels). |
action_controls |
No | Labels, placement, and applicable style for App-declared response controls and built-in Actions (named action controls). |
flatten |
No | Draws a struct's, List's, or Union's children at that node's own indentation (flattening): true; false, which undoes a flattening that a lower-ranked rule set; or { "name_row": false } on a struct to remove its name-only row as well. |
inline, fold, promote, when_single_choice, suppress_label, include_description, format, scale, guide, palette, media, chart |
No | Behavior hints. |
lock |
No | true or, on a Union, "variant": the person cannot change the value, while every other writer still can (presentation locks). |
switch |
No | A conditional switch whose chosen case supplies the presentation members. A rule with switch has no other presentation member. |
A rule has exactly one select and at least one presentation member, directly
or through its switch. Members it does not name keep their inherited or
default values. Studio rejects any other member.
Compiled template
Studio compiles one template document per package, in this order:
- Defaults: one rule per occurrence of a source presentation attribute,
at the lowest specificity. A field attribute becomes a rule selecting that
field:
^<Type>.fieldfor a field of a named type, the strict path of each occurrence for a field of an inline type, or^<function>.parameterfor a function parameter. A type-declaration attribute becomes a relaxed-type rule,^<Type>. Type-derived defaults precede field-derived ones, so a field's own attribute wins where both set the same hint. - Rules: the rules of
display.json, validated, in file order.
{ "DisplayVersion": 1, "defaults": [], "rules": [] }
That is the empty template. Each derived rule reproduces its source
attribute's meaning exactly, including direct declaration lookup: a Foldable
on a field yields a field fold rule and one on a type yields a type fold
rule, and neither is inherited through effective-attribute resolution. The compiled template is derived and never edited by hand. Its
identity is the SHA-256 digest of its canonical JSON, computed with the same
canonicalization as the schema revision digest.
compile_schema with includeArtifacts returns it beside each package's
default document.
Display Template cascade
A rule applies to a node when its selector matches the node, its when holds,
its data condition or pseudo-state holds, and, for a switch, a case or else
is chosen. Every presentation member resolves independently: the
highest-ranked applicable rule that sets that member wins, and a layout
resolves together with its layout-specific members. Each button_labels slot
resolves independently; setting one slot leaves the others' cascade intact.
Ranks, lowest first:
- the platform default;
- a rule of another package's compiled template, on a node that package declares; among these rules, that template's own ranks and order decide;
- a rule derived from a source attribute (
defaults); - a relaxed-type rule: a selector whose first form is
^<Type>, or any selector using...; - the same with a
whencondition; - a strict-path rule:
^<Variant>,^<Variant.field...>,^<function...>, or$.field, with or without[]; - the same with a
whencondition.
Among rules of equal rank, the later rule in compiled order wins. Data
conditions, pseudo-states, and switches gate a rule; they never change its
rank. The style tokens density and emphasis inherit down the value tree
unless a descendant resolves its own; no other member inherits, and a layout
never does.
A screen uses the full template of the package that owns its displayed root:
the Endpoint protocol Union's package for a protocol screen, and the function's
package for a configuration editor or stored data. Nodes declared by another
package additionally receive that package's compiled template, its
display.json rules included, at rank 2, below every rule of the screen's own
package. A dependency's rule applies where its selector, resolved against that
package's schema, matches a node that package declares. A type therefore keeps
its authored presentation wherever it is shown, the using package can still
change it, and a TypesOnly package may ship a display.json for its types.
Every package template a screen needs arrives with its schema.
Display Template conditions
A when object accepts only these keys, each with a closed value set. A value
may be a list, meaning any of the listed values ("form": ["tablet", "desktop"]); several keys in one when must all hold.
| Key | Values | Meaning |
|---|---|---|
width |
compact (under 600 dp), medium (600 dp up to 840 dp), expanded (840 dp up to 1200 dp), large (1200 dp up to 1600 dp), extra-large (1600 dp and up) |
How much fits across the pane now: the platform's window size class. |
height |
compact (under 600 dp), medium (600 dp up to 840 dp), expanded (840 dp and up) |
How much fits down the pane now. |
orientation |
portrait, landscape |
The pane's orientation. |
pointer |
coarse, fine |
Touch, or a mouse or trackpad. |
theme |
light, dark |
The active color theme. |
form |
phone, tablet, desktop, tv |
What the device is, not the window: phone when its smallest width in portrait is under 600 dp; tablet at 600 dp and up with a coarse pointer; desktop with a fine pointer and a resizable window; tv for a lean-back display. |
mode |
edit, view |
Whether the person is entering data in the node. See Editing and viewing. |
Conditions use size classes and a form factor, never pixels. A 2160 px phone
in landscape is about 800 dp wide (width: medium, height: compact,
form: phone); a 32-inch monitor is width: extra-large, height: expanded,
form: desktop. A rule for 840 dp and wider lists
["expanded", "large", "extra-large"]. A rule that widens a layout reads
form, or width together with height, never width alone. Pane count,
split lines, form-grid columns, and table fit need no condition: they follow
the width-based layout rules at every size.
Conditions have container semantics: width, height, and orientation are
evaluated against the nearest enclosing pane, not the window, while form,
pointer, and theme describe the device. A list rendered
in the left pane of a master-detail layout on a tablet sees that pane's width
class.
Editing and viewing
The client decides mode for each node from what it already renders; the App
supplies no flag and the protocol carries none. A node is in edit mode when it
lies in a Request the person is composing (a new Request, an EditRequest reply,
a WorkflowRequest editor, or a master-detail editing pane) and it, or a
descendant, holds a field the person can change: not ReadOnly, Hidden, or
Fixed. Every other node is in view mode: Responses, SubscriptionData, a
submitted Request shown read-only, and a Request subtree with nothing the
person can change. Composing a Request for the first time and reopening it to
review or change it are both edit; a rule that must tell them apart reads the
interaction reference context
with :condition.
The two modes have different obligations. In view the person browses, so
grouping that hides content until chosen, such as tabs, costs nothing, and
Studio warns about dense view screens. In edit the person must find and
answer every required field and see every error, so an editor is a scrolling
form that keeps its fields reachable in entry order: no density warning
applies, and titled sections under rows may group the fields under
headings. An editable item is never hidden: a Table that holds a field the
person can change is never laid out as tabs while edit may apply, and
Studio rejects such a rule. A Union's branch strip is not a grouping: choosing
the branch is the answer, and the chosen branch's fields are shown. A rule
without mode applies in both modes.
Unanswered fields. A field without a Default starts
undefined (unanswered), and a non-Nullable field never holds
null. When an editor opens, an undefined field shows only its neutral empty
state: its placeholder, such as an unselected choice or "Choose date…", never
an error look; an undefined Nullable's text uses the secondary text color.
When Next or Submit is blocked, the client marks the first field that blocks
it: below that field, an error icon and the missing-answer or validation
message, in the platform's error color role and announced politely to
assistive technology. It opens any fold or summary page holding the field and
scrolls it into view; it does not move focus. The mark clears once that field
passes, and it does not move to another field until the next blocked Next or
Submit. An undefined field is reported as a missing answer, never as a null
value.
A selector may end in one runtime state or one data condition:
| Condition | Holds when | May change |
|---|---|---|
:pending |
The node owns an operation still awaiting its reply and shows the existing pending indicator: a submit, a link, or a calculation. | Style only |
:error |
The node currently shows validation or submission feedback. | Style only |
:selected |
The node is the selected member: the member shown in a master-detail detail pane, or the current tabs section. |
Style only |
:condition(expression) |
The expression, evaluated on the displayed value, is true. | Style, layout, or fold state |
A data condition uses the ValidationRules expression language
rooted at the selected node: [] is the node, [status] a child, curly
references read the interaction reference context,
and the same |, is, and comparison operators apply. The expression must be
Boolean and reads only the selected node and the reference context. Absent
data follows the expression language's null rules, and a condition that does
not yield true does not apply.
Conditions are locale independent. Display text is localized, so a data
condition or switch case never inspects text: it may not compare a String or
FormattedText value with a string literal or another text value, nor apply a
string operation to one. It branches on locale-independent data instead: an
Enumeration or Union (is), a Boolean, a Number, a date, a null check, or a
FormattedText's resource key from
resource(...), for
example resource([status]) == "STATUS_NOT_STARTED", which compares the key
rather than its localized text. Studio rejects a condition that reads text any
other way.
^<TopicRow>:condition([status] is NeedsAttention) with color and icon
declares a status badge; no schema attribute is needed.
A condition is re-evaluated whenever its inputs change. It may change style, layout, or fold state only. It can never hide a field, make it read-only, change a value, or change what is sent; those are schema.
Display Template switch
A rule may carry a switch instead of a :condition(...) on its selector: an
ordered cases list, each with a condition (a Boolean ValidationRules
expression rooted at the selected node, with the same reads as a data
condition) and its presentation members, and an optional else. Cases are
evaluated in file order; the first true case applies and evaluation stops, so a
later case never repeats an earlier test. When no case is true, else
applies, and without else the rule does not apply. when stays at the rule
level and gates the whole switch. :condition(...) on the selector is the
one-case shorthand.
{ "select": "^<TopicRow>", "switch": {
"cases": [
{ "condition": "[status] is NeedsAttention", "style": { "color": "error", "icon": "warning" } },
{ "condition": "[status] is Done", "style": { "color": "primary", "icon": "check" } }
],
"else": { "style": { "emphasis": "muted" } } } }
A switch has at least one case and at most one else; every case has a
Boolean condition, and no case or else lacks a presentation member. A rule
cannot have both a :condition(...) selector and a switch. Cases and else
carry the same members as a rule except select, when, and switch.
Display Template catalog
The catalog is closed: every renderer implements each layout, token, and hint below. There is no free-form CSS, pixel value, literal color, or font. Studio rejects a layout on a node kind its row does not allow.
Sequential screens are a workflow, not a layout. A step-by-step layout would
add a second Previous and Next inside screens that already move through a
workflow's Previous and Next, which confuses users; an App that wants
sequential screens splits the request into a
workflow, where the service validates,
branches, and saves each step. tabs is how one request is split into
sections. stepper is not a Display Template layout: Studio rejects it, and a
client that meets it draws rows under the forward-tolerant rule.
| Layout | Applies to | Behavior |
|---|---|---|
rows |
Any node | The default: one row per child. On a Table, optional sections stack each section under its title on the same page, in order. |
form-grid |
Table | Fields flow in declaration order into at most columns (1 to 4) per row, each as label and value; the width-based layout decides how many fit. A field's span token takes more columns. columns: 1 is the label-and-value form. |
card |
Table or List member | A bordered block titled by the node's header text. |
table |
List of Tables | One row per member; columns lists the columns shown, in order (see Table columns below), each a scalar value below the member with an optional header; an optional sort names one column's value and orders the rows ascending by it for display only, never changing the List. Shows as a table only while its columns fit; otherwise rows. |
summary |
List of Tables or Unions | Read-only item previews from ordered member-relative paths; opening a preview shows the complete item. arrangement is list by default or table; a table whose columns do not fit becomes the same projected list. See Summary array items below. |
tabs |
Union, or Table with sections |
One tab per Union branch or section. |
master-detail |
Response | right_pane lists the editing Request variants that belong beside this leading Response; declaring it implies this layout. Required refresh loads and refreshes the leading page. Optional detail selectors name linked nodes eligible for selection highlighting; a selector ending in [] names List members. Membership depends on the current editing Request, including an editor opened directly through Go to or AI Search. Optional panes sizes the two panes. Shows two panes only at or above the pane threshold; otherwise rows. See master-detail panes. |
accordion |
Table or List | Folding sections with the folding behavior and initially_open. |
categorized |
Classifier Table | Only the item Lists that hold a member, plus one control that adds any kind; see categorized layout. |
split |
Table | Two or three regions in a direction, row or column; a row region may declare a min_width. See below. |
Categorized layout. categorized applies to a
classifier Table,
including one whose categories are classifiers themselves, and suits a
classifier with many categories of which a person usually has few. It draws:
- each item List under the classifier that holds at least one member, in declaration order and under its category's title when categories nest, as that List draws by its own rules. A List with no member is not drawn, unless it carries a validation message or is the target of a reveal or an issue link, which shows it;
- each single-document position (a selected field that is not a List) and the classifier's fields that are not import positions, as ordinary rows;
- one Add document control after them. It offers every kind that a List under the classifier holds, by its localized display name, grouped by category in declaration order, as one choice (filterable when it offers more than 12 kinds). Choosing a kind appends a member to the List whose import position holds that kind, with that kind selected (its form, or that variant of the item's marked Union) and the member's other fields at their normal initialization; the new member opens, as every value the person creates does. Cancelling adds nothing.
Hidden Lists and kinds are neither drawn nor offered, and a read-only
classifier or List offers no Add. The classifier's
categorized import and
each drawn List's own actions are unchanged. The layout is presentation only:
data, encoding, paths, validation, the walk and AI Import are the same as
with rows, and a client without it draws rows.
In ordinary label-and-value rows, every client measures the rendered label
and value, including icons and controls, using the current font and localized
text. It keeps both beside one another when their natural widths and the
platform's label-value gap fit the available row width; otherwise the value
goes below the label. An editable row's trailing link button counts as part of
the value; when value and button do not fit beneath the label either, the
button moves below the value at its trailing edge. The available width belongs
to the actual pane, region, or form-grid cell. This client-owned arrangement
needs no additional template rule.
Table columns. Each columns entry is { "value": path, "header": text },
with header optional; a bare path string is shorthand for
{ "value": path }:
"columns": [
{ "value": "label", "header": "RETURN_LINE" },
"amount",
{ "value": "explanation", "header": { "field": "explanation_heading" } }
]
-
`value` is a [Libertas Schema Path](schema.md#libertas-schema-path) of field
native names relative to the member, through struct fields only, never
through a List or a Union, as a
labelsource is; or a typed path; or an array of such paths, whose first path that resolves for the member supplies the cell. It ends at a scalar field (not a Table, List, or Union); a FormattedText field is a scalar. No two columns share avalue. -
A Hidden field remains a valid `value` source, as it is for `label` and
title: the column shows its value read-only, while the field still has no row of its own. Hidden stays a schema attribute that the template never sets or overrides. -
`header` is a package [`StringResources`](#string-resources-and-formatted-output)
key, or
{ "field": "path" }read, like alabel, through struct fields only below the struct that holds the List, ending at a String or FormattedText field that may be Hidden. The field form is invalid where the List has no holding struct or is reached only through a Union. Without a header, or when its text is null, unanswered, or empty, the column is headed by the value field's localized display name.
Summary array items. Use summary when a List needs brief item previews
and a separate complete item editor or viewer:
{
"select": "^<SaveIncomes>.items",
"layout": "summary",
"paths": [{"path": "document", "show": "variant"}, "owner", "document.W2.wages", "details"],
"arrangement": "list"
}
paths is a non-empty ordered array of native-name dotted paths or explicit
variant projections. A string path is relative to each member and traverses
struct fields and explicitly named Union
variants, such as document.W2.wages; for a Union member it begins with its
variant name. It may end at a scalar, struct, Union, or List, but never crosses
a List, uses an index or wildcard, or names a root or reference object. Studio
validates every path against the member schema, including inactive variants.
{"path": "document", "show": "variant"} selects the Union at document
and contributes one passive field: its ordinary field label and the localized
display name of its selected variant, such as Document — W-2. It does not
expand that variant's payload. For a Union-valued List member,
{"show": "variant"} selects the member itself. Only path and show are
accepted in these objects; an explicit path must be a non-empty valid path,
and its target must be a Union. An unanswered Union has an empty variant value;
a selected Nil branch still displays its variant name. This presentation
operation does not add a Schema Path operator or reserve native identifiers:
document.$variant continues to mean a real field or variant named $variant.
Multi-morphism table display (typed paths). Members of one List can hold different shapes: an item Table may hold one marked Union of forms, or one marked form, beside fields such as an owner. A typed path reads a field by its type, wherever that type sits in the member, so one column or summary path serves every shape:
"columns": [
{ "value": ["^<W2>.employer.name", "^<Form8919>.firm.name", "^<Patr1099>.payer.name"],
"header": "PAYER" },
"owner"
]
^<Type>.rest starts at the nearest descendant of the member whose effective
type is Type, reached through struct fields and the currently selected
variant of each Union, never through a List, and then follows rest through
struct fields. When the member holds no such descendant, for example because
another variant is selected, the path does not resolve: a column cell is empty
and a summary path contributes no field. A table column value, and each entry
of a summary paths array, may be a typed path or an array of plain and typed
paths whose first resolving path is used. Studio validates that each Type is
reachable from the member through fields and Union variants without crossing a
List, that each rest exists on Type, and that a column's paths end at a
scalar. An array holding one path is the same value as that path, and an array
may not repeat a path. Without a header, an array-valued column is headed by the
localized display name of its first path's field, whatever the rows hold; the
same holds for a summary arrangement: "table" column built from an array entry. The
path of a {"path": …, "show": "variant"} projection stays a plain path; a
typed or array path there is refused. A table's sort names a column by its whole value, so for a column
whose value is an array, sort is that same array; each row sorts by the value
its first resolving path supplies, and rows whose cell is empty sort last. A
typed path is presentation only: it reads a value and never changes what is
stored, validated, copied, calculated, encoded or imported.
A selected struct or active Union expands into its visible leaf descendants
in declaration order. A nested List stays one count boundary rather than
expanding its members. Paths contribute in authored order; when selections
overlap, each leaf appears only at its first occurrence. Hidden nodes and their
subtrees remain absent regardless of a path. Withholding an editor row through
when_single_choice does not withhold its selected summary value. A path into an inactive Union
variant contributes no list field and an empty table cell. Ordinary localized
value formatting and the template cascade apply to the selected original
nodes. arrangement: "list" shows one ordinary label/value row per projected
field, with a clear boundary between array items. It does not join several
fields into a single compact line. arrangement: "table" uses aligned columns,
one per projected leaf or explicit variant projection in path and declaration
order, with item rows in List order, while its columns fit.
Otherwise the same projection appears as the field list, including on narrow phones.
This differs from the scalar-only columns
of the table layout above.
The previews are passive even in an editable Request: their values, labels,
and cells open the whole item, never an inline field editor or a link on
a selected descendant. The client opens a full-page item editor within its
working surface, including on a desktop, using the original complete parent
draft and the same interaction reference context. Fields outside paths
remain available there, with ordinary promotion, flattening, structural
indentation, automatic editing, validation, copying,
calculations, and links. A read-only Response instead opens a complete item
viewer. The projection neither marks schema fields ReadOnly nor changes the
stored or submitted value.
Commit validates the complete item through the ordinary validation owners,
including dynamic rules when their context is available, and waits for available
pending calculations. Errors keep the item editor open. Successful Commit
returns to the summary, preserving committed fields and the list position,
without implicitly submitting the parent Request or making a backend workflow
transition. Back from an existing item preserves its ordinary committed-field
draft behavior. The client tracks the original item
through list changes by stable identity, and nested summary Lists use the same
navigation. An item with validation errors shows an error indicator on its
preview even when the offending field is outside paths. Add initializes a
provisional item through the ordinary List lifecycle and opens its complete
editor. The normal automatic editing flow still applies: an unanswered promoted
Union opens its variant chooser, while defaults and automatically resolved single
choices retain their ordinary behavior. Opening an existing unanswered item uses
the same automatic chooser; cancelling it retains the existing item. Opening an
answered or defaulted item does not ask again. Choosing a variant does not commit the
item. Cancelling this initial required chooser, or Cancel including local Back
from the new-item editor, removes that exact provisional item; only a
successful Commit retains it and returns to the summary. The schema's membership,
add, delete, and reorder rules still apply. Editable Lists expose a visible
Delete button for each complete item, including compact items; the button
removes the item through ordinary List mutation and never opens its editor.
ReadOnly List membership suppresses Add and Delete without recursively making
members read-only. While any new item is provisional,
the client pauses ordinary durable draft saves; Commit or Cancel resumes them
through the normal draft owner, so recovery never restores an uncommitted new
item. The parent Request cannot be submitted while a provisional item remains.
Clients that do not
recognize summary use ordinary rows under the forward-tolerant rule.
Content sizing in Studio and the web interface. Every full-screen protocol
page, including Request and Response pages, uses an automatic preferred width.
Simple forms use their labels and ordinary input space; tables reuse their
measured column minimums, while multi-column forms and split layouts include
their column and pane requirements. The page always fits its available space,
including a narrow window or a resized master-detail pane. Each pane computes
its own content and control requirements. The parent
combines those widths with the divider gap and fits the arrangement within
the window; a changed pane page or layout recalculates the combined width.
Without a saved divider position, the two panes use their content-based widths
rather than allocating unused width from the panes ratios. A manual divider
position takes precedence; the editor fills the pane the viewer allocates.
Typing ordinary field values does not widen a form. Summary item editors and
read-only item viewers use the same automatic content sizing on their own full
screen, including the header and action row; they retain the complete parent
draft and their ordinary item navigation.
Short forms place their actions immediately below the content. Long forms
scroll the data while keeping the action row visible. Actions align with the
form's left edge and stay together: eligible Go back controls followed by
primary Next, or Close followed by Next in an embedded editor. The existing
pane divider adjusts the master-detail allocation.
The top bar shares the content layout's width, with its buttons right-aligned
to the content inside the page's padding. The chosen arrangement of its
title and buttons supplies a minimum content width, capped by the available
window or pane space; controls follow the ordinary wrapping and placement rules
when that minimum cannot fit. A master-detail working surface retains its
shared top bar across the whole arrangement.
Master-detail panes. A rule on a leading Response declares right_pane,
a non-empty array of editing Request variant native names from the same
protocol, and refresh, a pure navigation Request that loads that Response.
right_pane implies layout: "master-detail"; an explicit layout must agree.
For example:
{
"select": "^<Income>",
"right_pane": ["Wages", "Interest", "InvestmentSales"],
"refresh": "OpenIncome",
"panes": { "trailing": { "min_width": "medium", "ratio": 3 } }
}
panes is optional. Its leading and trailing members are each optional
and accept only a min_width size-class token and a
positive ratio; an omitted member keeps its default,
{ "min_width": "compact", "ratio": 2 } for the leading pane and
{ "min_width": "compact", "ratio": 3 } for the trailing pane. panes sizes
the arrangement and never changes membership. A device condition is not needed:
the pane threshold decides when two panes show.
The current editing Request determines membership. Resolve a
WorkflowRequest to its Request editor,
or adopt the complete Request supplied by EditRequest, before looking up
right_pane. The carrier Response, originating link, Go to, AI Search, and
previously displayed section do not determine placement. At most one leading
Response may own that Request under overlapping device conditions. A rule's
relationship cannot depend on a leading-page data condition, pseudo-state,
or conditional switch. Ordinary presentation precedence resolves device rules.
An editor outside an applicable relationship uses the whole working surface.
A displayed Response also uses the whole working surface; a relationship names
editing Requests, not Response variants or navigation Requests that lead to them.
-
**Initial leading-page load.** On installing an associated editor at or
above the pane threshold, construct
refreshfrom the current complete editing Request by the shared construction-context rules, without a bound member, and send it as an ordinary correlated Request. Keep the editor's server-supplied interaction reference context unchanged. The reply must supply the declared leading Response before the editor is embedded; a same-named Response already on screen is not evidence that it is the right section or record. Resolve placement before rendering the editor: during this load keep an existing leading page in its current layout, or show the ordinary loading surface when no page is present. Render the editor directly in its resolved pane, without first showing it on the whole working surface. The same order applies when navigation chooses another editor while the leading pane is already visible. This supports entry into any section, including direct entry with no previous leading page. It never submits the editing Request automatically. On failure, report the ordinary error and keep the editor usable as a full page. A stale or duplicate reply cannot replace another editor. - Navigation ownership. Both panes belong to the endpoint's one working
surface. The leading pane owns complete navigation and the shared toolbar.
A stacked link covers only its sending pane; its pending
operation disables both panes under the shared rule below, navigate-away uses
this placement contract, and crossing the pane threshold preserves the stack
above the same live full-page editor.
Both panes fill the content height and scroll independently; the editor keeps
its heading and content actions without another screen toolbar.
An embedded workflow editor shows Close and primary Next in one
compact action row. Go back disappears, including every
PrevRequestchooser and shortcut, and the editor has no Back control of its own. This is unconditional while embedded; no comparison with Close or another action determines suppression. Next retains ordinary validation and explicit submission. Close clears the right pane, sends nothing, and keeps the leading page; accepted Close follows the ordinary draft lifetime. Outside the pane the editor's eligible Go back and Cancel controls return. Platform Back acts on the whole working surface under the landing and leaving rules. - **Complete navigation is the App's responsibility.** The leading page must expose the navigation needed to revisit editable sections and leave the arrangement. Its links and actions construct from its own context, never the right editor's current values or private workflow context. Selecting another destination closes an idle editor before opening it; selection does not submit or save the edit. A flow requiring a previous action that depends on the editing context must remain a full-page flow.
- One pending operation for both panes. While an operation awaits its reply, the covered surface is fully disabled, including edits, request-sending controls and the trailing pane's Close, because the pending reply may place a page in that pane. The waiting presentation owns screen versus link-panel progress; merely editing a linked request shows no waiting spinner. Auxiliary calculations retain their ordinary ownership. During a leading-page load or refresh the working surface accepts no edit, link, import, Submit, or Close; it shows loading until the reply. Native Back follows the leaving rule. All replies remain owned by their ordinary pending operations.
-
**Every successful right-panel commit refreshes.** After explicit Submit
receives any reply outside the failure class (neither a failure status nor
an Error), construct the declared
refreshfrom the current leading page with the ordinary construction rules and no bound member, and send it. This includes successful commits that install no next page. If the commit reply itself supplies the updated leading Response, it replaces that page and clears the editor; no redundant refresh Request is sent. Validation failures, rejected commits, Close, calculations, and a link return that keeps the editor open do not refresh. Never replay the Request that originally produced the leading page: it may have been a save. - Following a commit. Apply the refresh before presenting a new page from
the commit. A newly resolved editor is placed by its own
right_panerelationship; if it belongs to another leading page, load that page from that editor before embedding it. No row match is required. A terminal Response or an unassociated editor becomes the whole working surface. A refresh failure leaves the previous leading page in place, reports the ordinary error, and does not discard the valid commit reply. - Refresh replies. The declared leading Response replaces the leading page even without NextRequest or WorkflowRequest; the non-transition acknowledgement rule does not apply to this page load. An Error leaves the previous leading page in place and follows ordinary error handling. Any other admitted refresh reply becomes the whole working surface, as in a single-pane arrangement.
-
**Optional row highlighting.** `detail`, when present, is a non-empty array
of selectors naming linked nodes within the leading Response. An entry
ending in
[]selects List members; another entry selects one node. Paths traverse struct fields only, never another List or Union. Selected nodes and their ancestors must be visible, and each node must carry a Link with NextResponse. Highlighting uses the schema's explicit enum/protocol association, matching marked enum values in the current editing Request and linked row, including shared Request variants and direct entry. The matching node exposes the:selectedpseudo-state; the template supplies its visible style, for example"style": { "emphasis": "primary", "color": "primary" }. Restrictdetailto the intended navigation rows: including a summary link that repeats a topic creates an ambiguous match. Missing or ambiguous matches clear highlighting and never determine pane membership or require a full-page fallback. Clients do not compare link-request fields or choose the first row to infer selection. - Crossing the pane threshold. Narrowing shows the current editor as a full page above its leading page, with ordinary Back, Go back, and Cancel controls. Native Back follows the endpoint-exit rule after resizing too. This change sends no editing Request. Widening evaluates the current editing Request's relationship again and embeds it, restoring Close and suppressing Go back. If its leading page has not yet been loaded, perform the initial load described above. A pending operation retains ownership through resizing; apply its reply once, then place it by the current pane threshold.
- **Divider and collapse.** Two panes show one platform-owned divider in the gutter. Dragging it moves the leading pane's edge between the leading minimum and the width that leaves the trailing minimum; releasing more than half the leading minimum below it collapses the leading pane, and releasing between those points snaps to the leading minimum. A double click or double tap restores automatic content sizing in Studio and the web interface, and the default share on Android and iOS. The divider is focusable between the panes and is a splitter for assistive technology, valued in percent and named for the leading page ("Resize Income pane"): Left and Right (mirrored for right-to-left) move 5% of the width shared by the panes, Home and End go to the two limits, Enter toggles collapse, and Android and iOS add Collapse and Expand screen-reader actions. A collapsed leading pane leaves one button, titled with the leading page's title and a sidebar glyph, at the leading edge of the trailing pane's heading; it restores the leading pane, which keeps its scroll position and loaded value. Collapse is still the embedded arrangement: Close and Next stay, Go back stays suppressed, and collapse and expand send nothing and never refresh. Only the leading pane collapses. With no editor in the trailing pane the leading page shows expanded. Collapse moves focus to the expand button when focus was in the leading pane, and expand moves it to the leading pane's first control; both announce the new state.
-
**Viewer preference.** The divider position and collapse state are a
viewing preference kept on the viewer's device for that Hub, package, and
leading Response, separately for a
mediumwindow and for anexpandedor wider one. They are never App data, never sent to the Hub, never part of a draft, and never written todisplay.jsonor the compiled template. The stored share is applied within the current limits and kept as stored, so narrowing and widening again restores it; an unreadable value is ignored. A stacked arrangement ignores the preference without resetting it.
The loader must be constructible from both the leading Response and every
listed editing Request, using Fixed, Default, or supplied CopyFrom values.
It has no editable input, RequestConfirmation, or subscription behavior, changes
nothing in the service, and admits the leading Response in NextResponse.
When refresh carries earlier values such as a revision, the service admits
pure navigation by identity rather than rejecting a stale editing revision.
Below the pane threshold no pane-only load or refresh is sent.
Retained compiled templates without right_pane keep their earlier detail-link
arrangement; new relationships use the explicit editing-Request declaration,
and Studio rejects a master-detail rule without right_pane.
sections is an ordered list of { "fields": [...], "title": "KEY" }
objects. fields names direct children by native name; title optionally
names a package StringResources
key whose localized text titles the section, and without it the section is
titled by its first field's display name. A child appears in at most one
section, and unlisted children go to the last section. Each section shows its
listed fields in the listed order, followed by any unlisted children in
declaration order. The same sections serve tabs and rows, so a pair of
rules can show one Table as stacked sections in edit mode and as tabs in
view mode with the same titles. Under rows, each section title is a
heading above its fields; nothing is hidden, and moving to the next field
crosses section boundaries in order.
Because no tab hides an editable field in edit mode, a Submit blocked by a
field never has to reveal another tab.
On Android and iOS, each tab's direct children start at normal page or pane
padding, with indentation measured relative to that tab's content. This applies
to both Table sections and Union branches, including nested tabs. Nested
structs, Lists, and Unions within a tab retain their ordinary indentation and
flatten rules. The tab owner's row keeps its surrounding placement; entering
a tab changes only the content's visual indentation, not its value ancestry.
split is the one composable layout. Each region has a positive ratio, the
direct children it holds as fields, optional style, and its own nested
layout: rows (the default), card, form-grid with its columns, or
another split with its own direction and regions. A region of a row
split may also declare min_width, a size-class token
defaulting to compact. Splits nest at most three levels deep. A child
appears in at most one region, and unlisted children go to the last region. A
region shows its listed fields in the listed order, followed by any unlisted
children in declaration order. A split is how a photo on the left with name,
date of birth, and employee ID as rows on the right is expressed; every other
layout arranges all children one way. A row split places its regions in
lines under the width-based layout rules, so it needs no device
condition: on a phone its regions stack, and where they fit they share a line.
{ "select": "^<Employee>", "layout": "split", "direction": "row",
"regions": [
{ "fields": ["photo"], "ratio": 1, "style": { "align": "center" } },
{ "fields": ["name", "date_of_birth", "employee_id"], "ratio": 2,
"layout": "form-grid", "columns": 2 }
] }
Adjacent split regions have a platform-owned gutter in either direction and
at every nesting level: 16 dp on Android, 16 pt on iOS, and 16 CSS px in Studio.
Stacked lines of a row split are separated by the same gutter. The gutter is
automatic, requires no display.json member, and is independent of the row
spacing style token. A column split divides its height by the ratios after
the gutters are deducted and never wraps. Nested device conditions use the
resulting region size.
tabs shows every tab label in full. When the labels do not fit across the
pane, the tab strip scrolls horizontally; labels are never truncated and tabs
never fall back to stacked sections.
Width-based layout
Every client adapts pane count, split lines, form-grid columns, and tables to
the space it has, at every window size, in two layers: the window caps the
structure, and content fit, checked against the containing pane or region,
fills within that cap. None of this changes a value, validation, CopyFrom or
CopyTo, calculation, encoding, or AI Import or Search.
The size classes are one vocabulary. They are the values of the width
condition, and the same names are the
min_width tokens of split regions and panes, where each means its class's
lower edge:
| Class | width condition |
min_width |
|---|---|---|
compact |
under 600 dp | 360 dp, the narrowest supported phone in portrait (default) |
medium |
600 dp up to 840 dp | 600 dp |
expanded |
840 dp up to 1200 dp | 840 dp |
large |
1200 dp up to 1600 dp | 1200 dp |
extra-large |
1600 dp and up | 1600 dp |
dp is the Android density-independent pixel, an iOS point, and a CSS pixel in
Studio. A min_width scales with the platform's text-size setting, by its
factor clamped between 1 and 2, so larger text reflows sooner. A region that
holds a nested split needs at least the largest minimum among the nested
regions, because the nested split can itself stack.
The window's width class caps the structure. The window is the app window on Android and iOS, Studio's view, or the simulated device of a preview; a pane never raises the cap:
| Window width | Panes at most | Split line or form-grid columns at most |
|---|---|---|
compact |
1 | 1 |
medium |
2 | 2 |
expanded |
2 | 3 |
large, extra-large |
2 | 4 |
A window whose height class is compact shows one pane, so a phone in
landscape keeps room for the editor above the keyboard; its columns keep the
width cap.
Split lines. A row split places its regions greedily in declaration
order. A region joins the current line while the line holds fewer regions than
the column cap and the line's minimums plus one gutter per adjacent pair fit
the split's width; otherwise it starts the next line. A region alone on a line
takes the full width, and its content wraps; nothing scrolls horizontally.
Within a line the width after gutters is divided by ratio; a region that
would fall below its minimum gets its minimum, and the rest is divided again
by ratio among the others until no region is below its minimum. Lines stack in
order, and reading, focus, and field order stay region order then field order;
wrapping never reorders. Each nested split uses its region's resulting width
and the same window cap. Right-to-left text mirrors each line.
Pane threshold. A master-detail arrangement shows
two panes when the window allows two and the working surface is at least the
leading minimum plus the gutter plus the trailing minimum wide; otherwise it
shows one pane, stacked. With the default minimums the threshold is 736 dp at
normal text size. With two panes, the width after the gutter is shared by the
leading share, the leading ratio over both ratios by default (40% and 60%) or
the viewer's preference, held between the leading
minimum and the width that leaves the trailing minimum; the trailing pane takes
the rest.
form-grid columns. columns is a maximum. The grid uses as many columns
as fit its width at the compact minimum each, with the gutter between them,
at least one and at most the smaller of columns and the window's column cap.
Fields flow in declaration order as before, and a span larger than the
columns in use is reduced to them. For example, columns: 3 in an expanded
window shows one column below 736 dp, two from 736 dp, and three from 1112 dp.
Table column fit. A table, or a summary with arrangement: "table",
shows as a table when its column minimums plus a gutter between each pair fit
its width; otherwise it shows the rows or list it falls back to. The fit
measures text in the current font, text size, and locale, never a size-class
token. A column's minimum is the largest of its header's longest word and
each current cell's minimum: a number, money, date, unit, enumeration, or
Boolean cell counts its full width, and a wrapping text cell counts its full
width up to the larger of its longest word and twelve times its font size.
Width left over goes to the columns holding wrapping text, in proportion to
their full widths, or, when no column wraps, to every column in proportion to
its minimum. Nothing scrolls horizontally. The choice depends on the
displayed data and is the same for the same value; when a data change switches
between table and rows, the reading position stays on the same member.
Pane count, lines, columns, and table fit are evaluated again when the window or a pane changes size, when the text size changes, and when a divider drag ends. During a drag the panes resize and their content reflows, but template rules are resolved again only once the drag ends.
Labels and titles
Two members change the text a person sees on the node their rule selects:
-
`label` replaces only the node's displayed name. The row keeps its value:
the node's own value, or the child a
promoterule puts on the row. -
`title` replaces the whole row with its text. The row's label and value are
both removed, and nothing is promoted into it, because it has no value; a
child that a
promoterule names keeps its own row, and Studio warns that thepromotehas no effect.
Both change only the text, never its typography. The node keeps the
typography of its position: a page's title is drawn as a page title, a group
header inside a page as a group header, and a List member's row as a member's
row. A role style token changes that look where an App
wants another one, for example role: label for a quieter group header; no
client reads label or title as a typography instruction.
Both take the same two value forms:
-
`{ "field": "path" }` takes the text from the data. `path` is a dot path of
field native names below the selected node, through struct fields only,
never through a List or a Union, ending at a String or FormattedText field;
that field may be Hidden or ReadOnly. Its value, a FormattedText as its
localized text, is the text. A scalar has no fields below it, so on a
scalar field the path reads below the struct that holds the field, as a
tablecolumn header's reads below the struct that holds its List: a sibling field or one below a sibling. A scalar that no struct holds, such as a List's member, a function parameter, or a scalar selected by its type, takes only the"KEY"form. -
`"KEY"` gives fixed text: a package
StringResourceskey, as a section'stitleis.
A field path never passes through a Union, because the selected branch is
known only at run time. On a Union node, and on a node reached only through a
Union, such as a Union branch, a node below one, or a named type used only
there, only the "KEY" form is valid; Studio rejects the field form there
with a message that names the key form.
A label or title rule may select:
- a page, a displayed Request or Response variant (
^<Hub>): the text is the page title and replaces the page's top row as the member says; - a struct, by its type (
^<Type>) or by a field of it: the text replaces the struct's row as the member says; - a Union field, in the
"KEY"form only: withlabelthe selected variant stays its value; - a List's members (
^<Resp.list>[]): each member row takes its text from its own field, which replaces both the member name and its position number; - with
labelonly, a scalar field, such as a Number, String, Boolean, DateOnly, DateTime, Money, or Enumeration field, by a field path or by its type: the text replaces the row's name, and the row keeps its value and editor. A scalar List member is labelled through the List's members.
title never selects a scalar: a scalar's row is its value, which a title
would remove. Neither member selects a List itself (select its members with
[]) or a scalar Union branch, whose payload is drawn under the Union's row
with no row name of its own (select the Union field).
A rule selecting a field below a typed ancestor, such as
^<FarmRental>.expenses.chemicals, labels only the scalar reached that way.
The same struct reused elsewhere, such as ^<Farm>.expenses, keeps the
schema's display name:
{ "select": "^<FarmRental>.expenses.chemicals", "label": "DISPLAY_4835_LINE9_CHEMICALS" }
When the field is null, unanswered, or empty, the node draws as it would
without the member, with its position for a List member, so a row is never
blank. The display name stays the schema's meaning: AI tool descriptions and
every declaration keep it, and label and title change only what a person
sees. The source field is normally declared Hidden in the schema, so its
value is not drawn twice; a Hidden field remains a valid source. Hidden stays
a schema attribute that the template never sets: Studio neither hides a
visible source field nor rejects it, but reports a warning at the label or
title line of display.json that names the visible field and suggests
Hidden. A visible source field keeps its own row.
label and title exclude each other on one node, and neither combines with
suppress_label on that node. A label with a promoted child is how a row
is data-titled. A label on a promoted field labels the
row only with use_parent_name false; with true the row takes its struct's
displayed name, the field's label shows nowhere, and Studio warns. A Hidden
field has no row, so its label shows nowhere.
Both resolve through the
cascade and when like every other member; a
data condition, switch case, or pseudo-state changes style, layout, or fold
state only and never sets either.
On a struct, the two members combine with promote on a
child and with flatten on the struct as follows:
| On the struct | With | Result |
|---|---|---|
label |
promote on a child, use_parent_name true |
The row's label is the struct's displayed name, that is, the label text; the child is the value. |
label |
promote on a child, use_parent_name false |
The row's label is the child's own name. Studio warns that the struct's label shows nowhere. |
title |
promote on a child |
The child is not promoted and keeps its own row. Studio warns that the promote has no effect. |
title |
flatten with { "name_row": false } |
Rejected: one removes the name row and the other replaces it. |
With label, the link of a promoted value stays on the row; with title, a
link on the struct stays on the title row
(actions).
{ "select": "^<TopicRow>", "label": { "field": "title" } }
{ "select": "^<TopicRow>.status", "promote": true }
{ "select": "^<Hub>", "title": { "field": "topics.name" } }
The first two rules draw each topic row as "W-2 wages Entered": its title
field as the label and its promoted status as the value. The third draws the
hub page's top row as just "Income", the name of the section whose topics the
page lists.
Button labels
button_labels changes the captions of existing controls on one protocol
page. It is a non-empty object with one or more of these slots:
| Slot | Existing control | Behavior retained |
|---|---|---|
go_back |
Workflow Go Back | Constructs and sends the selected PrevRequest; native Back is unaffected. |
next |
Workflow Next/Submit | Validates and submits the current Request; it does not choose a NextRequest. |
close |
Page, detail-pane, or stacked-link page Close | Performs the existing local dismissal without submitting; a stacked page's system Close may only be recaptioned, never removed. |
actions |
Page Actions | No eligible choices hides the control; one selects directly; several open the ordinary chooser. |
Each slot accepts a package StringResources key, or null to restore the
localized platform caption. Literal display text, field sources, empty strings,
and unknown slots are invalid. Localization uses the package that authored the
winning rule and the ordinary resource fallback. If a client cannot resolve a
non-empty caption, it uses the platform caption, never a blank button or raw
resource key. Omission preserves a lower-ranked override or the platform default;
null overrides lower-ranked captions with that default. Slots do not inherit
into children or another page.
The actions slot also accepts exactly { "source": "single_action" }.
It uses the sole offered action's localized schema variant Name. Count the
final offered set after NextRequest, ConstrainedBy, access privileges, ExcludeUI, and
LinkOnly filtering, and an explicit Actions array when the page supplies
one (named action controls). With several choices, or a missing or blank name, the
source supplies no caption, and the next source in the
Actions caption precedence applies: an explicit
Actions field name, then the localized platform Actions caption, never a raw
native name or payload type name. With no choices, retain the existing hidden
control. This source
uses the action's owning document, not the
template author's resources or a display title. No other slot accepts it.
An explicit null resets this source just like a resource override.
{ "select": "^<Hub>", "button_labels": { "actions": "GO_TO" } }
{ "select": "^<SectionHub>", "button_labels": {
"actions": { "source": "single_action" } } }
{ "select": "^<SaveIncome>", "button_labels": {
"next": "SAVE_AND_CONTINUE", "close": "CLOSE_EDITOR" } }
{ "select": "^<Finish>", "button_labels": { "next": "FINISH_REVIEW" } }
{ "select": "^<SaveIncome>", "when": { "form": "phone" },
"button_labels": { "next": null } }
Rules select whole Request, Response, or SubscriptionData variants of an
Endpoint protocol. go_back and next require a Request variant. A rule
cannot select a field, reusable payload type, List member, configuration, or
persistence-only variant to rename page controls.
Device when conditions
are allowed; data conditions, switch cases, and pseudo-states cannot set
button_labels. A valid caption remains dormant when its control is absent.
The active displayed variant owns its captions. A workflow editor uses its Request, not its originating Response. In master-detail, the leading page owns its toolbar and the trailing page owns its own controls. An embedded editor's Go Back remains suppressed, even when a caption is configured. Captions never create controls, change eligible requests or their order, change input presentation, submit automatically, or alter validation, confirmation, pending-operation ownership, or draft lifetime. Request names inside choosers, native navigation, ordinary request-editor Send, and confirmation/picker Send and Cancel labels are unchanged.
Choose wording that remains truthful for every state in which the page uses it. For example, "Go to…" can describe a sole destination-chooser request; "Return to review" is appropriate only when the offered operation actually returns to review. Renaming Actions does not change its target. "Save changes" does not promise a particular next page when the service can ask follow-up questions; Close must not imply submission or saving.
Clients measure the resolved caption with the actual locale and accessibility text size under the existing responsive control and heading rules, allowing content action rows to wrap as needed while retaining readable text and touch targets. The visible caption is also the accessible name. Re-resolve it when the displayed variant, offered action set, template, locale, or device condition changes. Older clients ignore this optional member and retain their platform captions.
Named action controls
The ActionControl schema attribute declares the controls and their choices. No authored template is required. A named control uses its field's ordinary localized display name; this is a document name. The built-in Actions control keeps its platform caption when implicit, and an explicit Actions field can supply its default name. The selected Requests keep their own localized names inside a chooser.
Actions is the prominent page-heading control. Its default treatment uses a filled accent background, contrasting text and a semibold caption. Optional named controls use secondary emphasis: a transparent background, subtle outline and normal-weight caption. Controls share normal platform sizing, padding and corner treatment; emphasis never reduces their touch targets. Authored emphasis and density tokens retain their ordinary overrides.
When at least one optional named control is visible and eligible, keep all visible page controls, including Actions when eligible, together as one group. Named controls use field declaration order; Actions is always last, at the logical trailing end (right in a left-to-right layout). Put the group in the top bar only when the full title and complete group fit after native navigation and system controls reserve their space. Never promote a smaller subset or shorten a full title to admit buttons. With no eligible Actions, measure the named controls alone by the same rule.
When the group cannot fit, put every button on a new secondary row below the heading and above content, leading aligned with full captions. Neither Actions nor a custom button then shares the content title row. Show the full title in the top bar when it fits there alone; otherwise retain the full, wrapping content heading. Suppress a repeated content heading, but retain its other controls and values. The secondary row wraps as space requires, preserving order; an individually oversized caption wraps inside its button and grows its height. Actions retains its prominent styling.
With no visible eligible optional named control, Actions keeps the independent heading rule, including placement beside the content title. A custom Actions caption does not make it an optional named control. An empty eligible set hides its control and releases its space; an empty secondary region takes no space. In master-detail, the leading page owns global controls; trailing Response controls stay together below their local heading when any optional control is visible. An embedded Request editor keeps Close and Next, with no response controls.
On a whole Response or SubscriptionData protocol-page rule, action_controls
is an object keyed by an exact declared control name or Actions. A rule
cannot invent a control. Each entry can set label, placement, and the
emphasis and density style tokens. Members cascade independently under device when
conditions; data conditions, switches, and pseudo-states cannot set them.
{
"select": "^<Hub>",
"action_controls": {
"sections": { "label": { "field": "section_caption" }, "placement": "secondary" },
"Actions": { "label": "PAGE_ACTIONS", "placement": "secondary" }
}
}
placement is secondary or header. Omission and header request adaptive
placement. With any visible eligible optional named control, a secondary
placement on any visible control keeps the complete group below the heading.
A multiline caption or a button too tall for the native top bar likewise keeps
the complete group below. With Actions alone, secondary pins it to that
region; omission and header retain its independent heading rule. A trailing
pane does not get another screen toolbar. Styling never changes eligibility
or invocation.
label accepts a package StringResources key, { "field": "path" }, or
null to restore the default caption. The field form uses the existing
label path below this displayed payload through struct
fields only, ending at a String or statically ReadOnly FormattedText field.
It never reads the selected Request, a reference context, List or Union
member, or an expression. A Hidden caption source is allowed as an explicit
text source. Invalid paths and types fail Studio validation. An absent, null,
empty, or unresolvable current value falls back to the field's localized name
or platform Actions caption, never a blank or raw key. Caption updates do not
select or submit an action. For Actions, an explicit
action_controls.Actions.label takes precedence over button_labels.actions,
then an explicit Actions field name, then the platform caption.
Task-controlled section-hub action
For a walkthrough with section hubs and a main Review page, declare the
continuation and review Requests in each hub's NextRequest, then let the
task's ConstrainedBy value offer exactly one of them. Use the
single_action caption source so its name and destination change together.
Keep row-opening Requests LinkOnly; they remain available to their links
without becoming additional Actions choices. Clients need no walkthrough
flag, navigation history, or conditional caption rule.
For example, the tax task offers Continue while walkthrough steps remain. Each section hub has its own continuation Request, which records that section's completion and advances to its next applicable section using the task's existing navigation. When no walkthrough step remains, the task offers Review, opening the main Review page. If an accepted edit creates unfinished walkthrough work, the task offers continuation again. Derive this choice from existing completion state rather than persisting a separate presentation flag. Remove a redundant next-step or Review row when this button owns that operation.
The section-hub Actions control and an editor's Next control remain separate. An editor's Save and continue submits its answers and follows the task's save route; a hub's Continue follows that section's onward route. The routes may differ. A hub action cannot submit an open editor's uncommitted fields. Preserve other required leading-page navigation when replacing a chooser with one direct action. Keep full captions on phones and use the responsive sizing and wrapping above.
Style tokens
| Token | Values |
|---|---|
density |
compact, comfortable |
emphasis |
primary, secondary, muted |
align |
start, end, center |
role |
Typography role: title, body, label |
color |
Platform color roles only: primary, secondary, tertiary, error, surface, on-surface-variant |
spacing |
A five-step scale, 0 to 4 |
span |
1 to 4: the columns a field takes inside a form-grid |
icon |
One of check, warning, info, lock, link, drawn before the value; with :condition(...) it makes a status badge |
Style tokens are visual only. They never disable, hide, or change the
interaction of what they style: a muted row keeps its links and its editing,
and hiding stays the schema's Hidden. An App that wants a row out of reach
leaves it out of the data or its allowed choices instead.
Android maps tokens to Material 3 roles and window size classes, iOS to SwiftUI
roles and size classes, and Studio to its control-view variables. Larger
inline media is the media hint, not a style token; the
media field and its preview are defined below.
Behavior hints
Each hint has exactly the meaning its source attribute has, and promote,
when_single_choice, media, and chart, which have no source attribute, the
meaning their sections define; the linked section keeps that meaning and any
authoring syntax.
| Hint | Value | Source attribute and meaning |
|---|---|---|
inline |
true or false |
InlineChoices on an Enumeration or Union node. |
fold |
true, false, or { "initially_open": Boolean or "nondefault" } |
Foldable on a Table, Union, or List; true means initially_open true, false makes the node unfoldable; "nondefault" starts it open only when its value differs from its default. |
promote |
true, false, or { "use_parent_name": Boolean } |
Promotion, with no source attribute. Valid only on a rule selecting a field of a struct, never a function parameter: that field becomes its parent row's value. true means use_parent_name true; false turns promotion off. |
when_single_choice |
"show" (the default) or "hide" |
Single-choice rows, with no source attribute. Valid only on a rule selecting a struct field that carries ConstrainedBy or is a scalar EnumSource host: "hide" withholds the row while the field's allowed set leaves one answer without a payload. |
suppress_label |
true or false |
SuppressLabel: omit the selected value row’s label; on a List of structs omit the List-contributed member names and positions. |
include_description |
true or false |
UIIncludeDescription. |
format |
One pattern string | Format: only the portable pattern grammar that section defines, checked against the node's effective type; no other string is accepted. |
scale |
One scale type token | The chart scale type of a scale-bearing chart field, under the type matrix of the chart specification. |
guide |
One guide position token | The position of the selected field's chart guide, under the guide position matrix. |
palette |
auto, categorical, sequential, or diverging |
The discrete color list of a chart root's categorical paint, under the chart palette rules. |
media |
{ "max_height": token } |
Inline media, with no source attribute. Valid only on a Bytes node with effective MimeTypes: how tall the field's media draws inline. |
chart |
{ "aspect": token, "max_height": token }, at least one key |
Responsive chart preview, with no source attribute. Valid only on a two-dimensional chart root (Chart): aspect is square, 4:3, or 16:9 and replaces the family aspect; max_height replaces the preview's height cap. Each key resolves independently. |
Every hint keeps its source attribute's placement rules, including the node kinds it may select, and a hint never makes a read-only value editable.
Height tokens. max_height in media and chart names a share of the
visible height of the nearest enclosing pane, never pixels. Like the height
condition it has container semantics: a node in the right pane of a
master-detail layout measures that pane, and a node outside any pane
measures the usable window after safe areas, system bars, and persistent
application chrome. The value tracks the pane as it resizes or rotates.
| Token | Height cap |
|---|---|
thumbnail |
The compact, platform-bounded size: the current media thumbnail, or the 180-point chart preview region. The default for media. |
small |
About 25% of the pane's visible height. |
medium |
About 40% of the pane's visible height. |
large |
About 60% of the pane's visible height. |
A rule may name thumbnail to undo a larger size that a lower-ranked rule
set. media and chart are device-dependent presentation like every other
hint: they never change a stored value, its validation, CopyFrom or
CopyTo, calculation requests, encoding, or AI Import, and they may appear
under when but not in a data condition or switch case.
{ "DisplayVersion": 1, "rules": [
{ "select": "^<Employee>.photo", "media": { "max_height": "medium" } },
{ "select": "^<Employee>.photo", "when": { "height": "compact" }, "media": { "max_height": "small" } },
{ "select": "^<EnergyHistory>", "chart": { "aspect": "16:9" } },
{ "select": "^<RoomShare>", "when": { "form": "phone" }, "chart": { "max_height": "large" } }
] }
Presentation locks
lock keeps the person from changing a value the App's business logic
already set. Its most common use is with AskOnAdd: the
person gives a member's defining value at Add, and the lock keeps it fixed
afterwards. It is presentation only and restricts the person alone:
initialization, CopyFrom, EditRequest values, calculation and link-return
CopyTo results, AI Import, draft restoration, and the App write a locked
value exactly as an unlocked one, and its validation and save-path handling
are unchanged.
| Value | Protected from the person |
|---|---|
true |
What schema ReadOnly would protect on the node: the value and its subtree, or on a List its membership and order, its members keeping their own editability. |
"variant" |
On a Union only: the selected variant. Its payload keeps its own editability. |
-
**Not an invariant:** unlike `ReadOnly`, a lock never makes a
calculation input invariant; a
locked input that triggers a calculation still declares
CalculationRequest. - Errors stay visible: a locked value that is unanswered or fails validation shows read-only with its error, as a ReadOnly value does, and blocks submission. A lock that leaves the person a value to fix is the App's error.
-
**Which to use:** a field the person never changes anywhere is schema
ReadOnly;lockis for a value the person must not change in the contexts its rule selects, such as a document's kind afterAskOnAdd, withlock: "variant".
Studio rejects lock: "variant" on a node that is not a Union, lock on a
Hidden node, and either on a rule with a data condition or when.
Template validation
Studio's shared validator owns every template rule. It rejects a compiled template unless:
DisplayVersionis known and every rule has exactly oneselectand at least one presentation member;- every selector resolves against the package schema under the selector grammar, including its name-collision rule;
- a pseudo-state appears only on a rule whose only presentation member is
style; - every data condition and switch case parses under the ValidationRules
grammar, is Boolean, and reads only the selected node and the reference
context, every switch meets the rules above,
and neither a data condition nor a switch case sets
label,title,button_labels,action_controls,promote,flatten,when_single_choice,media, orchart; - no rule member is outside the catalog, including every schema-only attribute;
-
every layout selects a node kind its row allows; every `master-detail`
relationship names a non-empty list of distinct Request variants of the same
protocol, excludes a WorkflowRequest leading Response, has no overlapping
other owner, and does not depend on data conditions or switches; each optional
detailentry is a^<…>path within the selected Response, never a$…path or a descendant search, and resolves through struct fields only; no entry repeats another; no detail node is Hidden or lies under a Hidden node; and every detail node carries aLinkwhose request declaresNextResponse; arefreshnames a Request of the same protocol whoseNextResponseadmits the selected Response, whose every field is Hidden or ReadOnly and is filled by Fixed, Default, or aCopyFromalternative that the selected Response and every listed editing Request supply, and which is neither aSubscriptionRequestnor markedRequestConfirmation, and a rule withrefreshselects a Response that does not carrySubscriptionData; everytablecolumn is a string or an object withvalueand optionalheaderonly, itsvalueresolves as a table column path to a scalar field of the member type, Hidden allowed, novaluerepeats, aheadernames a packageStringResourceskey or resolves as a column header field, andsortnames one column'svalue;sections,regions, andfieldsname direct children; a section'stitleand a fixedlabelortitlename a packageStringResourceskey; splits nest at most three levels; a region'smin_widthis a size-class token and appears only on a region of arowsplit; andpanesappears only withmaster-detail, holds onlyleadingandtrailing, each with only a size-classmin_widthand a positiveratio, and is not set by a data condition or switch; - every
labelortitleselects a node its placement allows, atitlenever a scalar; every{ "field": "path" }of either resolves below the selected node, or below the struct that holds a selected scalar field, through struct fields only, never through a List or a Union, and ends at a String or FormattedText field; the selected node is neither a Union nor reached only through a Union, nor a scalar that no struct holds, where only the"KEY"form is valid; no node has bothlabelandtitle, and neither is combined withsuppress_labelon the same node; Studio reports these at the member's line ofdisplay.json; - every
button_labelsis a non-empty object of the four slots above, with package resource-key or null values, selects only whole protocol page variants, and setsgo_backornextonly on Request variants; - every
action_controlskey names a control declared on the selected whole Response or SubscriptionData page, or built-in Actions; its label source, placement, and style satisfy the named-control rule above; - every
promoterule selects a field of a struct, never a type, a List member, a Union branch, a function parameter, or a Response; everyflattenrule selects a struct, a List, or a Union, by type or by field, never a List member or a Union branch, and its value istrue,false, or{ "name_row": false };name_rowappears only on a struct; and no struct has bothtitleandflattenwith{ "name_row": false }; - every
when_single_choicerule selects, under the same placement rule aspromote, a field of a struct that carriesConstrainedByorEnumSource, and its value is"show"or"hide"; - every
mediarule selects aBytesnode with effectiveMimeTypes, and its value is an object whose only keymax_heightis a height token; everychartrule selects a node carryingChart, never aChart3Droot or a chart field, and its value is a non-empty object ofaspect(square,4:3, or16:9) andmax_height(a height token) only; - every hint satisfies its source attribute's placement rules and value
grammar, and every
whenkey and value is from the closed sets.
Rules of equal rank that set the same member on the same node are allowed, the
later one winning, and are reported as a warning. Studio also warns, at the
member's line, when the source field of a label or title is not Hidden,
naming that field; when a struct's label shows nowhere because its
promoted child has use_parent_name false, when a field's label shows
nowhere because the field is promoted with use_parent_name true, and when a promote has no effect because its
struct has a title. It warns when an InlineChoices field or type declares
more than 12 choices, which always use the selector. Studio additionally lints
more than about eight rendered (non-Hidden) fields
shown together on a screen reachable from a Request, Response, or
SubscriptionData payload or a function's configuration; a required field
(one the person must answer before saving: not Nullable, with no Default; a Hidden or ReadOnly field is never required, because the person cannot answer it)
folded closed while form: phone may apply; contrast by color
role; and a split whose regions leave a required field unlisted in a nested
region only.
The fields-per-screen lint reports each Table once, under its own name and
source line; an inline Union branch is named Union.Branch. A screen sums
the fields of its nested Tables, so several small Tables add up. A field
whose content shows only on demand counts one row: a Union, whose chosen
branch is linted as its own screen, so a Union counts by its largest branch
and never the sum; a Nullable container; and a nested Table reported on its
own. The fields-per-screen lint applies only in view mode: an editor is
an ordinary scrolling form, where every field stays reachable in entry order,
so many fields on one editing page draw no warning. Studio decides the mode
from where a screen is reachable: from a Request whose node holds a field the
person can change it is edit; from a Response, SubscriptionData, or a
ReadOnly subtree it is view; a Table reachable both ways is linted for its
view use only. In view mode a node is exempt under sections with tabs
or rows, a split, or a fold that starts closed, from display.json or a
source Foldable. Exemption holds only on the forms its rule's when.form
names, so a rule limited to tablet and desktop still leaves one phone warning,
which says so, and only in the modes its rule's when.mode names; a rule
without mode applies in both.
Common field metadata
SizeMin/SizeMax follow the shared size contract:
List element count, String UTF-8 byte count, and Bytes byte count. Both TypeScript
@LibertasSize(min=..., max=...) and Rust
#[libertas_size(min=..., max=...)] emit these same attributes. The bounds are
inclusive; size zero is valid unless excluded by the minimum. Nullable null
remains separate from an empty value.
User and group pickers do not implicitly select the current user or a singleton candidate. Declare an explicit default using the User and group default contract.
| Meaning | TypeScript | Rust |
|---|---|---|
| Initial value | @LibertasDefault(value) |
#[libertas_default(value)] |
| Inline long description | @LibertasUIIncludeDescription |
#[libertas_ui_include_description] |
| Not editable | @LibertasReadOnly |
#[libertas_read_only] |
| Hidden from ordinary editor | @LibertasHidden |
#[libertas_hidden] |
| Unordered List | @LibertasUnordered |
#[libertas_unordered] |
| Scoped uniqueness | @LibertasUnique(level) |
#[libertas_unique(level)] |
| Size bounds | @LibertasSize(min=...,max=...) |
#[libertas_size(min=...,max=...)] |
| Live Number/DateTime minimum | @LibertasRangeMin("expression") |
#[libertas_range_min("expression")] |
| Live Number/DateTime maximum | @LibertasRangeMax("expression") |
#[libertas_range_max("expression")] |
| Data-backed choice (index or key) | @LibertasEnumSource("$.items") |
#[libertas_enum_source("$.items")] |
| Stable choice key of a List member | @LibertasKey |
#[libertas_key] |
| Asked before a List member is added | @LibertasAskOnAdd |
#[libertas_ask_on_add] |
The inline-description row is compiled into the
Display Template; the others are schema. A summary value
is a template promote
rule, and flattening a flatten rule; neither has a source
attribute.
Hidden suppresses the complete value subtree, including list elements and
controls, nested fields, summaries, and promoted values. Hidden data remains
part of the typed value for validation and protocol encoding. flatten does
not expose hidden descendants. promote and flatten apply to the node their
rule selects, are not inherited through ordinary attribute resolution, and
never override Hidden.
ReadOnly restricts the user only: clients present the value and offer no
ordinary editing control for it. It never restricts the platform or the
application. Schema initialization (Fixed, Default, localized defaults),
CopyFrom from a Response or the reference context, EditRequest values,
calculation and link-return CopyTo results, draft restoration and the application's own
handling of the submitted value all write a ReadOnly field exactly as they
write an editable one. A value that must not change after initialization is
Fixed, not ReadOnly. Hidden fields are ReadOnly in effect, since no editor
shows them, and are written the same way.
ReadOnly on an Array/List prevents changing its membership or order: no Add,
Remove, or reorder action may mutate that list. It does not recursively make
member values read-only; members and their fields keep their own editability.
To prohibit member edits, mark the applicable member type or fields as well.
Context menus, swipe actions, pickers, and deferred actions must enforce the
same list-owner rule at both presentation and execution. A promoted row does
not change which list owns a removal or reorder operation.
For promotion, flattening, and nested Union layout, see Promotion, flattening, and Union selectors.
Add-time values. AskOnAdd marks a value the person gives before a List
member exists: a field of a List's member Table, reached from the member
through struct fields, never through another List; or a List field whose member
has a value of its own, which asks the member itself. A field's marker applies
wherever its Table is such a member. Any number of a member's fields may carry
it, of any type that has a value of its own: a scalar (String, Number, Boolean,
DateOnly, DateTime, Bytes), an Enumeration, Bitflags, or a Union. When the
person chooses Add on the List, the client first asks for every marked
value; confirming creates the member with those values and every other field at
its normal initialization, then shows the new member as the List's layout shows
any new member: a summary opens its item page, and
inline rows, a table, or an accordion show it in place. Without every value, no
member is added, and Cancel adds nothing.
The platform builds the Add step. The App declares nothing beyond the
markers. One marked choice (a Union, Enumeration, or Bitflags) is asked in the
one-field popup
form of its chooser; anything else, one scalar or several values, is one
compact Add editor holding exactly the marked nodes, in the member's
declaration order, each drawn, labeled, and validated as in the member's own
editor, with its allowed choices, RangeMin/RangeMax, Pattern, validation
rules, and calculations, and the member's display template applied. A marked
Union asks its variant only; its payload is edited after Add.
- A value is a real answer: a Union variant, an Enumeration member, at least one Bitflags flag, or a scalar that is neither null nor an empty String or Bytes and that passes the field's own validation. A Nullable's null answer is not a value here.
- Defaults prefill: a marked value's
Defaultis preselected in the Add step; the person still confirms it or changes it. - Allowed sets: a marked choice whose allowed set is a singleton takes that choice, as single-choice rows do, and is not asked; when nothing is left to ask, Add adds at once. An empty allowed set offers no Add.
-
**The person's Add only:** a member that AI Import, CopyTo, an EditRequest,
or the App adds is unaffected, and after Add each value is an ordinary field;
whether the person may change it is a template
lock's, a separate concept. - The categorized layout's Add document is this rule applied to its kind.
-
**Commonly paired with [`lock`](#presentation-locks).** The usual purpose of
AskOnAddis a value that decides what the member is, such as a document's form: the person gives it once at Add, and a templatelock(on a Union,lock: "variant") then keeps the person from changing it, so the item editor shows the chosen form and never offers to switch it.
Studio rejects AskOnAdd on a Table or List field of the member, on a field
below another List of the member, on a node that is itself Hidden, ReadOnly,
Fixed, or OutputOnly, on a List whose own membership is ReadOnly or whose member
is a Table, and on a marker no editable List member reaches. Where a marked
type is also used below a protected node, such as inside a ReadOnly view, no
person can Add there and the marker has no effect in that use, so one natural
type serves both.
EnumSource belongs on an unsigned integer that stores the selected List
item's zero-based index. When the List's member declares a
key field, the host instead has the key's type
and stores the selected member's key value. It never stores the item itself.
A stored index or key that matches no current member leaves the field
unanswered, like a required field without a value; clients never remap it to
another member by position, key, or label.
AI tool-call descriptions carry its value-representation hint, which names the source List path
without enumerating its current items.
Android, iOS, and Studio present a single-choice picker in source order, select the
member at the stored index or with the stored key, and write the chosen
member's index or key. Every choice is labelled by its text alone, with no
position number: in precedence, the
member's Display Template title; else its promoted
field's value or, for a scalar member, the scalar's display value, formatted
as the tree formats it (money and explicit formats keep their normal display);
else the member's Display Template label; else its display name. A composite
member never reads through a nested value. A Hidden member is still listed so
that indexes stay stable: Hidden affects the member's own row, not the picker.
A picker entry draws its source member as the source List draws it. When the
source List has a summary or table rule, each entry shows that rule's paths
or columns for its member, with the text above as the entry's first line, and
the filter matches that text too. The rule applies to the picker even when the
List itself is Hidden and never drawn. Without such a rule the entry is the text
alone. A host's chosen value, drawn outside the picker, shows only that text; a
hosting item that needs the fuller display carries it as its own ReadOnly,
OutputOnly field, normally of the same display-only type the source member uses,
so the item and the picker entry read the same.
A List host is edited with a multiple-choice chooser over the same members, in the same order and with the same labels, instead of List Add, Remove, and reorder controls. Each member the host holds is checked. Choosing a member appends its index or key; unchoosing one removes that element; the other elements and their order are unchanged. An element that names no current member is listed after the members as an unavailable, checked entry, so the person can remove it; it is never remapped. A ReadOnly host shows the same chooser disabled.
A chooser with more than twelve permitted choices — this EnumSource picker, an Enumeration picker, a Bitflags multiple-choice picker, or the Union variant chooser that opens when a promoted or unpromoted value is tapped — shows a text filter field above its list. Typing filters the visible choices by a case-insensitive substring match against each choice's localized display name and description; it never changes the permitted set, its order, or the selection, and clearing the field shows every choice again. The current selection stays visible and selected even when the query would otherwise filter it out.
The hierarchical single-field request builder also presents a choice-only Union/Enumeration hierarchy as expandable groups. It derives grouping from the App's types and removes groups with no eligible leaves recursively. This is the default compact request presentation for that shape; ordinary content folding and the Display Template do not change the underlying selection type, constraints or submission behavior.
The source uses a Libertas Schema Path in the current data tree; it does not read the interaction reference context. An explicit Default or Fixed retains ordinary initialization; otherwise the selection is unanswered. A non-nullable host whose source offers exactly one selectable member (a member the picker offers: with its key set, within the host's bounds and step) takes that member when it is unanswered, under the singleton rule of allowed sets: when its editor initializes it, and whenever the source changes while it is still unanswered. A stored index or key is never replaced, and a List host is never filled this way. Nullable absence remains valid. A present selection must identify a current List item and satisfy the field's numeric bounds and step, including dynamic bounds. An empty or unavailable source offers no selectable options.
Path traversal follows the shared resolution rules, including reusable named types, List/Union levels, and each enclosing use. A selector in a reusable type is validated in configuration, protocol, and stored data contexts where it occurs; its declaration does not introduce another root.
Cancel preserves the previous value. A source-row replacement, reorder, removal, label change, or change of a member's key value while the picker is open rejects that pending selection; reopen the picker to choose from the current list. A saved index remains a position, not a persistent record identity. Apps that need record identity must resolve the selection against the appropriate accepted-data revision.
RangeMin and RangeMax constrain numeric controls and date/time choosers
with the same inclusive limits used for final validation. Their expressions
must produce bounds compatible with the control’s semantic type; see
dynamic numeric ranges for
result typing, unavailable bounds, and intersection with static constraints.
UIIncludeDescription is a no-argument, client-only presentation marker. The
UI node keeps its localized short DisplayName and also renders its localized
long Description inline. Without the marker, clients retain their ordinary
compact short-name presentation, including a description tooltip where one is
available. The marker does not affect stored values, requests, or wire encoding.
Studio compiles it into the Display Template as the
include_description hint, which clients read at runtime.
For Unordered, AI descriptions carry its value-representation hint.
MIME-aware Bytes presentation
A Bytes field with effective MimeTypes uses
one media control in Android,
iOS, Studio, and the pure Web client. Its compact row shows the field name, a
bounded thumbnail for a safely decodable raster image or a generic media
icon, the exact MIME and content size when defined, and no local path.
Undefined media shows “No file selected.” It never displays Base64 or hex.
Ordinary Bytes keep their existing editor.
Editable media rows offer Choose file, Remove, and Take photo when the field accepts an image format that the client's camera can produce. The camera stores its output as a private temporary file and the editor holds a local link until outbound encoding. Camera permission denial or cancellation leaves the previous value intact; Choose file remains available. Read-only rows offer preview when supported. Tapping or clicking the thumbnail opens a larger platform preview. A corrupt or unsupported image shows the generic icon and an error when previewed; clients do not render SVG or arbitrary HTML as an inline image. Local paths and temporary link identifiers are not displayed or sent to the Hub. Camera availability is conditional on device, browser, and host permissions.
A missing temporary file during draft recovery makes the field undefined and identifies it for reselection. A file that disappears at submission produces a field error and stops that attempt. The draft and cleanup rules and LMB1 contract own those behaviors. The standard media control and explicit preview require no template rule.
Inline media size
A Display Template media rule draws the field's content
larger, inline in the same media row. Without a rule, or with
"max_height": "thumbnail", the row keeps the bounded thumbnail above. With
small, medium, or large, the media draws on its own line below the
field's label and summary, before its controls:
-
**Fit.** It uses the full width available to the value in its pane or
form-gridcell, preserves its aspect ratio, and is never drawn larger than its intrinsic size, one image pixel per logical point (dp on Android, pt on iOS, CSS px in Studio). Its height is capped by the height token: about 25%, 40%, or 60% of the nearest pane's visible height. A wide image therefore fills the width; a tall one stops at the cap and is centered. The cap follows the pane as it resizes. - **Images.** The raster formats the thumbnail decodes draw inline. SVG and every other vector or markup format are never rendered, at any size.
-
**PDF.** `application/pdf` draws its first page, rasterized at the fitted
size, under the same fit and cap; the page's intrinsic size is its media box
in PDF points. Android uses the platform
PdfRendererand iOS PDFKit. Studio has no PDF page renderer and shows the generic media icon for PDF. A PDF that is encrypted, has no page, or fails to render shows the generic icon. Thethumbnailsize keeps its existing behavior and does not render PDF pages. -
**Video.** Video is reserved for a later inline player. Until a client
implements it, a
video/*value keeps the thumbnail-size generic media icon under every token. - Interaction. Tapping or clicking the inline media still opens the full platform preview, and the row's Choose file, Remove, Take photo, and Preview controls are unchanged. Corrupt or unsupported content keeps the generic icon and the existing preview error. Decoding happens off the main thread, and a client may show the generic icon until the bytes are decoded.
media is presentation only: it never changes the stored bytes, their MIME
type, validation, CopyFrom or CopyTo, encoding, or AI Import.
Promotion, flattening, and Union selectors
Two Display Template members change how a node's children sit beside its row:
promote puts one child field's value on its parent row, and
flatten draws a node's children at that node's own indentation.
Neither changes the encoded data hierarchy.
when may change either per
device; no data condition, switch case, or pseudo-state sets either.
promote is valid only on a rule that selects a field of a struct (a Table),
for example ^<TopicRow>.status, which selects that field wherever the
TopicRow type is used, or a strict path to one field. Studio rejects
promote on a rule that selects a type, a List member, a Union branch, a
function parameter, which has no parent row, or a Response. Its value is true, false, or { "use_parent_name": Boolean }; true means
use_parent_name true, and false turns promotion off for the field, so a
higher-ranked rule can undo a lower one. promote never flattens the field's
children: select the node with flatten when wanted. Both members are authored
only in display.json. See removed annotations
when migrating older source.
Field promotion
On a field directly owned by a Table, the first visible promoted field in schema order becomes the Table row's value instead of occupying a separate child row. Additional promoted fields remain ordinary children. Promotion has no effect when the field's owner is not a Table, including a Union's selected branch.
The promoted field supplies the row value. Its use_parent_name option chooses
the row label, uniformly for scalar, Table, List, and Union fields:
-
`true` (the default): use the owning struct's displayed name, which is its
labelwhen a rule gives it one. -
`false`: use the promoted field's own displayed name, which is its `label`
when a rule gives it one. A
labelon the owning struct then shows nowhere, and Studio warns.
When the owning struct has a title, the field is not
promoted and keeps its own row, and Studio warns that the promote has no
effect.
For a promoted Union, the selected variant's display name supplies the row value;
the option changes the label, not that selected value. Tapping that value opens
the choice selector, unless the Union shows inline choices.
Selecting another variant replaces the applicable fields.
The selector remains available even when the selected variant has no fields.
A List member whose chosen row label is static text (a displayed name, or a
label in the "KEY" form) appends its one-based position to it (for example,
“Household #1” with the default or “Marital status #1” with use_parent_name
false), including for a Union. A label taken from the member's data (the
{ "field": … } form) already tells the members apart, so it gets no
position.
A scalar promoted value keeps its normal formatting and editor. A composite
one keeps its children, and a List retains its entries and Add action.
An unpromoted Union retains its field title after selection. For example, “Marital situation” remains the label and “Married” is the selectable value. A Union with a scalar payload also identifies the selected branch while keeping that payload editable. Branch selection remains available independently of promotion, including after choosing a fieldless variant.
Name option
{ "select": "^<Schedule>.starts_at", "promote": true }
{ "select": "^<HouseHold>.marital_status", "promote": { "use_parent_name": false } }
Both rules promote a field, the first labelled by its owner's name and the second by its own.
Example: use the parent name to identify the record
A struct has the display name Next schedule. Its Starts at field is
promoted with use_parent_name true (or promote: true), and duration is
an ordinary sibling field. For a start time displayed as 09:00 and a duration
of 30 minutes, the rows are:
Next schedule: 09:00
duration: 30 minutes
The parent name tells the reader what the time and duration describe: the next
schedule. Using the field name instead would produce Starts at: 09:00, which
describes the time but loses the record's identity when several schedules or
other timed activities appear on the same screen. The promoted start time acts
as the schedule's summary value, so the parent name is useful here.
Example: use the field name to identify the property
A struct has the display name HouseHold. Its Marital status field is a
Union promoted with use_parent_name false. The selected variant has the
display name Married, and Number of dependents is an ordinary sibling
field. The rows are:
Marital status: Married
Number of dependents: 2
The field name tells the reader exactly which property “Married” answers.
The default would produce HouseHold: Married; “HouseHold” identifies the
record but does not name that property. Keeping Marital status makes the
selected choice understandable without requiring the reader to infer the
question from its answer.
In both examples, the option changes only the promoted row's label. The field still supplies its value, sibling fields retain their normal placement, and the Union selector still changes the selected variant.
Flattening
The rule member flatten applies to a struct, an array (List), or a Union. Its
rule selects the node by type (^<Address>, applying wherever the type is
used) or by field (^<Household>.address). It is a rule member like layout,
not a style member, so no data condition, switch case, or pseudo-state can
change it; when may. Its value is true, false, or
{ "name_row": false }. false undoes a flattening that a lower-ranked rule
set in the cascade, such as a dependency
package's template for its own type: the node draws with normal indentation
and its name row.
-
**Struct.** `true` draws the struct's fields at the struct row's indentation
and keeps the struct's name row.
{ "name_row": false }also removes that name-only row; the fields stay visible and editable at the flattened level. A List member whose struct row is removed keeps its position and membership controls, and the fold control of a removed row passes to the first row the struct still produces, as folding describes. - **Array (List).** The List row keeps its title, count, and Add action; its members sit at the List's indentation instead of one level in. Each member keeps its own row, position, and actions.
- **Union.** The selected branch's fields sit at the Union row's indentation; the selector stays visible and editable.
Studio rejects flatten on any other node kind; on a List member (…[])
and on a Union branch, because neither has a row of its own to flatten, with
a message that names the owning List or Union; name_row on a List or a
Union; and { "name_row": false } on a struct that has a
title, because one removes the name row and the other
replaces it. Without a flatten rule, children keep normal indentation;
protocol page roots follow the automatic root alignment.
flatten never promotes a field, including a field that references the
flattened type: select that field with promote when wanted. Hidden still
suppresses the whole subtree.
{ "select": "^<Dependents>", "flatten": true }
{ "select": "^<Zone>", "flatten": { "name_row": false } }
For example, these independent rules combine promotion and flattening:
Household (Table)
dependents (display name: Dependents; type: Dependents)
Dependents (Union)
No
Yes (Table)
count (Number)
{ "select": "^<Household>.dependents", "promote": true }
{ "select": "^<Dependents>", "flatten": true }
With Yes selected, the schematic rows are:
Household: Yes [tap value to change choice]
Count: ...
Removing the flatten rule indents Count under Household. Removing the
promote rule restores a separate Union row under Household. The choices
can be repeated at each nesting level. An unrelated sibling is not flattened
because another field is a promoted or flattened Union. Existing expansion,
visibility, and read-only rules still apply; neither member makes a read-only
selector editable.
Actions on a promoted or suppressed row
Attributes that add an interaction to a value's row, currently Link and
LLMDocTrans (AI Import), follow that value when promote or flatten
changes its presentation. A promoted field's action appears on the owning
Table's visible row. If a Table has no row of its own, because flatten with
{ "name_row": false } removed it, its action appears on the first visible
row that represents that Table; when the Table is the selected payload of a
promoted Union, this is the Union's owning row. A List member keeps its own
row and its own actions, also inside a flattened List. If several values
represented by one row declare actions, the row offers each applicable action;
a child Link takes precedence over an enclosing Link for the row tap
target. With a label on the struct, a promoted value's
link stays on the row; with a title, a link on the struct stays on the title
row.
Only the affordance moves. Each action retains its declaring value, path,
request context, eligibility, and pending state. Neither promote nor
flatten copies an attribute to another schema or value node. Hidden
suppresses the action with its value subtree; ReadOnly, Fixed, and other
action-specific restrictions still apply. Data, validation, access, and
lifecycle attributes do not move.
The Import with AI action of a classifier follows this placement. A classifier that is the Request root has no row of its own, so its action is the editor's first row, a full-width action. Category Lists keep their ordinary layout; an empty one shows its header and controls with no rows, and no Display Template rule is specific to classifiers.
Presentation only
Promotion and flattening do not change the schema or encoded data hierarchy.
CopyFrom, CopyTo, EnumSource, validation paths, and backend field access
still use the original structural paths, including every nested Union/table
level. They do not supply a default, make required fields optional, or disable
fields based on another answer.
Protocol actions remain owned by the enclosing protocol variant, even when its header is another Union. See protocol transaction stack. Separate same-indent Union rows retain their own edit/removal ownership; sharing an indentation level does not make them the same data object.
Data-titled rows
A row is data-titled when its label comes from the data it shows, such as a calculated line labelled by its description with its amount as the value. Two independent rules express it:
-
a Display Template [`label`](#labels-and-titles) rule on the row's struct, usually
"select": "^<Type>"or, for a List's members,"^<Response.list>[]", names the field whose value labels the row; - a [`promote`](#field-promotion) rule on a child of the struct promotes that child's value into the row.
{ "select": "^<LineItem>", "label": { "field": "description" } }
{ "select": "^<LineItem>.amount", "promote": true }
With description Hidden, each LineItem renders as one row, "W-2 wages
$60,000.00", and its other visible children render beneath it. The names are
schematic.
Each rule also works alone: a label without a promoted child labels a row
whose children render beneath it, and a promoted child without a label keeps
the struct's display name. With the default use_parent_name, the row's
label is the struct's displayed name, which is the label text; with
use_parent_name false it is the child's own name, the label shows nowhere,
and Studio warns. Folding, flattening,
links, and a List member's membership controls stay on the row as they do on
any other row. A row that should show only text, with no value, such as a
heading taken from the data, uses title instead; a
title takes no promoted child, so the child keeps its own row.
Both rules are authored only in display.json. See
removed annotations when migrating older source.
SuppressLabel: label-free value rows
@LibertasSuppressLabel in TypeScript and #[libertas_suppress_label] in
Rust suppress the visual label contributed by the selected node. The parser
result carries {"Name":"SuppressLabel","Value":"true"}; Studio compiles
it into the Display Template as the Boolean
suppress_label hint and removes it from the final schema. A template rule
can set false to restore the ordinary label under the
cascade. The rule does not propagate to children.
| Selected node | With suppress_label: true |
|---|---|
| Scalar value, including an Enumeration | Omit its row label; keep its value and editor. |
| Struct | Omit its own row label; keep its promoted value, visible children, and actions. A struct without a promoted value keeps its children and any row controls. |
| Union | Omit the Union's own label; keep the localized selected variant value, selector, visible payload, and actions. An unselected editable Union keeps its selection affordance. |
| List whose effective member type is a struct (Table) | Omit the member name and position contributed by the List to each member row; keep each member's own template label or title, promoted value, visible children, and controls. Keep the List's heading, count, and membership actions. |
Other List member types and non-value declarations are invalid hosts. Studio rejects them; source syntax does not extend the placement rules. List suppression retains the previous List-of-struct behavior, rather than suppressing the List's own heading.
The hint suppresses a visual label, never a value or subtree. It does not
change folding, editing, links, menus, membership permissions, help, or the
semantic name used for accessibility and editor dialogs. A control without a
visible label still has its localized accessible name. On a promoted row,
only its displayed label is omitted; the promoted value and its declaring
link remain available under the action rule.
Hidden continues to suppress the entire presentation subtree and wins
before every template rule.
An explicit label or title and suppress_label are mutually exclusive on
one selected node, including false; List suppression can still coexist
with a label or title on its distinct member nodes. The old source names
@LibertasSuppressTitle and #[libertas_suppress_title], parser attribute
SuppressTitle, and template member suppress_title are retired authoring
forms. Modern compilation rejects them and names their corresponding
replacement; they are not aliases.
A reader of an already validated, retained compiled Display Template accepts
its historical suppress_title Boolean solely for the former List-of-struct
member-name and position behavior. At document ingress, translate that member
to suppress_label only when the rule has no suppress_label; an explicit
modern member, including false, wins. Normalize a runtime copy without
rewriting the retained document bytes or their digest. This isolated artifact
reader does not accept the old spelling in new source or display.json, and
renderers consume only the modern hint.
Protocol page heading and top bar
The title naming the active protocol page is its screen heading on every client, including editable Requests, workflow editors and Responses. Its placement follows the available top-bar space below. When the first content row retains the heading, use the platform's heading typography: larger, semibold text in the normal foreground color, wrapping for long localized titles, and extra space before the fields below. Omit the ordinary field-row divider and expose the title as an accessibility heading. Keep its existing controls and the typography of nested fields. This treatment is platform-owned and requires no App attribute.
The content heading, when present, and the page's top-level content share the
same leading edge within normal page or pane padding. Neither the root protocol
Union nor its selected Request or Response payload adds an indentation level. This automatic root
alignment applies to full-page Requests, workflow editors, and Responses, and
independently inside each master-detail pane. The master-detail arrangement
uses the full content width within the working surface's normal padding,
without a root indent around the pair of panes. Root alignment is independent
of label, title, and suppress_label, and requires no flatten rule;
flatten: false retains this normal root alignment. Nested structs, Lists,
and Unions continue to follow their ordinary indentation and template rules.
When the displayed root protocol Union has suppress_label: true, the page
heading and its native top bar use the localized selected variant name,
without the protocol label. Resolve an explicit title or label rule on
the displayed Request or Response variant first, using the same rendering
and empty-value fallback as labels and titles. Otherwise
use that variant's localized display name. With no selected variant, retain
the ordinary localized protocol title rather than a blank heading.
The title belongs to the active displayed value. Apply this rule to editable Request pages, including workflow editors, as well as Response pages; do not take an editor's title from its originating Response. An explicit variant rule also titles that active page when root suppression is absent. For example, a protocol labelled “Tax return” with selected variant “About you” uses “About you” as its page title under:
{ "select": "^<TaxProtocol>", "suppress_label": true }
The selector in an App must use its actual protocol type name. No duplicated resource key or field is needed solely to remove “Tax return”.
Adaptive heading and Actions placement. Apply this rule on every client
to a protocol page with its own screen top bar, at any screen width. It is
independent of the pane threshold for two-panel layouts. Actions is the
control for the displayed variant's eligible NextRequest choices, under the
request-selection rules;
it is separate from an editor's primary Next/Submit and previous-action
controls. Render its full localized caption, resolved by the
Actions caption precedence (Actions by default, or
the active page's button label), and retain the platform's normal
button target and spacing. The fit rules below measure that resolved caption.
They place Actions while it keeps this default heading placement; an
action_controls placement of secondary moves it into the
named-control region instead.
Measure the title and button with their actual localized text, current font, weight, and accessibility text size. Fits means the complete text renders without clipping or ellipsis in the normal top-bar layout; never reduce the font size or button target to make it fit. Available title width is the actual title-text slot after reserving system insets, navigation and other controls, icons, gaps, and padding, including any space required by native title alignment. A title icon does not count as title-text space.
Let T be the measured full title-text width. Its minimum reservation while
Actions occupies the top bar is min(T, 8em), where 1em is the current
rendered top-bar title font size. Thus 8em is 160 layout units for a title
font of size 20 and scales with accessibility text size; a short title needs
only its full measured width. This reservation is a minimum, not a fixed or
maximum width: the title uses additional available space.
Evaluate the following cases in order, with Actions included in the proposed top-bar layout:
| Available space | Top bar | First content row |
|---|---|---|
| Complete title and complete Actions button fit | Full title and Actions | Suppress the redundant page-heading row. |
Actions fits and the title-text slot is at least min(T, 8em), but the complete title does not fit |
Title with end ellipsis as needed, and Actions | Retain the full, wrapping title; remove Actions from this row. |
| Actions cannot fit while preserving that title reservation | Ordinary compact title, with ellipsis as needed | Retain the full, wrapping title and Actions at its trailing end. |
An ellipsized title never qualifies for suppressing the content heading. With no eligible action, omit Actions and apply the full-title fit test alone. Place the heading's Actions control in one of these two locations, never both. If Actions cannot fit beside the content title, wrap it beneath the title within the heading area, before the separate named-control region. Keep its full caption and prominent treatment in either location. This independent Actions rule applies only when no optional named control is visible and eligible. With one or more, place the complete button group under the named-control rule: full title and all buttons in the top bar when they fit, otherwise all buttons on a new row below the heading. Suppressing the heading removes only its redundant title presentation: retain any Union selector, promoted value, other row control, validation message, and requested inline description in the content, with their ordinary behavior. Content retains the root alignment and normal page padding above even when its heading moves to the top bar.
Description and accessibility. A top-bar title supports the same long press as a regular title row and opens the same localized description popup. Keep a keyboard-accessible and screen-reader-accessible Show description action when a description is available; help must not depend on hover or long press alone. Moving the heading preserves its full accessible name and heading semantics. If the content heading remains, expose that as the page heading without adding a duplicate heading for the compact top-bar title. Description lookup remains bound to the same active page when its title is shortened or relocated.
In a master-detail arrangement, only the leading page uses the working surface's top bar. The trailing page retains its own content heading and eligible Actions; selecting it does not take over the shared title or toolbar. A page shown on the whole working surface after narrowing uses the same placement rule for its own title and Actions. Re-evaluate the complete candidate set when the available width or height, title, control availability or captions, template, locale, font, text size or device conditions change, including rotation and window resizing. Restore controls to the top bar when they fit again. Measure actual localized controls and native slots, never a device-category breakpoint or a fixed estimate for system controls. Preserve logical order in right-to-left layouts and the full accessible names. Re-layout preserves values, focus, interaction and reference-context ownership, and pending-operation restrictions, and sends no request.
Presentation only
Encoding, paths, CopyFrom, CopyTo, EnumSource, validation, comparison,
digests, ordering, uniqueness and generated documents are unchanged. The
marker supplies no default and makes no field optional.
Date and time
| Type | Raw meaning | Presentation |
|---|---|---|
LibertasDateTime |
Epoch seconds | Viewer-local date/time; optional timezone in Format. |
LibertasTimeOnly |
0 <= seconds < 86400 |
Time chooser, no date. |
integer plus DateOnly |
Valid decimal Gregorian yyyyMMdd |
Calendar-date chooser, no timezone. |
number plus TimeInterval |
Duration seconds | Duration editor/label. |
Never convert DateOnly through an epoch or timezone. Defaults, fixed values,
requests, and runtime data must all be valid calendar dates.
AI descriptions carry each type's value-representation hint,
such as a YYYY-MM-DD string for DateOnly. These representations leave the
raw App values in the table above unchanged.
Display formats
Format accepts one double-quoted, programming-language-neutral Unicode LDML
pattern. It never changes parsing, storage, comparison, bounds, defaults,
requests, persistence, or chart geometry. Studio validates the pattern, compiles
it into the Display Template as the format hint, and
removes it from the final schema; clients read the display pattern only from
the template, where a rule may choose another portable pattern per device.
/**
* Invoice total
* Shows the billed total with two decimal places and parentheses when it is negative.
* @LibertasNumber(type=f64)
* @LibertasFormat("#,##0.00;(#,##0.00)")
*/
amount: number;
/**
* Service date
* Identifies the calendar day when the service occurs.
* @LibertasNumber(type=u32)
* @LibertasDateOnly
* @LibertasFormat("yyyy-MM-dd")
*/
serviceDay: number;
/// Invoice total
/// Shows the billed total with two decimal places and parentheses when it is negative.
#[libertas_format("#,##0.00;(#,##0.00)")]
pub amount: f64,
/// Service date
/// Identifies the calendar day when the service occurs.
#[libertas_date_only]
#[libertas_format("yyyy-MM-dd")]
pub service_day: u32,
The portable number grammar supports 0, #, ., ,, positive/negative
subpatterns separated by ;, %, ‰, E, affixes, and apostrophe quoting.
Currency symbol ¤ and currency code ¤¤ are supported with explicit
Money metadata. Significant digit @, padding *, wider currency tokens,
and rounding-increment digits are not supported.
Date fields are G y M d E; time fields are H h m s S a; z is available
only for DateTime. DateOnly accepts only date fields, TimeOnly only time fields,
and one pattern cannot combine H with h.
References:
Money
Use Money on a signed i64 Number to store an exact amount in minor units.
The currency code and scale are explicit schema metadata: USD/2 means that
integer 12345 represents USD 123.45. Rust storage and Avro remain i64,
not a decimal string or floating-point amount. Clients may retain an integer
string internally to avoid a JavaScript Number conversion.
AI descriptions carry the Money value-representation hint with the declared currency and scale.
/// Wage amount in USD cents.
#[libertas_money("USD", 2)]
pub wages: i64,
/** @LibertasNumber(type=i64) @LibertasMoney("USD", 2) */
wages: number;
Both annotations emit {"Name":"Money","Value":"USD/2"}. The code is three
uppercase ASCII letters identifying the currency; the scale is an integer
from 0 through 6. Choose the currency's applicable minor-unit scale, such as
USD/2, JPY/0, or KWD/3. Locale and currency-data updates never alter that saved
meaning. This uses CLDR currency formats and metadata;
CLDR does not prescribe the application's numeric storage type.
Without Format, the viewer's locale supplies currency placement, symbol,
digits, and separators, with the declared scale. An optional currency pattern,
for example ¤#,##0.00;(¤#,##0.00) or ¤¤ #,##0.00, controls presentation only.
Currency-pattern display rounding uses half up; it never rounds the stored
amount or implements tax-law rounding. Quoted ¤ remains literal text.
Money patterns cannot use percent, per-mille, or scientific notation.
Android and iOS edit money in place. The editor shows the currency code beside
an ungrouped amount using locale digits and its decimal separator. Committing
123.45 for USD/2 stores 12345. Grouping separators, currency symbols inside
the amount, excess fractional digits, and values outside signed i64 are
rejected without replacing the entered text. ReadOnly amounts remain visible
and cannot be edited. Nullability follows the ordinary schema contract.
Default, Fixed, NumberMin, NumberMax, and NumberStep are integer minor
units; the step must be positive. Dynamic bounds and ValidationRules also
operate on minor units and preserve exact numeric values. Currency identity
does not introduce conversion: applications own exchange rates and must keep
unrelated currencies separate. Money follows the
exclusions of numeric semantic attributes.
Charts retain each source integer for labels, tooltips, category identity, and accessibility. Coordinates and synthesized scale ticks use the chart's normal floating-point geometry; their presentation applies the declared currency and scale. Shared scales and paired interval endpoints require matching Money metadata. A display pattern can intentionally hide lower digits without changing the source amount.
Physical units
PhysicalUnit accepts a canonical CLDR core unit identifier on a Number:
/**
* Daily water depth
* Sets the depth of water applied during one day.
* @LibertasNumber(type=f64)
* @LibertasPhysicalUnit("millimeter-per-day")
*/
rate: number;
/// Daily water depth
/// Sets the depth of water applied during one day.
#[libertas_physical_unit("millimeter-per-day")]
pub rate: f64,
Simple, powered, product, and generated compound identifiers are valid under the UTS #35 unit-identifier contract. All physical units, including compound units, are localizable. Keep the canonical identifier in the schema; the viewing locale supplies unit presentation in ordinary UI and charts. Translate field names, descriptions, chart titles, and other authored text through the App's locale documents, not by changing unit identifiers or appending translated unit strings to numeric values. Number-wheel editors show the localized unit in place of the generic “Level” label, including when the field has no initial value. Prefer the full unit name when it fits the available label width; otherwise use its localized short form. Schema validity is independent of a particular client ICU catalog. A client may omit unit decoration when it cannot format an otherwise valid identifier; the numeric value and metadata remain intact.
The identifier states the provided unit and never performs conversion.
percent raw 50 means fifty percent; it does not rescale 0.5. Format may
coexist and formats only the numeric portion. PhysicalUnit follows the
exclusions of numeric semantic attributes.
AI descriptions carry the PhysicalUnit value-representation hint with the exact identifier.
Bitflags
Bitflags project a localized Enumeration onto an ordinary integer. Each nonzero catalog member is a unique single bit and one zero label is allowed. The field may combine declared bits but cannot contain undeclared bits. Clients show localized selected labels and use a multiple-choice editor. The integer wire value is unchanged.
AI descriptions carry the Bitflags value-representation hint with the declared flag values and available localized member descriptions. Author the meaning and consequences of each flag in its member comments.
Bitflags follows the exclusions of numeric semantic attributes.
String resources and formatted output
One package-owned catalog supplies notification templates, permission reasons,
data-name templates, FormattedText, and formatted chart labels for all
functions and types in the App. Task package context identifies the catalog;
encoded tuples carry no function/type owner or resource FQN.
Expanding long displayed text
Clients that shorten long displayed text behind Show more measure the full text using its actual font, styling and available width. If it fits within the combined height of the shortened preview, disclosure row and their spacing, display the full text and omit the disclosure. Do not reduce the text size to make it fit.
Otherwise, Show more reveals the complete text and Show less restores the preview. Reevaluate the fit when the value, locale, width or text size changes; a disclosure is unnecessary whenever the complete text fits. This behavior concerns displayed text, independently of structural folding in the Display Template. Clients that already display the complete wrapped text need no disclosure.
AI tool-call output
The Hub renders each FormattedText tool result as a localized JSON string
using the emitting package's resources and locale. Templates and the resource
catalog remain internal to rendering; they are not included in the AI tool
description or result. If a value cannot be resolved or rendered, its result
is {"error":"Unresolved FormattedText"} rather than guessed text.
This output view does not change the canonical LMF1 tuple. Read-only request
fields retain their original Base64 Bytes representation in tool arguments;
rendered text cannot replace that payload for copying or resubmission.
Ordinary Bytes remain unchanged. Declare FormattedText and document the
message's domain purpose; do not repeat templates or rendering instructions in
field comments for the AI.
Static read-only placement and request values
Every FormattedText node MUST be statically ReadOnly. Declare it on the
field or its effective declared type, or place the node inside a statically
read-only Table. ReadOnly on a List protects membership and order only; it
does not make its elements or their fields read-only. Mark the applicable
element type or field when that is the only enclosing protection.
This requirement applies to every use, including Response and SubscriptionData
output and dormant or TypesOnly declarations. An output role, hidden field,
or runtime editing restriction does not replace the static declaration. Shared
Studio validation checks resolved types, inheritance, containers, and
materialized external dependencies. Language parsers extract the attributes.
Request and SubscriptionRequest fields may carry these display-only values.
Their values come from CopyFrom or CopyTo, including a copy of an enclosing
record or request root; supplied EditRequest values; or Default/Fixed
initialization. A leaf does not need its own CopyFrom when one of those paths
supplies it. Apply the shared copy rules
to copied values. The client never asks the user to edit a formatted tuple.
An unanswered local field may remain undefined; a required undefined field
blocks submission. Null is valid only where the destination permits it.
FormattedText remains forbidden in function/configuration arguments, even
when statically read-only. Reusable types must satisfy this placement rule and
that runtime-use restriction on every path.
Persisting formatted record payloads
FunctionDataSchema record payloads may contain statically read-only
FormattedText. Persistence is supported, but generally discouraged for these
payload fields: FormattedText supplies runtime rendering, so prefer storing
structured domain values and constructing the formatted value at presentation
time. This is design guidance, not a schema rejection rule.
Formatted DataName labels remain supported outside this payload-design recommendation. Records use the transactional Data API's declared binary keys for identity, with localized human-readable labels constructed independently.
Declaring and encoding formatted values
/// Room condition reports
/// Gives occupants clear summaries of current room conditions.
/// @LibertasStringResources(STATUS_STRINGS)
const STATUS_STRINGS = {
READY: "Room condition reporting is ready.",
ROOM_STATUS: "{0,LibertasDevice} reports a room temperature of {1,number,0.0}."
} as const;
/**
* Room condition
* Describes the latest reported condition in one room.
*/
export interface RoomState {
/**
* Room summary
* Gives occupants a short overview of the room's latest condition.
* @LibertasReadOnly
* @LibertasFormattedText
*/
summary: Uint8Array;
/**
* Current condition
* Tells occupants what is happening in the room and includes the relevant measurement.
* @LibertasReadOnly
* @LibertasFormattedText
*/
status: Uint8Array;
}
//! Room condition reports
//! Gives occupants clear summaries of current room conditions.
//! #[libertas_string_resources(STATUS_STRINGS)]
pub const STATUS_STRINGS: &[(&str, &str)] = &[
("READY", "Room condition reporting is ready."),
("ROOM_STATUS", "{0,LibertasDevice} reports a room temperature of {1,number,0.0}."),
];
/// Room condition
/// Describes the latest reported condition in one room.
#[derive(LibertasExport, LibertasAvroDecode, LibertasAvroEncode)]
pub struct RoomState {
/// Room summary
/// Gives occupants a short overview of the room's latest condition.
#[libertas_read_only]
#[libertas_formatted_text]
pub summary: Vec<u8>,
/// Current condition
/// Tells occupants what is happening in the room and includes the relevant measurement.
#[libertas_read_only]
#[libertas_formatted_text]
pub status: Vec<u8>,
}
For ROOM_STATUS, encode argument 0 as the LibertasDevice object and
argument 1 as a unit-qualified temperature number. Do not pass a translated
state as a literal or resource-text grammar fragment. A closed Enumeration
instead uses {1,LibertasEnum,ActualEnumType} and an integer argument encoded
with that Enumeration's semantic type.
FormattedText always uses the public SDK's canonical Bytes encoding of a resource key plus ordered typed Libertas Message Arguments, including an empty argument list. Do not Base64-wrap it in App code. A String host would only encode the tuple and force the renderer to decode it again, providing no benefit; Bytes also keeps a sharp type distinction from literal String data and prevents authoring mistakes. A generic JSON byte codec may use Base64 as a transport spelling without changing the field's Bytes type. Runtime payloads never carry format templates.
TypeScript calls libertasFormattedText(resourceName, arguments) and Rust calls
libertas_formatted_text(resource_name, arguments). These public encoders
return the byte value directly. App code ordinarily does not decode it; the
presentation client resolves and renders LMF1.
Use zero-based LMF1 positions so translations may reorder arguments:
| Value | Form |
|---|---|
| Untyped value | {0} |
| Libertas device | {0,LibertasDevice} |
| Libertas enum | {0,LibertasEnum,StatusEnum} |
| Styled number | {0,number,0.00} |
| Plural | {0,plural,one {# item} other {# items}} |
Every translation preserves argument positions, types, styles, selector keys, plural offsets, braces, and apostrophe escapes. The encoded Libertas Message Argument type takes precedence over a conflicting template type.
Charts
This is a compact authoring summary. The normative vocabulary, validation matrix, renderer behavior, and audited text-field dispositions are in Client-side charting. Language parsers own native syntax and extraction; Studio's shared chart validator owns portable semantic validation.
Chart metadata belongs on a Response or SubscriptionData payload. A primitive chart is a List of row Tables; the List declares the mark, and direct scalar row fields declare channels.
/**
* Temperature reading
* Records the temperature observed at one time.
*/
export interface Sample {
/**
* Observation time
* Identifies when the temperature was observed.
* @LibertasChartChannel(x, tooltip)
* @LibertasChartScale(id=time, kind=utc)
*/
at: LibertasDateTime;
/**
* Observed temperature
* Reports the temperature measured at the observation time.
* @LibertasNumber(type=f64)
* @LibertasPhysicalUnit("celsius")
* @LibertasChartChannel(y, tooltip)
* @LibertasChartScale(id=temp, kind=linear)
*/
temperature: number;
}
/**
* Temperature history
* Lists temperature readings in observation order.
* ----
* Temperature reading
* Records the temperature observed at one time.
* @LibertasResponse
* @LibertasSubscriptionData
* @LibertasChart(line)
*/
export type History = Sample[];
/// Temperature reading
/// Records the temperature observed at one time.
#[derive(LibertasExport, LibertasAvroDecode, LibertasAvroEncode)]
pub struct Sample {
/// Observation time
/// Identifies when the temperature was observed.
#[libertas_chart_channel(x, tooltip)]
#[libertas_chart_scale(id = time, kind = utc)]
pub at: LibertasDateTime,
/// Observed temperature
/// Reports the temperature measured at the observation time.
#[libertas_physical_unit("celsius")]
#[libertas_chart_channel(y, tooltip)]
#[libertas_chart_scale(id = temp, kind = linear)]
pub temperature: f64,
}
/// Temperature updates
/// Describes the temperature observations that can arrive over time.
#[derive(LibertasExport, LibertasAvroDecode, LibertasAvroEncode)]
pub enum ChartProtocol {
/// Temperature history
/// Lists temperature readings in observation order.
/// ----
/// Temperature reading
/// Records the temperature observed at one time.
#[libertas_response]
#[libertas_subscription_data]
#[libertas_chart(line)]
History(Vec<Sample>),
}
Marks are point, line, area, bar, rect, rule, arc, text, and
polygon. Compositions are layer, hconcat, and vconcat; each direct
composition child is independently a chart.
The closed channel vocabulary is:
- positions:
x,x2,y,y2,xOffset,yOffset,theta,theta2,radius,radius2; - appearance:
color,fill,stroke,opacity,fillOpacity,strokeOpacity,size,shape,angle,strokeWidth,strokeDash; - text:
text,tooltip; - structure and identity:
detail,order,key; and - facets:
row,column.
A field has at most one scale-bearing channel. Secondary endpoints require
their primary and cannot own a scale. tooltip and detail may appear on
multiple fields.
Scale types are linear, log, symlog, sqrt, time, utc, ordinal,
band, point, and identity. Use continuous numeric scales for physical
quantities. DateTime position infers time; DateOnly uses timezone-free calendar
day geometry. Categorical Enumeration and Libertas object references retain
their raw identity while clients resolve localized labels. Plain Bytes are not chart scalars.
Every value that can become chart text keeps its semantic presentation source.
Use an Enumeration for a closed App vocabulary, FormattedText Bytes for dynamic
App-authored wording, and LibertasDevice, LibertasVirtualDevice,
LibertasUser, LibertasEndpoint, or LibertasLanDevice when the value is a
system object. A visible plain String is only genuine literal domain/user data,
such as a proper name or free-form annotation. This applies to axes, facets,
legends, text marks, tooltips, guides, and accessibility in every chart family.
Key-only and guide-less identity appearance Strings are the non-textual
exceptions. Never flatten an Enumeration or copy an object display name into a
chart-only String.
FormattedText remains a byte blob end to end. Its tuple carries a package-local
resource key and typed arguments, not a type owner or resource FQN; the task
identifies the package's one StringResources catalog. Generic JSON may encode
the Bytes value as Base64 at that transport boundary, but the chart field is
never a String. Encoding it as a String only to decode it for rendering has no
benefit and would erase the sharp schema distinction from literal text.
Source scale properties are id, kind, min, max, mid, zero, and
reverse; the extracted and final language-neutral spelling of source kind
is type.
Fields sharing a nonempty scale ID must have compatible scalar type, channel,
Format, and PhysicalUnit. Different but convertible units are still different
scales unless the App emits one common unit.
Chart appearance belongs to the Display Template. Studio
validates the complete chart metadata on the parser result, then compiles the
scale type of each ChartScale and Chart3DScale into a scale hint and
the position of each ChartGuide into a guide hint, and removes those
properties from the final schema; an attribute left with no property is
removed. Channels, scale IDs, domains, and every other chart property remain
schema. A template rule may also choose a chart palette. Clients read the
effective scale type, guide position, and palette only from the template, and
a template value is validated under the same chart matrices as the source.
Use @LibertasChartGuide(target=..., source=...) or
#[libertas_chart_guide(target = ..., source = ...)] only for an explicit
guide. Targets are x, y, xOffset, yOffset, theta, radius, color,
fill, stroke, opacity, fillOpacity, strokeOpacity, size, shape,
angle, strokeWidth, strokeDash, row, and column. Sources are scale,
value, span, or none; value is limited to x, y, theta, or
radius, while span is limited to continuous Cartesian x/x2 or y/y2.
The required properties are target and source. Optional properties are
position, title, domain, ticks, labels, and grid; positions are
auto, top, right, bottom, or left, and component switches are auto
or none. Prefer inferred scale guides unless the App needs an explicit
override.
3D charts
Use the parallel five-attribute namespace for 3D output:
@LibertasChart3D, @LibertasChart3DChannel, @LibertasChart3DScale,
@LibertasChart3DGuide, and @LibertasChart3DView in TypeScript, or
#[libertas_chart3d(...)], #[libertas_chart3d_channel(...)],
#[libertas_chart3d_scale(...)], #[libertas_chart3d_guide(...)], and
#[libertas_chart3d_view(...)] in Rust. Do not substitute the 2D attributes or
mix the two namespaces on one chart path.
Primitive marks are point, line, bar, surface, wireframe, and stem;
compositions are layer, hconcat, and vconcat. The channels are x, x2,
y, y2, z, z2, u, v, color, fill, stroke, opacity,
fillOpacity, strokeOpacity, size, shape, strokeWidth, strokeDash,
tooltip, detail, order, key, row, and column. Scale properties
mirror 2D scale properties; write kind in source and let extraction emit
schema type. A 3D scale also accepts logBase > 1 for kind=log,
non-negative growMin/growMax fractions for automatic continuous x/y/z domain
growth, and inclusive [0,1] padding on a bar base axis.
Map a requested chart family to geometry instead of inventing a mark token:
scatter and point clouds use point; bubble charts add size; 3D lines,
waterfalls, and spectrum traces use line; columns use bar; height maps,
non-uniform grids, and sampled mathematical functions use surface; wire grids
use wireframe; contours layer line over surface; and impulses use stem.
Use detail for homogeneous multiple series and layer for heterogeneous
series.
Bars accept discrete or sparse continuous x/y bases with a continuous z value.
x2, y2, and z2 may appear independently; omitted x/y extents and the z
baseline are inferred, while all six coordinates form an explicit cuboid. The
legacy any-orientation form remains valid when exactly two primary axes are
band scales and the sole continuous primary is the value axis. Surface and
wireframe u,v coordinates may be arbitrary finite topology values and may
contain holes.
View properties, in canonical order, are
projection,azimuth,elevation,zoom,fov,orbit,aspectX,aspectY,aspectZ,targetX,targetY,targetZ,pan,zoomEnabled.
Use perspective|orthographic for projection and booleans for orbit, pan, and
zoomEnabled. Set zoomEnabled=false to disable user zoom independently
without changing the authored initial zoom.
Azimuth, elevation, and target coordinates are finite; zoom and aspects are
greater than zero; and 0 < fov < 180. An orthographic view may carry fov,
which is ignored while orthographic. Keep camera choices in one Chart3DView
on a physical chart scene. Guide properties are required target,source plus
optional title,domain,ticks,labels,grid,tickCount,tickStep; detail is a
valid target. Do not invent fixed schema count caps; ordinary payload and
runtime resource admission still applies. Follow
Client-side 3D charting for the exact geometry,
topology, composition, validation, interaction, fallback, and resource rules.
Do not invent a camera or gesture mode, encode touch mappings in response
data, or add sensitivity or control-label fields. orbit, pan, and
zoomEnabled only gate independent client capabilities. The preview is
gesture-neutral; Explore 3D chart opens full-screen Chart Explorer, whose
fixed client vocabulary maps one-finger drag to Rotate, two-finger translation
to Move, and pinch/spread to Zoom. Tap-only and accessibility actions expose
Rotate, Move, Zoom, and Reset separately, without a Rotate/Move selector.
Point and stem-tip shape values use
circle,square,triangle,diamond,sphere,cube,pyramid,cylinder in that automatic
order; bar shapes use box,cylinder,pyramid,sphere. Size zero intentionally
suppresses or degenerates geometry while retaining the source row.
Always keep the ordinary structured data view meaningful. Charts are an additional presentation, not the only representation of returned data.
Translation workflow
Studio owns structural synchronization. A translation changes localized strings only. Preserve:
DocumentVersion, structure, arrays, ordering, numbers, and native names;- string-resource keys;
- schema identifiers; and
- every LMF1 argument position, type, style, selector, plural offset, brace, and apostrophe escape.
Use Studio's locale chooser and translation export/import workflow. Do not
manually invent a locale entry or restructure a generated .doc.json file.
Boolean answer controls
Required Booleans without a default are explicit, initially unselected choices. A non-nullable Boolean with an explicit Default permits a switch only when no custom labels exist. In an editor, nullable Booleans use one inline radio group for true, false, and explicit null. All three choices remain visible when null is selected; choosing the null option stores actual null. Use localized TrueText, FalseText, and NullText document properties for their captions, with localized Yes, No, and None when omitted. Display labels never change wire values. Every client arranges the choices by rungs 1 to 3 of the inline-choice ladder: beside the title, on one line below it, or one per line. ReadOnly and Fixed keep the current selection visible and disable editing.
Inline choices
InlineChoices shows an Enumeration's or Union's choices in its row as radio
controls, the way Boolean answer controls render,
instead of a value that opens the choice selector. Author it as
@LibertasInlineChoices in TypeScript or #[libertas_inline_choices] in
Rust. It takes no argument and emits {"Name":"InlineChoices","Value":"true"}
into the parser result; Studio compiles it into the
Display Template as the inline hint and removes it from
the final schema.
Place it on an Enumeration or Union field, or on an Enumeration or Union type declaration. Each declaration is read directly, not through effective-attribute resolution: a marker on either the field or its type enables inline choices, and a field cannot switch off its type's marker. Studio rejects the marker on any other effective type and on a Bitflags catalog, including a field typed by one.
- The radio controls offer the choices that remain after ConstrainedBy filtering, with their localized display names. Selecting one is the same edit as choosing it in the selector; an unanswered field shows no selected control.
-
For a Union, the selected branch's payload renders directly below its
chosen radio control, before the next choice, indented to the choice's
caption. Selecting another choice replaces the applicable fields. A Union
with any declared branch that carries data, every branch but a
Nilone, therefore stands one per line, as the ladder below states. Authors who want the choices on one line with the selected content below use thetabslayout instead. - The singleton allowed set and answered-Union rules of backend-constrained choices are unchanged.
- Visibility, read-only, promotion, and folding rules still apply; the marker does not make a read-only value editable.
Every client arranges the permitted choices by the first rung of this ladder that fits, measuring the localized captions and the title in the current font and text size:
- Beside. The title's readable minimum, the 16 dp gap, and one line of all choices with a 16 dp gap between each pair fit the row width: title and choices share the line. The readable minimum is the title's full width, or, if that is wider, the larger of its longest word and 40% of the row width.
- Below, one line. That line of choices fits the row width: the choices stand on one line beneath the title.
- **One per line.** Each choice gets its own line beneath the title, and a long caption wraps within its line. Choices never wrap onto a second horizontal line.
- **Selector.** More than 12 permitted choices use the ordinary choice selector with its filter, at any width.
The count includes the null answer of a nullable Enumeration and follows the
current ConstrainedBy set; the rung is chosen again when the width, text size,
or permitted set changes. A Union with any declared branch that carries data
skips the beside and one-line rungs: its choices stand one per line at every
width, so a branch's payload always has a place directly below its choice and
the choices never move when one is chosen. Only the declared branches decide
this, never the current selection or permitted set. Its payload renders
directly below its chosen choice; changing rung keeps the selected branch and
payload. A Union whose branches are all Nil uses every rung, like an
Enumeration. Every rung is one radio group named by the title, in the
permitted order, with arrow keys moving within it (mirrored for right-to-left).
Arrow keys move only between choices; the payload's fields follow their choice
in reading and focus order. A template rule with
inline: false still turns inline choices off. The marker changes presentation
only, never data, encoding, or paths. A client without inline support keeps
the choice selector.
Link rendering
A linked row may be highlighted through its enum/protocol association; the pane contract owns that use.
A link adds an affordance to its row. The upcoming
stacked=true form keeps this rendering and displays its reply above the live
sending surface with the mandatory system Close specified by the Links owner.
A link never removes the row's label or value and never replaces the value with a button. Whether
the value is editable decides how the link appears:
- **Value not editable:** every Response or SubscriptionData row, and a ReadOnly row in a request editor. The label stays plain, the value renders as link text in link styling, and the whole row is the tap target. On a struct, Union branch, or List member, the header text is the link text, and an expanded child row taps the nearest enclosing link unless the child row declares its own link, which wins. On a FormattedText field the whole block is the tap target; links inside FormattedText markup are not supported.
- **Value editable:** the label, value, and editing are unchanged. Tapping the value still edits it, and direct controls such as Boolean answers, inline choices, and a Bitflags editor still work. The link is a trailing link button beside the value, named after the target Request's localized name. This is the only case that renders a button.
A primitive with no label of its own, such as an item of a List of scalars, shows its value as the link text. A link retains transition ownership while its composer is open, so repeated taps never open or send twice. Editing the linked request shows no waiting spinner. Once it is submitted, the owning panel shows shared blocking progress until the reply or terminal failure, under the waiting presentation contract; rows do not show a separate link spinner. A link adds no chevron or other glyph and never edits its field. Hidden rows have no link, since Studio rejects that placement. Promotion, flattening, folding, and read-only rules are unchanged by a link.
Folding structs and unions
Structs (Table), unions, and lists remain expanded unless Foldable is declared
on their type or field. Author it as @LibertasFoldable in TypeScript or
#[libertas_foldable] in Rust, with an optional initially_open that is
true (the default), false, or "nondefault":
@LibertasFoldable(initially_open=false),
#[libertas_foldable(initially_open = false)] or
#[libertas_foldable(initially_open = "nondefault")]. Studio permits this marker
only on an effective Table, Union, or List. It changes presentation, not encoded data.
In the parser result the attribute's string value carries the option:
{"Name":"Foldable","Value":"true"} starts expanded, "false" starts
collapsed, and "nondefault" starts collapsed unless the value differs from
its default (below). Presence enables folding; false does not disable it. Studio
compiles it into the Display Template as the fold hint
and removes it from the final schema; clients read folding only from the
template, where a rule may also change it per device.
An unmarked struct, union, or list shows no disclosure control and cannot be
collapsed. A marked value shows one disclosure chevron at the leading edge of
its title line, pointing toward the text when collapsed and down when expanded
(mirrored for right-to-left text), and exposes its expanded state to
assistive technology. The title line toggles the fold, except on a row that is
also a link, where only the chevron toggles and the rest
of the row follows the link. A marked value starts expanded, or collapsed
when initially_open is false, and permits manual collapse and expansion; a
collapsed value shows only its row, its title and promoted value
included. The initial state applies to rows that exist when a page is
displayed. A value the user creates while the page is open, such as a new
List member or a materialized nullable, always opens, whatever its marker
says, so its fields are visible to fill in. With initially_open "nondefault", the value starts expanded
when anything under it differs from its default and collapsed otherwise, so
rarely used fields stay out of the way until they hold something. Its default
is the value the App's default document gives that node; for a List member,
or a node the default document does not reach, it is the default value of its
type. Values are compared decoded, at every depth, including a selected Union
variant and a List's member count, never by their displayed text, so the result
is independent of locale. Values the platform generates rather than the person,
such as a virtual device identity, are left out of the comparison, and working
out the default registers nothing (no identity, name or icon). Like every initial state, it is decided when the
page is displayed and does not close or open the value while the person edits.
Each nested value owns its folding permission;
a marker does not grant permission to its descendants. Lists follow the same explicit-permission rule. Changing a union variant reveals its new fields. Union selectors
remain editable independently of folding.
For a promoted union, the shared owning row may fold when either
the owning Table or the promoted union has Foldable on its type or field.
Promotion and flatten remain independent of folding.
The collapsed task editor value is a short presentation summary, never a recursive JSON-like rendering of its children. A Table with a promoted value field shows that field's normal formatted value. A Union shows its selected variant name, including when the variant has a scalar payload. A List shows its visible item count (for example, “3 items” or “0 items” when empty); a List whose members are all hidden shows no count. An ordinary Table with no promoted value shows no value text beside its title and fold control. Null remains distinct from an empty List or a Table without a summary.
When flatten with { "name_row": false } removes a struct's
own row, the struct still assigns a dropdown owner, in this order: the row of its promoted child, which is
the struct's surviving value row, else the row of its first visible child.
When the struct is Foldable, that row carries its disclosure chevron and
collapses the struct's remaining rows beneath it; its own value stays editable
as usual. An unmarked struct gives that row no chevron.
Hidden fields produce no row and are skipped; a struct that produces no rows
folds nothing. A List member
whose struct row is suppressed keeps its position and membership controls on
the row that owns its dropdown.
Unanswered fields and null
Required fields without an encodable value use subdued italic placeholders: “Select…” for choices, “Enter…” for typed input, “Choose date…” or “Choose time…” for temporal input, and “Set up…” for a non-nullable container needing initialization. Readonly fields without a value show “Not set.” Boolean choices retain their unselected Yes/No controls. These labels are localized, with English fallbacks.
A displayed nullable null is shown inside an outlined capsule using the field
document's NullText, or localized “None” when omitted. An editable Nullable
is the Nullable choice below, never a capsule with
unlabelled set and clear controls. An empty String is a present value,
distinct from null; incomplete native input remains invalid.
Nullable choices
A Nullable (optional) value is a two-state Union: one null state and one
state holding a value of its type, each with default label text unless the
field document names it ([NullText], default “None”; [SetText], default
“Specify”).
An editable Nullable asks one question with two answers: the null answer,
labelled by the field document's NullText,
and the value answer, labelled by SetText, which opens the field's
ordinary editor. NullText is required on every editable Nullable, and
SetText wherever this control draws a value answer: on a Number, String,
Bytes, date or time, Table or List
(Nullable choices). A Boolean's and a
picker's value answers are their own members, so they never show SetText. A
Nullable is editable when a Request variant or a function's configuration
reaches it without passing through a ReadOnly, Hidden or Fixed node; a type
owned by another package is checked by that package. A
client that meets a schema without a label falls back to localized “None” and
“Specify”. Choosing the
null answer discards the value, as switching Union branches does.
Every client draws the question with exactly this control, so the same field looks and behaves the same on Studio, Android, and iOS:
| Value type | Control |
|---|---|
| Boolean | Three radio choices on the field's row, in this order: TrueText (Yes), FalseText (No), NullText. |
| Enumeration, EnumSource, User, Endpoint | The type's single-select picker; its first entry is NullText, followed by the members in their declared order. |
| Number, String, Bytes, DateOnly, TimeOnly, DateTime | Two radio choices on the field's row, NullText first and SetText second; once SetText is chosen, the value editor appears directly below them, full width. |
| Table, List | Two radio choices on the field's header row, NullText first and SetText second; once SetText is chosen, the Table's fields or the List's members appear below the header. |
| Union | The Union's own branch chooser, with NullText as its first choice before the branches. |
The null answer always comes first. While unanswered, no choice is selected and a picker shows “Select…”. A value answer chosen with its value still blank stays unanswered; null is never inferred from a blank editor. Read-only display keeps the caption described above.
Initialization follows the Union
(type initialization): with no
Default the question starts with neither answer selected, shows “Select…”, and
blocks submission until the person chooses; the reserved Default
$null starts it on the null answer; any
other Default starts it on the value answer with that value. The stored value,
its encoding, CopyFrom, CopyTo, validation rules, AI Import, and calculations
still see null or the value: the unanswered state exists only in the editor
and is never encoded.
Revealed rows
When a person's own choice adds rows to an editor (choosing a Nullable's value answer, a Union branch, an Inline choice, or any edit that reveals fields below), the client brings the first new row into view if it would otherwise start below the visible area. Every client follows the same rule:
- Scroll only for an edit the person made on that control: a tap, click, or keypress. A calculation reply, a server-supplied value, a restored draft, a rule, or another row's change never scrolls the editor.
- Scroll only when the first new row starts below the visible area; when it is already visible, nothing moves.
- Scroll the least distance that shows the new rows, bringing as many of them into view as fit while the changed control stays visible. The changed control never leaves the visible area, and the editor never scrolls up for revealed rows.
- Follow the platform's reduced-motion setting: move without animation when it is on. Focus stays on the changed control; scrolling never moves it.
Rows that disappear, such as a Table's fields after choosing a Nullable's null answer, never trigger scrolling.
Single-choice rows
when_single_choice is a Display Template member with no source attribute.
"hide" withholds the row of a choice field while its allowed set leaves only
one answer, and that answer needs no input; "show", the default, draws the
row, so a higher-ranked rule can undo a lower one.
{ "select": "^<Filing>.status", "when_single_choice": "hide" }
The rule selects a field of a struct that carries
ConstrainedBy or a scalar
EnumSource host, by a relaxed ^<Type>.field or a strict
path, under the placement rule of
promote. Studio rejects it on
any other node, an EnumSource List host
included, with any other value, and in a data condition or switch case;
when may change it per device.
The client does not draw the row while all of these hold:
- its resolved allowed set, evaluated live as the chooser evaluates it, leaves exactly one answer: for a field that is not Nullable, one permitted declared choice for ConstrainedBy or one selectable List member for EnumSource; for a Nullable ConstrainedBy field, an empty set, whose only answer is null (a nonempty set always leaves null as a second answer);
- that answer carries no payload: null, an Enumeration value, a Union variant without a payload, or the EnumSource member;
- the field holds that answer, which the singleton rule writes when the field is unanswered.
Otherwise the row is drawn. A sole Union variant with a payload always keeps its row, so the payload stays reachable. An empty set on a field that is not Nullable, a prefill outside the set, or an unavailable source keeps the row and its field error visible. A calculated set that widens brings the row back, and one that shrinks to a single answer withholds it again. Read-only displays follow the same rule.
Withholding a row changes nothing else. The field keeps its value and is not
Hidden; validation rules, CopyFrom, CopyTo, calculation requests,
encoding, AI Import, and AI Search read and write it exactly as if its row
were shown. Like a Hidden field, a withheld field is not promoted into its
parent row and leaves its place in a layout; a label, title, or table
column naming it still reads its value. The sole-member answer is the
ConstrainedBy and EnumSource data rule, not this member's, so a client that
ignores the member still stores the same value and simply draws the row.