Fuaranfuaran

The source for generative UI

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).

Insights
Revenue
GBP 1234.50
▲7.0%
vs last month
{"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"}}

Open in fuaran-live ↗

Grid

A Box in grid layout – explicit columns or a template, for dashboard-style tiling.

Revenue
GBP 1234.50
▲7.0%
vs last month
{"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"}}

Open in fuaran-live ↗

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.

Revenue
GBP 1234.50
▲7.0%
vs last month
{"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"}]}}

Open in fuaran-live ↗

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.

Pick a view
No view selected
{"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"}}

Open in fuaran-live ↗

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.

{"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"}}

Open in fuaran-live ↗

SplitPanel

Two panes split along one axis – a primary and secondary region.

Revenue
GBP 1234.50
▲7.0%
vs last month

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}}

Open in fuaran-live ↗

SummaryList

A single card of label/value rows with divider rules – the "list of stats" container.

Stats
Total42.00
{"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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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.

Revenue
GBP 1234.50
▲7.0%
vs last month
{"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}}}

Open in fuaran-live ↗

Fact

The textual sibling of Metric – a labelled text fact with an optional icon, help, and emphasis.

Patient
Alice Smith
Primary insured
{"id":"fact-1--fact-1","kind":{"$type":"Fact","emphasis":true,"help":"Primary insured","icon":"user","label":"Patient","tone":"Brand","value":"Alice Smith"}}

Open in fuaran-live ↗

Badge

A small status pill with a tone.

Beta
{"id":"badge-1--badge-1","kind":{"$type":"Badge","label":"Beta","variant":"Info"}}

Open in fuaran-live ↗

Callout

A toned, optionally-dismissable notice with a heading, body, and icon.

Heads up
Live data is delayed.
{"id":"callout-1--callout-1","kind":{"$type":"Callout","body":"Live data is delayed.","dismissable":true,"heading":"Heads up","icon":"alert","tone":"Warning"}}

Open in fuaran-live ↗

Progress

A determinate progress indicator bound to a fraction.

Loading...
{"id":"progress-1--progress-1","kind":{"$type":"Progress","fraction":{"$type":"Static","value":0.42},"label":"Loading...","tone":"Brand"}}

Open in fuaran-live ↗

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}}

Open in fuaran-live ↗

List

An ordered or unordered list of items.

  1. First
  2. Second
{"id":"list-1--list-1","kind":{"$type":"List","items":["First","Second"],"ordered":true}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

Image

An image with a source and alt text.

User avatar
{"id":"image-1--image-1","kind":{"$type":"Image","alt":"User avatar","src":{"$type":"Static","value":"/avatar.png"},"variant":"Avatar"}}

Open in fuaran-live ↗

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"}]}}

Open in fuaran-live ↗

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
The harbour was rebuilt twice: once after the storm of 1908, and again in 1953.
{"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."}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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"}]}}

Open in fuaran-live ↗

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.

{"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"}}

Open in fuaran-live ↗

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.

[Grid: 2 rows — hydrates client-side]
{"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"}}}

Open in fuaran-live ↗

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.

[Grid: 2 rows — hydrates client-side]
[Grid: 1 rows — hydrates client-side]
[Grid: 0 rows — hydrates client-side]
{"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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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>"}}

Open in fuaran-live ↗

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>"}}

Open in fuaran-live ↗

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>"}}

Open in fuaran-live ↗

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"}}}

Open in fuaran-live ↗

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).

[Grid: 2 rows — hydrates client-side]
{"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"}}

Open in fuaran-live ↗

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.

Re-reads every document; takes about a minute on this corpus.
{"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."}

Open in fuaran-live ↗

Exports the rows currently shown, not the whole table.
{"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"}}

Open in fuaran-live ↗

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.

מספר האסמכתא שלך הוא

RR123456789IL
{"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"}}

Open in fuaran-live ↗

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.

Totals

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"}}

Open in fuaran-live ↗

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.

[Grid: 2 rows — hydrates client-side]
{"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"}}}

Open in fuaran-live ↗

Print

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"}}

Open in fuaran-live ↗

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"}}

Open in fuaran-live ↗

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.

Channel mixStacked bar chart. 2 series: revenue, cost. no categories.0.00.20.40.60.81.0MonthRevenuerevenuecostChannel mix
{"id":"chart-1--chart-1","kind":{"$type":"Chart","kind":"Bar","source":{"$type":"Static","value":[]},"stacked":true,"title":"Channel mix","xField":"month","yFields":["revenue","cost"]}}

Open in fuaran-live ↗

Drawing

The bounded vector-graphics primitive that charts lower to – a closed shape DU, no raw SVG.

The validation gateA 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.WireDecodeValid?yesRendernoReject + errorsrepair
{"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}}}

Open in fuaran-live ↗

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.

Monthly sessions
1,234,567.89
Formatted for the locale selected above.
{"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}}}}}

Open in fuaran-live ↗

Currency

The ISO 4217 code is part of the data; the symbol, its placement and the number of fraction digits are the locale's.

Contract value
€1,234.50
The ISO code is fixed; its symbol, placement and fraction digits are not.
{"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}}}}}

Open in fuaran-live ↗

Date

The source is a plain instant — whole Unix-epoch seconds — and the calendar, ordering, month name and script are all resolved at render.

Renews
Thursday 1 January 2026
One instant, four calendars' worth of convention.
{"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}}}}}

Open in fuaran-live ↗

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.

Filed (always en-US)
Thursday, January 1, 2026
Pinned with locale.explicit — the switcher does not move it.
{"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}}}}}

Open in fuaran-live ↗