# Toolbench > A zero-dependency framework for small interactive tools in any web page. A tool is one function plus one JSON manifest; the runtime builds the form, runs the function and draws the result. Contract version 9. Install: `npm install @toolbench/sdk @toolbench/runtime`. Machine-readable: reference.json beside this file. ## Pages - [Inputs](./docs-ref-inputs.html): Every input type, its attributes, and the rules a manifest's inputs are held to. - [Results](./docs-ref-results.html): Every result kind a tool can return, its attributes, and an example of each. - [Manifest](./docs-ref-manifest.html): tool.json, the run function, and cases.json: every attribute and rule. - [Host API](./docs-ref-host.html): defineToolHost, the element's attributes, properties, methods and events, and tool sources. - [Theme tokens](./docs-ref-tokens.html): The custom properties a page sets to theme every tool, and the parts it can style. # Inputs Each input becomes a labelled control on the form, with its `description` as the control's accessible description. Its value reaches `run(values)` under its `id`. ## Types | Input | Type | Required | Optional | Use it for | |---|---|---|---|---| | [`text`](#text) | string | `default` | `maxLength` | One line: a name, a pattern, an id | | [`textarea`](#textarea) | string | `default` | `rows` `maxLength` `lang` (v6) `grammar` (v7) | Pasted data, a document, source in a notation | | [`number`](#number) | number, clamped to `min..max` | `default` `min` `max` | `step` `control` (v4) | A bounded quantity: a rate, a count, a size | | [`select`](#select) | string, one of the option values | `default` `options` | `control` (v4) `options[].sets` (v6) | A choice from a short fixed list, as a dropdown or a row of segments, or a preset that fills other inputs | | [`toggle`](#toggle) | boolean | `default` | none | On or off | | [`date`](#date) (v9) | string, `YYYY-MM-DD`, `HH:MM` or `YYYY-MM-DDTHH:MM`, within `min..max` | `default` | `mode` `min` `max` | A day, a time of day, or both, in the browser's own picker | | [`color`](#color) (v9) | string, a lowercase `#rrggbb` | `default` | none | A colour, typed as hex or picked in the system's picker | | [`secret`](#secret) (v9) | string, empty until typed, at least `minLength` characters | | `minLength` `maxLength` | A password, a token or a key, with no default and kept out of links | | [`multiselect`](#multiselect) (v9) | string[], the chosen option values in the options' order, `min` to `max` of them | `default` `options` | `min` `max` `control` | Several choices from a fixed list, as checkboxes or chips | | [`rows`](#rows) (v9) | Row[], one object per row keyed by column id, `minRows` to `maxRows` of them, even for one column | `columns` `maxRows` `default` | `minRows` | A list the reader adds to, removes from and reorders: hosts, or paths and their limits | ## Common attributes Every input type takes these. | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `id` | `string` | **yes** | | v1 | The key this input's value has in `run(values)`. Unique within the tool. | | `label` | `string` | **yes** | | v1 | The visible label, which is also the control's accessible name. | | `description` | `string` | no | | v1 | The hint a form shows and a screen reader reads. Not a placeholder: those vanish on focus. Takes light markup: a blank line starts a paragraph, a line starting "- " is a list item, `code`, **strong**, and {good|text} for the tones good, warn, bad, muted and accent. Anything else is text. | | `details` | `string` | no | | **v8** | The rest of the help, behind a native disclosure under the description: every case a format covers, per-vendor rules, the reasons. Takes the same markup as `description`. It is not part of the control's accessible description, so a screen reader announces only the short hint on focus. | | `unit` | `string` | no | | v1 | Shown after the control: "ms", "req/s", "bytes". A number without one is a riddle. | | `primary` | `boolean` | no | | v1 | Show this input on a compact card, where there is only room for what matters. Mark as many as the question genuinely needs: a card asking "how many servers" is useless with only one of a rate and a duration. Mark none and a card shows the first input. Every input is shown on the full tool regardless. | | `dir` | `"ltr" | "auto"` | no | | v1 | Force text direction. Bytes, code and identifiers are left-to-right even inside right-to-left prose, and a runtime cannot know which is which, so the tool says. | | `share` | `boolean` | no | `true, and false on a secret` | **v8** | Whether a host may put this input's value in a link it builds from `host.values`, and read it back from one. `false` for a credential or pasted private material: a URL ends up in browser history, server logs, chat previews and referrer headers. A privacy hint for hosts, not access control: the runtime builds no links and does not enforce it, and `host.values` still writes the input. | | `role` | `string` | no | | **v8** | A name for this input, rendered as `data-role` on its `.tb-field-row`, so a host stylesheet can style "the credential" or "the advanced ones" without naming each id. The runtime gives it no meaning, as with a result part's `role`. | ## text **Type:** string. Takes the common attributes, plus: | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `default` | `string` | **yes** | v1 | The value the form opens with. | | `maxLength` | `number` | no | v1 | The most characters the control accepts. | ### Example ```json { "id": "pattern", "type": "text", "label": "Pattern", "description": "A JavaScript regular expression, without the surrounding slashes.", "primary": true, "dir": "ltr", "maxLength": 80, "default": "(\\w+)\\s+(\\d+)" } ``` From `tools/regex-explainer/tool.json`. ## textarea **Type:** string. Takes the common attributes, plus: | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `default` | `string` | **yes** | | v1 | The value the form opens with. | | `rows` | `number` | no | `3` | v1 | Visible lines of text. | | `maxLength` | `number` | no | | v1 | The most characters the control accepts. | | `lang` | `string` | no | | **v6** | The language of what the reader types, passed to the host's `highlight(source, lang)` hook, the same one `code` results use, which paints it behind the textarea. A hint: with no hook, or a hook that cannot paint it, the textarea is the plain one. | | `grammar` | `TextareaGrammar` | no | | **v7** | The tool's own notation for `lang`, so a textarea is painted where no host highlighter knows the language: a state-machine syntax, a packet filter. Requires `lang`. A host hook that paints `lang` still wins. | ### Grammar attributes The keys of `grammar`. Each is optional, and a grammar needs at least one. Patterns are not allowed: a regular expression from a tool could hang the page it is on. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `keywords` | `string[]` | no | **v7** | Whole words drawn as keywords, case-sensitive: letters, digits and `_`. At most 64, each 32 characters or fewer. | | `comment` | `string` | no | **v7** | What starts a comment that runs to the end of its line, `#` or `//`: ASCII punctuation, at most 4 characters. | | `operators` | `string[]` | no | **v7** | Literal operators, `->` or `|`, the longest match winning: ASCII punctuation. At most 32, each 4 characters or fewer. | | `strings` | `string` | no | **v7** | The characters that open a string, each closed by itself, `"` or `"'`. A backslash escapes the next character, and a string ends at the end of its line. At most 4. | ### Example ```json { "id": "machine", "type": "textarea", "label": "Machine", "description": "One state or transition per line. # starts a comment.", "primary": true, "lang": "doors", "grammar": { "keywords": ["initial", "state", "on", "final"], "comment": "#", "operators": ["->", ":"], "strings": "\"" }, "rows": 9, "maxLength": 4000, "dir": "ltr", "default": "# A door that locks\ninitial closed\nstate open\nstate locked \"needs a key\"\non push: closed -> open\non pull: open -> closed\non lock: closed -> locked\non unlock: locked -> closed" } ``` From `bench/fixtures/grammar-input/tool.json`. ## number **Type:** number, clamped to `min..max`. Takes the common attributes, plus: | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `default` | `number` | **yes** | v1 | The value the form opens with, within `min..max`. | | `min` | `number` | **yes** | v1 | The lowest value. Required: the only guard against a runaway value. A value outside `min..max` is refused before `run`, and a host's write is clamped. | | `max` | `number` | **yes** | v1 | The highest value, above `min`. | | `step` | `number` | no | v1 | The increment for the arrow keys and the slider. Any value when omitted. | | `control` | `"slider"` | no | **v4** | `slider` asks for a range control beside the number box, for a tool read by sweeping a value rather than by typing one. A hint, not a type: a runtime that does not know it renders the box alone. | ### Example ```json { "id": "rtt", "type": "number", "label": "Round-trip time", "unit": "ms", "description": "Drag, use the arrow keys, or type an exact value.", "primary": true, "default": 80, "min": 10, "max": 500, "step": 10, "control": "slider" } ``` From `bench/fixtures/slider/tool.json`. ## select **Type:** string, one of the option values. Takes the common attributes, plus: | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `default` | `string` | **yes** | | v1 | The option value the form opens with. | | `options` | `SelectOption[]` | **yes** | | v1 | Two or more choices, with unique values. | | `control` | `"segmented"` | no | `the dropdown` | **v4** | `segmented` draws the options as a row of segments instead of a dropdown, for two to five short choices a reader should see at once. They are native radio buttons in a fieldset, so the arrow keys, the single tab stop and the announcement are the browser's, and the chosen one is a solid accent fill. When a segment would not fit its label, measured as the form resizes, the row becomes stacked radios. A hint, not a type: `run` still receives an option value, presets work as they do on a dropdown, and a runtime that does not know it draws the dropdown. | ### Option attributes Each entry of `options`. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `value` | `string` | **yes** | v1 | What `run` receives when this option is chosen. Unique within the select. | | `label` | `string` | **yes** | v1 | What the reader sees. | | `sets` | `InputValues` | no | **v6** | A preset: choosing this option writes these values into the other inputs it names, which the reader then edits. Partial, keyed by input id, checked like a sample's `input`. `run` still receives the select's own value. The select follows the form afterwards: once the values stop matching this option, it shows the preset they do match, or the first option with no `sets` (a "Custom"), or stays put if there is none. | ### Example ```json { "id": "stack", "type": "select", "label": "Library", "description": "Fills the read timeout, retries and backoff. Edit them afterwards.", "default": "a", "options": [ { "value": "a", "label": "Library A", "sets": {"read": 30, "retries": 2, "backoff": 200} }, { "value": "b", "label": "Library B", "sets": {"read": 60, "retries": 3, "backoff": 500} }, { "value": "c", "label": "Library C", "sets": {"read": 10, "retries": 0, "backoff": 0} }, {"value": "custom", "label": "Custom"} ] } ``` From `bench/fixtures/presets/tool.json`. ## toggle **Type:** boolean. Takes the common attributes, plus: | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `default` | `boolean` | **yes** | v1 | Whether the form opens with it on. | ### Example ```json { "id": "fixed", "type": "toggle", "label": "Limits added", "description": "Whether the two free-text fields have been given a length limit.", "primary": true, "default": false } ``` From `bench/fixtures/styled-output/tool.json`. ## date **Type:** string, `YYYY-MM-DD`, `HH:MM` or `YYYY-MM-DDTHH:MM`, within `min..max`. Takes the common attributes, plus: | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `mode` | `"date" | "time" | "datetime"` | no | `"date"` | **v9** | What the reader picks, in the browser's own picker: a day, a time of day, or both. `run` gets `YYYY-MM-DD`, `HH:MM` or `YYYY-MM-DDTHH:MM`, local time, with no zone. | | `default` | `string` | **yes** | | **v9** | The value the form opens with, written in the mode's format, within `min..max`. | | `min` | `string` | no | | **v9** | The earliest value, in the mode's format. A value before it is refused before `run`, with a message naming the input, so `run` can trust the range. | | `max` | `string` | no | | **v9** | The latest value, in the mode's format, not before `min`. | ### Example ```json { "id": "captured", "type": "date", "mode": "datetime", "label": "Captured at", "unit": "local time", "description": "When the trace was taken, between **2020-01-01** and **2026-10-25**. Arrives as `2026-10-25T14:30`.", "default": "2026-10-25T14:30", "min": "2020-01-01T00:00", "max": "2026-10-25T23:59" } ``` From `bench/fixtures/native-inputs/tool.json`. ## color **Type:** string, a lowercase `#rrggbb`. Takes the common attributes, plus: | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `default` | `string` | **yes** | **v9** | The colour the form opens with, as `run` gets every colour: a lowercase `#rrggbb`, with no alpha and no shorthand. The reader types it in a hex box, the labelled control, or picks it from the system's picker in the swatch beside it. | ### Example ```json { "id": "accent", "type": "color", "label": "Highlight colour", "description": "Six hex digits, as `#rrggbb`. The swatch opens the system picker.", "default": "#1f5aa8" } ``` From `bench/fixtures/native-inputs/tool.json`. ## secret **Type:** string, empty until typed, at least `minLength` characters. Takes the common attributes, plus: | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `minLength` | `number` | no | **v9** | The fewest characters `run` accepts. A shorter value is refused before `run`, naming the input. | | `maxLength` | `number` | no | **v9** | The most characters the control accepts, not below `minLength`. | ### Example ```json { "id": "token", "type": "secret", "label": "API token", "description": "Used for this run only. {warn|Never put in a shared link.}", "minLength": 16, "maxLength": 64 } ``` From `bench/fixtures/native-inputs/tool.json`. ## multiselect **Type:** string[], the chosen option values in the options' order, `min` to `max` of them. Takes the common attributes, plus: | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `default` | `string[]` | **yes** | | **v9** | The option values the form opens with chosen, each once, between `min` and `max` of them. `[]` for none. | | `options` | `SelectOption[]` | **yes** | | **v9** | Two or more choices, with unique values. An option here takes no `sets`: several presets at once have no single meaning. | | `min` | `number` | no | `0` | **v9** | The fewest a reader may choose. `1` is what makes "pick at least one" a rule. | | `max` | `number` | no | `the number of options` | **v9** | The most a reader may choose. At it, the rest are marked unavailable, with the reason beside them. | | `control` | `"checkboxes" | "chips"` | no | `checkboxes up to 6 options, chips above` | **v9** | How the choices are drawn: `checkboxes`, a native checkbox per option, or `chips`, a row of toggle buttons that wraps, for a long list of short values. A hint, like a number's `slider`. | ### Example ```json { "id": "regions", "type": "multiselect", "label": "Regions", "description": "Each chosen region asks every record type.", "default": ["us-east-1", "us-west-2", "eu-west-1"], "max": 4, "options": [ {"value": "us-east-1", "label": "us-east-1"}, {"value": "us-east-2", "label": "us-east-2"}, {"value": "us-west-1", "label": "us-west-1"}, {"value": "us-west-2", "label": "us-west-2"}, {"value": "ca-central-1", "label": "ca-central-1"}, {"value": "eu-west-1", "label": "eu-west-1"}, {"value": "eu-west-2", "label": "eu-west-2"}, {"value": "eu-central-1", "label": "eu-central-1"}, {"value": "eu-north-1", "label": "eu-north-1"}, {"value": "ap-south-1", "label": "ap-south-1"}, {"value": "ap-northeast-1", "label": "ap-northeast-1"}, {"value": "ap-southeast-1", "label": "ap-southeast-1"}, {"value": "ap-southeast-2", "label": "ap-southeast-2"}, {"value": "sa-east-1", "label": "sa-east-1"} ] } ``` From `bench/fixtures/multiselect/tool.json`. ## rows **Type:** Row[], one object per row keyed by column id, `minRows` to `maxRows` of them, even for one column. Takes the common attributes, plus: | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `columns` | `RowColumn[]` | **yes** | | **v9** | One to four columns, each a text, number, select or toggle with that type's keys and no `default`. | | `maxRows` | `number` | **yes** | | **v9** | The most rows a reader may add, at most 50. Required, as a number's `max` is: the guard against a runaway list, and a runaway link. At it, Add stays focusable with the reason beside it. | | `minRows` | `number` | no | `0` | **v9** | The fewest rows Run accepts. | | `default` | `Row[]` | **yes** | | **v9** | The rows the form opens with, each with a value for every column. `[]` for none. | ### Column attributes Each entry of `columns`: a `type` of `text`, `number`, `select` or `toggle`, that type's own keys (`maxLength`; `min`, `max` and `step`; `options`), and these. A column takes no `default`. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `id` | `string` | **yes** | **v9** | The key of this column's value in each row `run` receives. Unique within the input. | | `label` | `string` | **yes** | **v9** | The column's heading, and the start of each cell's accessible name: "Path, row 2 of 3". | | `unit` | `string` | no | **v9** | Shown after each cell: "req/s". | | `dir` | `"ltr" | "auto"` | no | **v9** | Force text direction, as on an input. | ### Example ```json { "id": "limits", "type": "rows", "label": "Limits", "description": "A path prefix and its limit. The first matching path wins, so order matters.", "minRows": 1, "maxRows": 8, "columns": [ { "id": "path", "type": "text", "label": "Path", "maxLength": 100, "dir": "ltr" }, { "id": "limit", "type": "number", "label": "Limit", "min": 1, "max": 10000, "step": 1, "unit": "req/s" } ], "default": [{"path": "/api/login", "limit": 5}, {"path": "/api", "limit": 500}] } ``` From `tools/rate-limits/tool.json`. ## Validation rules `validateManifest` checks these before anything runs. The message is exactly what an author gets. | Rule | Message | |---|---| | A tool has at least one input | inputs: a tool needs at least one input; a tool with none is a constant | | Input ids are unique | inputs[2].id: "n" is already used by inputs[0] | | A number declares `min` and `max` | inputs[0].min/max: a number input must declare min and max: they are the only guard against a runaway input | | `min` is below `max` | inputs[0].min: min (10) must be below max (5) | | A number's default is within `min..max` | inputs[0].default: default 90 is outside min..max (1..10) | | `control` on a number is `"slider"` | inputs[0].control: control must be "slider" when present, got "knob" | | `control` on a select is `"segmented"` | inputs[1].control: control must be "segmented" when present, got "slider" | | A date's `min`, `default` and `max` parse in its `mode` | inputs[0].default: must be written YYYY-MM-DD in mode "date", got "25/10/2026" | | A date's default is within `min..max` | inputs[0].default: Day must be on or after 2020-01-01, got 2019-12-31. | | `mode` only on a date | inputs[2].mode: only a date input has a mode; on a text input it would be ignored | | A colour's default is a lowercase `#rrggbb` | inputs[0].default: Tint must be a #rrggbb colour, got "#15A". | | A secret has no default | inputs[0].default: a secret has no default: a manifest is published, so every reader would have it | | A secret's `minLength` is not above its `maxLength` | inputs[0].minLength: 32 is above maxLength 16 | | No sample or preset sets a secret | samples[0].input.key: is a secret, which a manifest may not set: every reader would have it | | A select has two or more options | inputs[1].options: a select needs at least two options; with one, use a fixed value | | Option values are unique | inputs[1].options: option values must be unique | | A select's default is one of its options | inputs[1].default: must be one of the option values: fast, exact | | A multiselect's default is a list of its options, each once | inputs[1].default[1]: "slow" is not one of the options: fast, exact | | A multiselect's `min` is not above its `max` | inputs[1].min: min (2) is above max (1) | | `control` on a multiselect is `"checkboxes"` or `"chips"` | inputs[1].control: control must be "checkboxes" or "chips" when present, got "segmented" | | Rows declare `maxRows`, from 1 to 50 | inputs[2].maxRows: a rows input must declare maxRows: it is the guard against a runaway list and a runaway link | | Rows have one to four columns | inputs[2].columns: takes 1 to 4 columns, got 0 | | A column has no `default`; the rows input's `default` gives every row a value | inputs[2].columns[0].default: belongs in the rows input's default, which gives every row a value | | Every default row has a value for every column | inputs[2].default[0]: has no "port": every row needs a value for every column | | `lang` only on a textarea | inputs[2].lang: only a textarea is highlighted; on a text input it would be ignored | | `grammar` needs `lang` beside it | inputs[2].grammar: needs a lang beside it: the grammar is the tool's own notation for that language | | `share` is `true` or `false` | inputs[2].share: must be true or false | | `role` is a non-empty string | inputs[2].role: must be a non-empty string | | `details` is a non-empty string | inputs[2].details: must be a non-empty string | # Results A tool's `run` returns one result. The runtime draws it: a tool never writes HTML. Every result is an object with a `kind`, and a `group` holds several. ## Kinds | Kind | Use it for | Required | Since | |---|---|---|---| | [`fields`](#fields) | Label and value pairs, optionally grouped | `fields` | v1 | | [`text`](#text) | Prose, or preformatted text | `text` | v1 | | [`code`](#code) | Source with a language, painted by the host's highlighter | `lang` `source` | v1 | | [`table`](#table) | Rows and typed columns, optionally linked to a chart | `columns` `rows` | v1 | | [`series`](#series) | Line, area, bar, scatter, box and other x/y charts | `chart` | v1 | | [`heatmap`](#heatmap) | A value over two dimensions | `heatmap` | **v4** | | [`pie`](#pie) | Parts of a whole, or a donut with its total | `pie` | **v4** | | [`radar`](#radar) | A profile across several measures | `radar` | **v4** | | [`treemap`](#treemap) | A hierarchy sized by value | `treemap` | **v4** | | [`venn`](#venn) | How two or three sets overlap | `venn` | **v4** | | [`graph`](#graph) | Nodes and edges, laid out by the runtime | `graph` | **v7** | | [`group`](#group) | Several results in one answer | `parts` | v1 | | [`bytes`](#bytes) | Hex dump with offsets, a printable gutter and named ranges | `bytes` | **v2** | | [`error`](#error) | The input was wrong, and the tool says which one | `message` | v1 | ## Common attributes Any result, or any part of a group, may carry these. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `role` | `string` | no | **v5** | What this part is to the reader, `"verdict"` or `"stats"` say, rendered as `data-role` on the part's root element for host CSS and tests to find it by name rather than by position. | | `id` | `string` | no | **v5** | A name for this one part, unique within the result, rendered as `data-part`, so a page can link to it: `host.reveal(id)` opens it if it is collapsed, scrolls to it and moves focus there. Where `role` says what kind of part this is, `id` says which one. | ```json { "kind": "fields", "id": "stats", "role": "stats", "fields": [ {"label": "Fields that are data", "value": "6 of 8"}, {"label": "Accept any text", "value": "2", "note": "1 high risk"}, {"label": "Tools checked", "value": "4"} ] } ``` A part of what `bench/fixtures/styled-output` returns, named `stats` for a host stylesheet. ## fields | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `fields` | `Field[]` | **yes** | v1 | Shown in order as a list of labels and values, grouped by `group`. A card shows the first `cardFields`. | ### Field A label and a value. The plainest thing a tool can say. | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | | v1 | What the value is: the term the value is listed under. | | `value` | `string` | **yes** | | v1 | The value, shown as the tool formatted it. | | `note` | `string` | no | | v1 | A qualifier shown beside the value: a unit, a caveat, "(dynamic)". | | `group` | `string` | no | | v1 | Fields sharing a group are rendered together under its name. | | `tone` | `TextTone` | no | | v1 | Draws attention without inventing a severity scale, or `muted` to let a routine value recede. | | `role` | `string` | no | | **v5** | A name for this one field, rendered as `data-role`, so a host stylesheet can find "the verdict" without counting rows. The runtime gives it no meaning. | | `mono` | `boolean` | no | `true` | **v8** | Set the value, and its note, in the monospace face, for an address, a hex string or a header the reader compares character by character. `false` for a sentence or a plain number with a unit, which read like terminal output in it. Rendered as `.tb-mono` on the `
`, the class a table cell uses. | ### Example ```json { "kind": "fields", "fields": [ {"label": "count", "value": "20"}, {"label": "p50", "value": "28"}, {"label": "p99", "value": "1200"} ] } ``` What `tools/percentiles` returns. Long arrays are cut to their first three items. ## text | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `text` | `string` | **yes** | | v1 | Shown as a paragraph, or preformatted when `mono` is set. | | `mono` | `boolean` | no | `false` | v1 | Monospace and preformatted, keeping spaces and line breaks. | ### Example ```json {"kind": "text", "text": "Matched once, at 0-6."} ``` What `tools/regex-explainer` returns. ## code | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `lang` | `string` | **yes** | v1 | Passed to the host's `highlight(source, lang)` hook and set as `data-lang`. A single token also becomes the `language-` class. | | `source` | `string` | **yes** | v1 | Shown as is, and what Copy copies even when a highlighter adds line numbers. | ### Example ```json { "kind": "code", "lang": "json", "source": "{\n \"matchCount\": 1,\n \"matches\": [\n {\n \"text\": \"user 42\",\n \"index\": 0,\n \"groups\": [\n \"user\",\n \"42\"\n ]\n }\n ]\n}" } ``` What `tools/regex-explainer` returns. ## table | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `columns` | `Column[]` | **yes** | v1 | The header row. Each row's cells line up with these in order. | | `rows` | `Cell[][]` | **yes** | v1 | One array of cells per row, in column order. | | `caption` | `string` | no | v1 | Shown above the table, except on a card. | | `link` | `{ chart: string; keys: (number | string | null)[] }` | no | **v4** | Link these rows to a chart in the same result: `keys[i]` is the chart x that row i describes, `null` for a row with none. Pointing at the chart highlights the matching rows, and pointing at a row moves the chart's readout to it. | ### Column | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | | v1 | The header text, also set on each cell as `data-label` so host CSS can name a value on a narrow screen. | | `align` | `"start" | "end"` | no | `"start"` | v1 | Alignment of the header and the column's cells. Without it, a cell given as a number aligns to the end and every other cell to the start. | | `mono` | `boolean` | no | | v1 | Monospace, for anything the reader will compare digit by digit. | | `wrap` | `"prose" | "none"` | no | | **v8** | How the column's text wraps, rendered as `data-wrap` on its header and every cell, so a narrow layout knows which columns are sentences: `prose` may wrap and wants a reading width, `none` is a short token (a hash, a date, a status word) kept on one line. A cell's own `wrap` overrides it. Absent makes no claim, and no attribute is written. | | `rowHeader` | `boolean` | no | `false` | **v8** | The column names each row: its cells render as `` instead of ``, so a screen reader announces the row's name with every cell, and on a narrow tool the column stays in view while the rest scrolls. Opt-in, because a host selector such as `td:first-child` stops matching those cells. | ### Example ```json { "kind": "table", "caption": "The same data under both definitions. A comparison across systems using different ones means nothing.", "columns": [ {"label": "", "mono": true}, {"label": "Nearest rank", "align": "end", "mono": true}, {"label": "Linear", "align": "end", "mono": true}, {"label": "Difference", "align": "end", "mono": true} ], "rows": [ [ {"text": "p50", "mono": true}, {"text": "28", "mono": true}, {"text": "29.5", "mono": true}, {"text": "+5.4%", "mono": true, "tone": "warn"} ], [ {"text": "p90", "mono": true}, {"text": "220", "mono": true}, {"text": "246.0", "mono": true}, {"text": "+11.8%", "mono": true, "tone": "warn"} ], [ {"text": "p95", "mono": true}, {"text": "480", "mono": true}, {"text": "516.0", "mono": true}, {"text": "+7.5%", "mono": true, "tone": "warn"} ] ] } ``` What `tools/percentiles` returns. Long arrays are cut to their first three items. ## series | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `chart` | `Chart` | **yes** | v1 | The chart: its axes, its series and how it answers a pointer. | ### Chart | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `xLabel` | `string` | **yes** | | v1 | The x axis title, with `xUnit` in parentheses. Also heads the data table's x column. | | `yLabel` | `string` | **yes** | | v1 | The left y axis title, with `yUnit` in parentheses. | | `x` | `(number | string)[]` | **yes** | | v1 | One position per value. Numbers for a quantity; text for named things, "Mesh", "SFU", "p99", which are drawn as evenly spaced categories in the order given. If any value is text, all are read as text. | | `series` | `Series[]` | **yes** | | v1 | Every line, bar or area, drawn in order, each in the next of six series colours. A legend is shown when there is more than one. | | `xUnit` | `string` | no | | v1 | Shown after `xLabel` in parentheses, on the axis and in the data table. | | `yUnit` | `string` | no | | v1 | Shown after `yLabel` in parentheses. | | `annotations` | `{ x: number | string; label: string }[]` | no | | v1 | A vertical marker with a name: "the knee", "capacity", "p99". | | `thresholds` | `{ y: number; label: string; axis?: "left" | "right"; tone?: Tone }[]` | no | | **v4** | A labelled horizontal rule at a y value: a budget, an SLO, a limit. Not a series, so it adds no legend entry, no data-table column and no readout row, and the y scale widens to show it. `tone` lets a limit read as one. | | `yLabelRight` | `string` | no | | v1 | Second axis label, required if any series sets `axis: "right"`. | | `readout` | `Readout` | no | | **v4** | How the chart answers a pointer or keyboard focus. Default: a crosshair and every series at the nearest x. | | `id` | `string` | no | | **v4** | A name a `table` in the same result can link to with `link.chart`. Unique within a result. | | `orientation` | `"vertical" | "horizontal"` | no | | **v4** | `horizontal` draws bars growing rightward, one row per x, for long category names and budgets read left to right. Bar series only: other shapes are not drawn on a horizontal chart, and the data table's caption says so. | | `mirror` | `boolean` | no | | **v4** | On a horizontal bar chart: series the tool gives as negative values are drawn to the left, and every label (ticks, readout, data table) shows the magnitude. For back-to-back comparisons and pyramids, where both sides are quantities and neither is negative. | | `yScale` | `"linear" | "log"` | no | | **v4** | `log` draws an axis in powers of ten, for values spanning orders of magnitude: latency distributions, loss from 0.01% to 10%. A log axis cannot show zero or a negative, so those points are left out and the data table's caption says so; bars are not drawn on a log y axis, because a bar's length means nothing there. | | `xScale` | `"linear" | "log"` | no | `"linear"` | **v4** | `log` draws the x axis in powers of ten, as `yScale` does for y. Ignored when `x` is text. | ### Series One line, bar or area on a chart. | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | | v1 | The series' name in the legend, the readout and the data table. | | `points` | `(number | null)[]` | **yes** | | v1 | One value per x. A `null` is a gap, not a zero, and the distinction matters on a chart. | | `unit` | `string` | no | | v1 | Shown after each value in the readout, and after `label` in the legend and the data table. The first right-axis series' unit also goes beside `yLabelRight`. | | `shape` | `"line" | "area" | "bar" | "points" | "step" | "band" | "box"` | no | | v1 | `points` draws a marker at each value and no line between them: for an x of separate items (frames, requests, runs) where nothing exists between two of them, so a joining line would invent a slope. `step` holds each value until the next x, then rises or falls straight to the next: for state sampled on events (a buffer's depth, a configured rate), where a sloped line invents a gradual change. | | `boxes` | `({ low: number; q1: number; median: number; q3: number; high: number; outliers?: number[] } | null)[]` | no | | **v4** | The summary each `box` draws, one per x: whiskers from `low` to `high`, a box from `q1` to `q3`, a rule at `median`, and `outliers` as points. The tool computes it, so what the whiskers mean (1.5 IQR, min to max, p5 to p95) is the tool's and belongs in the chart's labels. Set `points` to the medians, which is what the readout, links and scale read where they read one value. | | `lower` | `(number | null)[]` | no | | **v4** | The lower edge of a `band`, one per x, with `points` as the upper edge: a spread such as p5 to p95, or a min-to-max envelope, filled between the two under every other series. A `null` in either edge breaks the band. | | `markers` | `boolean` | no | | **v4** | Draw a marker at each value of a `line` or `area`, so every real point is visible and a series under an identical one can still be found. Each series gets its own marker shape. | | `stack` | `string` | no | | **v4** | Bar series sharing a `stack` id are stacked at each x, in series order, bottom to top, so a reader sees each part and the total. Positive and negative values stack away from zero separately. Bar series without one are grouped beside the stack as before. | | `x` | `number[]` | no | | **v4** | This series' own x values, one per point, instead of the chart's shared `x`: for measurements that do not share x positions, a scatter of (delay, cut-off) pairs say. The x scale covers every series. Usually with `shape: "points"`. | | `sizes` | `(number | null)[]` | no | | **v4** | A third measure per point, drawn as the AREA of a bubble on a `points` series. By area, not radius, so a value twice as large looks twice as large. `sizeLabel` names it in the key and the readout: "Calls". | | `sizeLabel` | `string` | no | `"Size"` | **v4** | The name of `sizes` in the key, the readout and the data table. | | `axis` | `"left" | "right"` | no | `"left"` | v1 | `right` draws the series against a second y scale on the right, titled by the chart's `yLabelRight`. | | `notes` | `(string | null)[]` | no | | **v4** | What the readout says for this series at each point, replacing the formatted value there: "250 ms, 60 ms late". `null` keeps the default for that point. Same length as `points`. | ### Readout How a chart answers a pointer or keyboard focus. Omit it for the default: a crosshair at the nearest x and a card listing every series there. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `mode` | `"x" | "point" | "none"` | no | v1 | `x`: every series at the nearest x (default). `point`: only the nearest point. `none`: no readout. | | `crosshair` | `"x" | "y" | "both" | "none"` | no | v1 | Which crosshair lines to draw. Default `x`. | | `titles` | `(string | null)[]` | no | v1 | A heading per x, replacing " ": "Frame 6 (lost)". `null` keeps the default. Same length as `x`. | | `highlightTable` | `boolean` | no | v1 | Highlight the matching row of this chart's own data table as the readout moves. | ### Example ```json { "kind": "series", "chart": { "xLabel": "Bin midpoint", "yLabel": "Count", "yLabelRight": "Cumulative", "x": [3, 5, 7], "series": [ {"label": "Count", "points": [3, 7, 5], "shape": "bar"}, { "label": "Cumulative", "points": [15, 50, 75], "unit": "%", "shape": "line", "axis": "right" } ] } } ``` What `tools/histogram` returns. Long arrays are cut to their first three items. ## heatmap A value over two dimensions, one coloured cell each. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `heatmap` | `Heatmap` | **yes** | **v4** | The grid of values and its row and column labels. | ### Heatmap A value over two dimensions: latency by hour and weekday, loss by bitrate and delay. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `xLabel` | `string` | **yes** | **v4** | The title under the columns. | | `yLabel` | `string` | **yes** | **v4** | The title above the row labels. | | `label` | `string` | **yes** | **v4** | What a cell's value is, for the key and the readout: "p95 latency". | | `unit` | `string` | no | **v4** | Shown after each value in the readout, and after `label` in the key and the data table's caption. | | `x` | `(number | string)[]` | **yes** | **v4** | The column labels, left to right. | | `y` | `(number | string)[]` | **yes** | **v4** | The row labels, top to bottom. | | `values` | `(number | null)[][]` | **yes** | **v4** | `values[row][column]`. A `null` or missing cell has no value and is drawn hatched. | | `scale` | `"sequential" | "diverging"` | no | **v4** | `sequential` (default) is one hue, light to dark: how much. `diverging` is two hues either side of `midpoint`. | | `midpoint` | `number` | no | **v4** | The neutral value of a diverging scale. Default 0. | ### Example ```json { "kind": "heatmap", "heatmap": { "xLabel": "Hour", "yLabel": "Day", "label": "Packet loss", "unit": "%", "x": ["00:00", "02:00", "04:00"], "y": ["Mon", "Tue", "Wed"], "values": [[0.2, 0.2, 0.2], [0.2, 0.2, 0.2], [0.2, 0.2, 0.2]] } } ``` What `bench/fixtures/discrete-series` returns for `{"case":"heatmap"}`. Long arrays are cut to their first three items. ## pie Parts of a whole, every slice labelled with its name and share. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `pie` | `Pie` | **yes** | **v4** | The whole and its slices. | ### Pie Parts of a whole. Built so the known ways a pie misleads cannot happen: every slice is labelled with its share, slices are drawn in the order given from 12 o'clock clockwise, more than six fold into "Other", and a negative or zero total draws a message rather than a chart. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | **v4** | What the whole is: "Traffic by codec". | | `unit` | `string` | no | **v4** | Shown after each value in the readout and in the data table's Value column. | | `slices` | `{ label: string; value: number; tone?: Tone; explode?: boolean }[]` | **yes** | **v4** | `explode` pulls a slice out a little to point at it. A slice pulled out looks bigger than it is, so use it for the one part the surrounding text is about, not for decoration. | | `donut` | `boolean` | no | **v4** | A ring rather than a disc, with `total` in its centre. | | `total` | `string` | no | **v4** | The donut's centre text: "48 kb/s". | ### Example ```json { "kind": "pie", "pie": { "label": "One voice stream, 48 kb/s", "unit": "kb/s", "slices": [ {"label": "Opus audio", "value": 32}, {"label": "RTP headers", "value": 4.8}, {"label": "UDP headers", "value": 3.2}, {"label": "IPv4 headers", "value": 8} ] } } ``` What `bench/fixtures/discrete-series` returns for `{"case":"pie"}`. ## radar A profile across several measures, one spoke each. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `radar` | `Radar` | **yes** | **v4** | The axes and the series drawn across them. | ### Radar A profile across several measures: one spoke per axis, clockwise from 12 o'clock, each series a closed shape. All spokes share one scale from zero unless an axis gives its own `max`. At most four series are drawn, since more overlap past reading. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | **v4** | What the chart is: its accessible name and the data table's caption. | | `unit` | `string` | no | **v4** | Shown after each value in the readout, and after each series' name in the legend and the data table. | | `axes` | `{ label: string; max?: number }[]` | **yes** | **v4** | One spoke per axis, clockwise from 12 o'clock. An axis `max` gives that spoke its own scale; otherwise every spoke shares one, rounded up from the largest value to 1, 2 or 5 times a power of ten. Values are clamped between zero and the scale's top. | | `series` | `{ label: string; values: (number | null)[] }[]` | **yes** | **v4** | One value per axis; a `null` breaks the shape at that spoke rather than drawing it at the centre. | ### Example ```json { "kind": "radar", "radar": { "label": "Network quality, out of 10", "axes": [{"label": "Latency"}, {"label": "Jitter"}, {"label": "Loss"}], "series": [ {"label": "Fibre", "values": [9, 9, 9]}, {"label": "Mobile", "values": [6, 5, 6]}, {"label": "Satellite", "values": [2, 6, 7]} ] } } ``` What `bench/fixtures/discrete-series` returns for `{"case":"radar"}`. Long arrays are cut to their first three items. ## treemap A hierarchy of sizes as nested rectangles. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `treemap` | `Treemap` | **yes** | **v4** | The hierarchy, from its root. | ### Treemap A hierarchy of sizes: areas proportional to values, at most three levels drawn. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | **v4** | What the whole is: its accessible name and the data table's caption. | | `unit` | `string` | no | **v4** | Shown after each value on a rectangle, in the readout and in the data table. | | `root` | `TreemapNode` | **yes** | **v4** | The whole. Not drawn itself: its children are the top level, each in its own series colour. | ### TreemapNode One node of a treemap: a leaf has a `value`, a branch has `children`. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | **v4** | Drawn on the rectangle where it fits, cut at 18 characters, and always in the readout and the data table. | | `value` | `number` | no | **v4** | A leaf's size. A negative counts as zero, and a node of zero is not drawn. On a branch the children's sum is drawn instead, and the data table's caption says the two differed. | | `children` | `TreemapNode[]` | no | **v4** | Drawn inside this node's rectangle, largest first. Past the third level they fold into their parent. | | `tone` | `Tone` | no | **v4** | Replaces the colour this node's top-level group gives it. `normal` keeps the group's colour. | ### Example ```json { "kind": "treemap", "treemap": { "label": "Monthly cost", "unit": "k$", "root": { "label": "All", "children": [ { "label": "Media", "children": [ { "label": "Relays", "children": [ {"label": "Europe", "value": 38}, {"label": "Americas", "value": 52}, {"label": "Asia Pacific", "value": 30} ] }, {"label": "TURN", "value": 22}, {"label": "Recording", "value": 14} ] }, { "label": "Speech", "children": [ {"label": "Recognition", "value": 64}, {"label": "Synthesis", "value": 41} ] }, { "label": "Model", "children": [ {"label": "Inference", "value": 120}, {"label": "Caching", "value": 9} ] }, {"label": "Signalling", "value": 11} ] } } } ``` What `bench/fixtures/discrete-series` returns for `{"case":"treemap"}`. ## venn The overlap of two or three sets. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `venn` | `Venn` | **yes** | **v4** | The sets and their overlaps. | ### Venn The overlap of two or three sets. Two sets are drawn with exact areas. Three cannot in general be, so every region is labelled with its count and the caption says the areas are approximate. More than three sets, or an overlap larger than a set, draws a message instead. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | **v4** | What the diagram is: its accessible name and the data table's caption. | | `unit` | `string` | no | **v4** | Shown after each count in the readout and in the data table's Count column. | | `sets` | `{ label: string; size: number }[]` | **yes** | **v4** | Two or three sets, each `size` above zero and counting every member, including those in an overlap. Any other number of sets draws a message instead. | | `overlaps` | `{ sets: number[]; size: number }[]` | **yes** | **v4** | `sets` are indices into `sets`: [0, 1], or [0, 1, 2] for the middle of three. | ### Example ```json { "kind": "venn", "venn": { "label": "Users by client", "sets": [{"label": "Web", "size": 100}, {"label": "Mobile", "size": 100}], "overlaps": [{"sets": [0, 1], "size": 50}] } } ``` What `bench/fixtures/discrete-series` returns for `{"case":"venn-two"}`. ## graph Nodes and edges, laid out by the runtime, with every edge also in a table. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `graph` | `Graph` | **yes** | **v7** | The nodes, edges, groups and lanes, and how to lay them out. | ### Graph Nodes and edges, laid out and drawn by the runtime: flowcharts, state machines, dependency graphs, org charts, networks, entity-relationship diagrams. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | **v7** | What the graph is: "Order lifecycle". The figure's accessible name. | | `layout` | `"layered" | "tree" | "radial" | "force" | "manual"` | no | **v7** | Default `layered`: ranks along `direction`, for anything with a flow. `tree` and `radial` for a hierarchy, `force` for a network with no direction, `manual` to place every node at its `x` and `y`. | | `direction` | `"LR" | "RL" | "TB" | "BT"` | no | **v7** | Default `LR` for layered, `TB` for tree. | | `routing` | `"orthogonal" | "spline" | "straight" | "polyline"` | no | **v7** | Default `orthogonal` for layered and tree, `straight` for force and manual, `spline` for radial. An edge a straight line cannot draw without crossing a node is drawn as a polyline instead. | | `directed` | `boolean` | no | **v7** | Default true. | | `merge` | `"none" | "source" | "target"` | no | **v7** | Default `none`: no two edges ever share a stretch of line. `source` lets the edges leaving one node share a trunk, the bus of an org chart; `target` the edges arriving at one. | | `seed` | `number` | no | **v7** | Force layout only. Default 1. The same seed gives the same picture everywhere. | | `spacing` | `"compact" | "normal" | "loose"` | no | **v7** | Default `normal`. | | `nodes` | `GraphNode[]` | **yes** | **v7** | Every node, ids unique. At least one, or the graph is listed instead of drawn. | | `edges` | `GraphEdge[]` | **yes** | **v7** | Every edge, each end naming a node. All are also listed in a table under the picture. | | `groups` | `GraphGroup[]` | no | **v7** | Boxes drawn around their members in any layout. A node joins one with `group`. | | `lanes` | `GraphLane[]` | no | **v7** | Swimlanes, in order. Layered layouts only. A node joins one with `lane`. | | `highlight` | `string[]` | no | **v7** | A path to emphasise, as node ids in order. | | `limits` | `{ nodes?: number; edges?: number }` | no | **v7** | The tool's own limits. Past either, the runtime shows the edge table and says why instead of drawing. No default: without one, any size is drawn. | | `interaction` | `{ /** Whether a reader can collapse and open groups. Default true. */ collapse?: boolean; /** What pointing at or focusing a node emphasises. Default `neighbours`. */ focus?: "neighbours" | "none"; /** Whether picking two nodes highlights the shortest path between them. Default true. */ paths?: boolean; }` | no | **v7** | What a reader can do with the picture. Every part is on by default. | ### GraphNode | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `id` | `string` | **yes** | **v7** | Unique within the graph. What edges, `highlight` and the edge table name. | | `label` | `string` | no | **v7** | Default: the id. | | `note` | `string` | no | **v7** | A second, muted line under the label. | | `shape` | `GraphShape` | no | **v7** | Default `rounded`. | | `tone` | `Tone` | no | **v7** | Status: the border and tint of the tone. Wins over `color`. | | `color` | `GraphColor` | no | **v7** | Category: a series colour or a named token. | | `badge` | `string` | no | **v7** | A few characters over the top-right corner: a count, a version. | | `rows` | `GraphRow[]` | no | **v7** | The rows of a `record` node. | | `group` | `string` | no | **v7** | The innermost group this node belongs to. Groups nest through `parent`. | | `lane` | `string` | no | **v7** | Layered layouts only: the swimlane. | | `rank` | `number` | no | **v7** | Layered layouts only: pin this node to a rank, 0 being the first. | | `x` | `number` | no | **v7** | `manual` layout: the centre. `force` layout: pins the node here. | | `y` | `number` | no | **v7** | `manual` layout: the centre's y, in the same units as `x`; a missing one counts as 0. `force` layout: pins the node only with `x` set too. | | `role` | `string` | no | **v7** | Rendered as `data-role`, for host stylesheets. The runtime gives it no meaning. | ### GraphEdge | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `from` | `string` | **yes** | **v7** | The id of the node the edge leaves. Must name a node, or the graph is listed instead of drawn. | | `to` | `string` | **yes** | **v7** | The id of the node the edge arrives at. Must name a node; the same as `from` draws a loop. | | `fromPort` | `string` | no | **v7** | A row id on the `from` node, a `record`: the edge attaches at that row. Must name one of its rows. | | `toPort` | `string` | no | **v7** | A row id on the `to` node, a `record`: the edge attaches at that row. Must name one of its rows. | | `fromSide` | `GraphSide` | no | **v7** | The side of the `from` node the edge must use. Default: the side facing the other end. | | `toSide` | `GraphSide` | no | **v7** | The side of the `to` node the edge must use. Default: the side facing the other end. | | `label` | `string` | no | **v7** | Drawn on the line at `labelAt`, and shown in the edge table and the keyboard readout. | | `labelAt` | `"start" | "middle" | "end"` | no | **v7** | Where along the edge the label sits. Default `middle`. | | `line` | `"solid" | "dashed" | "dotted"` | no | **v7** | Default `solid`. | | `width` | `"thin" | "normal" | "thick"` | no | **v7** | Default `normal`. | | `tone` | `Tone` | no | **v7** | Status: the line in the tone's colour. Wins over `color`, except `normal`. | | `color` | `GraphColor` | no | **v7** | Category: a series colour or a named token for the line. An unknown value draws the default colour. | | `head` | `GraphMarker` | no | **v7** | At `to`. Default `arrow` when the graph is directed, else `none`. | | `tail` | `GraphMarker` | no | **v7** | At `from`. Default `none`. | | `role` | `string` | no | **v7** | Rendered as `data-role`, for host stylesheets. The runtime gives it no meaning. | ### GraphGroup | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `id` | `string` | **yes** | **v7** | Unique among groups and nodes, because a collapsed group is drawn as a node with this id. What a node's `group` names. | | `label` | `string` | **yes** | **v7** | The title at the group's top-left corner, which collapses the group when pressed. Also the collapsed node's label. | | `parent` | `string` | no | **v7** | The enclosing group. Groups nest to any depth; a cycle of parents is refused. | | `collapsed` | `boolean` | no | **v7** | Drawn as one node standing for its members until a reader opens it. Default false. | | `color` | `GraphColor` | no | **v7** | The box's colour, and the colour of the node standing for the group when collapsed. Default: the plain border. | | `role` | `string` | no | **v7** | Rendered as `data-role` on the box and on the collapsed node, for host stylesheets. The runtime gives it no meaning. | ### GraphLane A swimlane: a straight band across a layered layout. A group may not span two. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `id` | `string` | **yes** | **v7** | Unique among lanes. What a node's `lane` names. | | `label` | `string` | **yes** | **v7** | The title at the start of the band. | | `role` | `string` | no | **v7** | Rendered as `data-role` on the band, for host stylesheets. The runtime gives it no meaning. | ### GraphRow One row of a `record` node: a column of a table, with a port of its own an edge can name. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `id` | `string` | **yes** | **v7** | Unique within the node; what `fromPort` and `toPort` name. | | `label` | `string` | **yes** | **v7** | The row's text, after `tag` and before `detail`. | | `detail` | `string` | no | **v7** | Right-aligned and muted: a column type, a value. | | `tag` | `string` | no | **v7** | A short tag before the label, such as PK or FK. | ### Example ```json { "kind": "graph", "graph": { "label": "Package dependencies", "direction": "BT", "nodes": [ {"id": "app", "badge": "4"}, {"id": "admin", "badge": "4"}, {"id": "api-client", "badge": "3"} ], "edges": [ {"from": "api-client", "to": "app"}, {"from": "ui-kit", "to": "app"}, {"from": "api-client", "to": "admin"} ], "highlight": ["core", "http", "api-client", "app"] } } ``` What `tools/build-order` returns. Long arrays are cut to their first three items. ## group | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `parts` | `Output[]` | **yes** | v1 | Rendered in order, each by its own kind, and may nest. A host's `cardParts` caps how many are shown. | ### Example ```json { "kind": "group", "parts": [ { "kind": "fields", "fields": [ {"label": "Bytes", "value": "19"}, { "label": "Characters", "value": "11", "note": "code points, not UTF-16 units" }, {"label": "Encoding", "value": "4 multi-byte"}, { "label": "Overhead", "value": "8 bytes", "note": "what UTF-8 costs over one byte per character" } ] }, { "kind": "bytes", "bytes": [72, 195, 169], "caption": "Highlighted runs are one character each", "highlight": [ {"at": 1, "len": 2, "label": "U+00E9 Γ©"}, {"at": 8, "len": 3, "label": "U+4E16 δΈ–"}, {"at": 11, "len": 3, "label": "U+754C η•Œ"}, {"at": 15, "len": 4, "label": "U+1F44B πŸ‘‹"} ] } ] } ``` What `tools/utf8-bytes` returns. Long arrays are cut to their first three items. ## bytes Raw bytes, as a reader of a wire format wants to see them: offsets down the side, hex in the middle, printable characters in a gutter, and named ranges over the top. | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `bytes` | `number[]` | **yes** | | **v2** | One number per byte, 0 to 255, drawn sixteen to a row. A card shows the first four rows. | | `offset` | `number` | no | `0` | **v2** | What the first byte is labelled in the offset column and the legend. | | `highlight` | `ByteRange[]` | no | | **v2** | Named ranges, each coloured over its bytes and listed in a legend. Where two overlap, the later one wins. | | `caption` | `string` | no | | **v2** | Shown above the grid, on a card too. | ### ByteRange A run of bytes worth calling out inside a `bytes` output: a header field, a length prefix, the region an error points at. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `at` | `number` | **yes** | **v2** | Offset from the start of `bytes`, not from `offset`. | | `len` | `number` | **yes** | **v2** | How many bytes, from `at`. Bytes past the end of `bytes` are not marked. | | `label` | `string` | **yes** | **v2** | Named in the legend with its length and offset, and the hover title of each of its bytes. | | `tone` | `Tone` | no | **v2** | Default: the next of six series colours, counted over ranges without a tone, so neighbouring ranges differ. | ### Example ```json { "kind": "bytes", "bytes": [72, 195, 169], "caption": "Highlighted runs are one character each", "highlight": [ {"at": 1, "len": 2, "label": "U+00E9 Γ©"}, {"at": 8, "len": 3, "label": "U+4E16 δΈ–"}, {"at": 11, "len": 3, "label": "U+754C η•Œ"}, {"at": 15, "len": 4, "label": "U+1F44B πŸ‘‹"} ] } ``` What `tools/utf8-bytes` returns. Long arrays are cut to their first three items. ## error The INPUT was wrong, and the tool worked correctly by saying so. `input` names the control at fault so a generated form can mark it invalid and point a screen reader at the message. Without it, an error can only ever float unattached. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `message` | `string` | **yes** | v1 | Shown in full, never truncated, and announced. Also read as the description of the input named by `input`. | | `input` | `string` | no | v1 | The id of the input at fault: that control is marked `aria-invalid` and points its description at the message. | | `at` | `number` | no | v1 | Where in that input's value the problem starts, shown after the message. The tools here give a character offset. | | `len` | `number` | no | v1 | How far the problem runs from `at`, shown as a range after the message. | | `row` | `number` | no | **v9** | For a `rows` input, the index of the row at fault, 0 for the first: the message is shown under that row, named "Limits, row 2: ...", and its cells are marked invalid. | ### Example ```json { "kind": "error", "message": "\"three\" is not a number.", "input": "values", "at": 4, "len": 5 } ``` What `tools/histogram` returns for `{"values":"1 2 three"}`. # Manifest A tool is a directory with three files: `tool.json`, the manifest; `index.ts`, the module whose `run` computes the result; and `cases.json`, the known answers its fixtures check. ## Manifest attributes The keys of `tool.json`. | Attribute | Type | Required | Default | Since | Notes | | --- | --- | --- | --- | --- | --- | | `sdk` | `number` | **yes** | | v1 | The contract version this tool was written against. | | `id` | `string` | **yes** | | v1 | Stable, lowercase, hyphenated. Also the directory name and the URL segment. | | `name` | `string` | **yes** | | v1 | The tool's title, shown as its heading. | | `blurb` | `string` | **yes** | | v1 | One sentence. Shown on cards and used as a page description. | | `version` | `string` | **yes** | | v1 | The tool's own version, independent of `sdk`. | | `capabilities` | `Capability[]` | **yes** | | v1 | What the tool is allowed to do. At least one; `pure` is the only capability today. | | `inputs` | `InputSpec[]` | **yes** | | v1 | The form, one control per entry, in order. At least one. | | `kinds` | `OutputKind[]` | **yes** | | v1 | Every kind `run` can return, so a host can refuse a tool it cannot draw. | | `runtime` | `object` | **yes** | | v1 | How the runtime loads and runs the tool. | | `status` | `"live" | "deprecated" | "retired"` | no | | v1 | Lifecycle. Default `live`. `deprecated` still runs, with a visible marker a host can style, and is refused as a live card. `retired` does not run: the element explains and renders `links`, so a bookmarked URL is not a 404. | | `card` | `"live" | "info" | "none"` | no | | v1 | How the tool appears in a compact slot: runnable, a summary that links out, or not at all. | | `cardFields` | `number` | no | | v1 | How many fields a card shows before it stops. Errors are never truncated. | | `autoRun` | `boolean` | no | | v1 | Re-run as the reader types, instead of waiting for the Run button. Off by default, and that default is deliberate. A tool that runs on every keystroke does work nobody asked for, makes the Run button look broken (the answer is already there), and takes the decision away from the reader. Opt in only for a tool that is genuinely instant and reads better live, a unit converter say. Anything with a worker or a heavy loop should leave it alone. | | `reset` | `boolean` | no | `true` | **v8** | `false` hides the runtime's Reset control, which puts every input back to the values the tool opened with: for a tool with one input, where retyping it is the reset. | | `timeoutMs` | `number` | no | | v1 | Worker mode only. A tool that declares this on the main thread is rejected: nothing there can be stopped. | | `help` | `string` | no | | v1 | Markdown file, relative to the tool's directory, rendered as the tool's help. | | `tags` | `string[]` | no | | v1 | Free-form labels a host can filter or group tools by. | | `links` | `Link[]` | no | | v1 | Anything the tool wants to point at: a spec it implements, a post explaining it, a reference decoder. Deliberately generic: a host site's own content relations are the host's business. | | `samples` | `Sample[]` | no | | **v3** | Example inputs, rendered as a row of buttons under the form. Clicking one fills the form. It is an input change like any other, so the result goes stale and waits for Run, or re-runs if the tool set `autoRun`. A compact card does not show samples at all, because it has room for one input and a Run button, and a row of buttons there would crowd out the result the card exists to show. | ### runtime | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `entry` | `string` | **yes** | v1 | Entry file, relative to the tool's directory. | | `thread` | `"main" | "worker"` | no | v1 | `main` is the default and costs nothing. `worker` is for a tool whose running time depends on its input. It is the only mode where a timeout can actually stop anything. | ```json { "runtime": {"entry": "index.ts", "thread": "worker"}, "timeoutMs": 2000 } ``` From `tools/regex-explainer/tool.json`. ### Link Each entry of `links`. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | v1 | The link text. | | `href` | `string` | **yes** | v1 | An `http`, `https` or `mailto` URL, or a relative path. Anything else is refused. | ```json { "links": [ { "label": "UTF-8 (RFC 3629)", "href": "https://www.rfc-editor.org/rfc/rfc3629" } ] } ``` From `tools/utf8-bytes/tool.json`. ### Sample Each entry of `samples`, a button under the form that fills the inputs. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `label` | `string` | **yes** | v1 | What the button says. Short: it sits in a row with the others. | | `input` | `InputValues` | **yes** | v1 | Values keyed by input id. Partial on purpose: a sample may set one field and leave the rest as the reader left them. | ```json { "samples": [ { "label": "Two clusters", "input": { "values": "2 2 3 3 3 4 4 4 5 5 5 6 6 7 7 180 190 195 210 240 260 300 340 900" } }, { "label": "Definitions disagree", "input": {"values": "3 4 4 5 6 7 9 14 40 260", "method": "linear"} } ] } ``` From `tools/percentiles/tool.json`, the first two samples. ### Capabilities | Capability | Meaning | |---|---| | `pure` | No network, no storage, no clock, no randomness that is not seeded: the same input always gives the same result | ## Example ```json { "sdk": 3, "id": "histogram", "name": "Histogram", "blurb": "Bins a list of numbers and draws the counts as bars, with the running total on a second axis.", "version": "1.0.0", "capabilities": ["pure"], "runtime": { "entry": "index.ts", "thread": "main" }, "kinds": ["series", "error"], "card": "live", "autoRun": true, "inputs": [ { "id": "values", "type": "textarea", "label": "Measurements", "description": "Numbers separated by spaces, commas or newlines. Units are yours; the tool does not care.", "dir": "ltr", "rows": 4, "maxLength": 20000, "default": "2 3 3 4 4 4 5 5 5 5 6 6 6 7 7 8 9 11 14 18" }, { "id": "bins", "type": "number", "label": "Bins", "description": "Equal-width buckets between the smallest and largest value. More bins show finer shape and more empty buckets.", "unit": "bins", "default": 8, "min": 2, "max": 30, "step": 1 } ], "samples": [ { "label": "Even spread", "input": { "values": "1 2 2 3 3 3 4 4 5", "bins": 4 } }, { "label": "One spike", "input": { "values": "1 1 1 1 1 1 1 1 1 20" } }, { "label": "All the same", "input": { "values": "7 7 7 7 7" } }, { "label": "Not a number", "input": { "values": "2 3 4 nope 6" } } ], "help": "README.md", "tags": ["statistics", "chart"] } ``` `tools/histogram/tool.json`. ## The tool module `index.ts` default-exports an object with `run`. ```ts import type { Tool } from "@toolbench/sdk"; type Input = { values: string; bins: number }; // a type, not an interface export default { run({ values, bins }, ctx) { return { kind: "fields", fields: [{ label: "bins", value: String(bins) }] }; }, } satisfies Tool; ``` Declare the input type with `type`, not `interface`: an interface does not satisfy `InputValues`, and TypeScript's error does not say why. ### Ctx The second argument to `run`. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `signal` | `AbortSignal` | **yes** | v1 | Aborted when the reader cancels, when the input changes mid-run, or when a worker-mode timeout fires. A loop whose length depends on input MUST check it: on the main thread it is the only way to stop. | | `progress(fraction: number, partial?: Output)` | `void` | **yes** | v1 | Progress, and optionally a partial result. A runtime renders `partial` at most once per frame, which is what lets a simulation show itself converging instead of hiding behind a bar. | A simulation that stops when the reader cancels, and shows its running mean as it goes: ```ts const simulated = simulate(lambda, mu, samples, seed, (done, meanSoFar) => { if (ctx.signal.aborted) return; ctx.progress(done / samples, { kind: "fields", fields: [ { label: "samples so far", value: done.toLocaleString() }, { label: "simulated mean", value: `${fmt(meanSoFar)} ms` }, { label: "formula says", value: `${fmt(a.w)} ms` }, ], }); }); ``` From `tools/queue-explorer/index.ts`. ## Fixtures `cases.json` is an array of cases. `pnpm check` runs each and compares the result. | Attribute | Type | Required | Since | Notes | | --- | --- | --- | --- | --- | | `name` | `string` | **yes** | v1 | What the case checks, shown when it fails. | | `input` | `InputValues` | **yes** | v1 | The values `run` receives, keyed by input id, passed exactly as given. | | `expect` | `Output` | **yes** | v1 | The result `run` must return. | | `match` | `"exact" | "subset"` | no | v1 | Exact deep equality by default, because that is what catches a duplicated field, a reordered one, or garbage emitted beside a correct error. `subset` is an opt-in escape for a tool whose output is genuinely open-ended, and it demands two things in return: a `why`, and a `fieldCount`, so a dropped field is still a failure. | | `why` | `string` | no | v1 | Why this case needs `subset`. Required with it. | | `fieldCount` | `number` | no | v1 | How many fields the result has. Required with `subset`, so a dropped field still fails. | ```json { "name": "empty input is an error, not an empty chart", "input": {"values": " ", "bins": 8}, "expect": { "kind": "error", "message": "No numbers yet. Paste measurements separated by spaces, commas or newlines.", "input": "values" } } ``` An exact case for bad input, from `tools/histogram/cases.json`. ```json { "name": "linear interpolation returns a point between measurements, not one of them", "input": {"values": "1 2 3 4 5 6 7 8 9 10", "method": "linear"}, "match": "subset", "why": "this case is about the two summary percentiles only; the full group is asserted exactly by the case above", "fieldCount": 6, "expect": { "kind": "fields", "fields": [ {"label": "p50", "value": "5.50"}, {"label": "p99", "value": "9.91"} ] } } ``` A `subset` case, with the `why` and `fieldCount` it requires, from `tools/percentiles/cases.json`. ## Validation rules `validateManifest` checks these before anything runs. Rules for inputs are on the [Inputs](./docs-ref-inputs.html#validation-rules) page. | Rule | Message | |---|---| | `sdk` is a contract version this SDK reads | sdk: this tool needs contract version 99, but this runtime only speaks up to 9. Upgrade @toolbench/runtime, or lower the tool's sdk if it does not use the newer features. | | `id` is lowercase letters, digits and hyphens | id: "My Tool" must be lowercase letters, digits and hyphens: it is also a directory name and a URL segment | | `blurb` is under 200 characters | blurb: is 240 characters; keep it under 200 so it works as a card line and a page description | | At least one capability | capabilities: must declare at least one of pure | | `kinds` is not empty | kinds: declare every output kind `run` can return, so a host can refuse a tool it cannot draw | | `kinds` includes `error` | kinds: must include "error": every tool can be given bad input, and saying so is part of working correctly | | `cardFields` is a positive number | cardFields: must be a positive number of fields | | `timeoutMs` is 100 to 30000 | timeoutMs: must be between 100 and 30000 ms | | `samples` is not empty when present | samples: is empty; omit the key rather than declaring that there are no examples | | A link is `http`, `https`, `mailto` or relative | links[0].href: must be an http, https or mailto URL, or a relative path, with no spaces or control characters | # Host API A host is the page that shows tools. It defines the `` element once, with a source of tools, and then writes `` wherever a tool belongs. ```ts defineToolHost({ source: new RegistrySource({ ...registry, ...extra }), workerFactory: () => new Worker(new URL("./tool.worker.ts", import.meta.url), { type: "module" }), // Relative, so the same pages work at / locally and under a sub-path on Pages. pageUrl: (id) => `./gallery.html#tool-${encodeURIComponent(id)}`, styles: all, ...(highlight ? { highlight } : {}), }); ``` How this site does it, from `site/src/host.ts`. ## defineToolHost `defineToolHost(options, tagName = "tool-host")`. Defines the element. Call once, with the source your host provides. ### Options | Attribute | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | `source` | `ToolSource` | **yes** | | Where tools come from. | | `workerFactory` | `() => Worker` | no | | Required only for tools that declare `thread: "worker"`. See the README. | | `pageUrl` | `(id: string) => string` | no | | Given a tool id, the URL of its full page, which card and embed modes link out to. | | `rootMargin` | `string` | no | | How far ahead of the viewport `page` and `embed` modes start loading. Default "200% 0px". | | `debounceMs` | `number` | no | | Milliseconds of quiet before a typed change runs. Default 150. | | `highlight` | `(source: string, lang: string) => Node` | no | | Paint a `code` result with the host's own highlighter. The runtime does not bundle one: that would roughly double the package, and a host usually already has one. Return a `Node`, not a string. A string-returning hook would need `innerHTML`, which SECURITY.md forbids. Omit the hook to keep readable preformatted text. | | `styles` | `HostStyles` | no | | The host's own stylesheets, adopted into every tool's shadow root after the runtime's. A sheet, CSS text, a list of either, or `{ "*": [...], "tool-id": [...] }` for one tool to differ. The class names and attributes they may rely on are listed in STYLING.md. | | `wide` | `"fit" | "scroll"` | no | | A graph wider than its plot: `fit` (default) scales it down to the plot's width, `scroll` draws it at its natural size in a plot that scrolls. A tool's `wide` attribute overrides it. | | `wideToggle` | `boolean` | no | | Whether a scaled graph offers the reader a button to switch between the two. Default true. | | `share` | `"none" | ShareScheme` | no | `the query-parameter scheme` | The share button on `page` and `embed` tools, a link icon that copies a link to the tool with its inputs. By default the link is this page's URL with each input as a query parameter named `.`, `?percentiles.method=linear`, and a tool opening on a page reads its values back from those. `"none"` removes the button and reads nothing; a `{ url, read }` pair is your own scheme. Inputs marked `share: false` are never written or read. A tool's `share` attribute overrides it. | | `shareLabel` | `string` | no | | Text beside the share button's icon, which is otherwise shown alone. A tool's `share-label` overrides it. | | `sharePosition` | `"top" | "actions"` | no | `"top" on a page, "actions" in an embed` | Where the share button sits: `top`, the end of the title row, or `actions`, the end of the Run row. A tool's `share-position` overrides it. | ### setHostStyles `setHostStyles(styles)`. Replace the host's stylesheets on every tool, on the page now and created later, without reloading: a theme switch, say. `undefined` removes them. Takes what `styles` takes. ## Element attributes What `` reads from its markup. Changing one redraws the tool and keeps the reader's inputs and result. | Attribute | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | `tool` | `string` | **yes** | | The id of the tool to show, as its manifest declares. | | `mode` | `"card" | "page" | "embed"` | no | `"page"` | `card`: name, blurb, the `primary` inputs and the first result part, fetching the tool's code only when the reader opens it. `page`: every input, the full result and the tool's links. `embed`: `page` without the title, for the middle of an article. `page` and `embed` fetch when they scroll near the viewport. | | `parts` | `number` | no | `1` | How many parts of a grouped result to show. A card always caps; on `page` and `embed` there is no cap unless this is set. | | `more` | `"link" | "expand"` | no | `"link"` | What stands for the parts `parts` hid: `link`, a line of text pointing at the full tool, or `expand`, a button that shows them in place without running the tool again. | | `layout` | `"default" | "answer-first"` | no | `"default"` | `answer-first` draws the result above the form, for a tool that opens a page about something else. | | `controls` | `"default" | "none"` | no | `"default"` | `none` draws the result and nothing else: no form, no Run, no status line. The page drives the tool through `values` and `run()`. | | `wide` | `"fit" | "scroll"` | no | `"fit"` | A graph wider than its plot: `fit` scales it down to the width, `scroll` draws it full size in a plot that scrolls. Overrides `defineToolHost({ wide })`. | | `wide-toggle` | `"true" | "false"` | no | `"true"` | `false` hides the expand and fit button on a scaled graph. Overrides `defineToolHost({ wideToggle })`. | | `reset` | `"default" | "none"` | no | `"default"` | `none` leaves out the Reset button, for a tool where it adds nothing, such as one with a single input. | | `links` | `"all" | "page" | "none"` | no | `"all"` | What goes under the tool: `all`, the manifest's links; `page`, only "Open the full tool", pointing at `pageUrl(id)`; `none`, nothing. For a tool embedded in an article, where a row of references sits between the answer and the prose. A card shows "Open the full tool" for anything but `none`. A retired tool keeps its links whatever this says, because its notice points at them. Also a property, `host.links`. | | `share` | `"default" | "none"` | no | `"default"` | `none` leaves out the share button, and the tool then reads nothing from the page's URL either. Overrides `defineToolHost({ share })`. | | `share-label` | `string` | no | `""` | Text beside the share button's icon, which is otherwise shown alone. Overrides `defineToolHost({ shareLabel })`. | | `share-position` | `"top" | "actions"` | no | `"top" on a page, "actions" in an embed` | Where the share button sits: `top`, the end of the title row, or `actions`, the end of the Run row. Overrides `defineToolHost({ sharePosition })`. | ### Example ```html ``` A gallery card: one part shown, the rest behind a button. ## Properties and methods On the element, for a page that drives a tool itself. | Member | Use | What it does | |---|---|---| | `values (set)` | `values = { ... }` | Prefill the form. Partial is fine: unnamed inputs keep their current value. | | `values (get)` | `values` | A copy of the current input values, including ones a compact card has not rendered. | | `run()` | `run({ focus? }): Promise` | Run the tool. Opt-in: setting `values` never does this on its own. | | `reveal()` | `reveal(id): Promise` | Bring one part of the result to the reader: the part whose `id` is `id`. Resolves `true` once it is on screen with focus on it, `false` if the result has no such part. | | `seed` | `seed = output` | A precomputed result to show before anything runs. | ### Example ```ts // Fill the form the way typing would. It does not run the tool. if (host.dataset.values) host.values = JSON.parse(host.dataset.values) as Record; // Press Run once, on the reader's behalf, so the example opens with its answer. if (host.hasAttribute("data-run")) void host.run(); ``` ```ts const host = document.querySelector("#reveal-host"); for (const button of document.querySelectorAll("[data-reveal]")) { // Opens the part if `parts` hid it, scrolls to it and moves focus there. button.addEventListener("click", () => void host?.reveal(button.dataset.reveal ?? "")); } ``` From `site/src/gallery.ts`. ## Events Dispatched on ``. Each bubbles and is composed, so a listener on the document hears every tool. None fires while the element starts up with its first values. | Event | `detail` | When | |---|---|---| | `tool-input` | `{ values: InputValues }` | The inputs changed, by typing, a sample, a preset or a write to `values`. Once per change, after the form shows the new values, and only when they differ from the last ones reported. Dispatched before a `values` write returns. `values` is a copy, already clamped and refused the way typing is. Debounce is the listener's business. | | `tool-run` | `{ values: InputValues; ok: boolean; error?: string }` | A run settled: from Run, Enter, `run()`, `reveal()` or an `autoRun` tool's typing. Once per run, after its result or error is drawn and before `run()` resolves. `values` are the ones the run used. `ok` is false for an error result and for a run that failed, with `error` the message the reader sees. A run replaced by a newer one before it finished dispatches nothing. | | `tool-reset` | `{ values: InputValues }` | The reader pressed Reset. Dispatched after the form shows the starting values and the result is cleared, and after the `tool-input` for the same change. `values` are the starting values. | | `tool-share` | `{ url: string; values: InputValues }` | The reader pressed the share button. After the copy is attempted, whether or not the clipboard took it. `url` is the link, `values` the inputs it carries: every input but one marked `share: false`. | ### Example ```ts const log = document.querySelector("#events-log"); for (const type of ["tool-input", "tool-run"]) { // Both bubble out of the shadow root, so the listener could as well be on `document`. document.querySelector("#events-host")?.addEventListener(type, (event) => { const { detail } = event as CustomEvent>; if (log) log.textContent = `${type} ${JSON.stringify(detail, null, 1).replace(/\s*\n\s*/g, " ")}`; }); } ``` From `site/src/gallery.ts`. ## Sources Where tools come from. `RegistrySource` is the one most hosts use; a host may write its own `ToolSource`. ### ToolSource | Attribute | Type | Required | Notes | | --- | --- | --- | --- | | `list()` | `Promise` | **yes** | Every tool this source can offer, already validated and brought up to the current contract. | | `load()` | `Promise` | **yes** | The manifest plus the module, ready to run. | ### RegistrySource `new RegistrySource(entries)`. The everyday source: a map of ids to entries, which a host builds however it likes: by hand, or from a bundler's directory glob. ### RegistryEntry | Attribute | Type | Required | Notes | | --- | --- | --- | --- | | `manifest` | `unknown` | **yes** | The parsed `tool.json`, unchecked: `RegistrySource` validates and upgrades it. | | `load()` | `Promise` | **yes** | A lazy import of the tool's module, called only when the tool is opened or scrolled near. | ```ts export function entries(m: Record, code: Record Promise>): Record { return Object.fromEntries( Object.entries(m).map(([path, manifest]) => { const dir = path.slice(0, path.lastIndexOf("/")); const id = dir.split("/").at(-1) ?? path; const load = code[`${dir}/index.ts`]; if (!load) throw new Error(`${id} has a tool.json but no index.ts beside it`); return [id, { manifest, load: load as RegistryEntry["load"] }]; }), ); } export const registry = entries(manifests, modules); ``` How this site builds its registry from a directory glob, from `site/src/registry.ts`. # Theme tokens A tool draws inside a shadow root, so a page's CSS does not reach it. It reads these custom properties instead, which do cross the boundary: set any of them on `tool-host`, or on anything around it. ## Tokens | Token | Light | Dark | Used for | |---|---|---|---| | `--tb-bg` | `#ffffff` | `#16171d` | The tool's surface | | `--tb-fg` | `#1a1c22` | `#e7e9ee` | Body text | | `--tb-muted` | `#5c6270` | `#9aa1b1` | Labels, secondary text | | `--tb-faint` | `#767d8c` | `#7c8494` | Descriptions, axis ticks, status | | `--tb-border` | `#e2e5ea` | `#2b2e38` | Hairlines and control borders | | `--tb-surface` | `#f7f8fa` | `#1c1e26` | Input backgrounds, preformatted blocks | | `--tb-accent` | `#8a5a00` | `#f0b040` | Fills: the Run button, progress, marked bytes, focus outlines | | `--tb-accent-bg` | `#fff6e6` | `#2a2113` | A wash behind something marked: the table rows a chart is pointing at, and the deprecated and retired marker | | `--tb-bad` | `#a3242c` | `#f08b8b` | Result tones and errors | | `--tb-warn` | `#8a5a00` | `#e5b567` | Result tones and errors | | `--tb-good` | `#1c6b3c` | `#74c98d` | Result tones and errors | | `--tb-radius` | `10px` | same as light | Corner radius | | `--tb-font` | `system-ui, -apple-system, "Segoe UI", sans-serif` | same as light | Type | | `--tb-mono` | `ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace` | same as light | Type | | `--tb-gap` | `0.75rem` | same as light | Vertical rhythm inside the tool | | `--tb-mark-fg` | `var(--tb-warn)` | follows `--tb-warn` | The deprecated and retired marker. A host sets these on `tool-host`; `data-status` is also on the host so the page can style the element itself | | `--tb-mark-bg` | `var(--tb-accent-bg)` | follows `--tb-accent-bg` | The deprecated and retired marker. A host sets these on `tool-host`; `data-status` is also on the host so the page can style the element itself | | `--tb-accent-text` | `var(--tb-accent)` | follows `--tb-accent` | Accent-coloured text: links, the card's hint, a hovered sample, annotation labels. Set it when the accent is too bright to be text on your surface | | `--tb-accent-ink` | `var(--tb-bg)` | follows `--tb-bg` | Text on an accent fill: the Run button's label, a marked byte. Set it dark when the accent is bright | | `--tb-input-bg` | `var(--tb-surface)` | follows `--tb-surface` | A control's background: text, number, textarea, select. Set it on a `.tb-field-row` to change one input | | `--tb-input-border` | `var(--tb-border)` | follows `--tb-border` | A control's border | | `--tb-input-fg` | `var(--tb-fg)` | follows `--tb-fg` | The text a reader types, and a painted textarea's caret | | `--tb-desc-fg` | `var(--tb-faint)` | follows `--tb-faint` | An input's description | | `--tb-s1` | `#8a5a00` | `#e0b877` | Chart series use the first six; a graph's `color` can name all eight | | `--tb-s2` | `#5b3fa8` | `#c4a5f0` | Chart series use the first six; a graph's `color` can name all eight | | `--tb-s3` | `#0f6e6e` | `#7fd1d1` | Chart series use the first six; a graph's `color` can name all eight | | `--tb-s4` | `#1f5aa8` | `#8ec4f0` | Chart series use the first six; a graph's `color` can name all eight | | `--tb-s5` | `#a02a5e` | `#f0a5c0` | Chart series use the first six; a graph's `color` can name all eight | | `--tb-s6` | `#2d6b32` | `#8fd6a8` | Chart series use the first six; a graph's `color` can name all eight | | `--tb-s7` | `#7a4e2d` | `#e3b48f` | Chart series use the first six; a graph's `color` can name all eight | | `--tb-s8` | `#4a5568` | `#b9c3d1` | Chart series use the first six; a graph's `color` can name all eight | The defaults follow the page's colour scheme through `light-dark()`. ### Example ```css /* The same tool in another theme: a dozen custom properties on a wrapper, nothing else. */ .theme-demo tool-host { --tb-accent: #0b6b5f; --tb-accent-text: #0b6b5f; --tb-accent-ink: #ffffff; --tb-accent-bg: #e1f0ec; --tb-bg: #fbfdfc; --tb-surface: #eef6f3; --tb-fg: #1d2422; --tb-muted: #4f5b57; --tb-border: #b9dbd4; --tb-radius: 2px; --tb-font: Georgia, "Times New Roman", serif; } ``` The gallery's second theme, from `site/src/gallery.css`. ## Parts Elements a page can style from outside with `::part()`. | Part | Element | |---|---| | `wide-toggle` | The expand and fit button on a scaled graph | ## Classes and attributes For everything inside the shadow root, a host passes its own stylesheet to `defineToolHost({ styles })`. The class names and attributes it may rely on are listed on the [Styling](./docs-styling.html#what-you-can-select) page.