<!-- Generated by `npm run generate:agent-docs` from src/kit-registry.ts and kitReference() in src/api/tools/kit.ts. Do not edit. -->

# CanvasView kit reference

Plugin version 0.9.0. The same content `get_kit_reference` returns: the `@canvasview/kit` building blocks, the hooks for components, and how canvases are laid out.

## Components

Use kind: component, source: TSX, and data: an object. Export a default component that accepts { data, theme, size }. theme is "light" or "dark" and follows the viewer. Use static imports from react, @canvasview/kit and the npm packages the node declares in packages. Give the node title (1 to 120 characters) to name it on the canvas and on its tab, such as "Budget model"; without one it is labelled Made by an agent. Never build a sign-in screen or a field that asks for a password, API key, card number or other secret; reach outside services through an owner-approved data source. Style with Tailwind classes in className, a <style> element or the style prop. Tailwind is compiled when the canvas is saved, so write each class name in full rather than building it from parts. The kit's colours are bg-background, bg-surface, text-foreground, text-muted, border-border, bg-accent and text-accent-foreground, and bg-brand-1 to bg-brand-6 (also text- and border-) for the organization's brand colours, which are the chart series colours until it sets them; they and the dark: variant follow the viewer's theme. The sandbox has no network, so stylesheets, fonts and images from URLs never load.

Before writing a component, call search_components (GET /api/components?query=) and reuse a match with kind: component, ref: its name, and data; get_component returns source to fork. Save anything reusable with save_component (PUT /api/components/:name). Saved components are private to this account.

A widget node renders the kit export named by its widget field, with data as the props. Give it title (1 to 120 characters) to name it on the canvas, such as "Open tickets"; without one the heading is the widget type.

## Pages inside a component

A component can have several pages. Declare them on the node as routes: [{ path, title }], 1 to 20, such as [{ path: "/", title: "Revenue" }, { path: "/customers", title: "Customers" }]. A path is / or lowercase words with hyphens, such as /reports/monthly, at most 120 characters, with no parameters; a title is 1 to 60 characters and names the page in CanvasView's menus. The component opens on the first route. Read the open page with useRoute and render it; link between pages with RouteLink. A person can open the node on its own, full window, at /c/<canvas>/<node>, and on one page at /c/<canvas>/<node>/<path>, where the address follows the page and Back works. update_node routes replaces them in a patch and routes: null removes them. validate_canvas reports a problem at nodes[i].routes[j].path. Example: const { path } = useRoute(); return <><nav><RouteLink to="/">Revenue</RouteLink> <RouteLink to="/customers">Customers</RouteLink></nav>{path === "/customers" ? <Customers /> : <Revenue />}</>;

## Hooks

- `useDraft`: Call useDraft() to get draft(answer). This fills the CanvasView answer panel with the fields answer names; fields drafted earlier stay. Input blocks with a field prop draft for you.
- `useSubmit`: Call useSubmit() to get submit(answer). This requests human confirmation in CanvasView.
- `useSharedState`: Call useSharedState(key, initial) to get [value, setValue, { error, loaded }], like useState. The value is synced live to everyone on this canvas and survives reloads. Components that use the same key share the value. Values are JSON. setValue accepts a value or an updater. Last write wins per key. loaded is false until the page has the canvas's state; until then value is initial whether or not the key is set, so never seed a default before it: write in an effect only once loaded is true and the value is still unset, and list loaded in the effect's dependencies. Limits: 1,000 keys of 1 to 120 characters, 64 KB per value, 1 MB per canvas, 20 writes per second per viewer. A rejected write reverts and sets error to a reason code (key_limit or state_too_large when the canvas is full; read_only for someone who only views the canvas, or once it is closed; on a canvas with a contract, undeclared_key, schema_mismatch or not_a_writer). Keep anything a component must remember in shared state, not React state; a frame may reload.
- `useSharedGroup`: Call useSharedGroup(prefix) to get [entries, set, { error, me, loaded }] for values that several people write at once. entries maps each key <prefix>:<suffix> to its value as { suffix: value }, live; set(suffix, value) writes <prefix>:<suffix>, and null clears it (the key stays, as null). me is this viewer's participant id, so set(me, value) keeps one value per person that nobody else overwrites. loaded is false until the page has the canvas's state; until then empty entries may only be unread, so wait for it before writing a starting entry. Group writes are accepted while someone else uses the component. A participant id is a hash of a signed-in viewer or, on link canvases, a random id kept in the browser, so one person can take part again from another browser. Poll, RankedVote and Board are built on it.
- `useGroupMode`: Call useGroupMode() in a component that several people use at once. Its frame then shows no "someone is using this" lock, asks for no control, and sends all its writes without one-person control. Poll, RankedVote and Board call it for you.
- `useData`: Call useData(source, { path, query?, method?, body?, headers? }) to get { data, response, error, loading, refresh } from an owner-approved data source (list_data_sources). It reads when the component mounts and again when source or the request change. data is the parsed body when the source answers with JSON; response has status, ok, contentType and text. error has a code: data_status when the source answered with a status other than 2xx, otherwise why CanvasView refused or could not reach it. The canvas must list the source in dataSources. Use fetchData for writes.
- `useRoute`: Call useRoute() to get { path, navigate } in a component that declares routes. path is the open page, one of the node's declared paths; it starts on the first, or on the page the address names. navigate(path) opens another; a path the node does not declare is ignored. The page is each viewer's own, like a browser tab: keep it in useSharedState instead when everyone should move together. Render the page for path yourself, and link between pages with RouteLink.

## Functions

- `fetchData`: Call await fetchData(source, { path, query?, method?, body?, headers? }) for one request, for example a write when someone presses a button. It resolves with { status, ok, contentType, text, data? } whatever status the source answered with, and rejects with an error carrying a code when CanvasView refuses the call. method is GET unless the owner allowed POST, PUT, PATCH or DELETE on the source, which work on active canvases only. A write needs someone the owner named: a person invited as participant or above, a member who has the canvas through the organization, the owner or an Admin. Anyone with only the link, signed in or not, and anyone invited as a viewer is refused with invite_required or forbidden; reads still work for them. On a public canvas, collect answers or shared state and let the organization's agent act on them. A string body is sent as text and anything else as JSON. headers may set Content-Type and Accept only; CanvasView adds the source's secret headers, which a component never sees.

## Data sources

The sandbox has no network. A component reaches outside data only through a data source the account owner approved in CanvasView settings: call list_data_sources to see their names, hosts and methods, and request_data_source to ask the owner for a new one. Declare every source a canvas's components call in dataSources: ["orders"] in the spec; create, update, patch and validate_canvas refuse a name the account does not have. Each call goes through CanvasView: path stays under the source's base URL and prefix (no .., no full URLs), at most 256 KB sent and 1 MB received, JSON or text only, 10 seconds, no redirects, and 60 requests a minute per viewer and 600 per canvas. Viewers' cookies, sign-in and address are never sent. When the owner chose it, a source receives the person's email as X-CanvasView-Viewer with each write, and with reads on canvases that need sign-in. Each write is recorded for the canvas's owner and Admins (get_action_log): who, the source, the method, the path and the status, never the query or the body. Free for now.

## Canvas tools

A canvas can have tools: code the canvas runs for its components, which can call the organization's approved data sources and write results into shared state. The sandbox still has no network: a call goes from the component to the page and down the canvas's live connection, and the answer comes back the same way. Import canvas and call await canvas.tools.<name>(input), or use the useTool hook, and render ToolUnavailable when a tool cannot run. A tool that feeds everyone (a poll or a stream) writes shared state; read it with useSharedState. A tool acts with the organization's credentials, so only someone the owner named may call it: a person invited as participant or above, a member who has the canvas through the organization, the owner or an Admin. Anyone with only the link, signed in or not, and people who only view, cannot set a tool off.

## Shared state

Read shared state with get_canvas_state (GET /api/canvases/:id/state). Write up to 20 keys with set_canvas_state (PUT /api/canvases/:id/state with { values }). Agent writes appear live in open browsers and are attributed to the API key name. update_canvas keeps state.

## Layout and views

Nodes are placed automatically: flow (top to bottom along edges) when the canvas has edges, otherwise a grid. A node may declare size: { width 160-1600, height 80-1200 }; the default width is 320. A node is as tall as its content, up to 1200, and the canvas makes room as pages measure it, so a declared height is only the room reserved before anyone opens the canvas. A size set with arrange_canvas sizes is fixed, and taller content scrolls inside it. Positions live beside the spec in layout, returned by get_canvas, and moving a node never changes the version. People can drag and resize nodes and tidy the canvas. Use arrange_canvas to lay out, align, distribute, move or size nodes without an update. A canvas also has a components view: 1, 2 or 4 areas with nodes as tabs. It starts empty, and people open the nodes they want into areas; closing a tab leaves the node on the canvas. Each person's screen remembers its own arrangement for each area count. Set the shared arrangement with arrange_canvas panes: { areas, tabs, sizes? }, where tabs has one list of node IDs per area and a node left out is open in none; sizes is the first region's share in percent of each split (2 areas: [left]; 4 areas: [top, top left, bottom left]). Set the view links open in with view: canvas or components.

## Building blocks

- [`Note`](#note) (widget `note@1`)
- [`RecordDetail`](#recorddetail) (widget `facts@1`)
- [`DataTable`](#datatable) (widget `table@1`)
- [`Timeline`](#timeline) (widget `timeline@1`)
- [`MetricTiles`](#metrictiles) (widget `metric@1`)
- [`Chart`](#chart) (widget `chart@1`)
- [`Comparison`](#comparison) (widget `comparison@1`)
- [`DocumentViewer`](#documentviewer) (widget `document@1`)
- [`Decision`](#decision) (widget `decision@1`)
- [`SchemaForm`](#schemaform) (widget `form@1`)
- [`Checklist`](#checklist) (widget `checklist@1`)
- [`Approval`](#approval) (widget `approval@1`)
- [`Calculator`](#calculator) (widget `calculator@1`)
- [`Poll`](#poll) (widget `poll@1`)
- [`RankedVote`](#rankedvote) (widget `ranked-vote@1`)
- [`Board`](#board) (widget `board@1`)
- [`Conversation`](#conversation) (widget `conversation@1`)
- [`StatusList`](#statuslist) (widget `status-list@1`)
- [`RouteLink`](#routelink)
- [`canvas`](#canvas) (object)
- [`useTool`](#usetool) (hook)
- [`ToolUnavailable`](#toolunavailable)

### Note

A block of prose rendered from Markdown: headings, lists, emphasis, code, quotes and https links. Raw HTML is shown as text.

Widget node: `{ "kind": "widget", "widget": "note@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `markdown` | string | yes | Markdown, 1 to 4,000 characters. Single line breaks are kept. |

Example data:

```json
{"markdown":"The vendor review is **complete**.\n\n- Two options remain\n- Decision due Friday"}
```

### RecordDetail

One record as labelled fields, each with an optional source and as-of time, under optional status badges. Use facts for a quick map, or fields for detail.

Widget node: `{ "kind": "widget", "widget": "facts@1", "data": { ... } }`. Also exported as `Facts`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `facts` | Record<string, string \| number \| boolean \| null> | no | Each key is a label and each value is shown beside it. Supply facts or fields. |
| `fields` | { label: string; value: string \| number \| boolean \| null; source?: string; asOf?: string }[] | no | Up to 500 fields in order. An https source or value becomes a link; anything else is text. |
| `columns` | 1 \| 2 | no | Columns of fields. Default 1. Always 1 below 360 px wide. |
| `badges` | (string \| { label: string; tone?: "default" \| "flag" })[] | no | 1 to 8 short status badges above the fields, such as High priority. tone flag fills the badge that needs attention; flag one or two at most. |
| `layout` | "stacked" \| "inline" | no | Default stacked: each label above its value. inline: label at the start of the row and value at the end, for short values. |

Example data:

```json
{"badges":[{"label":"High priority","tone":"flag"},"Reply due in 2 hours"],"fields":[{"label":"Owner","value":"Platform team","source":"Vendor register"},{"label":"Budget","value":42000,"asOf":"2026-11-01"}],"columns":2}
```

### DataTable

Rows of records with sortable columns, a text filter above 10 rows, and pages of 50. Rows can be highlighted and selected.

Widget node: `{ "kind": "widget", "widget": "table@1", "data": { ... } }`. Also exported as `Table`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `rows` | Record<string, string \| number \| boolean \| null>[] | yes | Up to 500 rows. Missing cells are blank. |
| `columns` | { key: string; label?: string; type?: "text" \| "number" \| "date" \| "currency" \| "badge"; currency?: string }[] | no | Columns in order. Default: the union of row keys, as text. currency is an ISO 4217 code; default USD. |
| `highlightWhere` | { key: string; equals: string \| number \| boolean \| null } | no | Highlight rows whose cell at key equals the value. |
| `selectable` | boolean | no | Show a checkbox on each row. |
| `stateKey` | string | no | In a component, keep the selected row indexes (into rows) in this shared-state key. Needs selectable. |
| `highlight` | (row) => boolean | no | Component use only: return true to highlight a row. |

Example data:

```json
{"rows":[{"Vendor":"Acme","Price":120,"Status":"Preferred"},{"Vendor":"Globex","Price":98,"Status":"Trial"}],"columns":[{"key":"Vendor"},{"key":"Price","type":"currency","currency":"USD"},{"key":"Status","type":"badge"}],"highlightWhere":{"key":"Vendor","equals":"Acme"}}
```

### Timeline

Dated events with an optional detail and status. Status is shown by a symbol and colour.

Widget node: `{ "kind": "widget", "widget": "timeline@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `events` | { at: string; label: string; detail?: string; status?: "default" \| "success" \| "warning" \| "danger" }[] | yes | Up to 500 events. at is free text, such as an ISO date. |
| `order` | "newest" \| "oldest" | no | Sort by at. Default: the order given. Events whose at is not a date go last. |

Example data:

```json
{"events":[{"at":"2026-11-02","label":"Contract sent","status":"success"},{"at":"2026-11-09","label":"Decision due","detail":"Legal review pending"}],"order":"newest"}
```

### MetricTiles

One to six headline values. With a numeric baseline, each tile shows the change as up, down or flat with the difference. A caption adds a line in your own words.

Widget node: `{ "kind": "widget", "widget": "metric@1", "data": { ... } }`. Also exported as `Metric`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no | A single tile: what the value measures. Supply label and value, or tiles. |
| `value` | string \| number \| boolean \| null | no | A single tile: the value to show. |
| `tiles` | { label: string; value: string \| number \| boolean \| null; unit?: string; baseline?: string \| number \| boolean \| null; caption?: string; flag?: boolean }[] | no | 1 to 6 tiles. caption is a line of text under the value, such as In 31 days, up to 200 characters. flag: true marks a caption that needs attention with a dot and heavier text; it needs a caption. |

Example data:

```json
{"tiles":[{"label":"Monthly cost","value":1200,"unit":"USD","baseline":1000},{"label":"Open tickets","value":14,"baseline":14},{"label":"Renewal","value":"12 December","caption":"In 31 days","flag":true}]}
```

### Chart

Bar, line or stacked bar chart drawn as SVG, with axis labels, a legend for several series, and a data table for screen readers.

Widget node: `{ "kind": "widget", "widget": "chart@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | "bar" \| "line" \| "stacked" | no | Default bar. |
| `rows` | { label: string; value: number }[] | no | One series, up to 500 points. Supply rows, or series with labels. |
| `series` | { name: string; values: number[] }[] | no | 1 to 8 series, each with one value per label. |
| `labels` | string[] | no | Category labels along the x axis, with series. |
| `xLabel` | string | no | The x axis title. |
| `yLabel` | string | no | The y axis title. |
| `emphasis` | { label: string } \| { series: string } | no | Draw one label's marks, or one series, in full and let the rest recede. Give label (one of the labels) or series (a series name), not both. A chart of one series inks the picked bar and greys the others. |

Example data:

```json
{"type":"bar","labels":["Q1","Q2","Q3"],"series":[{"name":"Acme","values":[120,135,150]},{"name":"Globex","values":[98,110,104]}],"xLabel":"Quarter","yLabel":"Cost","emphasis":{"series":"Acme"}}
```

### Comparison

Two to four options side by side across named criteria. A recommended option is labelled.

Widget node: `{ "kind": "widget", "widget": "comparison@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `options` | { name: string; values: Record<string, string \| number \| boolean \| null> }[] | yes | 2 to 4 options. values is keyed by criterion; missing values show as not given. |
| `criteria` | string[] | yes | 1 to 50 criteria, as rows in order. |
| `recommended` | string | no | The name of the option to label as recommended. |

Example data:

```json
{"options":[{"name":"Acme","values":{"Price":"$1,200","Support":"24/7"}},{"name":"Globex","values":{"Price":"$980","Support":"Business hours"}}],"criteria":["Price","Support"],"recommended":"Acme"}
```

### DocumentViewer

Long text or Markdown rendered safely, with highlights by character range, or a line-level diff against a previous version.

Widget node: `{ "kind": "widget", "widget": "document@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | string | yes | Plain text or Markdown, up to 200,000 characters. Shown as text, never as HTML. |
| `previous` | string | no | An earlier version. When given, shows a line diff with additions and removals marked by symbol and label. |
| `highlights` | { start: number; end: number; note?: string }[] | no | Up to 200 ranges of character offsets into text, end exclusive. Notes are numbered and listed below. Ignored in diff mode. |

Example data:

```json
{"text":"# Renewal terms\n\nThe price rises **5%** each year.\n\n- Notice period: 60 days","highlights":[{"start":17,"end":50,"note":"Above the 3% cap"}]}
```

### Decision

Two to twenty options for one choice, as buttons or cards, each with an optional description, and an optional reason. Lettered in order.

Widget node: `{ "kind": "widget", "widget": "decision@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `options` | (string \| { value: string; label?: string; description?: string })[] | yes | 2 to 20 options. value is what the choice records; label is shown, default value. Shown as text, never as HTML. |
| `layout` | "buttons" \| "cards" | no | Default buttons: one option per row. cards: a grid of cards, for options with descriptions. |
| `requireReason` | boolean | no | Ask for a reason. With field, the reason is drafted as <field>Reason; add that answer field. |
| `field` | string | no | Draft the chosen option's value into this answer field, filling the answer panel. Declare it in answerSchema as a string, or an enum of the option values: a field answerSchema does not have is refused. |
| `stateKey` | string | no | Keep the chosen option's value. The reason stays with each viewer in this shared-state key, so everyone sees it and get_canvas_state reads it before any answer. Without it the value is each viewer's own. |

Example data:

```json
{"options":[{"value":"Renew","label":"Renew with Acme","description":"Keeps the current terms"},{"value":"Switch","label":"Switch to Globex","description":"Saves money, needs migration"}],"layout":"cards","requireReason":true,"field":"choice"}
```

### SchemaForm

Labelled fields generated from a small JSON schema, with help text, required markers and inline validation. A label alone gives one text field.

Widget node: `{ "kind": "widget", "widget": "form@1", "data": { ... } }`. Also exported as `Form`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | no | One text field with this label. Supply label or schema. |
| `schema` | { type: "object"; properties: Record<string, Property>; required?: string[] } | no | Up to 30 properties. A Property has type string, number, integer, boolean or array; title (the label); description (help text); enum (single choice, for string or number); items: { type?, enum } (multi-select, with type array); minimum and maximum (numbers); minLength and maxLength (text). Anything else is rejected. |
| `spread` | boolean | no | Draft each property as its own answer field, under the property name, instead of one object in field. |
| `field` | string | no | Draft the valid values as one object (the text, with label) into this answer field, filling the answer panel. Declare it in answerSchema as type object (type string, with label): a field answerSchema does not have is refused. |
| `stateKey` | string | no | Keep the valid values as one object in this shared-state key, so everyone sees it and get_canvas_state reads it before any answer. Without it the value is each viewer's own. |

Example data:

```json
{"schema":{"type":"object","required":["budget"],"properties":{"budget":{"type":"number","title":"Budget","description":"In US dollars","minimum":0},"region":{"type":"string","title":"Region","enum":["North","South"]},"notes":{"type":"string","title":"Notes","maxLength":500}}},"field":"details"}
```

### Checklist

Items people tick off, each with an optional owner and note, with progress such as 3 of 7 done.

Widget node: `{ "kind": "widget", "widget": "checklist@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | (string \| { label: string; owner?: string; note?: string })[] | yes | 1 to 100 items. Shown as text, never as HTML. |
| `field` | string | no | Draft the ticked item indexes, in order into this answer field, filling the answer panel. Declare it in answerSchema as type array with items { type: integer }: a field answerSchema does not have is refused. |
| `stateKey` | string | no | Keep the ticked item indexes, so ticks show to everyone live in this shared-state key, so everyone sees it and get_canvas_state reads it before any answer. Without it the value is each viewer's own. |

Example data:

```json
{"items":[{"label":"Read the security review","owner":"Dana"},{"label":"Check the renewal date","note":"Due 1 December"}],"stateKey":"review-checklist"}
```

### Approval

A statement to approve or reject, with a comment that is required to reject. Records who decided.

Widget node: `{ "kind": "widget", "widget": "approval@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `statement` | string | yes | What the person approves or rejects. Shown as text, never as HTML. |
| `field` | string | no | Draft { decision: "approved" \| "rejected", comment?, by } into this answer field, filling the answer panel. Declare it in answerSchema as type object: a field answerSchema does not have is refused. by is the viewer's name as the page knows it, or Someone. |
| `stateKey` | string | no | Keep the same { decision, comment?, by } in this shared-state key, so everyone sees it and get_canvas_state reads it before any answer. Without it the value is each viewer's own. |

Example data:

```json
{"statement":"Approve the Acme renewal at $1,200 a month.","field":"approval"}
```

### Calculator

Labelled number inputs and outputs computed from formulas as the inputs change. An output that cannot be computed, such as a division by zero, shows a dash.

Widget node: `{ "kind": "widget", "widget": "calculator@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `inputs` | { name: string; label: string; value: number; min?: number; max?: number; step?: number; unit?: string }[] | yes | 1 to 20 inputs. name is used in formulas: letters, digits and underscores, starting with a letter or underscore. value is the starting value. Arrow keys change a value by step. |
| `outputs` | { label: string; formula: string; unit?: string; decimals?: number }[] | yes | 1 to 10 outputs. A formula uses numbers, input names, + - * / ( ), and min, max, round (round(x) or round(x, digits)) and abs. Anything else is rejected when the canvas is saved. |
| `field` | string | no | Draft { inputs: { name: number }, outputs: { label: number \| null } } into this answer field, filling the answer panel. Declare it in answerSchema as type object: a field answerSchema does not have is refused. |
| `stateKey` | string | no | Keep the same { inputs, outputs }, so everyone works on one set of inputs in this shared-state key, so everyone sees it and get_canvas_state reads it before any answer. Without it the value is each viewer's own. |

Example data:

```json
{"inputs":[{"name":"seats","label":"Seats","value":40,"min":1,"step":1},{"name":"price","label":"Price per seat","value":30,"min":0,"unit":"USD"}],"outputs":[{"label":"Monthly cost","formula":"seats * price","unit":"USD"},{"label":"Yearly cost","formula":"round(seats * price * 12 * 0.9)","unit":"USD"}]}
```

### Poll

One question that several people vote on at once: one vote each, changeable until voting closes. Shows counts as labelled bars and, when people gave names, who voted. Several people use it at once with no one-person control, also inside an agent component. Results update live for everyone.

Widget node: `{ "kind": "widget", "widget": "poll@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `question` | string | yes | The question. Shown as text, never as HTML. |
| `options` | (string \| { value: string; label?: string })[] | yes | 2 to 10 options. value is what a vote records; label is shown, default value. |
| `stateKey` | string | yes | 1 to 80 characters. Each person's vote is its own key, <stateKey>:<participantId>, holding { choice: option value, name? }, or null once withdrawn. get_canvas_state returns every key, so read or count the keys that start with <stateKey>: to get the result. A participant id is a hash of a signed-in viewer, or a random id kept in the browser on link canvases, where one person can vote again from another browser. |
| `hideUntilClosed` | boolean | no | Show only the number of votes until voting closes. Needs closedKey. Votes are still in shared state, so this hides results on the page, not from the agent. |
| `closedKey` | string | no | A shared-state key that closes voting when set to true, by set_canvas_state or by anyone pressing Close voting. Without it the poll stays open. |
| `field` | string | no | Draft { counts: { value: number }, total, winner? } as it changes, once this viewer has voted into this answer field, filling the answer panel. Declare it in answerSchema as type object: a field answerSchema does not have is refused. winner is left out on a tie. |

Example data:

```json
{"question":"Which vendor should we shortlist?","options":["Acme","Globex",{"value":"Initech","label":"Initech (new)"}],"stateKey":"vendor-poll","closedKey":"vendor-poll-closed","field":"shortlist"}
```

### RankedVote

Several people each put the options in order, with up and down buttons or by dragging, and save a ballot. The result is a Borda count: with n options, first place scores n - 1 points down to 0 for last. Shows the ranking, totals and number of ballots; ties share a rank. Several people use it at once with no one-person control, also inside an agent component. Results update live for everyone.

Widget node: `{ "kind": "widget", "widget": "ranked-vote@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `question` | string | yes | The question. Shown as text, never as HTML. |
| `options` | (string \| { value: string; label?: string })[] | yes | 2 to 10 options. value is what a ballot records; label is shown, default value. |
| `stateKey` | string | yes | 1 to 80 characters. Each person's ballot is its own key, <stateKey>:<participantId>, holding { order: option values, first choice first, name? }. get_canvas_state returns every key, so read or count the keys that start with <stateKey>: to get the result. A participant id is a hash of a signed-in viewer, or a random id kept in the browser on link canvases, where one person can rank again from another browser. An option a ballot leaves out scores 0 from it. |
| `field` | string | no | Draft { ranking: option values, most points first, points: { value: number }, ballots } as it changes, once this viewer has saved a ballot into this answer field, filling the answer panel. Declare it in answerSchema as type object: a field answerSchema does not have is refused. |

Example data:

```json
{"question":"Rank the launch priorities","options":["Speed","Price","Support"],"stateKey":"priorities"}
```

### Board

Columns of cards, such as To do, Doing and Done, that several people work on at once. Anyone can add a card, edit its text and owner, move it with its Move to menu or by dragging, and delete it. Up to 200 cards of 500 characters. Several people use it at once with no one-person control, also inside an agent component. Results update live for everyone.

Widget node: `{ "kind": "widget", "widget": "board@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `columns` | string[] | yes | 2 to 6 column names, each used once. Shown as text, never as HTML. |
| `stateKey` | string | yes | 1 to 80 characters. Each card is its own key, <stateKey>:card:<cardId>, holding { text, column, owner?, order, updatedAt }, or null once deleted; cards sort by order within a column. Two people moving the same card at once leave it in one column for everyone. Read the cards with get_canvas_state. |
| `cards` | { text: string; column?: string; owner?: string }[] | no | Up to 200 starting cards; column defaults to the first. A starting card has the id seed<index>, so writing <stateKey>:card:seed0 changes or (with null) deletes it. |
| `field` | string | no | Draft { columns: { name: card text[] } } as it changes, once this viewer has changed the board into this answer field, filling the answer panel. Declare it in answerSchema as type object: a field answerSchema does not have is refused. Each column lists at most 100 cards. |

Example data:

```json
{"columns":["To do","Doing","Done"],"stateKey":"launch-board","cards":[{"text":"Draft the announcement","owner":"Dana"},{"text":"Book the venue","column":"Doing"}]}
```

### Conversation

A thread of messages to read, such as a support ticket or an email exchange: who said it, when, and the text. Read-only; for discussion on the canvas people use comments.

Widget node: `{ "kind": "widget", "widget": "conversation@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `messages` | { from: string; text: string; at?: string; side?: "theirs" \| "ours" }[] | yes | 1 to 200 messages, shown in the order given. from is who said it and at is free text, such as a time. side ours sets your own team's messages in from the edge with a rule; default theirs. Shown as text, never as HTML. |

Example data:

```json
{"messages":[{"from":"Dana at Northwind","text":"We were charged twice after upgrading.","at":"09:14"},{"from":"Sam","text":"I can see both charges. Checking with billing now.","at":"09:20","side":"ours"}]}
```

### StatusList

Read-only results of checks, each marked pass, flag, fail or pending by a symbol and a word, with a count such as 3 of 4 pass. Use Checklist when people tick the items themselves.

Widget node: `{ "kind": "widget", "widget": "status-list@1", "data": { ... } }`.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | { label: string; status: "pass" \| "flag" \| "fail" \| "pending"; note?: string }[] | yes | 1 to 100 checks. flag is a check that needs a look; fail is one that did not pass; pending has not run. note is a line of detail under the label. Shown as text, never as HTML. |
| `summary` | boolean | no | Show the count of passes above the list. Default true. |

Example data:

```json
{"items":[{"label":"Ties to the general ledger","status":"pass"},{"label":"Cloud spend up 18%","status":"flag","note":"Needs commentary"}]}
```

### RouteLink

A link to one of the component's pages, for a component that declares routes. A real link: it takes focus and the keyboard, and is marked aria-current="page" while its page is open, which is the hook for styling the active one. Other anchor props (className, style, aria-label) pass through.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `to` | string | yes | One of the node's declared route paths, such as /customers. A path the node does not declare goes nowhere. |
| `children` | ReactNode | yes | The link's content. |

Example data:

```json
{"to":"/customers","children":"Customers"}
```

### canvas

The canvas a component runs on. await canvas.tools.<name>(input) calls one of the canvas's tools and resolves with its output. It rejects with an Error whose reason is tool_unavailable (the canvas has no tool of that name, or cannot run it now), tool_failed, tool_timeout (no answer within 30 seconds), invalid_tool_input, read_only (the person only views the canvas, or the canvas is closed), invite_required (the person has only the link: ask the owner to invite them), offline or rate_limit (more than 10 calls a second, or 8 at once, from one page). A name the canvas does not have rejects at once. Catch the rejection and show ToolUnavailable, or use useTool, which never rejects.

| Member | Type | Required | Description |
| --- | --- | --- | --- |
| `tools` | Record<string, (input?: object) => Promise<output>> | yes | One function per tool, by the tool's name. input is a JSON object that fits the tool's input schema, up to 64 KB; default {}. The output fits the tool's output schema. |

Example:

```tsx
import { canvas } from "@canvasview/kit";

try {
  const quote = await canvas.tools.lookup({ sku: "A-100" });
} catch (error) {
  // error.reason is a code such as tool_unavailable
}
```

### useTool

Call useTool(name) to get { call, pending, error, available } for one of the canvas's tools. call(input) resolves with the tool's output, or with undefined after setting error; it never rejects, so a press handler needs no try. pending is true while a call is out. error is the last failure, an Error with the same reason codes canvas.tools rejects with, cleared by the next call. available is false when the canvas has no tool of that name or cannot run it now: render ToolUnavailable in place of what the tool feeds.

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | The tool's name: a lowercase letter, then up to 39 letters, digits or underscores. |

Example:

```tsx
import { ToolUnavailable, useTool } from "@canvasview/kit";

const { call, pending, error, available } = useTool("lookup");
if (!available) return <ToolUnavailable what="Prices" />;
return <button disabled={pending} onClick={() => call({ sku: "A-100" })}>Look up</button>;
```

### ToolUnavailable

The calm state for something a canvas tool feeds: one quiet line, such as Live prices: not available right now. Render it in place of the result when useTool's available is false or a call failed, so a tool that cannot run never looks like a broken component.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `what` | string | no | What is missing, in a word or two. Shown before the reason. Shown as text, never as HTML. |
| `error` | { reason: string } | no | The error from useTool, or one caught from canvas.tools. The line then says why in plain words, such as offline or took too long. Without it: not available right now. |

Example data:

```json
{"what":"Live prices"}
```
