pi-openui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,575 @@
1
+ // Hand-written guidance for each component. The signatures and argument types come from the table in library.ts, so a page
2
+ // can never disagree with what the validator accepts; scripts/gen_docs.ts joins the two and test/docs.test.ts checks that
3
+ // every example compiles and every screenshot exists.
4
+
5
+ export interface Shot {
6
+ file: string;
7
+ caption: string;
8
+ }
9
+
10
+ export interface ComponentDoc {
11
+ group: "Layout and text" | "Data" | "Charts" | "Diagrams and maps" | "Interactive" | "Rich content";
12
+ summary: string;
13
+ shots: Shot[];
14
+ /** helper components documented on this page (they only exist inside this one) */
15
+ helpers?: string[];
16
+ use: string[];
17
+ avoid?: string;
18
+ /** notes for the arguments table, keyed "Component.argument" */
19
+ args: Record<string, string>;
20
+ /** OpenUI Lang that compiles; shown as the example */
21
+ example: string;
22
+ exampleNote?: string;
23
+ interaction?: string[];
24
+ tips: string[];
25
+ related: string[];
26
+ }
27
+
28
+ export const DOCS: Record<string, ComponentDoc> = {
29
+ Stack: {
30
+ group: "Layout and text",
31
+ summary: "Lays its children out in a column (the default) or a row. Every widget starts with `root = Stack([...])`.",
32
+ shots: [{ file: "stack", caption: "A row of tiles, then a row of buttons that wraps when it runs out of width" }],
33
+ use: ["The root of every widget.", "A row of metric tiles or buttons.", "Grouping a few components with a chosen gap."],
34
+ avoid: "Do not put a Stack inside a Stack just to add space: use `gap`.",
35
+ args: {
36
+ "Stack.children": "The components inside, top to bottom or left to right. At least one is useful; an empty Stack draws nothing.",
37
+ "Stack.direction": '`"column"` (default) stacks them; `"row"` puts them side by side.',
38
+ "Stack.gap": "Space between children. In a column the steps are none/xs/s = no blank line, m/l = one, xl = two. In a row they are 0, 1, 1, 2, 3 and 4 columns for none, xs, s, m, l, xl. Default `m`.",
39
+ "Stack.wrap": "In a row, start a new line when the next child does not fit.",
40
+ },
41
+ example: `root = Stack([title, row])
42
+ title = Text("Two ways to lay out", "heading")
43
+ row = Stack([a, b, c], "row", "l")
44
+ a = Metric("Left", "1")
45
+ b = Metric("Middle", "2")
46
+ c = Metric("Right", "3")`,
47
+ tips: [
48
+ "In a row, buttons, metric tiles and short text keep their natural width; every other child shares what is left equally.",
49
+ "A row that is wider than the terminal shrinks its widest children first, never below 6 columns.",
50
+ "Inside Tabs, wrap a tab's content in one Stack: items placed directly in a tab touch each other.",
51
+ ],
52
+ related: ["Card", "Tabs", "Text"],
53
+ },
54
+ Text: {
55
+ group: "Layout and text",
56
+ summary: "A line or paragraph of text. It wraps to the available width.",
57
+ shots: [{ file: "text", caption: "The five variants, and the four Callout tones that share this page's screenshot" }],
58
+ use: ["Titles and headings.", "Short explanations next to a chart.", "A muted note or a one-line code fragment."],
59
+ avoid: "For several paragraphs, lists or links use Markdown.",
60
+ args: {
61
+ "Text.text": "The words. `\\n` starts a new line. No markup is interpreted.",
62
+ "Text.variant": "`title` (accent, bold), `heading` (bold), `body` (default), `muted` (dim) or `code`.",
63
+ },
64
+ example: `root = Stack([t1, t2, t3])
65
+ t1 = Text("Cluster status", "title")
66
+ t2 = Text("Four nodes, all serving.")
67
+ t3 = Text("Updated 2 minutes ago", "muted")`,
68
+ tips: ["A word longer than the line is broken rather than cut.", "There is no bold or italic inside Text; use Markdown for that."],
69
+ related: ["Markdown", "Callout"],
70
+ },
71
+ Card: {
72
+ group: "Layout and text",
73
+ summary: "A bordered panel with an optional title in the top border.",
74
+ shots: [{ file: "card", caption: "A card with a nested card" }],
75
+ use: ["Giving a chart or table a heading and a frame.", "Separating sections inside one widget."],
76
+ args: { "Card.children": "The components inside. They are laid out as a column with the default gap.", "Card.title": "Shown in the top border. Cut with an ellipsis if it does not fit." },
77
+ example: `root = Stack([c])
78
+ c = Card([chart], "Load by node")
79
+ chart = BarChart(["a", "b", "c"], [3, 5, 2])`,
80
+ tips: ["The content is 4 columns narrower than the card: border and padding on both sides.", "Use Tabs for sections the reader switches between, Cards for sections shown together."],
81
+ related: ["Stack", "Tabs"],
82
+ },
83
+ Callout: {
84
+ group: "Layout and text",
85
+ summary: "A highlighted note with an icon: information, success, a warning or an error.",
86
+ shots: [{ file: "callout", caption: "The four tones" }],
87
+ use: ["Drawing the eye to one fact: a failure, a caveat, a result."],
88
+ args: { "Callout.text": "The message. It wraps.", "Callout.tone": "`info` (default, ◉), `success` (✓), `warn` (⚠) or `error` (✗). The icon and border take the theme colour for the tone." },
89
+ example: `root = Stack([a, b])
90
+ a = Callout("Disk usage is above 80% on spark-3.", "warn")
91
+ b = Callout("Deploy finished without errors.", "success")`,
92
+ tips: ["Keep it to a sentence or two; long text belongs in Markdown."],
93
+ related: ["Text", "Markdown"],
94
+ },
95
+ Metric: {
96
+ group: "Data",
97
+ summary: "A KPI tile: a label, a big value and an optional change line coloured by tone.",
98
+ shots: [{ file: "metric", caption: "Four tones; the last tile carries a tooltip (the ◆ on its border)" }],
99
+ use: ["Headline numbers at the top of a dashboard.", "Status values with a trend."],
100
+ args: {
101
+ "Metric.label": "The small caption on top. This is also the tooltip target.",
102
+ "Metric.value": "The number or word, as a string: `\"87%\"`, `\"4 / 4\"`.",
103
+ "Metric.delta": "The line under the value, for example `\"+4% vs 1h ago\"`.",
104
+ "Metric.tone": "Colours the delta: `good` green, `warn` amber, `bad` red, `neutral` grey.",
105
+ "Metric.tooltips": "At most one, aimed at the label. Write `null` for `delta` and `tone` if you skip them.",
106
+ },
107
+ example: `root = Stack([tiles])
108
+ tiles = Stack([a, b], "row")
109
+ a = Metric("Requests", "12.4k", "+3%", "good")
110
+ b = Metric("p95 latency", "180 ms", "+22 ms", "warn")`,
111
+ tips: ["Values and deltas are strings, so format them yourself.", "Put tiles in a `\"row\"` Stack; in a column each one spans the full width."],
112
+ related: ["BarChart", "Table", "Tooltip"],
113
+ },
114
+ Table: {
115
+ group: "Data",
116
+ summary: "A table with a header row. Columns that hold only numbers are right-aligned.",
117
+ shots: [
118
+ { file: "table", caption: "Numbers right-aligned, long text truncated with an ellipsis" },
119
+ { file: "table-tooltip", caption: "Hovering the row with a tooltip shows its hint in a bubble" },
120
+ { file: "table-panel", caption: "Clicking it, or pressing Enter, opens the detail panel" },
121
+ ],
122
+ use: ["Comparing a handful of items across several fields.", "A list of results with a status column."],
123
+ avoid: "More than about 12 rows: summarise, or split across Tabs.",
124
+ args: {
125
+ "Table.columns": "The header strings.",
126
+ "Table.rows": "An array of rows. Each row has exactly one cell per column, a string or a number. A mismatch is a validation error that names the row.",
127
+ "Table.tooltips": "Targets: `row:3` (1-based, not counting the header), `col:status`, or `cell:3.status`. Duplicate header names make `col:` and `cell:` ambiguous.",
128
+ },
129
+ example: `root = Stack([t])
130
+ t = Table(["Node", "State", "GPU %"], [["spark-1", "serving", 91], ["spark-2", "draining", 84]], [tip])
131
+ tip = Tooltip("row:2", "Rebooting for a driver update")`,
132
+ interaction: ["A tooltip target shows a `◆` in the cell padding after its text.", "Hover, or Tab to the row, for the bubble; click or Enter for the panel."],
133
+ tips: [
134
+ "When the table is wider than the terminal the widest columns shrink first, to a minimum of 3 characters.",
135
+ "A table that still does not fit becomes a scrollable window; moving focus to a target scrolls it into view.",
136
+ ],
137
+ related: ["Markdown", "Metric", "Tooltip"],
138
+ },
139
+ Tabs: {
140
+ group: "Interactive",
141
+ summary: "A strip of tab labels over the content of the selected tab.",
142
+ helpers: ["TabItem"],
143
+ shots: [
144
+ { file: "tabs", caption: "The first tab is open when the widget is drawn" },
145
+ { file: "tabs-nodes", caption: "After clicking Nodes" },
146
+ ],
147
+ use: ["Alternate views of one subject: overview, nodes, notes.", "The same snippet in several languages."],
148
+ avoid: "Steps that must be read in order, or content the reader must see at once: a tab that is not selected is hidden.",
149
+ args: {
150
+ "Tabs.items": "One TabItem per tab. The first is open at the start.",
151
+ "TabItem.label": "The text in the strip. Cut to 22 characters, and shorter when the strip does not fit.",
152
+ "TabItem.content": "The components in the tab. Wrap them in a single Stack: `[panel]`.",
153
+ },
154
+ example: `root = Stack([tabs])
155
+ tabs = Tabs([a, b])
156
+ a = TabItem("Overview", [kpis])
157
+ kpis = Stack([m1, m2], "row")
158
+ m1 = Metric("Nodes up", "4 / 4", "healthy", "good")
159
+ m2 = Metric("Queue", "12", "-3", "good")
160
+ b = TabItem("Nodes", [tbl])
161
+ tbl = Table(["Node", "State"], [["spark-1", "serving"], ["spark-2", "serving"]])`,
162
+ interaction: ["Click a tab, or Tab to it and press Enter. Clicking only switches the view; nothing is sent to the assistant.", "Tabs nest, and each remembers its own selection."],
163
+ tips: ["You cannot start on another tab: put the most useful one first.", "Every TabItem and everything in it must be reachable from `root`, like any other variable."],
164
+ related: ["Card", "Stack"],
165
+ },
166
+ Button: {
167
+ group: "Interactive",
168
+ summary: "A clickable button that sends text to the assistant as your next message.",
169
+ shots: [{ file: "button", caption: "A plain button, one with an action, and a link button" }],
170
+ use: ["Offering the next step: Restart, Show logs, Drain node."],
171
+ args: {
172
+ "Button.label": "The text. A button without an action sends this label.",
173
+ "Button.action": '`@ToAssistant("text")` sends that text (500 characters at most). `Action([...])` holds several steps. `@OpenUrl("https://...")` copies the link to the clipboard, because a terminal cannot open a browser from here.',
174
+ },
175
+ example: `root = Stack([b])
176
+ b = Button("Restart vllm", @ToAssistant("Restart the vllm service on dgx-spark-1"))`,
177
+ interaction: [
178
+ "In a widget the agent drew, a click (or Enter on the focused button) sends the text as a real user turn. The agent acts, then draws a new widget.",
179
+ "In `/openui-gallery` the click is only echoed locally, so trying it costs nothing.",
180
+ ],
181
+ tips: ["Write the action text as a clear instruction: it is what the assistant reads."],
182
+ related: ["Choice", "Faq"],
183
+ },
184
+ Choice: {
185
+ group: "Interactive",
186
+ summary: "A question with clickable options for you to answer.",
187
+ shots: [{ file: "choice", caption: "Options on one line when they fit, stacked when they do not" }],
188
+ use: ["Asking a clarifying question instead of guessing."],
189
+ args: {
190
+ "Choice.question": "The question.",
191
+ "Choice.options": "Two to six short answers.",
192
+ "Choice.hint": "A line of explanation under the question.",
193
+ },
194
+ example: `root = Stack([q])
195
+ q = Choice("Which model should serve chat?", ["qwen3-32b", "llama-3.3-70b"], "Pick one")`,
196
+ interaction: ["Click an option, or move with the arrow keys and press Enter. The pick is filled (●) and `<question> — <option>` is sent as your next message."],
197
+ tips: ["You can change the pick; each click sends a new message."],
198
+ related: ["Button"],
199
+ },
200
+ Faq: {
201
+ group: "Interactive",
202
+ summary: "A list of questions that expand to show their answers.",
203
+ helpers: ["FaqItem"],
204
+ shots: [{ file: "faq", caption: "The first answer is open" }],
205
+ use: ["Reference material the reader opens on demand."],
206
+ args: {
207
+ "Faq.items": "One FaqItem per question.",
208
+ "Faq.firstOpen": "`true` opens the first answer at the start.",
209
+ "FaqItem.question": "The heading.",
210
+ "FaqItem.answer": "Markdown: lists, bold, code and tables all work.",
211
+ },
212
+ example: `root = Stack([f])
213
+ f = Faq([a, b], true)
214
+ a = FaqItem("What does draining do?", "It stops **new** requests.\\n\\n1. queue pauses\\n2. running requests finish")
215
+ b = FaqItem("How do I undo it?", "Run \`uncordon\`.")`,
216
+ interaction: ["Click a question, or Tab to it and press Enter, to open or close it. Each item toggles on its own."],
217
+ tips: ["Open answers push the content below them down."],
218
+ related: ["Markdown", "Tabs"],
219
+ },
220
+ Markdown: {
221
+ group: "Rich content",
222
+ summary: "Formatted text from a markdown string.",
223
+ shots: [
224
+ { file: "markdown", caption: "Headings, emphasis, lists, a quote, a table, a rule and a link" },
225
+ { file: "markdown-image", caption: "A picture inside the text, written as a line of its own, with its caption underneath" },
226
+ ],
227
+ use: ["Release notes, explanations, anything with structure.", "A README-style view: text with pictures in it."],
228
+ args: { "Markdown.text": "Markdown. Use `\\n` for newlines. Supported: `#`-`######` headings, **bold**, *italic*, `code`, ~~strike~~, `[text](https://example.com)`, lists (nested), `>` quotes, tables, fenced code, `---`, and a picture: a line that is only `![alt](path \"caption\")`." },
229
+ example: `root = Stack([m])
230
+ m = Markdown("# Notes\\n\\nThis adds **streaming** and \`--fast\` mode.\\n\\n- faster start\\n- fewer errors\\n\\n![Request flow](flow.png \\"Before and after\\")")`,
231
+ exampleNote: "The picture file must exist when the widget is drawn, as for Image: a missing file is reported to the assistant as an error.",
232
+ tips: [
233
+ "A picture is a line of its own: `![alt](path)`, with an optional caption in quotes after the path. In the middle of a sentence, inside a code fence or inside a quote the syntax stays text. Put a path that has spaces or parentheses in angle brackets.",
234
+ "It is drawn by the same code as [Image](Image.md): a local file or a `data:` URI, sized by pi-tui, the labelled frame where the terminal has no graphics, a missing file or a web URL reported as an error. FAQ answers are markdown too, so they can hold pictures.",
235
+ "A picture in Markdown has no tooltip. Use an Image component when it needs one. A widget holds 12 pictures in all.","Links are shown as `text (url)`; they are not clickable.", "Fenced code blocks are highlighted like CodeBlock but have no copy button.", "Headings beyond the third level look like the third."],
236
+ related: ["Text", "CodeBlock", "Faq", "Image"],
237
+ },
238
+ CodeBlock: {
239
+ group: "Rich content",
240
+ summary: "Syntax-highlighted code in a frame, with a copy button.",
241
+ shots: [{ file: "code", caption: "Highlighting for JavaScript, SQL, YAML, JSON, bash, Go and Rust" }],
242
+ use: ["Commands to run, config to paste, a query."],
243
+ args: {
244
+ "CodeBlock.code": "The source. Use `\\n` for new lines. Tabs become two spaces; more than 200 lines are cut.",
245
+ "CodeBlock.language": "`python`, `js` (also ts, jsx, tsx), `bash` (sh, zsh), `sql`, `yaml`, `json`, `go` or `rust`. Anything else is shown plain.",
246
+ "CodeBlock.title": "A file name or caption in the top border, shown with the language.",
247
+ },
248
+ example: `root = Stack([c])
249
+ c = CodeBlock("kubectl rollout restart deploy/api\\nkubectl rollout status deploy/api", "bash", "restart.sh")`,
250
+ interaction: ["Click `[copy]` (or Tab to it and press Enter) to put the exact code on the clipboard with an OSC 52 escape. The label changes to `✓ copied` for two seconds."],
251
+ tips: ["Long lines are cut with an ellipsis, not wrapped, so the code stays aligned. The copy still has the full line.", "The clipboard needs a terminal that accepts OSC 52 (most modern ones do)."],
252
+ related: ["Markdown"],
253
+ },
254
+ Image: {
255
+ group: "Rich content",
256
+ summary: "A picture, drawn for real in terminals that support images, framed, with an optional caption and tooltip.",
257
+ shots: [
258
+ { file: "image", caption: "A picture inside its frame (generated in code, so the example needs no file)" },
259
+ { file: "image-tooltip", caption: "Hovering shows the tooltip hint; the frame lights up" },
260
+ ],
261
+ use: ["A screenshot or diagram the agent produced.", "Anything where a picture says it better than a chart."],
262
+ args: {
263
+ "Image.src": "An absolute path, `~/...`, a path relative to the working directory, or a base64 `data:` URI. PNG, JPEG, GIF or WebP, up to 12 MB. SVG and web URLs are not supported.",
264
+ "Image.alt": "A short name, shown in the frame's top border.",
265
+ "Image.caption": "A line under the picture.",
266
+ "Image.tooltips": "One at most, aimed at the word `image`.",
267
+ },
268
+ example: `root = Stack([i])
269
+ i = Image("/Users/me/Desktop/dashboard.png", "Cluster dashboard", "Taken after the deploy", [t])
270
+ t = Tooltip("image", "Screenshot at 14:02", "Taken right after the deploy finished.")`,
271
+ exampleNote: "The path must exist when the widget is drawn: a missing file is reported to the assistant as an error so it can fix it.",
272
+ interaction: ["Hover or Tab to it: the frame turns bold and accent-coloured and the hint bubble appears. Click or Enter opens the tooltip panel."],
273
+ tips: [
274
+ "Pictures use the Kitty graphics protocol: Kitty, Ghostty, WezTerm, Warp and herdr. Elsewhere the frame shows the file name, size and format instead. `PI_IMAGE_PROTOCOL=none` forces that.",
275
+ "pi-tui sizes the picture (at most 64 columns by 22 rows, scaled down to fit) and pi crops it when you scroll, so it behaves like pi's own images.",
276
+ "JPEG, GIF and WebP are converted by pi after startup. For a moment they may show the frame, then the picture.",
277
+ "A tooltip belongs to the whole picture, not to a spot inside it.",
278
+ "A picture can also be written inside a [Markdown](Markdown.md) (or an FAQ answer) as a line `![alt](path)`. It is drawn the same way, but cannot carry a tooltip: use an Image for that.",
279
+ ],
280
+ related: ["Tooltip", "Card", "Markdown"],
281
+ },
282
+ BarChart: {
283
+ group: "Charts",
284
+ summary: "Bars for one series of categories. Vertical when it fits, horizontal otherwise.",
285
+ shots: [{ file: "bar", caption: "Vertical bars with a tooltip on one bar, then horizontal bars with a negative value" }],
286
+ use: ["One value per category: load per node, count per type."],
287
+ avoid: "Trends over time with several series (LineChart) or parts of a whole (PieChart).",
288
+ args: {
289
+ "BarChart.labels": "One name per bar.",
290
+ "BarChart.values": "Numbers, same length as `labels`. Mixed signs switch to horizontal bars.",
291
+ "BarChart.title": "A bold line above the chart.",
292
+ "BarChart.unit": "A suffix for the value labels, for example `%`.",
293
+ "BarChart.tooltips": "Target: a bar's label. Duplicate labels are ambiguous. Write `null` for `title` and `unit` if you skip them.",
294
+ },
295
+ example: `root = Stack([c])
296
+ c = BarChart(["spark-1", "spark-2", "spark-3"], [91, 84, 77], "GPU load", "%")`,
297
+ tips: ["Bar heights are drawn to an eighth of a cell, so close values still differ.", "Large values are shortened: 12400 is shown as `12.4k`."],
298
+ related: ["LineChart", "PieChart", "Table"],
299
+ },
300
+ LineChart: {
301
+ group: "Charts",
302
+ summary: "Lines over categories, one per series, drawn with braille dots.",
303
+ helpers: ["Series"],
304
+ shots: [
305
+ { file: "line", caption: "A smooth two-series chart, then a linear and a step chart" },
306
+ { file: "line-tooltip", caption: "Hovering the Thu column: the ◆ sits on the curve at that point" },
307
+ ],
308
+ use: ["Trends with several series: requests per day, latency per hour."],
309
+ args: {
310
+ "LineChart.labels": "The x categories, in order.",
311
+ "LineChart.series": "One Series each. Every series needs one value per label.",
312
+ "LineChart.variant": "`natural` (smooth, default, never overshoots), `linear`, or `step`.",
313
+ "LineChart.xLabel": "Axis caption, shown as `→ day`.",
314
+ "LineChart.yLabel": "Axis caption, shown as `↑ requests`.",
315
+ "LineChart.height": "Roughly in pixels; one terminal row is about 20. Default 200 (10 rows), limited to 6-16 rows.",
316
+ "LineChart.tooltips": "Targets: an x label (that point, hoverable over its whole column) or `series:<name>` (its legend entry).",
317
+ "Series.category": "The series name, shown in the legend.",
318
+ "Series.values": "Numbers parallel to the chart's labels.",
319
+ },
320
+ example: `root = Stack([c])
321
+ c = LineChart(["Mon", "Tue", "Wed", "Thu", "Fri"], [api, jobs], "natural", null, null, null, [t])
322
+ api = Series("api", [12, 19, 15, 32, 28])
323
+ jobs = Series("worker", [8, 11, 14, 12, 18])
324
+ t = Tooltip("Thu", "Release v2.3 went out", "Traffic jumped 60% after the release.")`,
325
+ tips: ["The y axis starts at 0, unless the smallest value is at least 60% of the largest: then it starts near the smallest, so small changes stay visible.", "A single point is drawn as a dot."],
326
+ related: ["AreaChart", "BarChart", "RadarChart"],
327
+ },
328
+ AreaChart: {
329
+ group: "Charts",
330
+ summary: "Like LineChart, but the series are stacked and filled, so it shows parts of a growing total.",
331
+ helpers: ["Series"],
332
+ shots: [{ file: "area", caption: "Three stacked series" }],
333
+ use: ["Volume or cumulative totals made of several parts: tokens per model."],
334
+ avoid: "Comparing series directly: stacking hides that. Use LineChart.",
335
+ args: {
336
+ "AreaChart.labels": "The x categories.",
337
+ "AreaChart.series": "One Series each, stacked in order (the first at the bottom).",
338
+ "AreaChart.variant": "`natural` (default), `linear` or `step`.",
339
+ "AreaChart.xLabel": "Axis caption.",
340
+ "AreaChart.yLabel": "Axis caption.",
341
+ "AreaChart.height": "As LineChart.",
342
+ "AreaChart.tooltips": "As LineChart. The badge sits on the top edge of the stack.",
343
+ "Series.category": "The series name.",
344
+ "Series.values": "Numbers parallel to the labels.",
345
+ },
346
+ example: `root = Stack([c])
347
+ c = AreaChart(["00:00", "08:00", "16:00"], [a, b])
348
+ a = Series("qwen3", [10, 30, 38])
349
+ b = Series("llama", [5, 15, 18])`,
350
+ tips: ["The top line is the total of all series."],
351
+ related: ["LineChart", "PieChart"],
352
+ },
353
+ PieChart: {
354
+ group: "Charts",
355
+ summary: "Parts of a whole as a coloured disc with a legend that shows each value and share.",
356
+ shots: [
357
+ { file: "pie", caption: "A pie, a donut and a semicircle" },
358
+ { file: "pie-tooltip", caption: "Hovering a legend row (or its wedge) shows the tooltip" },
359
+ ],
360
+ use: ["Shares of one total at one moment: memory per model, requests per type."],
361
+ avoid: "More than 8 slices, or close values that should be compared (use BarChart).",
362
+ args: {
363
+ "PieChart.labels": "One name per slice.",
364
+ "PieChart.values": "Raw numbers, not percentages, same length as `labels`, none negative.",
365
+ "PieChart.variant": "`pie` (filled, default) or `donut`.",
366
+ "PieChart.appearance": "`circular` (default) or `semiCircular` (the top half, drawn larger).",
367
+ "PieChart.tooltips": "Target: a slice label. Hover or click either the wedge or its legend row.",
368
+ },
369
+ example: `root = Stack([c])
370
+ c = PieChart(["qwen3", "llama", "gemma", "free"], [38, 52, 18, 14], null, null, [t])
371
+ t = Tooltip("llama", "Biggest tenant", "Serves the support bot.")`,
372
+ tips: ["Slices are sorted largest first and start at 12 o'clock (9 o'clock for a semicircle).", "The disc is drawn with half blocks in two colours per cell, so it needs a terminal with colour."],
373
+ related: ["BarChart", "AreaChart"],
374
+ },
375
+ RadarChart: {
376
+ group: "Charts",
377
+ summary: "One axis per metric, one polygon per series: comparing entities across several measures.",
378
+ helpers: ["Series"],
379
+ shots: [{ file: "radar", caption: "Two series over five axes" }],
380
+ use: ["Comparing a few options on the same scale: speed, cost, quality."],
381
+ args: {
382
+ "RadarChart.labels": "The axes: at least three.",
383
+ "RadarChart.series": "One Series per entity, each with one value per axis.",
384
+ "RadarChart.tooltips": "Targets: an axis label, or `series:<name>` (its legend entry).",
385
+ "Series.category": "The entity name.",
386
+ "Series.values": "Numbers parallel to the axes. Negative values count as 0.",
387
+ },
388
+ example: `root = Stack([r])
389
+ r = RadarChart(["Speed", "Cost", "Quality"], [a, b])
390
+ a = Series("qwen3", [80, 90, 70])
391
+ b = Series("llama", [55, 60, 88])`,
392
+ tips: ["The scale runs from 0 to a round number above the largest value, printed under the chart."],
393
+ related: ["BarChart", "LineChart"],
394
+ },
395
+ GeoMap: {
396
+ group: "Diagrams and maps",
397
+ summary: "An offline world map zoomed to fit your pins, with optional dashed links.",
398
+ helpers: ["GeoPoint"],
399
+ shots: [
400
+ { file: "geo", caption: "Four pins and one link on country-scale coastlines" },
401
+ { file: "geo-tooltip", caption: "A pin's tooltip panel (click or Enter)" },
402
+ ],
403
+ use: ["Where regions, offices or servers are."],
404
+ avoid: "City-level detail: the coastlines are 110m, country scale.",
405
+ args: {
406
+ "GeoMap.points": "One GeoPoint per pin.",
407
+ "GeoMap.links": "Optional `[labelA, labelB]` pairs drawn dashed.",
408
+ "GeoMap.title": "A bold line above the map.",
409
+ "GeoMap.tooltips": "Targets: a point's label, or `A>B` for a link.",
410
+ "GeoPoint.label": "The pin's name. Unique within the map.",
411
+ "GeoPoint.lat": "Decimal degrees, -90 to 90.",
412
+ "GeoPoint.lon": "Decimal degrees, -180 to 180.",
413
+ "GeoPoint.tone": "Colours the pin: `good`, `warn`, `bad`, `info` (default) or `neutral`.",
414
+ },
415
+ example: `root = Stack([m])
416
+ m = GeoMap([a, b], [["Tokyo", "Frankfurt"]], "Regions")
417
+ a = GeoPoint("Tokyo", 35.68, 139.69, "good")
418
+ b = GeoPoint("Frankfurt", 50.11, 8.68, "warn")`,
419
+ tips: ["The land is dotted and its coast is drawn solid; the map uses a plain equirectangular projection, with square dots.", "A pin whose label has no room is listed under the map as `also: ...`."],
420
+ related: ["InfraMap"],
421
+ },
422
+ Dag: {
423
+ group: "Diagrams and maps",
424
+ summary: "A directed graph with an automatic layered layout, for pipelines, task graphs and dependencies.",
425
+ helpers: ["DagNode"],
426
+ shots: [
427
+ { file: "dag", caption: "A CI run with every status: done, failed, running, skipped and pending" },
428
+ { file: "dag-tooltip", caption: "A focused node: bold accent border, hint bubble, and the tooltip panel below" },
429
+ ],
430
+ use: ["A pipeline where each step has a state.", "A fan-out and fan-in.", "Which job waits for which."],
431
+ avoid: "Database tables (Erd) or a plain list of steps (Table).",
432
+ args: {
433
+ "Dag.nodes": "One DagNode per box.",
434
+ "Dag.edges": "`[fromId, toId]` pairs. Use the id strings, not the variable names. An id with no node is a validation error.",
435
+ "Dag.direction": "`LR` (left to right) or `TB` (top to bottom). Leave it out or pass `null` for automatic: left to right when it fits, otherwise top to bottom.",
436
+ "Dag.title": "A bold line above the graph.",
437
+ "Dag.tooltips": "Target: a node id.",
438
+ "DagNode.id": "Unique in the graph. Edges refer to it.",
439
+ "DagNode.label": "The text in the box. Shortened with an ellipsis when space is tight.",
440
+ "DagNode.status": "`done` ✓, `running` ◔, `failed` ✗, `pending` ○ or `skipped` ⊘ (also a dashed box). Omit it for a plain box. A legend names the statuses in use.",
441
+ },
442
+ example: `root = Stack([flow])
443
+ flow = Dag([a, b, c], [["build", "test"], ["test", "deploy"]], null, "Release")
444
+ a = DagNode("build", "Build", "done")
445
+ b = DagNode("test", "Test", "running")
446
+ c = DagNode("deploy", "Deploy", "pending")`,
447
+ interaction: ["Each node is a target: hover, Tab or arrows move between them, and a tooltip opens on click or Enter."],
448
+ tips: [
449
+ "A cycle is allowed: the edge that closes it is drawn dashed, pointing back.",
450
+ "Every edge has its own wire, wires cross only at right angles (`┼`), and the layout is checked by decoding the picture back into edges. An edge that truly cannot be routed is left out and listed under the graph as `a -> b (not drawn)`.",
451
+ "Wider than the terminal: labels shrink first, then the direction switches, then the view scrolls sideways.",
452
+ ],
453
+ related: ["Erd", "InfraMap", "SequenceDiagram"],
454
+ },
455
+ Erd: {
456
+ group: "Diagrams and maps",
457
+ summary: "An entity-relationship diagram: tables with their columns, and relations with one-or-many ends.",
458
+ helpers: ["Entity", "Relation"],
459
+ shots: [{ file: "erd", caption: "Three tables. `├` or `┤` is the bar (one) and `<` `>` the crow's foot (many)" }],
460
+ use: ["The tables, columns and keys of a schema.", "How tables point at each other."],
461
+ avoid: "Steps that run in order (Dag).",
462
+ args: {
463
+ "Erd.entities": "One Entity per table.",
464
+ "Erd.relations": "One Relation per line.",
465
+ "Erd.title": "A bold line above the diagram.",
466
+ "Erd.tooltips": "Targets: an entity name, or `entity.column`.",
467
+ "Entity.name": "The table name. Relations refer to it.",
468
+ "Entity.fields": 'One string per column: `"name type flags"`, such as `"id uuid PK"` or `"email text UNIQUE"`. `PK` and `FK` are highlighted.',
469
+ "Relation.from": "An entity name.",
470
+ "Relation.to": "An entity name.",
471
+ "Relation.kind": '`"1:N"` (default: the first entity is the one side), `"1:1"`, `"N:1"` or `"N:M"`.',
472
+ "Relation.label": "Short text on the line, such as `owns`.",
473
+ },
474
+ example: `root = Stack([s])
475
+ s = Erd([users, projects], [r], "Schema")
476
+ users = Entity("users", ["id uuid PK", "email text UNIQUE"])
477
+ projects = Entity("projects", ["id uuid PK", "owner_id uuid FK"])
478
+ r = Relation("users", "projects", "1:N", "owns")`,
479
+ interaction: ["A table is a target, and so is each column."],
480
+ tips: [
481
+ "Each relation end is one character so the picture stays small: a bar fused with the wire (`├ ┤ ┬ ┴`) means one, a foot facing the table (`< > Λ V`) means many.",
482
+ "Two relations between the same tables are both drawn.",
483
+ "A relation from a table to itself is shown as a `∞` in the title.",
484
+ ],
485
+ related: ["Dag", "Table"],
486
+ },
487
+ InfraMap: {
488
+ group: "Diagrams and maps",
489
+ summary: "Zones holding nodes (servers, databases, queues), with labelled arrows showing which talks to which.",
490
+ helpers: ["InfraZone", "InfraNode", "InfraLink"],
491
+ shots: [{ file: "infra", caption: "Three zones, with a degraded worker and a dashed link" }],
492
+ use: ["How a system fits together.", "Where a fault is: nodes carry a status."],
493
+ avoid: "More than about 4 zones and 12 nodes: split it in two.",
494
+ args: {
495
+ "InfraMap.zones": "One InfraZone per box, in order.",
496
+ "InfraMap.links": "One InfraLink per arrow (`[]` for none).",
497
+ "InfraMap.title": "A bold line above the map.",
498
+ "InfraMap.direction": '`"LR"` makes each zone a column (default); `"TB"` makes each zone a row.',
499
+ "InfraMap.tooltips": "Targets: a node id, `zone:<zone name>`, or `<fromId>><toId>` for a link.",
500
+ "InfraZone.name": "The caption in the top border.",
501
+ "InfraZone.nodes": "At least one InfraNode.",
502
+ "InfraZone.tone": "Tints the zone: `neutral` (default), `info`, `good`, `warn`, `bad`.",
503
+ "InfraNode.id": "Unique across the whole map. Links use it.",
504
+ "InfraNode.label": "Bold text in the box.",
505
+ "InfraNode.kind": "Picks the icon: client □, gateway ↔, service ◇, gpu ⌸, database ◎, storage ☰, queue ≡, cloud ◌.",
506
+ "InfraNode.status": "`up` (green dot), `degraded` (amber), `down` (red) or `unknown` (hollow, dashed box). Omit it for no dot.",
507
+ "InfraNode.detail": "A short second line, such as a port or a count.",
508
+ "InfraLink.from": "The id of the node where the arrow starts (marked with a dot).",
509
+ "InfraLink.to": "The id of the node where it ends (the arrowhead).",
510
+ "InfraLink.label": "A protocol or purpose such as `SQL`, written on the line.",
511
+ "InfraLink.style": "`solid` (default) or `dashed` for an optional or indirect path.",
512
+ },
513
+ example: `root = Stack([m])
514
+ m = InfraMap([edge, app], [l], "Platform")
515
+ edge = InfraZone("Edge", [lb], "info")
516
+ app = InfraZone("App", [api])
517
+ lb = InfraNode("lb", "Load balancer", "gateway", "up", "HTTPS :443")
518
+ api = InfraNode("api", "API", "service", "degraded", "2 of 3 running")
519
+ l = InfraLink("lb", "api", "HTTP")`,
520
+ tips: ["Zones, nodes and links are each tooltip targets.", "Wires may cross a zone border at a right angle but never run along it."],
521
+ related: ["Dag", "SequenceDiagram", "GeoMap"],
522
+ },
523
+ SequenceDiagram: {
524
+ group: "Diagrams and maps",
525
+ summary: "Calls between participants: columns left to right, steps top to bottom, with notes and alt/loop frames.",
526
+ helpers: ["SeqParticipant", "SeqMessage", "SeqNote", "SeqGroup"],
527
+ shots: [{ file: "sequence", caption: "An alt/else frame, a note, a self-call and activation bars" }],
528
+ use: ["The order of calls in a request.", "Who calls whom, and what comes back."],
529
+ avoid: "More than about 5 participants or 20 steps: split it.",
530
+ args: {
531
+ "SequenceDiagram.participants": "One per column.",
532
+ "SequenceDiagram.steps": "Messages, notes and groups in order.",
533
+ "SequenceDiagram.title": "A bold line above.",
534
+ "SequenceDiagram.numbered": "Number the messages (default yes). Pass `false` to turn it off.",
535
+ "SequenceDiagram.tooltips": "Targets: a participant id, or a SeqMessage `id` (a message without an id cannot carry a tooltip).",
536
+ "SeqParticipant.id": "Unique. Messages and notes use it. Keep it short.",
537
+ "SeqParticipant.label": "The header text (22 characters at most).",
538
+ "SeqParticipant.kind": "The header icon: `actor` ⚇, `service` ◇ (default), `database` ◎, `queue` ≡, `external` ◌.",
539
+ "SeqMessage.from": "A participant id. `from` equal to `to` is a self-call (a small loop).",
540
+ "SeqMessage.to": "A participant id.",
541
+ "SeqMessage.label": "Shown above the arrow (48 characters).",
542
+ "SeqMessage.style": "`sync` (solid, filled head, opens an activation bar on the receiver), `async` (solid, open head) or `return` (dashed, open head, closes the bar).",
543
+ "SeqMessage.id": "Only needed to attach a tooltip. Unique.",
544
+ "SeqNote.over": "A participant id.",
545
+ "SeqNote.text": "Wrapped to the note's width.",
546
+ "SeqNote.to": "A second participant: the note spans from `over` to `to`.",
547
+ "SeqGroup.kind": "`alt` (if/else), `opt`, `loop` or `par`.",
548
+ "SeqGroup.label": "Shown in the frame's tab.",
549
+ "SeqGroup.steps": "The messages, notes and groups inside.",
550
+ "SeqGroup.elseSteps": "A second branch below a dashed divider.",
551
+ "SeqGroup.elseLabel": "Names the divider.",
552
+ },
553
+ example: `root = Stack([d])
554
+ d = SequenceDiagram([u, api, db], [m1, m2, m3])
555
+ u = SeqParticipant("u", "User", "actor")
556
+ api = SeqParticipant("api", "API")
557
+ db = SeqParticipant("db", "Postgres", "database")
558
+ m1 = SeqMessage("u", "api", "GET /item/42")
559
+ m2 = SeqMessage("api", "db", "SELECT * FROM items")
560
+ m3 = SeqMessage("db", "api", "row", "return")`,
561
+ tips: ["Groups nest up to three deep.", "One SeqGroup per decision: put the other side in `elseSteps`, not in a second group.", "Participant headers are targets for tooltips."],
562
+ related: ["Dag", "InfraMap"],
563
+ },
564
+ };
565
+
566
+ /** Tooltip is a helper of every host, so it has a page of its own. */
567
+ export const TOOLTIP_DOC = {
568
+ summary: "A hint and an optional detail panel for one element of a host component. The element shows a `◆`; hover for the hint, click or press Enter for the panel.",
569
+ shots: [
570
+ { file: "table-tooltip", caption: "Hover: a bubble with the hint appears next to the element" },
571
+ { file: "table-panel", caption: "Click or Enter: the panel under the widget, with detail, code and a link" },
572
+ { file: "line-tooltip", caption: "On a line chart the badge sits on the curve" },
573
+ { file: "pie-tooltip", caption: "On a pie the wedge and its legend row are both the target" },
574
+ ],
575
+ };