UI files
A UI file sits beside a contract (order.ui.md beside order.dhl) and says what its records, actions and queries mean to the people and agents who use them: labels, money and dates, status badges, form steps, and a sentence for each refusal. The contract stays pure logic, and a UI file changes without upgrading any data.
Every field, argument, column, status value and validation line a UI file names is checked against the contract. If it does not match, it does not compile: a typo never silently disappears.
Example
---
contract: com.acme.orders.order
---
```ui
present Order
label "Order" plural "Orders"
title subject, placed_at
status: states label "Status"
0 Draft tone neutral
1 Placed tone info
2 Shipped tone success
total_minor: money currency=currency minor label "Total"
present action Place
label "Place order"
tone primary
confirm "Place order {subject} for the cart's buyer?"
explain entries.len > 0 "Add at least one line."
form Place
step "Cart" cart, buyer
step "Lines" entries, total_minor
buyer: prefill signer hidden
entries: table columns line, product_id, quantity
```
Front matter has one key, contract: the contract's namespace. Indentation is four spaces; # and // start comments.
Blocks
| Block | Holds |
|---|---|
present <Aggregate> | The record: label "X" [plural "Xs"], help, title <field>, …, and annotations per state field |
present struct <Name> | Labels and annotations for a struct's fields (sub-tables, rows) |
present action <Name> | label, help, tone neutral|primary|destructive, confirm "… {arg} … {subject}", annotations per argument, explain <validate line> "text" |
present query <Name> | label, help, columns a, b, …, annotations per column, arg <name>: … |
form <Action> | section "Title" a, b or step "Title" a, b (not both), and form annotations per argument |
Each block appears at most once. record, browse, compose, view and relation are reserved Coming soon.
Field annotations
| Annotation | For | Effect |
|---|---|---|
label "…", help "…", placeholder "…" | any | Header, form label, help line; also your accomplice's argument descriptions |
money currency=<field> or money currency="EUR", minor | Int, Decimal | €4,250.00; input in major units |
quantity [unit "kg"] | Int, Decimal | A number with its unit |
percent | Int, Decimal, Ratio | 12 % |
date, datetime, relative | Timestamp | Formatting and a picker |
email, phone, url, image, file, color, markdown | Text | Renderer and input |
textarea [rows=N] | Text | Multi-line input |
identity | Identity | Shown by display name |
states + lines <value> <Name> [tone …] [label "…"] | Int, Text, Enum | Status badges, filter chips, a status select |
choice + lines <value> "Label" | Enum, Text, Int | A select with labels |
refers <namespace> | Identity, Ref, SubjectRef | A link to that record |
sensitive | any | Masked until revealed (display only) |
hidden, priority N, align start|end | any | Column visibility, order, alignment |
default <value> | arguments | A starting value |
One meaning annotation (money, quantity, states, choice, date…) per field.
explain
explain <validation line> "text" gives a refusal its words. The line must exist in the action (whitespace does not matter). When that rule refuses, the text is the error, shown on the fields the rule reads: in the form before submitting when it can tell, otherwise from the server.
Form annotations
| Annotation | Effect |
|---|---|
visible when <condition> | Shown only while the condition holds (DHL syntax, this action's arguments) |
table columns a, b | A list argument as an editable sub-table |
prefill signer | The acting identity's key |
prefill subject | The record id |
prefill record.<field> | The record's field, when the form opens from a row |
hidden | Not shown; needs a prefill or a default |
readonly | Shown, not editable |
Without a UI file
Every contract still gets a description derived from its rules: each validation line becomes a rule with its kind, and simple argument rules become input limits (x > 0 → minimum 1; name.len > 0 → required). Refusals then name the rule's source line.
Where to put it
Beside the contract, with the same stem, in MANDALA_CONTRACTS or the installed contracts directory. Mandala compiles descriptions at startup; a UI file that does not match its contract is logged and ignored, never stopping the node. dhl-engine describe <contract> <out> writes the description, the diagnostic module and the enriched OpenAPI.
Coming soon — UI files for the built-in standard contracts, record pages, server-side browse tables, compose forms, and the Workbench checking a UI file with its contract.