Components
Components
Every example below is a fuaran-wire block. At build time its wire JSON is
decoded, pre-emit-validated, round-trip-checked and server-rendered
by Fuaran.UI.Renderer.Server, then spliced into this page as a first-class node
in the page's own Fuaran tree. The rendered component and the canonical JSON that
produced it are emitted side by side, with a deep link that opens the same tree in
the playground. A block that fails any gate fails the site build: an example on
this page cannot be wrong, because publishing it runs it through the language's own
gates.
The controls at the top of the page set the theme and the locale every
example below is shown under, and both are carried in the address bar — so
/components?theme=dark&locale=de-DE is a link to exactly the view you are
looking at, and selecting either rewrites the URL to match. The two work
differently under the surface, which is worth knowing because it is a real
distinction in the language rather than an implementation detail of this page. A
theme is presentation: one attribute on the document, the same markup repainted,
which is why it applies the instant you press it. A locale reaches the
renderer — it is what a Format binding resolves against — so each locale is
a separately server-rendered page, and the switch is an ordinary link that works
with scripting turned off.
Layout
Box
The unified container. A Box arranges its children and carries an optional heading and role (here, card chrome around a metric).
{"id":"card-1--card-1","kind":{"$type":"Box","children":[{"id":"card-1--metric-1","kind":{"$type":"Metric","format":{"$type":"Currency","code":"GBP"},"icon":"trending-up","label":"Revenue","subtext":"vs last month","tone":"Brand","trend":{"$type":"Static","value":0.07},"trendFormat":{"$type":"Percent","decimals":1},"value":{"$type":"Static","value":1234.5}}}],"heading":"Insights","layout":{"$type":"Flex","direction":"Vertical","wrap":false},"role":"Card"}}
Grid
A Box in grid layout – explicit columns or a template, for dashboard-style tiling.
{"id":"glayout-1--glayout-1","kind":{"$type":"Box","children":[{"id":"glayout-1--metric-1","kind":{"$type":"Metric","format":{"$type":"Currency","code":"GBP"},"icon":"trending-up","label":"Revenue","subtext":"vs last month","tone":"Brand","trend":{"$type":"Static","value":0.07},"trendFormat":{"$type":"Percent","decimals":1},"value":{"$type":"Static","value":1234.5}}}],"layout":{"$type":"Grid","cols":12},"role":"Group"}}
Tabs
A tabbed container; each tab owns a child subtree, with the active tab selected by binding. Tab labels come from tabHeaders – omit them and a host falls back to the child ids.
{"id":"tabs-1--tabs-1","kind":{"$type":"Tabs","children":[{"id":"tabs-1--metric-1","kind":{"$type":"Metric","format":{"$type":"Currency","code":"GBP"},"icon":"trending-up","label":"Revenue","subtext":"vs last month","tone":"Brand","trend":{"$type":"Static","value":0.07},"trendFormat":{"$type":"Percent","decimals":1},"value":{"$type":"Static","value":1234.5}}}],"onSelect":"<closure>","tabHeaders":[{"label":"Revenue"}]}}
Switch
A state-bound conditional: the child whose match equals the bound state value renders; otherwise the default does. Branching is wire data, not host-language if.
The key a Switch selects on needs something that can write it, or one branch renders forever. Here two buttons carry SetState on the same view key the switch reads — the gesture and the branch are both wire data.
{"id":"switch-1--switch-1","kind":{"$type":"Box","children":[{"id":"switch-1--pick-details","kind":{"$type":"Button","label":"Details","onClick":{"$type":"SetState","key":"view","value":"details"},"variant":"Secondary"}},{"id":"switch-1--pick-summary","kind":{"$type":"Button","label":"Summary","onClick":{"$type":"SetState","key":"view","value":"summary"},"variant":"Secondary"}},{"id":"switch-1--view","kind":{"$type":"Switch","cases":[{"child":{"id":"switch-1--details","kind":{"$type":"Markdown","text":"Details view"}},"match":"details"},{"child":{"id":"switch-1--summary","kind":{"$type":"Markdown","text":"Summary view"}},"match":"summary"}],"default":{"id":"switch-1--default","kind":{"$type":"Callout","body":"No view selected","heading":"Pick a view","tone":"Info"}},"stateKey":"view"}}],"layout":{"$type":"Flex","direction":"Vertical","wrap":false},"role":"Group"}}
Switch — timed advance
A Switch that carries autoAdvanceMs moves on its own, that often. That number is
the only thing on the wire: which gesture advances it, which key does, how far a
finger must travel, what pauses the timer and what stops it for good all belong to
the renderer. A reader who swipes, presses an arrow key or clicks inside it has taken
control, and the advance stops for good — there is deliberately no way back. Under
prefers-reduced-motion: reduce the timer is never started at all, which is a
renderer decision rather than a stylesheet one: a stylesheet can suppress the
transition and cannot suppress the advance.
The two buttons are here because the pre-emit gate insists on them: a Switch selects
on a key, and a key nothing can write is a branch that renders forever. The timed
advance writes that key at run time, but the validator judges the document rather than
the runtime — so a carousel a reader can also drive is the shape that passes, which is
the shape you wanted anyway.
Built for the long term
{"id":"carousel-1--carousel-1","kind":{"$type":"Box","children":[{"id":"carousel-1--pick-one","kind":{"$type":"Button","label":"One","onClick":{"$type":"SetState","key":"slide","value":"one"},"variant":"Secondary"}},{"id":"carousel-1--pick-two","kind":{"$type":"Button","label":"Two","onClick":{"$type":"SetState","key":"slide","value":"two"},"variant":"Secondary"}},{"id":"carousel-1--stage","kind":{"$type":"Switch","autoAdvanceMs":5000,"cases":[{"child":{"id":"carousel-1--one","kind":{"$type":"Markdown","text":"Built for the long term"}},"match":"one"},{"child":{"id":"carousel-1--two","kind":{"$type":"Markdown","text":"Typed all the way down"}},"match":"two"},{"child":{"id":"carousel-1--three","kind":{"$type":"Markdown","text":"One wire, many hosts"}},"match":"three"}],"default":{"id":"carousel-1--default","kind":{"$type":"Markdown","text":"Built for the long term"}},"stateKey":"slide"}}],"layout":{"$type":"Flex","direction":"Vertical","wrap":false},"role":"Group"}}
SplitPanel
Two panes split along one axis – a primary and secondary region.
Updated hourly.
{"id":"split-1--split-1","kind":{"$type":"SplitPanel","children":[{"id":"split-1--metric-1","kind":{"$type":"Metric","format":{"$type":"Currency","code":"GBP"},"icon":"trending-up","label":"Revenue","subtext":"vs last month","tone":"Brand","trend":{"$type":"Static","value":0.07},"trendFormat":{"$type":"Percent","decimals":1},"value":{"$type":"Static","value":1234.5}}},{"id":"split-1--markdown-1","kind":{"$type":"Markdown","text":"Updated hourly."}}],"weight":0.6}}
SummaryList
A single card of label/value rows with divider rules – the "list of stats" container.
{"id":"summary-1--summary-1","kind":{"$type":"SummaryList","children":[{"id":"summary-1--lvr-1","kind":{"$type":"LabelValueRow","emphasis":true,"format":{"$type":"Number","decimals":2},"help":"Last 30 days","label":"Total","value":{"$type":"Static","value":42}}}],"heading":"Stats"}}
Display
Heading
A section heading at a chosen level.
Channel performance
{"id":"heading-1--heading-1","kind":{"$type":"Heading","level":2,"text":"Channel performance","variant":"Standard"}}
Metric
A formatted single value – currency / percent / number – with an optional label, icon, and subtext. The displayed scalar is the value slot; source is reserved for collection feeds.
{"id":"metric-1--metric-1","kind":{"$type":"Metric","format":{"$type":"Currency","code":"GBP"},"icon":"trending-up","label":"Revenue","subtext":"vs last month","tone":"Brand","trend":{"$type":"Static","value":0.07},"trendFormat":{"$type":"Percent","decimals":1},"value":{"$type":"Static","value":1234.5}}}
Fact
The textual sibling of Metric – a labelled text fact with an optional icon, help, and emphasis.
{"id":"fact-1--fact-1","kind":{"$type":"Fact","emphasis":true,"help":"Primary insured","icon":"user","label":"Patient","tone":"Brand","value":"Alice Smith"}}
Badge
A small status pill with a tone.
{"id":"badge-1--badge-1","kind":{"$type":"Badge","label":"Beta","variant":"Info"}}
Callout
A toned, optionally-dismissable notice with a heading, body, and icon.
{"id":"callout-1--callout-1","kind":{"$type":"Callout","body":"Live data is delayed.","dismissable":true,"heading":"Heads up","icon":"alert","tone":"Warning"}}
Progress
A determinate progress indicator bound to a fraction.
{"id":"progress-1--progress-1","kind":{"$type":"Progress","fraction":{"$type":"Static","value":0.42},"label":"Loading...","tone":"Brand"}}
CodeBlock
First-class code display – deterministic <pre><code> with a language tag.
let x = 1
let y = 2{"id":"code-1--code-1","kind":{"$type":"CodeBlock","code":"let x = 1\u000alet y = 2","copyable":true,"highlightLines":[1,2],"language":"fsharp","lineNumbers":true}}
List
An ordered or unordered list of items.
- First
- Second
{"id":"list-1--list-1","kind":{"$type":"List","items":["First","Second"],"ordered":true}}
Link
A hyperlink with a label and an href binding.
{"id":"link-1--link-1","kind":{"$type":"Link","download":false,"href":{"$type":"Static","value":"/about"},"label":"About us","rel":"noopener","target":"_blank"}}
Image
An image with a source and alt text.
{"id":"image-1--image-1","kind":{"$type":"Image","alt":"User avatar","src":{"$type":"Static","value":"/avatar.png"},"variant":"Avatar"}}
Media
One media kind carrying a Video or Audio variant — never a Video kind beside an
Audio kind. Everything the two share sits in one record: the source, the label, the
transport, the loop, the tracks list and the transcript. label is mandatory and
has no decorative case, because a media element is a transport and therefore always an
interactive control; one with no accessible name is announced as "video" and nothing
more. controls defaults to true and is omitted at that default — a media element
with no transport cannot be paused, seeked or muted by a keyboard user at all.
{"id":"media-1--media-1","kind":{"$type":"Media","kind":{"$type":"Video"},"label":"Studio walkthrough","src":{"$type":"Static","value":"/walkthrough.mp4"},"tracks":[{"default":true,"kind":"Captions","label":"English captions","src":{"$type":"Static","value":"/walkthrough.en.vtt"},"srcLang":"en"},{"kind":"Subtitles","label":"Sous-titres","src":{"$type":"Static","value":"/walkthrough.fr.vtt"},"srcLang":"fr"}]}}
transcript puts the words on the wire whether or not anyone presses play — so a
machine reading the document gets them without decoding a single audio frame. There is
no autoplay slot on the audio variant at all: a slot that defaults to off is one a
caller can switch on, and there is no document this language wants to be able to state
in which a page begins making sound unbidden.
Transcript
{"id":"media-2--media-2","kind":{"$type":"Media","kind":{"$type":"Audio"},"label":"Curator's commentary","src":{"$type":"Static","value":"/commentary.mp3"},"transcript":"The harbour was rebuilt twice: once after the storm of 1908, and again in 1953."}}
Embed
A third-party document inside a maximally-sandboxed browsing context. A separate kind
from Mount, which composes a cooperating guest — a scope id, a declared message
channel, a capability request list — none of which a third-party page has or could
acquire. title is mandatory: a frame is a focus container a reader tabs into, and one
with no accessible name is announced as "frame". permissions is omitted at empty, and
empty is total denial — so the wire-cheapest document is also the safest one.
{"id":"embed-1--embed-1","kind":{"$type":"Embed","src":{"$type":"Static","value":"https://player.example/embed/harbour"},"title":"Harbour restoration, part two"}}
Each relaxation is named, and the closed set is four: AllowScripts, AllowSameOrigin,
AllowForms, AllowFullscreen. Top-level navigation and downloads are not admitted and
not reserved — the first is the drive-by redirect, the second puts a file-save prompt in
a third party's hands. src does not ride the ordinary URL accept set: the embed egress
class admits https and nothing else, not even a schemeless same-origin reference.
{"id":"embed-2--embed-2","kind":{"$type":"Embed","aspectRatio":"SixteenNine","permissions":["AllowScripts","AllowFullscreen"],"src":{"$type":"Static","value":"https://player.example/embed/harbour"},"title":"Harbour restoration, part two"}}
Tree
A hierarchy the reader walks with ONE focus. It is not a List of Disclosures, and
the discriminator is behaviour rather than nesting: a list of disclosures is N
independent tab stops, where a tree is one composite widget with a roving tabindex —
the whole thing is a single tab stop and the arrow keys move a focus inside it. Static
recursive items only; finite static nesting stays with Disclosure.
- Goods
- Cocoa
- Yarn
- Ledger
{"id":"tree-1--tree-1","kind":{"$type":"Tree","items":[{"children":[{"id":"tree-1--cocoa","label":"Cocoa"},{"id":"tree-1--yarn","label":"Yarn"}],"id":"tree-1--goods","label":"Goods"},{"id":"tree-1--ledger","label":"Ledger"}]}}
Both reader-driven behaviours are named State keys — expandedStateKey carries the set
of open rows, selectionStateKey the focused one. The key is the affordance: there is
no expandable flag, because a flag with no key behind it is a decorative control
writing state nothing reads, and no per-item expanded flag, because a node-local
shadow copy is free to disagree with the key.
- Archive
{"id":"tree-2--tree-2","kind":{"$type":"Tree","expandedStateKey":"openRows","items":[{"children":[{"children":[{"id":"tree-2--manifest","label":"Manifest"}],"id":"tree-2--1823","label":"1823"}],"icon":"folder","id":"tree-2--archive","label":"Archive"}],"selectionStateKey":"selectedRow"}}
DataGrid
The tabular kind. Its reader-driven behaviours are declarations, never affordances: a
document says what the grid may do and the renderer draws the control, so a decorative
handle or a download button that downloads nothing cannot be authored. exportable is
the only one that names no State key — sort, page, edit and transfer each write
something back into the document, where an export writes nothing anywhere and simply
hands the reader the rows the client already holds, as RFC 4180 CSV.
{"id":"grid-x--grid-x","kind":{"$type":"DataGrid","columns":[{"field":"reference","kind":{"$type":"Text"},"label":"Reference"},{"field":"amount","format":{"$type":"Currency","code":"GBP"},"kind":{"$type":"Numeric"},"label":"Amount"}],"exportable":true,"rowKeyField":"reference","source":{"$type":"State","defaultValue":[{"amount":1284,"reference":"INV-0417"},{"amount":980,"reference":"INV-0418"}],"key":"grid-x-rows"}}}
transferOutKey and transferInKey are two declarations, not one symmetric slot,
because they are two permissions: a backlog that hands work out is not thereby a backlog
that takes work back. The key is what pairs them — two grids naming the same key are two
ends of one channel, and two grids naming different keys cannot exchange a row however
adjacent they look, because adjacency is layout and a permission that came from layout
is a permission nobody granted. The three panes below are one of each: the backlog
releases only, the middle pane does both, and the last accepts only. A pane that
released onto a key nothing accepted would be refused at pre-emit as a dead pairing —
which is the gate noticing that a declaration with no counterpart is not a permission,
it is a mistake.
{"id":"grid-t--grid-t","kind":{"$type":"Box","children":[{"id":"grid-t--todo","kind":{"$type":"DataGrid","columns":[{"field":"card","kind":{"$type":"Text"},"label":"Card"}],"rowKeyField":"card","source":{"$type":"State","defaultValue":[{"card":"Draft the brief"},{"card":"Size the work"}],"key":"grid-t-todo"},"transferOutKey":"grid-t-move"}},{"id":"grid-t--doing","kind":{"$type":"DataGrid","columns":[{"field":"card","kind":{"$type":"Text"},"label":"Card"}],"rowKeyField":"card","source":{"$type":"State","defaultValue":[{"card":"Write the walk"}],"key":"grid-t-doing"},"transferInKey":"grid-t-move","transferOutKey":"grid-t-move"}},{"id":"grid-t--done","kind":{"$type":"DataGrid","columns":[{"field":"card","kind":{"$type":"Text"},"label":"Card"}],"rowKeyField":"card","source":{"$type":"State","defaultValue":[],"key":"grid-t-done"},"transferInKey":"grid-t-move"}}],"heading":"Sprint board","layout":{"$type":"Auto"},"role":"Group"}}
Input
Button
An action button – its Action is data (dispatch / invoke / copy-link), not a code callback.
{"id":"btn-1--btn-1","kind":{"$type":"Button","disabled":{"$type":"State","defaultValue":false,"key":"loading"},"icon":"refresh","label":"Refresh","onClick":{"$type":"Chain","ops":[]},"variant":"Primary"}}
Form
A typed form – fields, validation, and a submit action, all expressed as wire data.
{"id":"form-1--form-1","kind":{"$type":"Form","disabled":{"$type":"State","defaultValue":false,"key":"formBusy"},"fields":[{"help":"Full legal name","id":"form-1--name","kind":{"$type":"Text","onChange":"<closure>","value":{"$type":"Static","value":""}},"label":"Name","required":true},{"id":"form-1--age","kind":{"$type":"Number","onChange":"<closure>","value":{"$type":"Static","value":0}},"label":"Age","required":false},{"id":"form-1--agree","kind":{"$type":"Checkbox","onToggle":"<closure>","value":{"$type":"Static","value":false}},"label":"I agree","required":true},{"id":"form-1--tier","kind":{"$type":"Choice","onChange":"<closure>","options":{"$type":"Static","value":[{"label":"Basic","value":"basic"},{"label":"Pro","value":"pro"}]},"value":{"$type":"Static","value":"basic"}},"label":"Tier","required":false},{"id":"form-1--notes","kind":{"$type":"TextArea","onChange":"<closure>","rows":5,"value":{"$type":"Static","value":""}},"label":"Notes","required":false}],"onSubmit":{"$type":"Chain","ops":[]},"submitLabel":"Save"}}
A field may omit its value entirely – it auto-binds to $state.<field id> with a
typed placeholder default, so a minimal declarative form is just fields, labels, and
a submit. The field vocabulary includes date pickers and the dual-thumb Range:
{"id":"form-2--form-2","kind":{"$type":"Form","fields":[{"id":"form-2--guest-name","kind":{"$type":"Text"},"label":"Name","required":true},{"id":"form-2--party-size","kind":{"$type":"Number"},"label":"Party size","required":false},{"id":"form-2--seating","kind":{"$type":"Choice","options":{"$type":"Static","value":[{"label":"Indoor","value":"indoor"},{"label":"Terrace","value":"terrace"}]}},"label":"Seating","required":false},{"id":"form-2--budget","kind":{"$type":"Range"},"label":"Budget","required":false},{"id":"form-2--visit-date","kind":{"$type":"Date","variant":"Date"},"label":"Date","required":true}],"onSubmit":{"$type":"Chain","ops":[]},"submitLabel":"Book"}}
Combobox
A closed list you can type into — neither a Choice that makes you scroll ninety
options nor a Text field that admits "Untied Kingdom". allowFreeText says whether a
value outside the list is admissible, and the two answers are genuinely different
fields, which is why it is one slot rather than two kinds. It is omitted at false:
the default a careless emitter gets is the closed one.
{"id":"combo-1--combo-1","kind":{"$type":"Form","fields":[{"help":"A country not on this list does not exist, so free text is refused.","id":"combo-1--country","kind":{"$type":"Combobox","options":{"$type":"Static","value":[{"label":"United Kingdom","value":"gbr"},{"label":"France","value":"fra"},{"label":"Germany","value":"deu"}]},"value":{"$type":"State","defaultValue":"fra","key":"combo-1-country"}},"label":"Country","required":true},{"help":"A proposal that fits no published track is a real thing to be able to say.","id":"combo-1--tag","kind":{"$type":"Combobox","allowFreeText":true,"options":{"$type":"Static","value":[{"label":"Urgent","value":"urgent"},{"label":"Blocked","value":"blocked"}]},"value":{"$type":"State","defaultValue":"needs-a-second-look","key":"combo-1-tag"}},"label":"Tag","required":false}],"onSubmit":{"$type":"Chain","ops":[]},"submitLabel":"Save"}}
Tokens
Several values in one control, each removable. Its allowFreeText defaults to true —
the opposite polarity to Combobox, and the difference is the field: a combobox narrows
an existing list, where a token field collects what the reader has to say. A Tokens
declaring allowFreeText: false with no suggestion source is refused at decode: it
could admit no token by any gesture, so it is a control that cannot be used.
A field pinned to a Static value with no handler is refused at pre-emit as an inert
control — the reader could type into it and nothing anywhere would read what they typed.
So a field either names a writable binding, as the two comboboxes above do, or omits
value entirely and auto-binds to $state.<field id>, as the token and colour fields
below do.
{"id":"tokens-1--tokens-1","kind":{"$type":"Form","fields":[{"id":"tokens-1--countries","kind":{"$type":"Tokens","allowFreeText":false,"suggestions":{"$type":"Static","value":[{"label":"France","value":"fra"},{"label":"Germany","value":"deu"},{"label":"Spain","value":"esp"}]}},"label":"Countries","required":true}],"onSubmit":{"$type":"Chain","ops":[]},"submitLabel":"Search"}}
Rating
A bounded ordinal. max must be at least one, refused at decode — a scale with no
positions cannot be rendered, and cannot be announced to a reader who is not looking at
it. allowHalf admits half steps.
A rating is a control the reader gives, and only that. A displayed average is not a
form field at all — it is a Metric or a Fact, and the pre-emit gate says so: a
rating field bound to a query with nothing to receive an answer is refused as an inert
control. That refusal is the difference between the two gates in one line — the decoder
judges shape, and the pre-emit validator judges sense.
{"id":"rating-1--rating-1","kind":{"$type":"Form","fields":[{"help":"Half steps are admitted here; a scale that did not want them would omit allowHalf.","id":"rating-1--stars","kind":{"$type":"Rating","allowHalf":true,"max":5},"label":"Your rating","required":false}],"onSubmit":{"$type":"Chain","ops":[]},"submitLabel":"Save"}}
Colour
A colour as a VALUE, not as styling. The distinction is who chose it: a semantic style is the document's intent, and this is the reader's answer — so it travels in the submission like every other field.
{"id":"colour-1--colour-1","kind":{"$type":"Form","fields":[{"id":"colour-1--brand","kind":{"$type":"Color"},"label":"Brand colour","required":false}],"onSubmit":{"$type":"Chain","ops":[]},"submitLabel":"Save"}}
File upload
The picker, plus four independent declarations about how a reader may reach it — all
four off by default, so the plainest document is the plain picker. dropTarget accepts
a dragged file; acceptPaste accepts a pasted one. That second one is the other half of
the clipboard boundary: an action may never read the clipboard, because that is
keylogger-adjacent, but a control may accept a paste, because a paste is
user-initiated by construction. A declared gesture with no onSelect to receive what it
picks up is refused at pre-emit: the gesture would work and the file would go nowhere.
{"id":"upload-1--upload-1","kind":{"$type":"FileUpload","accept":[".csv","text/csv"],"acceptPaste":true,"dropTarget":true,"label":"Drop a spreadsheet, or paste one","multiple":true,"onSelect":"<closure>"}}
capture names the reader's own camera or microphone. Two cases and no third, and no
"none" case: an upload that names no device is asking for the picker, which is a
different statement from asking for a device and being unable to say which. It is a
request rather than a promise — a surface with no camera shows an ordinary picker.
{"id":"upload-2--upload-2","kind":{"$type":"FileUpload","accept":["image/*"],"capture":"Camera","label":"Photograph the receipt","multiple":false,"onSelect":"<closure>"}}
destination is where the selected files go — and it is a NAME, never a URL. The
document says where a file belongs; the host decides whether it serves that name, what
endpoint it means and what credentials it uses, none of which belongs in a tree that may
have been written by a model. A host that registers no sink for the name refuses
loudly rather than appearing to upload.
{"id":"upload-3--upload-3","kind":{"$type":"FileUpload","accept":["video/*"],"destination":"session-recordings","label":"Upload your recordings","multiple":true,"onSelect":"<closure>"}}
Select
A single- or multi-select control over a set of options.
{"id":"select-1--select-1","kind":{"$type":"Select","disabled":{"$type":"State","defaultValue":false,"key":"selectBusy"},"label":"Region","onChange":"<closure>","placeholder":"Choose one","source":{"$type":"Static","value":[{"label":"UK","value":"uk"}]},"value":{"$type":"Static","value":"uk"}}}
Filters
A filter bar of chips wired to the data they filter. Each chip's control is an
ordinary form-field kind, and a chip with no explicit value auto-binds to the
named filter ($filters.<name>). The pre-emit validator rejects a decorative
bar – every declared chip must be consumed downstream (here, as Transform
params feeding the grid).
{"id":"filters-1--filters-1","kind":{"$type":"Box","children":[{"id":"filters-1--bar","kind":{"$type":"Filters","items":[{"kind":{"$type":"Text"},"label":"Search","name":"q"},{"kind":{"$type":"Choice","options":{"$type":"Static","value":[{"label":"Basic","value":"basic"},{"label":"Pro","value":"pro"}]}},"label":"Tier","name":"tier"}]}},{"id":"filters-1--grid","kind":{"$type":"DataGrid","columns":[{"field":"name","kind":{"$type":"Text"},"label":"Name"},{"field":"tier","kind":{"$type":"Text"},"label":"Tier"}],"rowKeyField":"name","source":{"$type":"Transform","params":[{"from":{"$type":"Filter","name":"q"},"name":"q"},{"from":{"$type":"Filter","name":"tier"},"name":"tier"}],"pipeline":[{"$type":"filter","pred":{"$type":"binary","left":{"$type":"col","name":"name"},"op":"contains","right":{"$type":"param","name":"q"}}},{"$type":"filter","pred":{"$type":"binary","left":{"$type":"col","name":"tier"},"op":"eq","right":{"$type":"param","name":"tier"}}}],"source":{"columns":{"name":{"validity":[true,true],"values":["Ada","Grace"]},"tier":{"validity":[true,true],"values":["pro","basic"]}},"schema":[{"name":"name","type":"string"},{"name":"tier","type":"string"}]}}}}],"layout":{"$type":"Auto"},"role":"Group"}}
Traits and actions
Everything above is a kind. This section is the other two ways a document says something: a trait, which any node may carry whatever its kind, and an action, which is what a control does when a reader uses it. Both are wire data like the rest.
Tooltip
A supplementary hint ABOUT a node, revealed by the renderer's own hover, focus and
long-press affordance and announced through aria-describedby. A hint describes; it
does not name. On a control whose own text is empty — an icon-only button is the
case this was built for — a tooltip alone leaves the element with a description and no
name at all, so the accessible name is declared separately and the pre-emit validator
says so rather than letting it ship. A declared hint that hints nothing is a defect, not
a no-op: no renderer emits a hint element for one.
{"id":"tip-1--tip-1","kind":{"$type":"Button","label":"Rebuild index","onClick":{"$type":"Chain","ops":[]},"variant":"Secondary"},"tooltip":"Re-reads every document; takes about a minute on this corpus."}
{"id":"tip-2--tip-2","kind":{"$type":"Box","children":[{"accessibility":{"label":{"$type":"Static","value":"Download CSV"}},"id":"tip-2--control","kind":{"$type":"Button","icon":"download","label":"","onClick":{"$type":"Chain","ops":[]},"variant":"Tertiary"},"tooltip":"Exports the rows currently shown, not the whole table."}],"layout":{"$type":"Flex","direction":"Horizontal","wrap":false},"role":"Group"}}
Direction
style.direction names the direction of ONE VALUE. It defaults to auto and is omitted
at that default, because the bidirectional algorithm reads the text and gets it right
almost every time. It fails in exactly one shape, and that shape is common enough to be
worth a slot: an opaque identifier — a reference code, an account number, a SKU, a URL —
inside prose of the other direction. Such a run has no strong character of its own, so
it inherits the surrounding paragraph and comes back reversed.
It names nothing about the document: not a page direction, not a locale, not a layout side. Those are the host's, and a tree carrying them would be asserting something about a page it was pasted into.
מספר האסמכתא שלך הוא
{"id":"dir-1--dir-1","kind":{"$type":"Box","children":[{"id":"dir-1--prose","kind":{"$type":"Markdown","text":"מספר האסמכתא שלך הוא"}},{"id":"dir-1--reference","kind":{"$type":"Badge","label":"RR123456789IL","variant":"Neutral"},"style":{"direction":"ltr"}}],"layout":{"$type":"Flex","direction":"Vertical","wrap":false},"role":"Group"},"style":{"direction":"rtl"}}
Print breaks
Paper is a medium the tree cannot see and cannot be told about. What a document can
say is which of its own parts are indivisible — a fact only the tree has, because a host
laying out pages sees boxes and cannot infer that the totals block is one thing that
reads wrong when halved. keepTogether keeps a box and everything under it on one page;
breakBefore starts one at the top of a fresh page. There is deliberately no
break-after: a break after this box is a break before the next one.
Both are inert on a continuous medium, so a screen rendering is byte-for-byte the rendering it always was. Neither names anything about the medium — no page size, no margin, no sheet number, no running header.
Net 1,200.00
VAT 240.00
Gross 1,440.00
{"id":"brk-1--brk-1","kind":{"$type":"Box","children":[{"id":"brk-1--net","kind":{"$type":"Markdown","text":"Net 1,200.00"}},{"id":"brk-1--vat","kind":{"$type":"Markdown","text":"VAT 240.00"}},{"id":"brk-1--gross","kind":{"$type":"Markdown","text":"Gross 1,440.00"}}],"heading":"Totals","keepTogether":true,"layout":{"$type":"Flex","direction":"Vertical","wrap":false},"role":"Card"}}
A DataGrid carries the same pair in its own terms: keepRowsTogether says a row is one
thing, and repeatHeader brings the column headings back at the top of every page the
table continues onto — because on the second sheet, a column of numbers with no heading
is a column of numbers.
{"id":"brk-2--brk-2","kind":{"$type":"DataGrid","columns":[{"field":"line","kind":{"$type":"Text"},"label":"Line"},{"field":"net","format":{"$type":"Currency","code":"GBP"},"kind":{"$type":"Numeric"},"label":"Net"}],"keepRowsTogether":true,"repeatHeader":true,"rowKeyField":"line","source":{"$type":"State","defaultValue":[{"line":"Discovery workshop","net":2900},{"line":"Specification review","net":980}],"key":"brk-2-rows"}}}
The first payload-free action, and the emptiness is the ruling: the paged medium is host chrome, so a document may say print now and nothing at all about how — no page size, no margin, no sheet range, no target subtree. It is not an escape hatch. It opens a dialogue the reader operates and can cancel, hands the page to no third party, and returns nothing the tree can read, so it discloses less than the clipboard write below it — and it is gated all the same, because a host rendering trees it did not write must be able to refuse an unbidden dialogue.
{"id":"print-1--print-1","kind":{"$type":"Button","icon":"printer","label":"Print this invoice","onClick":{"$type":"Print"},"variant":"Primary"}}
Copy to clipboard
The payload is a TextSource, so what a reader copies may be a bound value rather than
only a literal the author typed — and a bound payload resolves at DISPATCH time, through
the same binding resolver the surrounding tree renders through. The reader gets what they
were looking at, not what the document held when it arrived. A literal payload is
canonically the bare JSON string, so a document written before this widening encodes
exactly as it did.
There is deliberately no clipboard read: a tree that could read the clipboard without a paste gesture is a keylogger-adjacent capability, and paste is user-initiated by construction.
{"id":"copy-1--copy-1","kind":{"$type":"Button","icon":"copy","label":"Copy the current share link","onClick":{"$type":"WriteToClipboard","text":{"$type":"Bound","binding":{"$type":"State","key":"shareUrl"}}},"variant":"Secondary"}}
Visualisation
Chart
A chart lowered to the bounded drawing primitive – series and axes from wire data.
Line, bar (grouped or stacked), area, pie/donut, and scatter arms all lower to the
same Drawing vocabulary, with schema-grounded validation rejecting meaningless
combinations at pre-emit time.
{"id":"chart-1--chart-1","kind":{"$type":"Chart","kind":"Bar","source":{"$type":"Static","value":[]},"stacked":true,"title":"Channel mix","xField":"month","yFields":["revenue","cost"]}}
Drawing
The bounded vector-graphics primitive that charts lower to – a closed shape DU, no raw SVG.
{"id":"drawing-1--drawing-1","kind":{"$type":"Drawing","description":"A flowchart of the validation gate: wire JSON is decoded and validated – a valid tree renders, an invalid one is rejected with typed errors and repaired.","shapes":[{"$type":"Ellipse","cx":25,"cy":50,"rx":16,"ry":10,"style":{"fill":{"$type":"Static","value":"#3366cc"},"opacity":{"$type":"Static","value":0.15},"stroke":{"$type":"Static","value":"#3366cc"},"strokeWidth":{"$type":"Static","value":1}}},{"$type":"Line","style":{"stroke":{"$type":"Static","value":"#8a93a0"},"strokeWidth":{"$type":"Static","value":1}},"x1":41,"x2":58,"y1":50,"y2":50},{"$type":"Polygon","points":[{"x":58,"y":47},{"x":63,"y":50},{"x":58,"y":53}],"style":{"fill":{"$type":"Static","value":"#8a93a0"}}},{"$type":"Rectangle","cornerRadius":3,"height":18,"style":{"fill":{"$type":"Static","value":"#3366cc"},"opacity":{"$type":"Static","value":0.15},"stroke":{"$type":"Static","value":"#3366cc"},"strokeWidth":{"$type":"Static","value":1}},"width":30,"x":63,"y":41},{"$type":"Group","children":[{"$type":"Line","style":{},"x1":93,"x2":110,"y1":50,"y2":50},{"$type":"Polygon","points":[{"x":110,"y":47},{"x":115,"y":50},{"x":110,"y":53}],"style":{}}],"style":{"fill":{"$type":"Static","value":"#8a93a0"},"stroke":{"$type":"Static","value":"#8a93a0"},"strokeWidth":{"$type":"Static","value":1}}},{"$type":"Polygon","points":[{"x":115,"y":50},{"x":133,"y":36},{"x":151,"y":50},{"x":133,"y":64}],"style":{"fill":{"$type":"Static","value":"#cc6633"},"opacity":{"$type":"Static","value":0.15},"stroke":{"$type":"Static","value":"#cc6633"},"strokeWidth":{"$type":"Static","value":1}}},{"$type":"Line","style":{"stroke":{"$type":"Static","value":"#8a93a0"},"strokeWidth":{"$type":"Static","value":1}},"x1":151,"x2":161,"y1":50,"y2":50},{"$type":"Polygon","points":[{"x":161,"y":47},{"x":166,"y":50},{"x":161,"y":53}],"style":{"fill":{"$type":"Static","value":"#8a93a0"}}},{"$type":"Rectangle","cornerRadius":3,"height":18,"style":{"fill":{"$type":"Static","value":"#33aa55"},"opacity":{"$type":"Static","value":0.18},"stroke":{"$type":"Static","value":"#2f7a45"},"strokeWidth":{"$type":"Static","value":1}},"width":28,"x":166,"y":41},{"$type":"Circle","cx":194,"cy":39,"r":2.5,"style":{"fill":{"$type":"Static","value":"#33aa55"}}},{"$type":"Polyline","points":[{"x":133,"y":64},{"x":133,"y":82},{"x":112,"y":82}],"style":{"stroke":{"$type":"Static","value":"#8a93a0"},"strokeWidth":{"$type":"Static","value":1}}},{"$type":"Polygon","points":[{"x":112,"y":79},{"x":107,"y":82},{"x":112,"y":85}],"style":{"fill":{"$type":"Static","value":"#8a93a0"}}},{"$type":"Rectangle","cornerRadius":3,"height":16,"style":{"fill":{"$type":"Static","value":"#aa3344"},"opacity":{"$type":"Static","value":0.12},"stroke":{"$type":"Static","value":"#aa3344"},"strokeWidth":{"$type":"Static","value":1}},"width":44,"x":63,"y":74},{"$type":"Curve","commands":[{"$type":"MoveTo","to":{"x":63,"y":82}},{"$type":"CubicTo","control1":{"x":40,"y":82},"control2":{"x":25,"y":76},"to":{"x":25,"y":65}}],"style":{"stroke":{"$type":"Static","value":"#8a93a0"},"strokeWidth":{"$type":"Static","value":1}}},{"$type":"Polygon","points":[{"x":22,"y":65},{"x":25,"y":60},{"x":28,"y":65}],"style":{"fill":{"$type":"Static","value":"#8a93a0"}}},{"$type":"Label","style":{"fill":{"$type":"Static","value":"#223344"},"fontFamily":"system-ui, sans-serif","fontSize":6,"textAnchor":"Middle"},"text":"Wire","x":25,"y":52},{"$type":"Label","style":{"fill":{"$type":"Static","value":"#223344"},"fontFamily":"system-ui, sans-serif","fontSize":6,"textAnchor":"Middle"},"text":"Decode","x":78,"y":52},{"$type":"Label","style":{"fill":{"$type":"Static","value":"#223344"},"fontFamily":"system-ui, sans-serif","fontSize":6,"textAnchor":"Middle"},"text":"Valid?","x":133,"y":52},{"$type":"Label","style":{"fill":{"$type":"Static","value":"#55606c"},"fontFamily":"system-ui, sans-serif","fontSize":5,"textAnchor":"Middle"},"text":"yes","x":156,"y":45},{"$type":"Label","style":{"fill":{"$type":"Static","value":"#223344"},"fontFamily":"system-ui, sans-serif","fontSize":6,"textAnchor":"Middle"},"text":"Render","x":180,"y":52},{"$type":"Label","style":{"fill":{"$type":"Static","value":"#55606c"},"fontFamily":"system-ui, sans-serif","fontSize":5,"textAnchor":"Middle"},"text":"no","x":138,"y":72},{"$type":"Label","style":{"fill":{"$type":"Static","value":"#223344"},"fontFamily":"system-ui, sans-serif","fontSize":5,"textAnchor":"Middle"},"text":"Reject + errors","x":85,"y":84},{"$type":"Label","style":{"fill":{"$type":"Static","value":"#55606c"},"fontFamily":"system-ui, sans-serif","fontSize":5,"textAnchor":"Middle"},"text":"repair","x":40,"y":78}],"style":{},"title":"The validation gate","viewBox":{"height":100,"minX":0,"minY":0,"width":200}}}
Locale
Formatting that follows the reader, not the author. A Format binding wraps a
numeric source with a Format case and a locale source: locale.ambient defers
to whatever locale the host is rendering under — the one the switcher above
selects — and locale.explicit pins a tag regardless. Nothing about the tree
changes between locales: the same wire bytes render four ways, which is the point.
Note the distinction from the format field on Metric and LabelValueRow
further up this page. That is a CellFormat, and it is deliberately
locale-INdEPENDENT — it renders identically on every host so that a server-rendered
figure and the same figure after client hydration can never disagree. When the
figure should follow the reader's locale, the value is a Format binding; when it
must be stable across hosts, it is a CellFormat. Both are below, and switching
the locale shows which is which.
Number
Grouping and decimal separators come from the locale, the digit count from the binding.
{"id":"locale-number-1--fact-1","kind":{"$type":"Fact","help":"Formatted for the locale selected above.","label":"Monthly sessions","value":{"$type":"Bound","binding":{"$type":"Format","format":{"$type":"Number","decimals":2},"locale":{"$type":"Ambient"},"source":{"$type":"Static","value":1234567.891}}}}}
Currency
The ISO 4217 code is part of the data; the symbol, its placement and the number of fraction digits are the locale's.
{"id":"locale-currency-1--fact-1","kind":{"$type":"Fact","help":"The ISO code is fixed; its symbol, placement and fraction digits are not.","label":"Contract value","value":{"$type":"Bound","binding":{"$type":"Format","format":{"$type":"Currency","isoCode":"EUR"},"locale":{"$type":"Ambient"},"source":{"$type":"Static","value":1234.5}}}}}
Date
The source is a plain instant — whole Unix-epoch seconds — and the calendar, ordering, month name and script are all resolved at render.
{"id":"locale-date-1--fact-1","kind":{"$type":"Fact","help":"One instant, four calendars' worth of convention.","label":"Renews","value":{"$type":"Bound","binding":{"$type":"Format","format":{"$type":"Date","dateStyle":"Long"},"locale":{"$type":"Ambient"},"source":{"$type":"Static","value":1767225600}}}}}
Pinned locale
locale.explicit overrides the ambient locale for one value. For the cases where
a figure belongs to a jurisdiction rather than to a reader — a filing date, a
contract governed by one country's conventions — this one does not move when the
switcher does.
{"id":"locale-explicit-1--fact-1","kind":{"$type":"Fact","help":"Pinned with locale.explicit — the switcher does not move it.","label":"Filed (always en-US)","value":{"$type":"Bound","binding":{"$type":"Format","format":{"$type":"Date","dateStyle":"Long"},"locale":{"$type":"Explicit","tag":"en-US"},"source":{"$type":"Static","value":1767225600}}}}}