Snapshots and the canvas
Snapshots are the substrate a rib publishes data onto; the canvas is the closed
set of shapes the browser knows how to render. For the design rationale, read
Snapshots and surfaces first. What
follows is the contract underneath it: the SnapshotManager interface, the
frame envelope, and the canvas vocabulary a view must satisfy.
The SnapshotManager
Section titled “The SnapshotManager”The harness owns one SnapshotManager. A rib receives a namespace-scoped view
of it through ctx.getSnapshotManager?.().
interface SnapshotManager { register<T>( key: string, compose: () => Promise<T> | T, opts?: { validate?: (data: unknown) => T }, ): () => void; // returns an unregister handle recompose<T>(key: string): Promise<SnapshotFrame<T> | undefined>; latest<T>(key: string): SnapshotFrame<T> | undefined; keys(): string[]; dispose(): Promise<void>;}| Method | Behavior |
|---|---|
register | Registers a composer under a key. Throws on a duplicate key; call the returned handle before re-registering. An optional validate runs on every composed payload. |
recompose | Runs the composer, validates, caches, and broadcasts a frame. Concurrent calls coalesce into one composer run. Returns undefined if the key is unregistered or the composer threw. |
latest | The last cached frame for a key, or undefined. Wrapped (not bare data) so a late client can compare version. |
keys | The registered keys; backs GET /api/snapshots. |
dispose | Idempotent teardown; closes subscriber sockets and drops cached state. |
A rib’s composeBundle is registered as the composer for rib.id
automatically. Additional keys are registered imperatively. Every key a rib
registers must live under rib:<id> or rib:<id>:*; registering outside the
namespace throws, and reading another rib’s keys returns nothing.
The frame
Section titled “The frame”A frame is the versioned envelope every snapshot travels in, both in latest()
and over the WebSocket.
{ type: "snapshot_update", key: "rib:example:board", version: 7, // non-negative int, increments per compose composedAt: "2026-06-09T18:20:04Z", // ISO 8601 with offset data: /* the payload */}Each successful compose increments version, replaces the cached frame, and
broadcasts to subscribers. Versions let a late-joining client read the cached
frame first, then subscribe and discard any frame it has already seen.
Fail closed, three times
Section titled “Fail closed, three times”A frame is checked at three points, and every rejection preserves the last good state.
- At the producer. A
validateregistered with the key (canonically a zodparse) runs before caching or broadcast. An invalid payload is dropped and the prior frame survives. - At the composer. If the composer throws, the failure is logged once and the cached frame is untouched. A flaky external system degrades a board to stale, never to broken.
- At the renderer. The browser parses every frame against the canvas schema before rendering. A mismatch renders a one-line note instead of a component.
For a view payload, expectView(key, kind) is the canonical producer guard: it
parses data through the full canvas-view union (so the uniqueness checks the
render gate runs are enforced producer-side) and asserts the expected
discriminant.
Canvas kinds
Section titled “Canvas kinds”A payload declares what it is. The renderer set is closed: the browser either has a renderer for the declared kind or refuses politely.
| Kind | Renders as |
|---|---|
markdown | Formatted text. |
view | A typed visualization: a table, a graph, or a board. |
html | Rib-authored markup rendered in a sandboxed iframe. The frame runs with sandbox="allow-scripts" (no allow-same-origin) and a CSP whose connect-src 'none' blocks script-initiated network connections (fetch, XHR, WebSocket). Frame-originated actions (posted via keelson.action(type, payload) or a [data-canvas-action] click) arrive at the rib’s onAction stamped with origin: "canvas-html", letting the rib gate untrusted verbs. |
HTML state bridge
Section titled “HTML state bridge”The injected window.keelson bridge lets an HTML view opt into transient UI
state, such as a task field that should survive replacement markup when a
project or provider switches. It does not capture the DOM automatically.
window.keelson.saveState(state: JsonObject): void;window.keelson.onRestore(handler: (state: JsonObject) => void): void;Saving. Each valid save replaces the entire prior object, without merging.
The root must be a plain JSON object. Nested plain objects, arrays, strings,
booleans, null, and finite numbers are accepted. Undefined, functions, symbols,
bigint, non-finite numbers, cycles, sparse arrays, and non-JSON objects such as
Date, Map, and Set are rejected. The serialized JSON must fit within 65,536
UTF-8 bytes, including keys and JSON punctuation. Invalid or unserializable
values leave the last accepted save intact; diagnostics never contain the
payload. saveState({}) replaces the saved draft with an empty object.
Restoring. After a replacement document loads, the host sends the latest saved object once. The bridge buffers it if the handler registers later; an early handler receives it when it arrives. A later registration replaces an undelivered handler. Once consumed, the value is never replayed to another registration in that document. With no saved value the handler is not called; an empty object is a real saved value. Handler errors remain visible.
Identity and lifetime. The host uses the exact snapshot key, not the HTML content or a frame-supplied identifier. Different keys are isolated, including views owned by one rib. Inline surface regions and snapshot-backed drawers use the same key: state survives collapse/remount, reopening, and expansion into a drawer. Multiple placements of one key share the latest accepted save, but there is no live synchronization; another placement receives it on its next document load. Theme toggles neither reload the frame nor replay state.
State lives only in browser-tab memory. The host retains at most 64 view keys, evicting the least recently saved or restored key when a new save exceeds that count. A page reload loses all saved state. Inline text and direct artifact sources without a snapshot key retain isolated component-lifetime state only. No state is written to disk, browser storage, SQLite, or a snapshot. Saving and restoring invoke no rib action, workflow, or API and show no toast. Use the rib’s own storage for durable data.
Register restoration before attaching edit handlers, then save user edits rather than initialization defaults. The rib owns validation and versioning of restored fields. Optional feature detection lets markup run on older harnesses without state preservation:
<textarea id="task"></textarea><script> const task = document.getElementById("task"); window.keelson.onRestore?.((state) => { if (typeof state.task === "string") task.value = state.task; }); task.addEventListener("input", () => { window.keelson.saveState?.({ task: task.value }); });</script>The additive state channel is keelson:canvas:html:state. Its strict envelope
is { channel, type: "save" | "restore", state }, with no target or view fields.
The host accepts saves only from its iframe; the bridge accepts restores only
from its parent. The sandbox remains exactly allow-scripts, and the existing
CSP, action, theme, and sizing channels are unchanged.
A view payload carries its own view discriminant, one of three:
{ view: "table", columns: [{ key, label? }], // keys must be unique rows: [ { [columnKey]: Cell } ], caption? }A Cell is a bare scalar (string | number | boolean | null) or a wrapped
{ value?, tone?, badges?, href? }, where each badge is { text, tone? }. A wrapped
cell must render something: a value or at least one badge. An href renders the
cell’s value as an external link, but only a safe http(s):// URL becomes an
anchor. Other schemes fall back to plain text.
{ view: "graph", nodes: [{ id, label?, kind?, tone? }], // ids must be unique edges: [{ source, target, label? }] }kind is a generic category string, never a base-side enum. Optional tone
colors the node’s left rule using the tone vocabulary.
The workhorse: an ordered stack of dashboard sections.
{ view: "board", title?, header?: { status?: Pill, chip?, segments?: Segment[], people?: [{ name, tone? }], defaultCollapsed? }, sections: BoardSection[] }The header’s people is a roster peek: an identity-toned dot per person, names
shown on hover of the status count. It shares CanvasPerson with a card’s
people field and the seats section but is its own primitive, and it carries the
same name-required rule the identity tones state. defaultCollapsed is
a one-shot hint that the region head may start collapsed once the board is
populated: the host collapses once on the first transition to populated, a manual
toggle wins after, and emptying the board re-arms it.
Board sections
Section titled “Board sections”Each section is discriminated on kind. Thirteen are leaf sections; columns and
tabs are one-level layout wrappers that nest leaf sections side by side or one
group at a time.
kind | Shape | For |
|---|---|---|
stats | items: [{ label, value | clock, sub?, tone?, delta?, spark? }] | KPI tiles. A clock tile shows a relative time in place of value (see below). |
segments | items: [{ label, n, tone? }] | A summary pulse. |
bars | inline?, items: [{ label, value, total, tone?, segments?, trailing?, href? }] | Meters and offender lists. A safe http(s):// href makes the whole bar row a link; other schemes fall back to plain text. segments: [{ label, n, tone? }] splits the fill into stacked parts, each n of total, with the label and amount on hover. |
table | columns, rows, caption? | The same table shape as the view. |
cards | boxed?, grid?, columns?, items: [{ title, titleTone?, mono?, stacked?, dot?, edge?, pill?, href?, action?, selected?, bar?, fields?, actions?, footnote?, ghost?, reason? }] | Rich item cards. A card may carry actions: ActionItem[] for inline per-card buttons, distinct from a standalone actions section. action: { type, payload? } makes the whole card body a select/toggle target that dispatches to the owning rib like a button, for pick benches where the card is the selection control (its own actions buttons keep working, layered above); selected marks the card chosen with a brand ring and is conveyed to assistive tech as a pressed toggle, so it reads even on an inline board with no dispatcher. stacked renders the card’s fields as a column (one per line) instead of the inline ·-joined meta row, for line-oriented readouts. grid lays the cards out side by side as an auto-fit grid instead of the stacked full-width column, for fixed-capacity rosters where the row is the bench; the host owns the responsive column count, and an open fields-form stays inside its card’s column. columns (2–6) declares a fixed bench capacity: the host lays exactly that many set-size tracks per row when the panel is wide enough (fewer when narrow) instead of stretching cards to fill the row; a producer that declares capacity typically rounds the roster up with trailing decorative ghost seats (ghost cards without actions: the host hides them from assistive tech and drops them when the panel can’t fit full capacity) so the bench reads as full rows. A ghost card renders as an open-seat placeholder (dashed border, centered body): the empty-seat affordance in a grid bench, e.g. an authoring launchpad or an unfilled casting slot; while a ghost card’s form is open it sheds the dashes for a solid brand edge. edge (a tone) draws a colored left rule that lifts the one card needing attention out of a board of look-alikes. It’s decoration only, so the card must still say why in text (a pill, a reason); ghost cards ignore it. |
rows | boxed?, items: [{ icon?, glyph?, chip?, text, href?, trailing?, detail? }] | A status or feed list. A row with detail (long-form plain text, up to 4,000 chars) renders as a disclosure that expands beneath the row. |
actions | wrap?, tabs?, items: ActionItem[] | Buttons. wrap lays them out as an inline wrapping row of compact chips (a selection strip) instead of a full-width stacked column; an action whose form is open breaks to its own full-width line. tabs renders a single-select strip instead: opening one action’s form closes the others’, and the open form is a stable full-width panel beneath the whole strip (at rest none is active unless an item sets defaultOpen; clicking the active tab closes it; takes precedence over wrap, and expanded is inert). Only this layout renders an item’s subtitle: a second muted line under the tab’s label. See below. |
grid | cells: [{ label, href?, badge?: { text, tone? } }] | A dense at-a-glance matrix. Omit badge for a cell that is a bare labelled link, so a set of href cells reads as a compact strip rather than a stack of rows. |
chart | yLabel?, series: [{ label, points: [{ x, y }] }] (1 to 6 series, unique labels, unique x per series) | A deterministic line/timeseries plot. Series wear the fixed-order categorical palette by index, never cycled: fold a seventh series into an “other” bucket. All-numeric x scales linearly; any string x renders as ordered categories in first-appearance order. Endpoint dots always mark line ends; direct endpoint labels appear up to 4 series, a legend appears for 2+, and hovering shows a crosshair with per-series values. Single y-axis by construction; pair the chart with a table section when exact values matter. |
seats | items: [{ tone?, filled?, label? }] | A fixed-capacity identity row an actor roster fills. A dashed empty seat becomes solid when filled; a label (the actor’s name) becomes the seat’s tooltip and accessible name, and an unlabelled seat is decorative (aria-hidden). Identity colour never renders without a name (see Tones). |
journey | items: [{ title, text? }] | A numbered step strip (numbers derive from order) that says what will happen next. Replaces stacks of “nothing here yet” placeholder chrome on first-run surfaces. |
graph | title?, columns?: string[], nodes: [{ id, label, sublabel?, tone?, kind?, rank?, badges?: [{ text, tone? }], action?: { type, payload? }, selected? }], edges: [{ source, target, label?, tone?, dashed? }] | A ranked dependency, lineage, or parent-child map. Accepts 1 to 48 nodes and at most 200 edges. Node ids must be unique, both edge ends must name nodes, selected: true requires an action, and rank must be an integer at least zero. Section, node, and edge objects reject unknown keys. |
timeline | title?, window, lanes: [{ id, label, tone?, group? }], spans: [{ lane, from, to?, tone?, hatched?, title }], marks: [{ lane, at, glyph, title }], legend? | Spans on lanes over a window; a chart is for values. Accepts 1 to 12 lanes, at most 400 spans and 200 marks. All three arrays are required; spans and marks may be empty. |
columns | columns: [{ weight?, sections: LeafSection[] }] | Lays leaf sections side by side. One level deep only. |
tabs | tabs: [{ label, badge?, sections: LeafSection[] }] | Shows one tab’s leaf sections at a time behind a tab strip. The first tab opens first and the host keeps the open tab across frames. badge is a short count or total beside the label. One level deep only. Older hosts reject tabs. |
Graph layout. Nodes occupy left-to-right rank columns and keep input order
within each column. columns[rank] supplies the heading; unused ranks are
compacted away. Explicit ranks are honored. Unranked nodes derive their rank from
the longest dependency path from a source, including explicitly ranked
predecessors. Cycles are accepted: remaining unranked nodes use the deepest
processed predecessor plus one, or zero if none exists. Self-loops are not drawn;
backward and same-column edges use a simple curve that may cross nodes. Forward
edges that skip columns route through node gaps. Edge label is a tooltip, not
text drawn on the curve.
Graph interaction. Hover or keyboard focus lights the node’s upstream and
downstream chain and dims unrelated nodes and edges. With neither active,
selected nodes keep their chains lit. A node with an action is a native button:
click or Enter dispatches { type, payload? } to the owning rib without an
origin stamp, just like a card action. It is disabled without a dispatcher or
while pending. A reply { effect: "open-canvas", key, placement: "side" } opens an
inspector without hiding the map. Other nodes remain keyboard-focusable for
chain highlighting.
Graph fallback. Below 720px of section width, columns stack, the SVG edge
layer is omitted, and each dependent node lists the source labels it waits on.
The host remeasures edges on resize. Prefer this section over an html region
for node-link maps: the host owns layout, theme, highlighting, the narrow
fallback, and trusted action dispatch. Keep html for bespoke drawings, and
prefer journey or table when relationships are not the story. For larger
graphs, publish the relevant slice and put “showing N of M” in title.
Graph sections require a harness that supports this board-section kind. An older
harness may support the top-level view: "graph" while rejecting a graph inside
a board; check support before a rib adopts the section.
Timeline contract. window is exactly { from, to } or
{ from, clock: { until } }. All timestamps are ISO 8601 with an offset.
The window end must be after its start; a closed span’s end may equal, but
never precede, its start. Lane ids must be unique. Every span and mark must
reference a declared lane. Ids, labels, and item titles are nonempty; a mark’s
glyph is one Unicode code point, including supplementary-plane characters.
Section, window, lane, span, and mark objects reject unknown keys.
Publish through expectView(key, "board") for these checks at either the board’s
top level or inside columns. @keelson/shared exports CanvasTimelineSection,
CanvasTimelineWindow, CanvasTimelineLane, CanvasTimelineSpan, and
CanvasTimelineMark.
const activity: CanvasTimelineSection = { kind: "timeline", title: "Activity window", window: { from: "2026-10-05T12:00:00Z", clock: { until: "2026-10-05T13:00:00Z" }, }, lanes: [ { id: "prepare", label: "Prepare", tone: "id-blue", group: "Work" }, { id: "review", label: "Review", tone: "id-teal", group: "Checks" }, ], spans: [ { lane: "prepare", title: "Prepare inputs", from: "2026-10-05T12:00:00Z", to: "2026-10-05T12:12:00Z", }, { lane: "review", title: "Review in progress", from: "2026-10-05T12:15:00Z" }, ], marks: [ { lane: "prepare", at: "2026-10-05T12:12:00Z", glyph: "*", title: "Inputs ready" }, ], legend: "Dashed outline: open-ended. Marks: checkpoints.",};Timeline rendering. Lanes keep input order; adjacent groups are divided.
Lane tones are id-blue, id-amber, id-teal, id-rose, id-olive, brand,
neutral, or info. A span’s tone override accepts the full tone vocabulary,
otherwise it inherits the lane tone, then neutral. Labels stay in readable ink.
hatched adds a hatch; omitted to adds a dashed open-ended outline. These
treatments can coexist. Marks expose their titles as tooltips.
A clock window uses the host’s shared 30-second clock, without new frames.
until is a fixed upper bound, not a sliding window: running spans extend to
the lesser of now and until, and the now rule disappears outside the window.
Future-starting open spans wait until their start. Fixed windows have no now
rule, and open spans extend to to.
Ticks, window captions, item descriptions and tooltips, and narrow-list times
render in the viewer’s local timezone. Short-window ticks include dates when the
window crosses local midnight. Repeated local tick labels include UTC offsets
to distinguish instants across a fall-back DST change. Caption and item
timestamps keep full local calendar dates and millisecond precision. The caption
names the zone once,
using Intl.DateTimeFormat with timeZoneName: "short" at the window’s start,
including windows spanning a day or a DST change. Inputs remain offset-aware ISO
instants; tick and item placement stays instant-based across DST changes.
Data outside the window is valid: the plot clips intersecting spans and omits
fully outside spans and marks. Zero-duration spans get a small visible width.
Overlapping spans draw in source order and may overpaint. Below 720px of
section width, the plot becomes lane lists containing every span and mark,
including items outside the window, with times and explicit open-ended/hatched
status. Publish a bounded slice when the full history exceeds the caps.
Older hosts reject the timeline section kind; check support before adoption.
A card field is { label?, value, tone?, href?, copyable?, copyAction? },
or { label?, people: [{ name, tone?, face?, status?, lead?, hint? }] }, which renders
identity-toned names in the value slot (a dot per name, the name itself in ink), or
{ label?, tone?, clock }; a field carries exactly one of value / people /
clock. A person with face (one or two characters) renders as an avatar in
its tone with the name beside it: status busy pulses the face, waiting
badges it, idle fades it, and open draws an empty seat with the name kept
for assistive tech; lead rings it and hint is its hover text. A clock is { at, mode }, where at is an ISO 8601 timestamp with an
offset and mode is "since" or "until": the host renders “4 min ago” or
“53 min left” and re-ticks it locally every 30 seconds, so a producer needn’t
republish just to keep a time current. A card may also carry a
reason: { label?, text } annotation line.
A card or row bar is { value, total, label?, trailing? } for a plain fill
(value: null hatches as unmeasured) or { segments, label?, trailing? } for a
stage composition at item scale. label names what the meter measures and
trailing carries its reading (“Turn budget used”, “18 of 80”); the host draws
both with the track and makes them the meter’s accessible name and value text,
so the reading doesn’t need a separate field.
Every section primitive accepts a tone, a generic visual category the renderer
maps to a color, never a domain enum. The semantic core is the first four; the
rest extend the ramp for grade scales, decorative lane glyphs, and hash-keyed
category hues.
| Tone | Use |
|---|---|
ok / warn / error / neutral | The semantic core: status. |
info / caution | Extend a multi-step scale (an A–E grade chip). |
brand / accent | Decorative identity, distinct neighboring hues. |
id-blue / id-amber / id-teal / id-rose / id-olive | Reserved identity hues. Assign one per repeatedly-rendered actor at creation and persist it, never hash per render, and never seat an actor in a status hue. Always render the actor’s name alongside the colour; a sixth actor folds to neutral plus a name rather than minting a hue. |
Actions
Section titled “Actions”An actions section gives a board buttons. Clicking one dispatches a
rib-defined verb to the rib that owns the board’s key, resolved from the key’s
namespace, and reaches the rib’s onAction.
On success the host toasts <type> ✓, or the rib’s own wording when onAction
returns { ok: true, data: { message } } with a non-blank string (trimmed, cut
to 200 chars). While the dispatch is in flight the button is disabled and
aria-busy, and shows pendingLabel when the item sets one.
ActionItem = { type, // a rib-defined verb the base never enumerates label, subtitle?, // a second muted line under the label, rendered only by the tabs layout glyph?, tone?, destructive?, // marks the action dangerous: danger styling and a confirm inline?, // surface a destructive card action as a visible button, not the overflow menu confirm?: Confirm, // confirm before dispatch, destructive or not; simple or typed dialog wording hint?, // descriptive hover tooltip (WHAT it does), any state; joins with reason when disabled disabled?, reason?, // gate the button (dim + seal); reason is the tooltip WHY, only with disabled: true selected?, // a toggle's ON state (brand ring + aria-pressed). Absent ≠ false: absent is a plain verb action with no pressed semantics, false is an off toggle. Only on a click-dispatching action, and each exclusion for its own reason: no fields (an expanded action draws no trigger, a solo picker's opens a popover), not destructive (a one-shot verb, and non-inline on a card it becomes a menu item), not inside a tabs strip (which owns its own active state), never a headAction (always a menu item) expanded?, // render fields as an always-open hero form (inert without fields) defaultOpen?, // in a tabs strip, open the first enabled, field-bearing item marked true on first render, except a solo model-picker, which opens as a popover rather than a form and never seeds; the operator's toggles win after (inert elsewhere and without fields) submitLabel?, // the fields form's submit button text, defaulting to label (inert without fields) submitTone?, // tone for the fields form's submit button only, defaulting to tone, for a neutral tab whose submit is the board's primary verb (inert without fields) pendingLabel?, // button text while the dispatch is in flight ("Sending…"); absent keeps the label with a busy mark payload?, // opaque rib-defined context, dispatched verbatim fields?: ActionField[] // input collected before dispatch}An ActionField is { name, label, placeholder?, required?, multiline?, half?, segmented?, options?, modelPicker?, defaultValue?, showWhen? }. When a button declares
fields, clicking it opens a small form, and the collected { name: value }
map is merged into the dispatched payload. Field names must be unique within an
action. options (an array of { value, label, hint? }) renders a select instead of a
text input: the dispatched value is the chosen option’s value, a non-required
select offers placeholder as an empty “none” option, and the option values
must be unique. It is mutually exclusive with multiline. defaultValue
pre-fills the control; for a select it must name an option value or "".
half renders the field at half width so adjacent half fields share a two-track
row (a lone half field keeps its own row). segmented renders options as a
single-select segment strip instead of a select (a non-required field leads
with a clear segment labelled by placeholder, dispatching "" like the
select’s empty option) and is rejected without options. An option’s hint
is its hover text on the segment or the select option.
showWhen ({ field, equals? }) renders the field only once the named sibling
field has a non-empty value, or exactly equals when set (a workflow picker
that appears after a project is chosen). A hidden field, and a hidden model
picker’s providerField companion, is left out of the dispatched payload
(a same-named static payload default too, though never a binding key) and
skips required; a field whose controller is hidden is hidden too. The schema
rejects a showWhen.field that names the field itself or a field the action
doesn’t have.
modelPicker ({ providerField?, providerDefault? }) renders the host’s live
provider/model catalog as a searchable picker instead of a producer-supplied
choice set, so a board never hardcodes a model list. The dispatched value is the
chosen model id; providerField names a companion payload key that carries the
chosen model’s provider id, and providerDefault seeds that key so an untouched
submit re-affirms the current provider/model pair rather than clearing it. A
non-required picker offers placeholder as its clear row (dispatching "" for
both keys), and defaultValue may be any model id (on- or off-catalog), so a
hand-pinned model stays visible. It is mutually exclusive with multiline and
options. providerField must not collide with the field’s own name, any
sibling field’s name, or another modelPicker field’s providerField: every
one of those lands in the same dispatched payload map, so a collision would
silently overwrite a value; the schema rejects it at publish. When the picker is
the action’s only field and the action is not expanded, the host skips the
intermediate form: the action button opens the catalog directly and picking
dispatches immediately (dismissing the popover is the cancel). An expanded
action always renders its always-open form and requires submit, even with a
single picker field.
inline surfaces a destructive card action as a visible, still-confirm-guarded
button on the card instead of hiding it in the card’s overflow (⋯) menu. It is
inert on a non-destructive action, which always renders inline.
confirm is confirmation-dialog metadata,
{ irreversible?, subject?, title?, body?, label?, confirmLabel?, cancelLabel? }.
An action that sets confirm or destructive asks before it dispatches; with
fields the dialog opens after the form submits and dispatches the collected
values on confirm. An action with neither dispatches immediately. destructive
alone decides the styling: a destructive action’s confirm button is danger red,
a non-destructive one’s is the normal brand style, so a weighty but safe verb
(apply a reviewed plan) can ask “Apply 4 changes?” without reading as a deletion
or moving into a card’s overflow menu. An irreversible confirm requires a
subject the operator types before confirm enables.
disabled renders the button non-interactive, dimmed and its form sealed, when a
precondition the state cannot satisfy fails (a capability-gated tab whose current
cast cannot run it). reason is the human explanation of why, surfaced as a
tooltip, and is valid only alongside disabled: true. A producer that recomputes
preconditions re-emits both on each recompose, so a state change flips the gate.
hint is a short descriptive tooltip (what the action does), surfaced on hover
whether the action is enabled or disabled, so a producer can remind the operator
what an unfamiliar button is for. Unlike reason it needs no disabled; on a
disabled action the host shows both, the hint then the reason.
subtitle is a second muted line under the label, rendered only by the tabs
layout, where a mode picker’s one-line description should read inline rather
than hide in a hover tooltip. Stacked buttons and wrap chips ignore it, and
hint still carries the hover explanation.
expanded renders fields as an always-open form whose submit button carries
the action’s label, glyph, and tone, for a hero action whose input is the
affordance. It is inert without fields.
submitLabel overrides the text on a fields form’s submit button, which
otherwise carries the action’s label, so a tab named for its mode (“Debate”)
can submit with a verb (“Convene”). It applies to every action form and is inert
without fields.
Bound workflows
Section titled “Bound workflows”A rib’s contributeWorkflows can bind a workflow’s structured output to one of
its snapshot keys with bindSnapshotKey. When that workflow runs, its output is
run through the contribution’s validate (the producer-side fail-closed gate)
and republished to the key, so a board refresh is a workflow run. A surface
region names that workflow in its workflow field, and cadenceMs (floored at
30s) re-runs it at that cadence while the key has a live subscriber and at six
times that cadence while idle. serverRefresh: false opts the region out of the
server heartbeat; its client cadence still applies while the surface is open.
Related
Section titled “Related”- Snapshots and surfaces: the design rationale and the surface layout model.
- The Rib contract:
composeBundle,views,surfaces, andonAction, where these payloads are declared and handled. - Workflow nodes: the runs that repopulate a bound key.