ionbase-ui 0.22.0 → 0.25.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.
Files changed (56) hide show
  1. package/dist/components/AgentActivity.d.ts +41 -0
  2. package/dist/components/AgentActivity.d.ts.map +1 -0
  3. package/dist/components/AgentActivity.js +84 -0
  4. package/dist/components/AgentActivity.js.map +1 -0
  5. package/dist/components/AgentStop.d.ts +58 -0
  6. package/dist/components/AgentStop.d.ts.map +1 -0
  7. package/dist/components/AgentStop.js +82 -0
  8. package/dist/components/AgentStop.js.map +1 -0
  9. package/dist/components/ApprovalGate.d.ts +72 -0
  10. package/dist/components/ApprovalGate.d.ts.map +1 -0
  11. package/dist/components/ApprovalGate.js +68 -0
  12. package/dist/components/ApprovalGate.js.map +1 -0
  13. package/dist/components/Citation.d.ts +51 -0
  14. package/dist/components/Citation.d.ts.map +1 -0
  15. package/dist/components/Citation.js +41 -0
  16. package/dist/components/Citation.js.map +1 -0
  17. package/dist/components/ConfidenceIndicator.d.ts +37 -0
  18. package/dist/components/ConfidenceIndicator.d.ts.map +1 -0
  19. package/dist/components/ConfidenceIndicator.js +41 -0
  20. package/dist/components/ConfidenceIndicator.js.map +1 -0
  21. package/dist/components/StreamingText.d.ts +47 -0
  22. package/dist/components/StreamingText.d.ts.map +1 -0
  23. package/dist/components/StreamingText.js +44 -0
  24. package/dist/components/StreamingText.js.map +1 -0
  25. package/dist/components/index.d.ts +12 -0
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/components/index.js +6 -0
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/figma-descriptions.json +147 -0
  30. package/dist/figma-map.json +3 -1
  31. package/dist/meta/AgentActivity.json +103 -0
  32. package/dist/meta/AgentActivityStep.json +117 -0
  33. package/dist/meta/AgentStop.json +164 -0
  34. package/dist/meta/ApprovalGate.json +255 -0
  35. package/dist/meta/Avatar.json +2 -2
  36. package/dist/meta/AvatarGroup.json +2 -2
  37. package/dist/meta/Citation.json +101 -0
  38. package/dist/meta/CitationList.json +94 -0
  39. package/dist/meta/CitationListItem.json +88 -0
  40. package/dist/meta/ConfidenceIndicator.json +115 -0
  41. package/dist/meta/Input.json +2 -2
  42. package/dist/meta/PhoneInput.json +38 -38
  43. package/dist/meta/StreamingText.json +99 -0
  44. package/dist/meta/components.json +2125 -989
  45. package/dist/meta/contrast.json +1086 -32
  46. package/dist/meta/index.json +100 -5
  47. package/dist/meta/patterns/index.json +1 -1
  48. package/dist/styles/agent-activity.css +95 -0
  49. package/dist/styles/agent-stop.css +109 -0
  50. package/dist/styles/approval-gate.css +181 -0
  51. package/dist/styles/citation.css +93 -0
  52. package/dist/styles/confidence-indicator.css +97 -0
  53. package/dist/styles/index.css +34 -0
  54. package/dist/styles/streaming-text.css +56 -0
  55. package/llms.txt +2 -2
  56. package/package.json +4 -3
@@ -1,675 +1,560 @@
1
1
  {
2
2
  "package": "ionbase-ui",
3
- "version": "0.22.0",
3
+ "version": "0.25.0",
4
4
  "generated": "by scripts/build-meta.mjs — do not edit; intent lives in meta/*.json",
5
5
  "hooks": [
6
6
  "useToast"
7
7
  ],
8
8
  "components": {
9
- "Alert": {
10
- "name": "Alert",
11
- "source": "src/components/Alert.tsx",
12
- "propsType": "AlertProps",
13
- "description": "Alert — Figma `Alert` (812:1902).\n\n`Intent` x `Emphasis` x `Layout`, with the parts as props rather than\nvariants. Intents match `Badge` exactly, so a status mapped to one can be\npassed to the other.\n\nROLE IS CHOSEN BY INTENT, NOT PASSED IN. `error` and `warning` render\n`role=\"alert\"`, which interrupts a screen reader; the rest render\n`role=\"status\"`, which waits for a pause. Getting this backwards is the\ncommon failure — a page of `role=\"alert\"` status messages talks over the\nuser, and an error announced as a status is missed.\n\nThe icon is decorative: it repeats what the intent's colour and copy already\nsay, so it is `aria-hidden` and the live region carries the text alone.",
14
- "import": "import { Alert } from 'ionbase-ui';",
9
+ "AgentActivity": {
10
+ "name": "AgentActivity",
11
+ "source": "src/components/AgentActivity.tsx",
12
+ "propsType": "AgentActivityProps",
13
+ "description": "AgentActivity — what the agent is doing, in plain language.\n\nAn ordered list, because the steps happened in an order and a screen reader\nshould say \"3 of 7\". Not a log viewer: this is the account a person reads to\ndecide whether to let the run continue, so the text belongs in their\nlanguage — \"Searched the invoice archive\", not `searchIndex(q, {limit:50})`.\n\nSTATUS IS NEVER CARRIED BY THE ICON ALONE. Every step renders its status as\ntext as well as a glyph. The glyphs differ in shape rather than only in\ncolour, so the list survives greyscale, colour blindness and forced-colours\nmode — WCAG 1.4.1, which a row of coloured dots fails outright.\n\nONE POLITE ANNOUNCEMENT PER STEP, not one per render. The active step's text\nis announced when it changes, so a user who is not watching still knows where\nthe run has got to. Watching the DOM instead would re-announce on every\nunrelated update, which is the failure mode that makes people turn logs off.",
14
+ "import": "import { AgentActivity } from 'ionbase-ui';",
15
15
  "status": "stable",
16
- "since": "0.13.1",
17
- "summary": "A persistent, in-page status message. Intent picks the ARIA role for you.",
16
+ "summary": "An ordered, plain-language log of what the agent is doing. Status is text and shape, never colour alone.",
18
17
  "useWhen": [
19
- "the message belongs to the page and should stay until the condition clears",
20
- "form-level validation summaries, permission notices, degraded-state banners"
18
+ "a run takes long enough that the user needs to know where it has got to",
19
+ "a person will decide whether to let it continue — which means they need to read what it did"
21
20
  ],
22
21
  "useInstead": [
23
22
  {
24
- "when": "the message is transient and self-dismissing",
25
- "use": "Toast",
26
- "why": "an Alert stays in the layout; a Toast does not"
23
+ "when": "the output is prose rather than steps",
24
+ "use": "StreamingText"
27
25
  },
28
26
  {
29
- "when": "it labels a value inline rather than telling the user something",
30
- "use": "Badge",
31
- "why": "Badge shares Alert's intent vocabulary exactly, so a status mapped to one passes to the other"
32
- }
33
- ],
34
- "variants": {
35
- "intent": {
36
- "error": {
37
- "use": "something failed and the user must act",
38
- "aria": "role=alert — interrupts the screen reader"
39
- },
40
- "warning": {
41
- "use": "something may fail or has degraded",
42
- "aria": "role=alert — interrupts the screen reader"
43
- },
44
- "success": {
45
- "use": "an operation completed",
46
- "aria": "role=status"
47
- },
48
- "information": {
49
- "use": "neutral context the user did not ask for",
50
- "aria": "role=status"
51
- },
52
- "primary": {
53
- "use": "product announcements and promotions",
54
- "aria": "role=status"
55
- },
56
- "neutral": {
57
- "use": "low-emphasis context",
58
- "aria": "role=status"
59
- }
60
- },
61
- "emphasis": {
62
- "subtle": {
63
- "use": "the default — tinted surface, readable in long pages"
64
- },
65
- "solid": {
66
- "use": "reserve for a single high-stakes banner; it dominates the layout"
67
- }
27
+ "when": "you need a decision, not a report",
28
+ "use": "ApprovalGate"
68
29
  },
69
- "layout": {
70
- "inline": {
71
- "use": "inside a form or card, in the flow of content"
72
- },
73
- "banner": {
74
- "use": "full-bleed at the top of a page or region"
75
- }
30
+ {
31
+ "when": "it is one finished outcome",
32
+ "use": "Alert"
76
33
  }
34
+ ],
35
+ "composition": {
36
+ "order": [
37
+ "AgentActivity",
38
+ "AgentActivityStep"
39
+ ],
40
+ "note": "An <ol>, so a screen reader can say \"3 of 7\" and the order is structural rather than typed into each row.",
41
+ "example": "<AgentActivity><AgentActivityStep status=\"done\">Searched the invoice archive</AgentActivityStep><AgentActivityStep status=\"active\">Reading 12 matches</AgentActivityStep></AgentActivity>"
77
42
  },
78
43
  "a11y": {
44
+ "role": "list",
79
45
  "guarantees": [
80
- "the role follows the intent — error and warning are role=alert, everything else role=status",
81
- "the icon is aria-hidden, so the live region announces the text once and not the glyph"
46
+ "every step renders its status as visually hidden text as well as a glyph, and the glyphs differ in shape rather than only in colour — a row of coloured dots fails WCAG 1.4.1 outright",
47
+ "the active step is announced once, when it changes, in a polite live region",
48
+ "the active step's spinner stops under `prefers-reduced-motion`"
82
49
  ],
83
50
  "requires": [
84
- "`dismissLabel` when `onDismiss` is set and the surrounding copy does not name the action"
51
+ "step text in the user's language — \"Searched the invoice archive\", not a function signature",
52
+ "`announceActive={false}` when several logs are on screen, or they narrate over each other"
85
53
  ],
86
54
  "notes": [
87
- "role=alert interrupts whatever the screen reader is saying; role=status waits for a pause. This is why the role is not a prop."
55
+ "The active step is read out of the children rather than taken as a prop. Two sources for one fact is how they come to disagree."
88
56
  ]
89
57
  },
90
58
  "antiPatterns": [
91
59
  {
92
- "dont": "using intent=\"error\" for a message that is merely informational",
93
- "why": "error renders role=alert, which interrupts the screen reader. A page of interrupting status messages talks over the user."
60
+ "dont": "raw tool calls or JSON as the step label",
61
+ "why": "this is the account a person reads to decide whether to let the run continue"
94
62
  },
95
63
  {
96
- "dont": "using intent=\"information\" for a genuine failure",
97
- "why": "role=status waits for a pause, so a real error can be missed entirely"
64
+ "dont": "more than one step with `status=\"active\"`",
65
+ "why": "the announcement names one thing, and two claims about what is happening now cannot both be true"
98
66
  },
99
67
  {
100
- "dont": "rendering an Alert conditionally for a transient confirmation",
101
- "do": "useToast",
102
- "why": "an Alert that appears and disappears shifts the layout under the user"
68
+ "dont": "several announcing logs on one screen",
69
+ "do": "`announceActive={false}` on all but one",
70
+ "why": "they interrupt each other and the user turns the whole thing off"
103
71
  }
104
72
  ],
105
- "stylesheet": "src/styles/alert.css",
73
+ "stylesheet": "src/styles/agent-activity.css",
106
74
  "tokens": [
107
- "--border-default",
108
- "--border-error-strong",
109
- "--border-error-subtle",
110
- "--border-information-strong",
111
- "--border-information-subtle",
112
- "--border-inverse",
113
- "--border-primary-strong",
114
- "--border-primary-subtle",
115
- "--border-success-strong",
116
- "--border-success-subtle",
117
- "--border-warning-strong",
118
- "--border-warning-subtle",
119
- "--border-width-default",
120
- "--border-width-thick",
121
75
  "--font-family-sans",
122
- "--font-weight-medium",
123
- "--font-weight-regular",
124
76
  "--icon-error",
125
- "--icon-information",
126
- "--icon-on-color",
127
77
  "--icon-primary",
128
- "--icon-secondary",
129
- "--icon-size-md",
78
+ "--icon-size-sm",
130
79
  "--icon-success",
131
- "--icon-warning",
132
- "--ion-alert-border",
133
- "--ion-alert-icon",
134
- "--ion-alert-surface",
135
- "--ion-alert-text",
136
- "--ion-duration-base",
137
- "--ion-ease-out",
138
- "--radius-md",
139
- "--radius-sm",
140
- "--ring-focus",
141
- "--spacing-12",
142
- "--spacing-16",
143
- "--spacing-32",
80
+ "--icon-tertiary",
81
+ "--spacing-2",
144
82
  "--spacing-8",
145
- "--surface-error",
146
- "--surface-error-subtle",
147
- "--surface-hover",
148
- "--surface-information",
149
- "--surface-information-subtle",
150
- "--surface-inverse",
151
- "--surface-muted",
152
- "--surface-primary",
153
- "--surface-primary-subtle",
154
- "--surface-success",
155
- "--surface-success-subtle",
156
- "--surface-warning",
157
- "--surface-warning-subtle",
83
+ "--text-default",
158
84
  "--text-error",
159
- "--text-information",
160
- "--text-link",
161
- "--text-on-color",
162
85
  "--text-secondary",
163
- "--text-success",
164
- "--text-warning",
165
- "--type-body",
166
- "--type-body-line-height"
86
+ "--text-tertiary",
87
+ "--type-body-sm",
88
+ "--type-body-sm-line-height",
89
+ "--type-caption",
90
+ "--type-caption-line-height"
167
91
  ],
168
92
  "props": {
169
- "intent": {
170
- "type": "AlertIntent | undefined",
171
- "required": false,
172
- "origin": "own",
173
- "description": "Matches the Figma `Intent` variant. Same vocabulary as `Badge`.",
174
- "values": [
175
- "neutral",
176
- "primary",
177
- "success",
178
- "warning",
179
- "error",
180
- "information"
181
- ],
182
- "default": "information"
183
- },
184
- "emphasis": {
185
- "type": "AlertEmphasis | undefined",
186
- "required": false,
187
- "origin": "own",
188
- "description": "Matches the Figma `Emphasis` variant.",
189
- "values": [
190
- "subtle",
191
- "solid"
192
- ],
193
- "default": "subtle"
194
- },
195
- "layout": {
196
- "type": "AlertLayout | undefined",
197
- "required": false,
198
- "origin": "own",
199
- "description": "Matches the Figma `Layout` variant.",
200
- "values": [
201
- "inline",
202
- "banner"
203
- ],
204
- "default": "inline"
205
- },
206
- "title": {
93
+ "children": {
207
94
  "type": "React.ReactNode",
208
95
  "required": false,
209
- "origin": "own",
210
- "description": "Optional heading above the message."
96
+ "origin": "own"
211
97
  },
212
- "hideIcon": {
98
+ "announceActive": {
213
99
  "type": "boolean | undefined",
214
100
  "required": false,
215
101
  "origin": "own",
216
- "description": "Hide the leading icon. Shown by default."
217
- },
218
- "icon": {
219
- "type": "React.ReactNode",
220
- "required": false,
221
- "origin": "own",
222
- "description": "Replace the intent's glyph."
223
- },
224
- "actions": {
225
- "type": "React.ReactNode",
226
- "required": false,
227
- "origin": "own",
228
- "description": "Action buttons, rendered under the message."
229
- },
230
- "onDismiss": {
231
- "type": "(() => void) | undefined",
232
- "required": false,
233
- "origin": "own",
234
- "description": "Called when the dismiss button is pressed. Omit for no dismiss button."
235
- },
236
- "dismissLabel": {
237
- "type": "string | undefined",
238
- "required": false,
239
- "origin": "own",
240
- "description": "Accessible name for the dismiss button.",
241
- "default": "Dismiss"
242
- },
243
- "children": {
244
- "type": "React.ReactNode",
245
- "required": false,
246
- "origin": "own",
247
- "description": "The message."
102
+ "description": "Announce the step that just became active, once, in a polite live region.\n\nOn by default because the whole point of an activity log is knowing what\nthe agent is doing without watching it. Turn it off when several logs are\non screen at once, or they narrate over each other."
248
103
  }
249
104
  },
250
105
  "propCounts": {
251
- "own": 10,
106
+ "own": 2,
252
107
  "aria": 0,
253
- "dom": 276,
108
+ "dom": 277,
254
109
  "other": 0
255
110
  }
256
111
  },
257
- "Avatar": {
258
- "name": "Avatar",
259
- "source": "src/components/Avatar.tsx",
260
- "propsType": "AvatarProps",
261
- "description": "Avatar picks its content the way Figma's `Type` variant does, but by\nprecedence rather than by a prop: image, then initials, then icon. A caller\npassing `src` and `initials` gets the image with the initials as its alt\nfallback, which is what you want when the image 404s.\n\nA failed `src` falls through to initials (then icon) rather than leaving the\nbrowser's broken-image glyph — that is why initials exist alongside `src`.\n\nIt is a `<span>`, not a `<div>`, so it can sit inline beside text without the\ncaller fighting a block element.",
262
- "import": "import { Avatar } from 'ionbase-ui';",
112
+ "AgentActivityStep": {
113
+ "name": "AgentActivityStep",
114
+ "source": "src/components/AgentActivity.tsx",
115
+ "propsType": "AgentActivityStepProps",
116
+ "import": "import { AgentActivityStep } from 'ionbase-ui';",
263
117
  "status": "stable",
264
- "summary": "A person or entity marker. Content is chosen by precedence — image, then initials, then icon — not by a type prop.",
118
+ "summary": "One step in an activity log: what happened, in the user's language, with a status carried by shape and text.",
265
119
  "useWhen": [
266
- "identifying who owns, authored or is assigned to something",
267
- "a table cell, menu row or header needs a compact identity marker beside a name"
120
+ "a child of AgentActivity — it is not usable elsewhere"
268
121
  ],
269
122
  "useInstead": [
270
123
  {
271
- "when": "the thing being marked is a status, count or category rather than a person",
272
- "use": "Badge"
124
+ "when": "it is a row in a dropdown",
125
+ "use": "MenuItem"
273
126
  },
274
127
  {
275
- "when": "you only need a decorative graphic with no identity behind it",
276
- "use": "Icon"
128
+ "when": "it is a data row",
129
+ "use": "TableRow"
277
130
  }
278
131
  ],
279
132
  "variants": {
280
- "size": {
281
- "mini": {
282
- "use": "24px — inline beside body text, dense table rows"
133
+ "status": {
134
+ "pending": {
135
+ "use": "the default — queued, not started"
283
136
  },
284
- "sm": {
285
- "use": "32px — menu rows, compact lists"
137
+ "active": {
138
+ "use": "happening now. Only one step should carry it"
286
139
  },
287
- "md": {
288
- "use": "the default, 40px"
140
+ "done": {
141
+ "use": "finished successfully"
289
142
  },
290
- "lg": {
291
- "use": "48px — profile headers and detail pages"
292
- }
293
- },
294
- "shape": {
295
- "circle": {
296
- "use": "the default — people"
143
+ "failed": {
144
+ "use": "did not finish. Say why in `detail`"
297
145
  },
298
- "square": {
299
- "use": "organisations, projects, workspaces and other non-human entities"
146
+ "skipped": {
147
+ "use": "deliberately not run — a branch not taken, a cached result"
300
148
  }
301
149
  }
302
150
  },
303
151
  "slots": {
304
- "icon": {
305
- "accepts": "Icon",
306
- "note": "the last fallback — rendered only when there is neither `src` nor `initials`"
152
+ "children": {
153
+ "accepts": "text",
154
+ "note": "the step in plain language"
155
+ },
156
+ "detail": {
157
+ "accepts": "text",
158
+ "note": "the result, a count, a tool name — whatever makes the step checkable"
307
159
  }
308
160
  },
309
161
  "a11y": {
310
162
  "guarantees": [
311
- "`alt` falls back to `initials`, so the avatar is never announced as an unlabelled image",
312
- "a `src` that fails to load falls through to initials, then icon — never the browser's broken-image glyph"
313
- ],
314
- "requires": [
315
- "`alt` describing the person, not the picture — \"Ada Lovelace\", not \"profile photo\""
163
+ "the status name is rendered as visually hidden text, so it is never carried by the glyph's colour alone",
164
+ "each status has a differently shaped glyph, which survives greyscale and forced-colours mode"
316
165
  ]
317
166
  },
318
167
  "antiPatterns": [
319
168
  {
320
- "dont": "passing `src` without `initials`",
321
- "why": "a 404 leaves the fallback empty; initials are what make the failure invisible"
169
+ "dont": "`failed` with no `detail`",
170
+ "why": "a failure the reader cannot diagnose is just a red mark"
322
171
  },
323
172
  {
324
- "dont": "using Avatar as a button",
325
- "do": "put the Avatar inside a Button or Link",
326
- "why": "Avatar renders a <span> with no role and takes no focus"
173
+ "dont": "using it outside AgentActivity",
174
+ "why": "it renders an <li>, which is only valid inside a list"
327
175
  }
328
176
  ],
329
- "stylesheet": "src/styles/avatar.css",
177
+ "stylesheet": "src/styles/agent-activity.css",
330
178
  "tokens": [
331
- "--border-strong",
332
- "--border-width-default",
333
179
  "--font-family-sans",
334
- "--font-weight-medium",
335
- "--icon-size-2xs",
336
- "--icon-size-lg",
337
- "--icon-size-md",
180
+ "--icon-error",
181
+ "--icon-primary",
338
182
  "--icon-size-sm",
183
+ "--icon-success",
339
184
  "--icon-tertiary",
340
- "--ion-avatar-font-size",
341
- "--ion-avatar-group-overlap",
342
- "--ion-avatar-icon",
343
- "--ion-avatar-line-height",
344
- "--ion-avatar-radius",
345
- "--ion-avatar-size",
346
- "--radius-full",
347
- "--radius-md",
348
- "--radius-sm",
349
- "--radius-xs",
350
- "--spacing-24",
351
- "--spacing-32",
352
- "--spacing-40",
353
- "--spacing-48",
354
- "--surface-muted",
185
+ "--spacing-2",
186
+ "--spacing-8",
187
+ "--text-default",
188
+ "--text-error",
189
+ "--text-secondary",
355
190
  "--text-tertiary",
356
- "--type-body",
357
- "--type-body-line-height",
358
191
  "--type-body-sm",
359
192
  "--type-body-sm-line-height",
360
193
  "--type-caption",
361
- "--type-caption-line-height",
362
- "--type-h5",
363
- "--type-h5-line-height"
194
+ "--type-caption-line-height"
364
195
  ],
365
196
  "props": {
366
- "size": {
367
- "type": "AvatarSize | undefined",
197
+ "children": {
198
+ "type": "React.ReactNode",
368
199
  "required": false,
369
200
  "origin": "own",
370
- "description": "Matches the Figma `Size` variant: Mini 24, Small 32, Medium 40, Large 48.",
371
- "values": [
372
- "mini",
373
- "sm",
374
- "md",
375
- "lg"
376
- ]
201
+ "description": "What the agent did, in the user's language. Not a function name."
377
202
  },
378
- "shape": {
379
- "type": "AvatarShape | undefined",
203
+ "status": {
204
+ "type": "AgentActivityStatus | undefined",
380
205
  "required": false,
381
206
  "origin": "own",
382
- "description": "Matches the Figma `Shape` variant.",
383
207
  "values": [
384
- "circle",
385
- "square"
208
+ "pending",
209
+ "active",
210
+ "done",
211
+ "failed",
212
+ "skipped"
386
213
  ]
387
214
  },
388
- "src": {
389
- "type": "string | undefined",
390
- "required": false,
391
- "origin": "own",
392
- "description": "Image source. Figma's `Type=Image`."
393
- },
394
- "alt": {
395
- "type": "string | undefined",
396
- "required": false,
397
- "origin": "own",
398
- "description": "Describes the person, not the picture. Falls back to `initials` so the\navatar is never announced as an unlabelled image."
399
- },
400
- "initials": {
401
- "type": "string | undefined",
402
- "required": false,
403
- "origin": "own",
404
- "description": "Figma's `Type=Character`. Rendered when there is no `src`, or when `src`\nfails to load."
405
- },
406
- "icon": {
215
+ "detail": {
407
216
  "type": "React.ReactNode",
408
217
  "required": false,
409
218
  "origin": "own",
410
- "description": "Figma's `Type=Icon`. Rendered when there is neither `src` nor `initials`."
219
+ "description": "The result, a tool name, a count — whatever makes the step checkable."
411
220
  }
412
221
  },
413
222
  "propCounts": {
414
- "own": 6,
223
+ "own": 3,
415
224
  "aria": 0,
416
225
  "dom": 278,
417
226
  "other": 0
418
227
  }
419
228
  },
420
- "AvatarGroup": {
421
- "name": "AvatarGroup",
422
- "source": "src/components/Avatar.tsx",
423
- "propsType": "AvatarGroupProps",
424
- "description": "AvatarGroup overlaps its children by a quarter of the avatar size, matching\nFigma's -6 / -8 / -10 / -12 gaps at Mini / Small / Medium / Large.\n\nGroup defaults fill in `size` / `shape` only when a child Avatar has not set\nthem itself — an explicit child prop wins. Non-Avatar children are left\nalone so `size`/`shape` are never pushed onto arbitrary DOM nodes.\n\nChildren render in source order because that is Figma's stacking: each avatar\npaints over the one before it, so the `+N` overflow ends up on top. Later\nsiblings paint later, so this needs no z-index.",
425
- "import": "import { AvatarGroup } from 'ionbase-ui';",
229
+ "AgentStop": {
230
+ "name": "AgentStop",
231
+ "source": "src/components/AgentStop.tsx",
232
+ "propsType": "AgentStopProps",
233
+ "description": "AgentStop — the always-visible way to end a run.\n\nNOT OPTIONAL, AND NOT BEHIND A MENU. A user who cannot stop an agent is\nwatching it, not supervising it. Human oversight of an automated process is\na compliance surface in regulated contexts, not a UX preference, and the\ncontrol that provides it has to be present and reachable at the moment it is\nwanted — not two clicks into an overflow menu.\n\nWHY IT IS NOT JUST `<Button variant=\"destructive\">`\n\nThree things it owns that a Button does not:\n\n It keeps its place while stopping. A control that vanishes the instant it\n is pressed leaves the user unsure whether the press registered, and the\n run is usually still going. It stays, relabels, and disables — the layout\n does not move.\n\n It announces. The label change is only announced to a screen reader if the\n button happens to hold focus, which it usually does not when a run was\n started elsewhere. A polite live region says \"Stopping\" once.\n\n It is not destructive-red. Stopping is a normal, expected, reversible-in-\n spirit action — you can run it again. Colouring it like `delete` teaches\n hesitation about the one control that must never be hesitated over.\n\nWHAT IT DOES NOT DO. It does not stop anything. It reports intent; the\ncaller aborts the request, closes the stream and settles the state. A stop\nbutton that resolves optimistically while tokens keep arriving is worse than\nnone, because it lies about a guarantee the user is relying on.",
234
+ "import": "import { AgentStop } from 'ionbase-ui';",
426
235
  "status": "stable",
427
- "summary": "Overlapping Avatars with a `+N` overflow, for showing several people in one row's width.",
236
+ "summary": "The always-visible way to end a running agent. Not optional, and not behind a menu.",
428
237
  "useWhen": [
429
- "several people share one thing — assignees, participants, collaborators",
430
- "the exact list matters less than the fact that there are several"
238
+ "anything runs long enough that a person might want it to stop — a generation, a tool chain, a batch job",
239
+ "the run was started by the user and is still going"
431
240
  ],
432
241
  "useInstead": [
433
242
  {
434
- "when": "each person needs their own name, role or action visible",
435
- "use": "Table",
436
- "why": "an overlapping stack hides everything but the picture"
243
+ "when": "the action is instant and there is nothing to interrupt",
244
+ "use": "Button"
245
+ },
246
+ {
247
+ "when": "you are asking permission before something happens rather than ending something already happening",
248
+ "use": "ApprovalGate"
249
+ },
250
+ {
251
+ "when": "the thing to undo has already finished",
252
+ "use": "Toast",
253
+ "why": "an undo affordance belongs with the confirmation of what was done"
437
254
  }
438
255
  ],
439
256
  "variants": {
440
257
  "size": {
441
- "mini": {
442
- "use": "24px — inline beside body text, dense table rows"
443
- },
444
258
  "sm": {
445
- "use": "32px — menu rows, compact lists"
259
+ "use": "inside a toolbar or beside an inline activity row"
446
260
  },
447
261
  "md": {
448
- "use": "the default, 40px"
262
+ "use": "the default"
449
263
  },
450
264
  "lg": {
451
- "use": "48px — profile headers and detail pages"
452
- }
453
- },
454
- "shape": {
455
- "circle": {
456
- "use": "the default — people"
457
- },
458
- "square": {
459
- "use": "organisations, projects, workspaces"
265
+ "use": "a full-width run surface where stopping is the primary available action"
460
266
  }
461
267
  }
462
268
  },
463
- "slots": {
464
- "children": {
465
- "accepts": "Avatar",
466
- "note": "group `size`/`shape` fill in only where a child has not set its own; a child's explicit prop wins. Non-Avatar children are left untouched."
467
- }
468
- },
469
269
  "a11y": {
270
+ "role": "button",
271
+ "guarantees": [
272
+ "the control keeps its place while stopping — it relabels and disables rather than disappearing, so the layout does not move under the pointer",
273
+ "the transition to stopping is announced once through a polite live region, because the label change alone is only heard if the button happens to hold focus",
274
+ "`stopOnEscape` ignores Escape raised inside a dialog, so closing a modal never cancels a background run"
275
+ ],
470
276
  "requires": [
471
- "`alt` on every child Avatar — the overflow avatar is the only one the group names for you"
277
+ "keeping it rendered and visible for the whole run — a stop control that scrolls away or hides in an overflow menu is the failure this component exists to prevent"
472
278
  ],
473
279
  "notes": [
474
- "Children paint in source order, so the `+N` overflow lands on top without any z-index."
280
+ "It does not stop anything. It reports intent; aborting the request and settling the state is the caller's. A stop that resolves optimistically while output keeps arriving lies about the one guarantee the user is relying on."
475
281
  ]
476
282
  },
477
283
  "antiPatterns": [
478
284
  {
479
- "dont": "omitting `max` on an unbounded list",
480
- "why": "twenty overlapping avatars is not a summary; it is a smear"
285
+ "dont": "putting the stop control in an overflow menu",
286
+ "why": "a user who cannot reach it is watching the agent, not supervising it"
287
+ },
288
+ {
289
+ "dont": "disabling it while the run is going",
290
+ "why": "the only correct disabled state is `isStopping` — after the request, never before"
291
+ },
292
+ {
293
+ "dont": "unmounting it the moment it is pressed",
294
+ "do": "pass `isStopping`",
295
+ "why": "the run is usually still going, and a control that vanishes leaves the user unsure the press registered"
296
+ },
297
+ {
298
+ "dont": "styling it as destructive",
299
+ "why": "stopping is normal and repeatable; red teaches hesitation about the one control that must never be hesitated over"
300
+ },
301
+ {
302
+ "dont": "`stopOnEscape` on a page with modals over the run",
303
+ "why": "Escape already means dismiss to every overlay in this system"
481
304
  }
482
305
  ],
483
- "stylesheet": "src/styles/avatar.css",
306
+ "stylesheet": "src/styles/agent-stop.css",
484
307
  "tokens": [
308
+ "--border-default",
309
+ "--border-error",
310
+ "--border-focus",
485
311
  "--border-strong",
486
312
  "--border-width-default",
313
+ "--border-width-thick",
487
314
  "--font-family-sans",
488
315
  "--font-weight-medium",
489
- "--icon-size-2xs",
490
- "--icon-size-lg",
491
316
  "--icon-size-md",
492
317
  "--icon-size-sm",
493
- "--icon-tertiary",
494
- "--ion-avatar-font-size",
495
- "--ion-avatar-group-overlap",
496
- "--ion-avatar-icon",
497
- "--ion-avatar-line-height",
498
- "--ion-avatar-radius",
499
- "--ion-avatar-size",
500
- "--radius-full",
318
+ "--icon-size-xs",
319
+ "--ion-duration-base",
320
+ "--ion-ease-out",
501
321
  "--radius-md",
502
- "--radius-sm",
503
- "--radius-xs",
504
- "--spacing-24",
322
+ "--spacing-12",
323
+ "--spacing-16",
324
+ "--spacing-20",
505
325
  "--spacing-32",
326
+ "--spacing-4",
506
327
  "--spacing-40",
507
328
  "--spacing-48",
329
+ "--spacing-8",
330
+ "--surface-default",
331
+ "--surface-error-subtle",
332
+ "--surface-error-subtle-hover",
508
333
  "--surface-muted",
509
- "--text-tertiary",
334
+ "--text-default",
335
+ "--text-error",
336
+ "--text-secondary",
510
337
  "--type-body",
338
+ "--type-body-lg",
339
+ "--type-body-lg-line-height",
511
340
  "--type-body-line-height",
512
341
  "--type-body-sm",
513
- "--type-body-sm-line-height",
514
- "--type-caption",
515
- "--type-caption-line-height",
516
- "--type-h5",
517
- "--type-h5-line-height"
342
+ "--type-body-sm-line-height"
518
343
  ],
519
344
  "props": {
520
- "size": {
521
- "type": "AvatarSize | undefined",
345
+ "onStop": {
346
+ "type": "() => void",
347
+ "required": true,
348
+ "origin": "own",
349
+ "description": "Called when the user asks the run to stop."
350
+ },
351
+ "isStopping": {
352
+ "type": "boolean | undefined",
522
353
  "required": false,
523
354
  "origin": "own",
524
- "values": [
525
- "mini",
526
- "sm",
527
- "md",
528
- "lg"
529
- ]
355
+ "description": "The request is in flight. The control stays visible and keeps its place;\nit does not disappear while the thing it cancels is still running."
530
356
  },
531
- "max": {
532
- "type": "number | undefined",
357
+ "label": {
358
+ "type": "string | undefined",
533
359
  "required": false,
534
360
  "origin": "own",
535
- "description": "Cap on avatars shown; the remainder becomes a `+N` overflow avatar."
361
+ "description": "Visible label."
536
362
  },
537
- "shape": {
538
- "type": "AvatarShape | undefined",
363
+ "stoppingLabel": {
364
+ "type": "string | undefined",
365
+ "required": false,
366
+ "origin": "own",
367
+ "description": "Visible label once `isStopping` is set."
368
+ },
369
+ "size": {
370
+ "type": "AgentStopSize | undefined",
539
371
  "required": false,
540
372
  "origin": "own",
541
373
  "values": [
542
- "circle",
543
- "square"
374
+ "sm",
375
+ "md",
376
+ "lg"
544
377
  ]
545
378
  },
546
- "children": {
547
- "type": "React.ReactNode",
379
+ "stopOnEscape": {
380
+ "type": "boolean | undefined",
548
381
  "required": false,
549
- "origin": "own"
382
+ "origin": "own",
383
+ "description": "Bind Escape, at the document, to stop the run.\n\nOff by default and deliberately so: Escape already means \"dismiss the\nthing in front of me\" to every modal, popover and menu in this system, and\na document-level handler would cancel a background run when the user meant\nto close a dialog. Turn it on only where the run IS the foreground task."
550
384
  }
551
385
  },
552
386
  "propCounts": {
553
- "own": 4,
387
+ "own": 6,
554
388
  "aria": 0,
555
- "dom": 277,
389
+ "dom": 286,
556
390
  "other": 0
557
391
  }
558
392
  },
559
- "Badge": {
560
- "name": "Badge",
561
- "source": "src/components/Badge.tsx",
562
- "propsType": "BadgeProps",
563
- "description": "Badge is presentational — no role, no interaction. React Aria has nothing to\noffer here, so this is a plain span rather than a hook wrapper for its own\nsake.\n\nIt carries no `status` or `alert` role on purpose: a badge is a label on\nsomething else, and announcing it as a live region would interrupt screen\nreader users on every render. Wrap it yourself if the value genuinely changes\nand matters.",
564
- "import": "import { Badge } from 'ionbase-ui';",
393
+ "Alert": {
394
+ "name": "Alert",
395
+ "source": "src/components/Alert.tsx",
396
+ "propsType": "AlertProps",
397
+ "description": "Alert — Figma `Alert` (812:1902).\n\n`Intent` x `Emphasis` x `Layout`, with the parts as props rather than\nvariants. Intents match `Badge` exactly, so a status mapped to one can be\npassed to the other.\n\nROLE IS CHOSEN BY INTENT, NOT PASSED IN. `error` and `warning` render\n`role=\"alert\"`, which interrupts a screen reader; the rest render\n`role=\"status\"`, which waits for a pause. Getting this backwards is the\ncommon failure — a page of `role=\"alert\"` status messages talks over the\nuser, and an error announced as a status is missed.\n\nThe icon is decorative: it repeats what the intent's colour and copy already\nsay, so it is `aria-hidden` and the live region carries the text alone.",
398
+ "import": "import { Alert } from 'ionbase-ui';",
565
399
  "status": "stable",
566
- "summary": "A small, non-interactive label that classifies the thing next to it. Presentational — no role, no live region.",
400
+ "since": "0.13.1",
401
+ "summary": "A persistent, in-page status message. Intent picks the ARIA role for you.",
567
402
  "useWhen": [
568
- "labelling a record's status, type, count or category",
569
- "the label belongs to something else on the page and is read as part of it"
403
+ "the message belongs to the page and should stay until the condition clears",
404
+ "form-level validation summaries, permission notices, degraded-state banners"
570
405
  ],
571
406
  "useInstead": [
572
407
  {
573
- "when": "the message is a standalone statement the user must read",
574
- "use": "Alert",
575
- "why": "Badge carries no role and is not announced on its own"
408
+ "when": "the message is transient and self-dismissing",
409
+ "use": "Toast",
410
+ "why": "an Alert stays in the layout; a Toast does not"
576
411
  },
577
412
  {
578
- "when": "the label should be clickable — a filter chip, a removable tag",
579
- "use": "Button",
580
- "why": "Badge takes no focus and fires nothing"
413
+ "when": "it labels a value inline rather than telling the user something",
414
+ "use": "Badge",
415
+ "why": "Badge shares Alert's intent vocabulary exactly, so a status mapped to one passes to the other"
581
416
  }
582
417
  ],
583
418
  "variants": {
584
419
  "intent": {
585
- "neutral": {
586
- "use": "the default — a category with no judgement attached"
420
+ "error": {
421
+ "use": "something failed and the user must act",
422
+ "aria": "role=alert — interrupts the screen reader"
587
423
  },
588
- "primary": {
589
- "use": "brand emphasis: Beta, New, Pro"
424
+ "warning": {
425
+ "use": "something may fail or has degraded",
426
+ "aria": "role=alert — interrupts the screen reader"
590
427
  },
591
428
  "success": {
592
- "use": "a terminal good state: Active, Paid, Passed"
593
- },
594
- "warning": {
595
- "use": "needs attention but is not broken: Pending, Expiring"
596
- },
597
- "error": {
598
- "use": "a failed or blocked state: Failed, Overdue, Rejected"
429
+ "use": "an operation completed",
430
+ "aria": "role=status"
599
431
  },
600
432
  "information": {
601
- "use": "a neutral fact worth marking: Draft, Scheduled"
433
+ "use": "neutral context the user did not ask for",
434
+ "aria": "role=status"
435
+ },
436
+ "primary": {
437
+ "use": "product announcements and promotions",
438
+ "aria": "role=status"
439
+ },
440
+ "neutral": {
441
+ "use": "low-emphasis context",
442
+ "aria": "role=status"
443
+ }
444
+ },
445
+ "emphasis": {
446
+ "subtle": {
447
+ "use": "the default — tinted surface, readable in long pages"
448
+ },
449
+ "solid": {
450
+ "use": "reserve for a single high-stakes banner; it dominates the layout"
451
+ }
452
+ },
453
+ "layout": {
454
+ "inline": {
455
+ "use": "inside a form or card, in the flow of content"
456
+ },
457
+ "banner": {
458
+ "use": "full-bleed at the top of a page or region"
602
459
  }
603
- }
604
- },
605
- "slots": {
606
- "icon": {
607
- "accepts": "Icon",
608
- "note": "mutually exclusive with `dot` — `dot` wins and `icon` is dropped when both are set"
609
460
  }
610
461
  },
611
462
  "a11y": {
612
463
  "guarantees": [
613
- "the dot marker is `aria-hidden`, so the badge announces its text only",
614
- "no `status` or `alert` role, so a re-render never interrupts a screen reader"
464
+ "the role follows the intent — error and warning are role=alert, everything else role=status",
465
+ "the icon is aria-hidden, so the live region announces the text once and not the glyph"
466
+ ],
467
+ "requires": [
468
+ "`dismissLabel` when `onDismiss` is set and the surrounding copy does not name the action"
615
469
  ],
616
470
  "notes": [
617
- "If the value genuinely changes and the change matters, wrap the Badge in your own live region — the component will not do it for you."
471
+ "role=alert interrupts whatever the screen reader is saying; role=status waits for a pause. This is why the role is not a prop."
618
472
  ]
619
473
  },
620
474
  "antiPatterns": [
621
475
  {
622
- "dont": "relying on intent colour alone to carry the meaning",
623
- "why": "colour is not a label (WCAG 1.4.1); the text has to say Failed, not just be red"
476
+ "dont": "using intent=\"error\" for a message that is merely informational",
477
+ "why": "error renders role=alert, which interrupts the screen reader. A page of interrupting status messages talks over the user."
624
478
  },
625
479
  {
626
- "dont": "putting a Button or Link inside a Badge",
627
- "why": "it renders a plain <span>; interactive content there has no affordance and no focus style"
480
+ "dont": "using intent=\"information\" for a genuine failure",
481
+ "why": "role=status waits for a pause, so a real error can be missed entirely"
482
+ },
483
+ {
484
+ "dont": "rendering an Alert conditionally for a transient confirmation",
485
+ "do": "useToast",
486
+ "why": "an Alert that appears and disappears shifts the layout under the user"
628
487
  }
629
488
  ],
630
- "stylesheet": "src/styles/badge.css",
489
+ "stylesheet": "src/styles/alert.css",
631
490
  "tokens": [
632
491
  "--border-default",
492
+ "--border-error-strong",
633
493
  "--border-error-subtle",
494
+ "--border-information-strong",
634
495
  "--border-information-subtle",
496
+ "--border-inverse",
497
+ "--border-primary-strong",
635
498
  "--border-primary-subtle",
499
+ "--border-success-strong",
636
500
  "--border-success-subtle",
501
+ "--border-warning-strong",
637
502
  "--border-warning-subtle",
638
503
  "--border-width-default",
504
+ "--border-width-thick",
639
505
  "--font-family-sans",
640
506
  "--font-weight-medium",
507
+ "--font-weight-regular",
641
508
  "--icon-error",
642
509
  "--icon-information",
510
+ "--icon-on-color",
643
511
  "--icon-primary",
644
512
  "--icon-secondary",
513
+ "--icon-size-md",
645
514
  "--icon-success",
646
515
  "--icon-warning",
647
- "--radius-full",
648
- "--spacing-2",
649
- "--spacing-4",
650
- "--spacing-6",
516
+ "--ion-alert-border",
517
+ "--ion-alert-icon",
518
+ "--ion-alert-surface",
519
+ "--ion-alert-text",
520
+ "--ion-duration-base",
521
+ "--ion-ease-out",
522
+ "--radius-md",
523
+ "--radius-sm",
524
+ "--ring-focus",
525
+ "--spacing-12",
526
+ "--spacing-16",
527
+ "--spacing-32",
651
528
  "--spacing-8",
529
+ "--surface-error",
652
530
  "--surface-error-subtle",
531
+ "--surface-hover",
532
+ "--surface-information",
653
533
  "--surface-information-subtle",
534
+ "--surface-inverse",
654
535
  "--surface-muted",
536
+ "--surface-primary",
655
537
  "--surface-primary-subtle",
538
+ "--surface-success",
656
539
  "--surface-success-subtle",
540
+ "--surface-warning",
657
541
  "--surface-warning-subtle",
658
542
  "--text-error",
659
543
  "--text-information",
660
544
  "--text-link",
545
+ "--text-on-color",
661
546
  "--text-secondary",
662
547
  "--text-success",
663
548
  "--text-warning",
664
- "--type-caption",
665
- "--type-caption-line-height"
549
+ "--type-body",
550
+ "--type-body-line-height"
666
551
  ],
667
552
  "props": {
668
553
  "intent": {
669
- "type": "BadgeIntent | undefined",
554
+ "type": "AlertIntent | undefined",
670
555
  "required": false,
671
556
  "origin": "own",
672
- "description": "Matches the Figma `Intent` variant.",
557
+ "description": "Matches the Figma `Intent` variant. Same vocabulary as `Badge`.",
673
558
  "values": [
674
559
  "neutral",
675
560
  "primary",
@@ -677,743 +562,1895 @@
677
562
  "warning",
678
563
  "error",
679
564
  "information"
680
- ]
565
+ ],
566
+ "default": "information"
681
567
  },
682
- "dot": {
568
+ "emphasis": {
569
+ "type": "AlertEmphasis | undefined",
570
+ "required": false,
571
+ "origin": "own",
572
+ "description": "Matches the Figma `Emphasis` variant.",
573
+ "values": [
574
+ "subtle",
575
+ "solid"
576
+ ],
577
+ "default": "subtle"
578
+ },
579
+ "layout": {
580
+ "type": "AlertLayout | undefined",
581
+ "required": false,
582
+ "origin": "own",
583
+ "description": "Matches the Figma `Layout` variant.",
584
+ "values": [
585
+ "inline",
586
+ "banner"
587
+ ],
588
+ "default": "inline"
589
+ },
590
+ "title": {
591
+ "type": "React.ReactNode",
592
+ "required": false,
593
+ "origin": "own",
594
+ "description": "Optional heading above the message."
595
+ },
596
+ "hideIcon": {
683
597
  "type": "boolean | undefined",
684
598
  "required": false,
685
599
  "origin": "own",
686
- "description": "Show the leading dot marker. Figma's `Show Dot` boolean.\nThe dot inherits the intent's foreground colour, so it needs no prop."
600
+ "description": "Hide the leading icon. Shown by default."
687
601
  },
688
602
  "icon": {
689
603
  "type": "React.ReactNode",
690
604
  "required": false,
691
605
  "origin": "own",
692
- "description": "Leading icon. Figma's `Show Icon` + `Icon` swap. Mutually exclusive with `dot`."
606
+ "description": "Replace the intent's glyph."
607
+ },
608
+ "actions": {
609
+ "type": "React.ReactNode",
610
+ "required": false,
611
+ "origin": "own",
612
+ "description": "Action buttons, rendered under the message."
613
+ },
614
+ "onDismiss": {
615
+ "type": "(() => void) | undefined",
616
+ "required": false,
617
+ "origin": "own",
618
+ "description": "Called when the dismiss button is pressed. Omit for no dismiss button."
619
+ },
620
+ "dismissLabel": {
621
+ "type": "string | undefined",
622
+ "required": false,
623
+ "origin": "own",
624
+ "description": "Accessible name for the dismiss button.",
625
+ "default": "Dismiss"
693
626
  },
694
627
  "children": {
695
628
  "type": "React.ReactNode",
696
629
  "required": false,
697
- "origin": "own"
630
+ "origin": "own",
631
+ "description": "The message."
698
632
  }
699
633
  },
700
634
  "propCounts": {
701
- "own": 4,
635
+ "own": 10,
702
636
  "aria": 0,
703
- "dom": 277,
637
+ "dom": 276,
704
638
  "other": 0
705
639
  }
706
640
  },
707
- "Button": {
708
- "name": "Button",
709
- "source": "src/components/Button.tsx",
710
- "propsType": "ButtonProps",
711
- "import": "import { Button } from 'ionbase-ui';",
641
+ "ApprovalGate": {
642
+ "name": "ApprovalGate",
643
+ "source": "src/components/ApprovalGate.tsx",
644
+ "propsType": "ApprovalGateProps",
645
+ "description": "ApprovalGate — a proposed action, held until a person decides.\n\nThe human-in-the-loop control. An agent proposes; this is where a person\napproves, rejects, or amends before anything happens. In regulated contexts\ndemonstrable human oversight is a compliance surface rather than a nicety,\nwhich is why this is a first-class component and not a Modal with two\nbuttons in it.\n\nIT ENFORCES NOTHING, AND SAYING SO IS THE POINT\n\nThis renders a decision. It does not gate execution — the caller does, by not\nacting until `onApprove` fires. A component that *looked* like it enforced a\npolicy would be the worst possible thing to ship here: teams would rely on a\nguarantee that lives entirely in their own call site. `risk` changes emphasis\nand nothing else, for the same reason.\n\nNOT A MODAL, DELIBERATELY\n\nA modal steals focus and hides the page. But the page is the evidence: the\nuser needs the plan, the diff, the tool call and the context in front of them\nwhile deciding. This sits inline, keeps everything visible, and never\ntraps focus — an approval a user was rushed through is not oversight.\n\nNEITHER BUTTON IS FOCUSED ON MOUNT. Autofocusing approve turns a decision\ninto an Enter keypress on a page the user has not read. Focus is moved to the\nregion's heading instead, so a screen-reader user lands on what is being\nasked rather than on the answer.\n\nTHE RESOLVED STATES ARE NOT DECORATION. `approved`, `rejected` and `expired`\nreplace the actions with what happened, so the record of the decision stays\non the page. `expired` exists because an approval request that nobody answers\nis the common real outcome, and a gate that sits pending for ever is\nindistinguishable from one that is broken.",
646
+ "import": "import { ApprovalGate } from 'ionbase-ui';",
712
647
  "status": "stable",
713
- "summary": "Triggers an action in place. Never navigates.",
648
+ "summary": "A proposed action held until a person approves, rejects or amends it. It renders the decision; it does not enforce it.",
714
649
  "useWhen": [
715
- "the interaction performs an action — submit, save, delete, open a dialog",
716
- "the result stays on the current page"
650
+ "an agent proposes something with consequences — spending, sending, deleting, deploying, changing permissions",
651
+ "a regulated or audited process needs demonstrable human oversight at a specific point",
652
+ "the proposal is worth showing in full, in context, before it runs"
717
653
  ],
718
654
  "useInstead": [
719
655
  {
720
- "when": "it navigates to a URL or changes route",
721
- "use": "Link",
722
- "why": "Link renders an <a>. Middle-click, cmd-click, copy-link-address and 'open in new tab' all work on an anchor and none of them work on a button."
656
+ "when": "the action is already happening and needs stopping",
657
+ "use": "AgentStop"
723
658
  },
724
659
  {
725
- "when": "it toggles a persistent boolean setting",
726
- "use": "Toggle",
727
- "why": "a toggle announces its on/off state; a button announces only its name"
660
+ "when": "a person initiated the action themselves and only needs to confirm it",
661
+ "use": "Modal",
662
+ "why": "confirming your own request is a dialog; reviewing something proposed for you is a decision"
663
+ },
664
+ {
665
+ "when": "you are reporting what happened rather than asking",
666
+ "use": "Alert"
728
667
  }
729
668
  ],
730
669
  "variants": {
731
- "variant": {
732
- "primary-brand": {
733
- "use": "the single most important action in a view",
734
- "limit": "one per view or dialog"
735
- },
736
- "primary-neutral": {
737
- "use": "a primary action that should not carry brand colour"
738
- },
739
- "primary-soft": {
740
- "use": "a primary action on a dense or already-colourful surface"
741
- },
742
- "secondary": {
743
- "use": "supporting actions shown alongside a primary one"
670
+ "risk": {
671
+ "low": {
672
+ "use": "reversible and cheap — a draft, a local edit. Neutral surface"
744
673
  },
745
- "tertiary": {
746
- "use": "low-emphasis actions — Cancel, Back, inline row actions"
747
- },
748
- "destructive": {
749
- "use": "irreversible actions",
750
- "requires": "a Modal confirmation before the action runs"
674
+ "medium": {
675
+ "use": "the default. Real but recoverable consequences"
751
676
  },
752
- "success": {
753
- "use": "confirms the end of a flow. Rarely the right answer — prefer primary-brand."
677
+ "high": {
678
+ "use": "spending, sending externally, deleting, granting access. Warning surface, deliberately not error"
754
679
  }
755
680
  },
756
- "size": {
757
- "sm": {
758
- "use": "table rows, toolbars, dense layouts"
681
+ "status": {
682
+ "pending": {
683
+ "use": "the default, and the only state with actions"
759
684
  },
760
- "md": {
761
- "use": "the default; forms and page-level actions"
685
+ "approved": {
686
+ "use": "resolved — the actions are replaced by the record"
762
687
  },
763
- "lg": {
764
- "use": "prominent standalone actions"
688
+ "rejected": {
689
+ "use": "resolved, declined"
765
690
  },
766
- "xl": {
767
- "use": "marketing and empty-state calls to action"
691
+ "expired": {
692
+ "use": "nobody answered. The common real outcome, and the reason a gate cannot sit pending for ever"
768
693
  }
769
694
  }
770
695
  },
771
696
  "slots": {
772
- "startIcon": {
773
- "accepts": "Icon",
774
- "note": "omit Icon's `label` — the Button's own text already names it, and a labelled icon is announced twice"
697
+ "title": {
698
+ "accepts": "text",
699
+ "note": "required, and the region's accessible name. Name the action and its object — \"Delete 14 projects\", never \"Confirm action\""
775
700
  },
776
- "endIcon": {
777
- "accepts": "Icon",
778
- "note": "same as startIcon"
701
+ "children": {
702
+ "accepts": "any",
703
+ "note": "the evidence: the plan, the diff, the tool call. This is what the person is deciding about"
704
+ },
705
+ "resolution": {
706
+ "accepts": "text",
707
+ "note": "shown in place of the actions once resolved; defaults to the status"
779
708
  }
780
709
  },
781
710
  "a11y": {
782
- "role": "button",
711
+ "role": "region, named by its title",
783
712
  "guarantees": [
784
- "the focus ring appears for keyboard focus and not for a mouse click",
785
- "hover does not latch on touch devices"
713
+ "neither button is focused on mount — autofocusing approve turns a decision into an Enter keypress on an unread page",
714
+ "the title is programmatically focusable, so the caller can move focus to the question rather than to an answer",
715
+ "reject precedes approve in the DOM, so a keyboard reaches the safe answer first",
716
+ "resolution is announced once through a polite live region",
717
+ "focus is never trapped: an approval a user was rushed through is not oversight"
786
718
  ],
787
719
  "requires": [
788
- "an accessible name — `children`, or `aria-label` when the button is icon-only"
720
+ "not acting until `onApprove` fires — this component gates nothing on its own",
721
+ "a `title` that names the action and its object"
722
+ ],
723
+ "notes": [
724
+ "`risk` changes emphasis and nothing else. It does not add confirmation, does not disable anything, and must never be read as a policy the component enforces."
789
725
  ]
790
726
  },
791
727
  "antiPatterns": [
792
728
  {
793
- "dont": "<Button onPress={() => router.push('/settings')}>Settings</Button>",
794
- "do": "<Link href=\"/settings\">Settings</Link>",
795
- "why": "a button that navigates breaks middle-click, cmd-click and copy-link, and is announced as a button rather than a link"
729
+ "dont": "treating the gate as the enforcement point",
730
+ "why": "it renders a decision. If your code acts before `onApprove`, there is no gate — only a picture of one"
796
731
  },
797
732
  {
798
- "dont": "two variant=\"primary-brand\" buttons in the same dialog",
799
- "do": "one primary-brand, the rest secondary or tertiary",
800
- "why": "two primaries is the same as none — nothing indicates the expected action"
733
+ "dont": "putting it in a Modal",
734
+ "why": "the page is the evidence; hiding it to ask about it removes what the decision needs"
801
735
  },
802
736
  {
803
- "dont": "<Button variant=\"destructive\" onPress={deleteAccount}>",
804
- "do": "open a Modal first and put the destructive Button in its footer",
805
- "why": "irreversible actions need a confirmation step"
737
+ "dont": "autofocusing the approve button",
738
+ "why": "Enter on an unread page is not oversight, and it is exactly what an audit will look for"
806
739
  },
807
740
  {
808
- "dont": "<Button><Icon as={Trash} /></Button>",
809
- "do": "<Button aria-label=\"Delete\"><Icon as={Trash} /></Button>",
810
- "why": "an icon-only button with no aria-label is announced as just 'button'"
741
+ "dont": "an approve-only gate with no reject",
742
+ "why": "a decision with one answer is a notification wearing a button"
811
743
  },
812
744
  {
813
- "dont": "<Button onClick={...}>",
814
- "do": "<Button onPress={...}>",
815
- "why": "onPress is React Aria's unified pointer/keyboard/touch handler. onClick works but skips the touch and keyboard normalisation the rest of the system relies on."
745
+ "dont": "`onEdit` when the proposal cannot actually be amended",
746
+ "why": "an edit button that discards the edit is worse than no edit button"
747
+ },
748
+ {
749
+ "dont": "leaving gates pending indefinitely",
750
+ "do": "the `expired` status",
751
+ "why": "a gate nobody answered is indistinguishable from one that is broken"
816
752
  }
817
753
  ],
818
- "deprecated": [
819
- {
820
- "prop": "disabled",
821
- "replacement": "isDisabled",
822
- "note": "still type-checks, so this will not fail loudly"
823
- }
824
- ],
825
- "stylesheet": "src/styles/button.css",
754
+ "stylesheet": "src/styles/approval-gate.css",
826
755
  "tokens": [
756
+ "--border-default",
827
757
  "--border-disabled",
828
- "--border-error-strong",
829
- "--border-inverse",
830
- "--border-primary",
831
- "--border-primary-strong",
832
- "--border-primary-subtle",
758
+ "--border-focus",
759
+ "--border-information",
833
760
  "--border-strong",
834
- "--border-stronger",
835
- "--border-success-strong",
761
+ "--border-success",
762
+ "--border-warning-strong",
836
763
  "--border-width-default",
764
+ "--border-width-thick",
765
+ "--border-width-thicker",
837
766
  "--font-family-sans",
838
767
  "--font-weight-medium",
839
- "--icon-size-lg",
768
+ "--font-weight-semibold",
769
+ "--icon-information",
770
+ "--icon-secondary",
840
771
  "--icon-size-md",
841
- "--icon-size-xs",
842
- "--ion-button-elevation",
843
- "--ion-button-font-size",
844
- "--ion-button-gap",
845
- "--ion-button-icon-size",
846
- "--ion-button-inset-flush",
847
- "--ion-button-inset-lifted",
848
- "--ion-button-line-height",
849
- "--ion-button-padding-x",
850
- "--ion-button-pressed",
851
- "--ion-button-raised",
852
- "--ion-button-ring",
853
- "--ion-button-size",
772
+ "--icon-success",
773
+ "--icon-tertiary",
774
+ "--icon-warning",
775
+ "--ion-approval-accent",
776
+ "--ion-approval-border",
777
+ "--ion-approval-icon",
778
+ "--ion-approval-surface",
854
779
  "--ion-duration-base",
855
- "--ion-duration-fast",
856
780
  "--ion-ease-out",
857
- "--ion-shadow-focus-md",
858
- "--ion-shadow-inset-flush-lg",
859
- "--ion-shadow-inset-flush-sm",
860
- "--ion-shadow-inset-flush-xs",
861
- "--ion-shadow-inset-lifted-lg",
862
- "--ion-shadow-inset-lifted-sm",
863
- "--ion-shadow-inset-lifted-xs",
864
- "--ion-shadow-none",
865
- "--ion-shadow-raised-lifted-lg",
866
- "--ion-shadow-raised-lifted-sm",
867
- "--ion-shadow-raised-lifted-xs",
868
781
  "--radius-md",
782
+ "--radius-sm",
783
+ "--radius-xs",
869
784
  "--spacing-12",
870
785
  "--spacing-16",
871
- "--spacing-20",
872
- "--spacing-24",
873
786
  "--spacing-32",
874
- "--spacing-40",
875
- "--spacing-48",
876
- "--spacing-56",
877
- "--spacing-6",
787
+ "--spacing-4",
878
788
  "--spacing-8",
879
789
  "--surface-default",
880
790
  "--surface-disabled",
881
- "--surface-error",
882
- "--surface-error-hover",
883
- "--surface-error-pressed",
884
- "--surface-inverse",
885
- "--surface-inverse-subtle",
791
+ "--surface-hover",
792
+ "--surface-information-subtle",
886
793
  "--surface-muted",
887
- "--surface-page",
888
794
  "--surface-primary",
889
795
  "--surface-primary-hover",
890
- "--surface-primary-pressed",
891
- "--surface-primary-subtle",
892
- "--surface-primary-subtle-hover",
893
- "--surface-primary-tint",
894
- "--surface-success",
895
- "--surface-success-hover",
896
- "--surface-success-pressed",
796
+ "--surface-warning-subtle",
797
+ "--text-default",
897
798
  "--text-disabled",
799
+ "--text-information",
898
800
  "--text-on-color",
899
- "--text-primary",
900
801
  "--text-secondary",
802
+ "--text-success",
803
+ "--text-warning",
901
804
  "--type-body",
902
- "--type-body-lg",
903
- "--type-body-lg-line-height",
904
805
  "--type-body-line-height",
905
- "--type-body-md",
906
- "--type-body-md-line-height",
907
806
  "--type-body-sm",
908
807
  "--type-body-sm-line-height"
909
808
  ],
910
809
  "props": {
911
- "variant": {
912
- "type": "\"success\" | \"primary-brand\" | \"primary-neutral\" | \"primary-soft\" | \"secondary\" | \"tertiary\" | \"destructive\" | undefined",
913
- "required": false,
914
- "origin": "own",
915
- "description": "The visual style variant of the button.",
916
- "values": [
917
- "success",
918
- "primary-brand",
919
- "primary-neutral",
920
- "primary-soft",
921
- "secondary",
922
- "tertiary",
923
- "destructive"
924
- ],
925
- "default": "primary-brand"
926
- },
927
- "size": {
928
- "type": "\"sm\" | \"md\" | \"lg\" | \"xl\" | undefined",
929
- "required": false,
930
- "origin": "own",
931
- "description": "The size of the button.",
932
- "values": [
933
- "sm",
934
- "md",
935
- "lg",
936
- "xl"
937
- ],
938
- "default": "md"
939
- },
940
- "startIcon": {
941
- "type": "React.ReactNode",
942
- "required": false,
943
- "origin": "own",
944
- "description": "Optional icon to render before the label."
945
- },
946
- "endIcon": {
810
+ "title": {
947
811
  "type": "React.ReactNode",
948
- "required": false,
949
- "origin": "own",
950
- "description": "Optional icon to render after the label."
951
- },
952
- "className": {
953
- "type": "string | undefined",
954
- "required": false,
812
+ "required": true,
955
813
  "origin": "own",
956
- "description": "Additional CSS class names."
814
+ "description": "What is being approved, stated as the action and its object — \"Delete 14\nprojects\", not \"Confirm action\". Required: it is the region's accessible\nname, and a decision the user cannot name is not a decision."
957
815
  },
958
816
  "children": {
959
817
  "type": "React.ReactNode",
960
818
  "required": false,
961
819
  "origin": "own",
962
- "description": "Children element to render inside the button."
820
+ "description": "Why it stopped here, and what happens on approval."
963
821
  },
964
- "disabled": {
965
- "type": "boolean | undefined",
822
+ "risk": {
823
+ "type": "ApprovalGateRisk | undefined",
966
824
  "required": false,
967
825
  "origin": "own",
968
- "tags": {
969
- "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
970
- }
971
- },
972
- "isDisabled": {
973
- "type": "boolean | undefined",
974
- "required": false,
975
- "origin": "aria",
976
- "description": "Whether the button is disabled."
977
- },
978
- "onPress": {
979
- "type": "((e: PressEvent) => void) | undefined",
980
- "required": false,
981
- "origin": "aria",
982
- "description": "Handler that is called when the press is released over the target."
983
- },
984
- "onPressStart": {
985
- "type": "((e: PressEvent) => void) | undefined",
986
- "required": false,
987
- "origin": "aria",
988
- "description": "Handler that is called when a press interaction starts."
989
- },
990
- "onPressEnd": {
991
- "type": "((e: PressEvent) => void) | undefined",
992
- "required": false,
993
- "origin": "aria",
994
- "description": "Handler that is called when a press interaction ends, either\nover the target or when the pointer leaves the target."
995
- },
996
- "onPressChange": {
997
- "type": "((isPressed: boolean) => void) | undefined",
998
- "required": false,
999
- "origin": "aria",
1000
- "description": "Handler that is called when the press state changes."
1001
- },
1002
- "onPressUp": {
1003
- "type": "((e: PressEvent) => void) | undefined",
1004
- "required": false,
1005
- "origin": "aria",
1006
- "description": "Handler that is called when a press is released over the target, regardless of\nwhether it started on the target or not."
1007
- },
1008
- "onClick": {
1009
- "type": "((e: MouseEvent<FocusableElement>) => void) | undefined",
1010
- "required": false,
1011
- "origin": "aria",
1012
- "description": "**Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress`\nprovided for compatibility with other libraries. `onPress` provides\nadditional event details for non-mouse interactions."
1013
- },
1014
- "autoFocus": {
1015
- "type": "boolean | undefined",
1016
- "required": false,
1017
- "origin": "aria",
1018
- "description": "Whether the element should receive focus on render."
1019
- },
1020
- "onFocus": {
1021
- "type": "((e: FocusEvent<Element, Element>) => void) | undefined",
1022
- "required": false,
1023
- "origin": "aria",
1024
- "description": "Handler that is called when the element receives focus."
826
+ "description": "How much is at stake. Drives emphasis only — it does NOT change what the\ncomponent enforces, because the component enforces nothing.",
827
+ "values": [
828
+ "low",
829
+ "medium",
830
+ "high"
831
+ ]
1025
832
  },
1026
- "onBlur": {
1027
- "type": "((e: FocusEvent<Element, Element>) => void) | undefined",
833
+ "status": {
834
+ "type": "ApprovalGateStatus | undefined",
1028
835
  "required": false,
1029
- "origin": "aria",
1030
- "description": "Handler that is called when the element loses focus."
836
+ "origin": "own",
837
+ "description": "Where the decision has got to. `pending` is the only state with buttons.",
838
+ "values": [
839
+ "pending",
840
+ "approved",
841
+ "rejected",
842
+ "expired"
843
+ ]
1031
844
  },
1032
- "onFocusChange": {
1033
- "type": "((isFocused: boolean) => void) | undefined",
845
+ "onApprove": {
846
+ "type": "(() => void) | undefined",
1034
847
  "required": false,
1035
- "origin": "aria",
1036
- "description": "Handler that is called when the element's focus status changes."
848
+ "origin": "own"
1037
849
  },
1038
- "onKeyDown": {
1039
- "type": "((e: KeyboardEvent) => void) | undefined",
850
+ "onReject": {
851
+ "type": "(() => void) | undefined",
1040
852
  "required": false,
1041
- "origin": "aria",
1042
- "description": "Handler that is called when a key is pressed."
853
+ "origin": "own"
1043
854
  },
1044
- "onKeyUp": {
1045
- "type": "((e: KeyboardEvent) => void) | undefined",
855
+ "onEdit": {
856
+ "type": "(() => void) | undefined",
1046
857
  "required": false,
1047
- "origin": "aria",
1048
- "description": "Handler that is called when a key is released."
858
+ "origin": "own",
859
+ "description": "Offer \"edit\" as a third way out. Omit it when the proposal cannot be\namended — an approve/reject pair the user cannot influence is honest;\nan edit button that discards their edit is not."
1049
860
  },
1050
- "href": {
861
+ "approveLabel": {
1051
862
  "type": "string | undefined",
1052
863
  "required": false,
1053
- "origin": "aria",
1054
- "description": "A URL to link to if elementType=\"a\"."
864
+ "origin": "own"
1055
865
  },
1056
- "target": {
866
+ "rejectLabel": {
1057
867
  "type": "string | undefined",
1058
868
  "required": false,
1059
- "origin": "aria",
1060
- "description": "The target window for the link."
869
+ "origin": "own"
1061
870
  },
1062
- "rel": {
871
+ "editLabel": {
1063
872
  "type": "string | undefined",
1064
873
  "required": false,
1065
- "origin": "aria",
1066
- "description": "The relationship between the linked resource and the current page. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel)."
1067
- },
1068
- "elementType": {
1069
- "type": "JSXElementConstructor<any> | \"button\" | undefined",
1070
- "required": false,
1071
- "origin": "aria",
1072
- "description": "The HTML element or React element used to render the button, e.g. 'div', 'a', or `RouterLink`.",
1073
- "tags": {
1074
- "default": "'button'"
1075
- }
1076
- },
1077
- "aria-disabled": {
1078
- "type": "boolean | \"true\" | \"false\" | undefined",
1079
- "required": false,
1080
- "origin": "aria",
1081
- "description": "Indicates whether the element is disabled to users of assistive technology."
1082
- },
1083
- "aria-expanded": {
1084
- "type": "boolean | \"true\" | \"false\" | undefined",
1085
- "required": false,
1086
- "origin": "aria",
1087
- "description": "Indicates whether the element, or another grouping element it controls, is currently expanded\nor collapsed."
874
+ "origin": "own"
1088
875
  },
1089
- "aria-haspopup": {
1090
- "type": "boolean | \"true\" | \"false\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\" | undefined",
876
+ "isSubmitting": {
877
+ "type": "boolean | undefined",
1091
878
  "required": false,
1092
- "origin": "aria",
1093
- "description": "Indicates the availability and type of interactive popup element, such as menu or dialog, that\ncan be triggered by an element."
879
+ "origin": "own",
880
+ "description": "A decision is in flight. Both actions disable; neither disappears."
1094
881
  },
1095
- "aria-controls": {
1096
- "type": "string | undefined",
882
+ "resolution": {
883
+ "type": "React.ReactNode",
884
+ "required": false,
885
+ "origin": "own",
886
+ "description": "Shown in place of the actions once the decision is made."
887
+ }
888
+ },
889
+ "propCounts": {
890
+ "own": 12,
891
+ "aria": 0,
892
+ "dom": 275,
893
+ "other": 0
894
+ }
895
+ },
896
+ "Avatar": {
897
+ "name": "Avatar",
898
+ "source": "src/components/Avatar.tsx",
899
+ "propsType": "AvatarProps",
900
+ "description": "Avatar picks its content the way Figma's `Type` variant does, but by\nprecedence rather than by a prop: image, then initials, then icon. A caller\npassing `src` and `initials` gets the image with the initials as its alt\nfallback, which is what you want when the image 404s.\n\nA failed `src` falls through to initials (then icon) rather than leaving the\nbrowser's broken-image glyph — that is why initials exist alongside `src`.\n\nIt is a `<span>`, not a `<div>`, so it can sit inline beside text without the\ncaller fighting a block element.",
901
+ "import": "import { Avatar } from 'ionbase-ui';",
902
+ "status": "stable",
903
+ "summary": "A person or entity marker. Content is chosen by precedence — image, then initials, then icon — not by a type prop.",
904
+ "useWhen": [
905
+ "identifying who owns, authored or is assigned to something",
906
+ "a table cell, menu row or header needs a compact identity marker beside a name"
907
+ ],
908
+ "useInstead": [
909
+ {
910
+ "when": "the thing being marked is a status, count or category rather than a person",
911
+ "use": "Badge"
912
+ },
913
+ {
914
+ "when": "you only need a decorative graphic with no identity behind it",
915
+ "use": "Icon"
916
+ }
917
+ ],
918
+ "variants": {
919
+ "size": {
920
+ "mini": {
921
+ "use": "24px — inline beside body text, dense table rows"
922
+ },
923
+ "sm": {
924
+ "use": "32px — menu rows, compact lists"
925
+ },
926
+ "md": {
927
+ "use": "the default, 40px"
928
+ },
929
+ "lg": {
930
+ "use": "48px — profile headers and detail pages"
931
+ }
932
+ },
933
+ "shape": {
934
+ "circle": {
935
+ "use": "the default — people"
936
+ },
937
+ "square": {
938
+ "use": "organisations, projects, workspaces and other non-human entities"
939
+ }
940
+ }
941
+ },
942
+ "slots": {
943
+ "icon": {
944
+ "accepts": "Icon",
945
+ "note": "the last fallback — rendered only when there is neither `src` nor `initials`"
946
+ }
947
+ },
948
+ "a11y": {
949
+ "guarantees": [
950
+ "`alt` falls back to `initials`, so the avatar is never announced as an unlabelled image",
951
+ "a `src` that fails to load falls through to initials, then icon — never the browser's broken-image glyph"
952
+ ],
953
+ "requires": [
954
+ "`alt` describing the person, not the picture — \"Ada Lovelace\", not \"profile photo\""
955
+ ]
956
+ },
957
+ "antiPatterns": [
958
+ {
959
+ "dont": "passing `src` without `initials`",
960
+ "why": "a 404 leaves the fallback empty; initials are what make the failure invisible"
961
+ },
962
+ {
963
+ "dont": "using Avatar as a button",
964
+ "do": "put the Avatar inside a Button or Link",
965
+ "why": "Avatar renders a <span> with no role and takes no focus"
966
+ }
967
+ ],
968
+ "stylesheet": "src/styles/avatar.css",
969
+ "tokens": [
970
+ "--border-strong",
971
+ "--border-width-default",
972
+ "--font-family-sans",
973
+ "--font-weight-medium",
974
+ "--icon-size-2xs",
975
+ "--icon-size-lg",
976
+ "--icon-size-md",
977
+ "--icon-size-sm",
978
+ "--icon-tertiary",
979
+ "--ion-avatar-font-size",
980
+ "--ion-avatar-group-overlap",
981
+ "--ion-avatar-icon",
982
+ "--ion-avatar-line-height",
983
+ "--ion-avatar-radius",
984
+ "--ion-avatar-size",
985
+ "--radius-full",
986
+ "--radius-md",
987
+ "--radius-sm",
988
+ "--radius-xs",
989
+ "--spacing-24",
990
+ "--spacing-32",
991
+ "--spacing-40",
992
+ "--spacing-48",
993
+ "--surface-muted",
994
+ "--text-tertiary",
995
+ "--type-body",
996
+ "--type-body-line-height",
997
+ "--type-body-sm",
998
+ "--type-body-sm-line-height",
999
+ "--type-caption",
1000
+ "--type-caption-line-height",
1001
+ "--type-h5",
1002
+ "--type-h5-line-height"
1003
+ ],
1004
+ "props": {
1005
+ "size": {
1006
+ "type": "AvatarSize | undefined",
1007
+ "required": false,
1008
+ "origin": "own",
1009
+ "description": "Matches the Figma `Size` variant: Mini 24, Small 32, Medium 40, Large 48.",
1010
+ "values": [
1011
+ "sm",
1012
+ "md",
1013
+ "lg",
1014
+ "mini"
1015
+ ]
1016
+ },
1017
+ "shape": {
1018
+ "type": "AvatarShape | undefined",
1019
+ "required": false,
1020
+ "origin": "own",
1021
+ "description": "Matches the Figma `Shape` variant.",
1022
+ "values": [
1023
+ "circle",
1024
+ "square"
1025
+ ]
1026
+ },
1027
+ "src": {
1028
+ "type": "string | undefined",
1029
+ "required": false,
1030
+ "origin": "own",
1031
+ "description": "Image source. Figma's `Type=Image`."
1032
+ },
1033
+ "alt": {
1034
+ "type": "string | undefined",
1035
+ "required": false,
1036
+ "origin": "own",
1037
+ "description": "Describes the person, not the picture. Falls back to `initials` so the\navatar is never announced as an unlabelled image."
1038
+ },
1039
+ "initials": {
1040
+ "type": "string | undefined",
1041
+ "required": false,
1042
+ "origin": "own",
1043
+ "description": "Figma's `Type=Character`. Rendered when there is no `src`, or when `src`\nfails to load."
1044
+ },
1045
+ "icon": {
1046
+ "type": "React.ReactNode",
1047
+ "required": false,
1048
+ "origin": "own",
1049
+ "description": "Figma's `Type=Icon`. Rendered when there is neither `src` nor `initials`."
1050
+ }
1051
+ },
1052
+ "propCounts": {
1053
+ "own": 6,
1054
+ "aria": 0,
1055
+ "dom": 278,
1056
+ "other": 0
1057
+ }
1058
+ },
1059
+ "AvatarGroup": {
1060
+ "name": "AvatarGroup",
1061
+ "source": "src/components/Avatar.tsx",
1062
+ "propsType": "AvatarGroupProps",
1063
+ "description": "AvatarGroup overlaps its children by a quarter of the avatar size, matching\nFigma's -6 / -8 / -10 / -12 gaps at Mini / Small / Medium / Large.\n\nGroup defaults fill in `size` / `shape` only when a child Avatar has not set\nthem itself — an explicit child prop wins. Non-Avatar children are left\nalone so `size`/`shape` are never pushed onto arbitrary DOM nodes.\n\nChildren render in source order because that is Figma's stacking: each avatar\npaints over the one before it, so the `+N` overflow ends up on top. Later\nsiblings paint later, so this needs no z-index.",
1064
+ "import": "import { AvatarGroup } from 'ionbase-ui';",
1065
+ "status": "stable",
1066
+ "summary": "Overlapping Avatars with a `+N` overflow, for showing several people in one row's width.",
1067
+ "useWhen": [
1068
+ "several people share one thing — assignees, participants, collaborators",
1069
+ "the exact list matters less than the fact that there are several"
1070
+ ],
1071
+ "useInstead": [
1072
+ {
1073
+ "when": "each person needs their own name, role or action visible",
1074
+ "use": "Table",
1075
+ "why": "an overlapping stack hides everything but the picture"
1076
+ }
1077
+ ],
1078
+ "variants": {
1079
+ "size": {
1080
+ "mini": {
1081
+ "use": "24px — inline beside body text, dense table rows"
1082
+ },
1083
+ "sm": {
1084
+ "use": "32px — menu rows, compact lists"
1085
+ },
1086
+ "md": {
1087
+ "use": "the default, 40px"
1088
+ },
1089
+ "lg": {
1090
+ "use": "48px — profile headers and detail pages"
1091
+ }
1092
+ },
1093
+ "shape": {
1094
+ "circle": {
1095
+ "use": "the default — people"
1096
+ },
1097
+ "square": {
1098
+ "use": "organisations, projects, workspaces"
1099
+ }
1100
+ }
1101
+ },
1102
+ "slots": {
1103
+ "children": {
1104
+ "accepts": "Avatar",
1105
+ "note": "group `size`/`shape` fill in only where a child has not set its own; a child's explicit prop wins. Non-Avatar children are left untouched."
1106
+ }
1107
+ },
1108
+ "a11y": {
1109
+ "requires": [
1110
+ "`alt` on every child Avatar — the overflow avatar is the only one the group names for you"
1111
+ ],
1112
+ "notes": [
1113
+ "Children paint in source order, so the `+N` overflow lands on top without any z-index."
1114
+ ]
1115
+ },
1116
+ "antiPatterns": [
1117
+ {
1118
+ "dont": "omitting `max` on an unbounded list",
1119
+ "why": "twenty overlapping avatars is not a summary; it is a smear"
1120
+ }
1121
+ ],
1122
+ "stylesheet": "src/styles/avatar.css",
1123
+ "tokens": [
1124
+ "--border-strong",
1125
+ "--border-width-default",
1126
+ "--font-family-sans",
1127
+ "--font-weight-medium",
1128
+ "--icon-size-2xs",
1129
+ "--icon-size-lg",
1130
+ "--icon-size-md",
1131
+ "--icon-size-sm",
1132
+ "--icon-tertiary",
1133
+ "--ion-avatar-font-size",
1134
+ "--ion-avatar-group-overlap",
1135
+ "--ion-avatar-icon",
1136
+ "--ion-avatar-line-height",
1137
+ "--ion-avatar-radius",
1138
+ "--ion-avatar-size",
1139
+ "--radius-full",
1140
+ "--radius-md",
1141
+ "--radius-sm",
1142
+ "--radius-xs",
1143
+ "--spacing-24",
1144
+ "--spacing-32",
1145
+ "--spacing-40",
1146
+ "--spacing-48",
1147
+ "--surface-muted",
1148
+ "--text-tertiary",
1149
+ "--type-body",
1150
+ "--type-body-line-height",
1151
+ "--type-body-sm",
1152
+ "--type-body-sm-line-height",
1153
+ "--type-caption",
1154
+ "--type-caption-line-height",
1155
+ "--type-h5",
1156
+ "--type-h5-line-height"
1157
+ ],
1158
+ "props": {
1159
+ "size": {
1160
+ "type": "AvatarSize | undefined",
1161
+ "required": false,
1162
+ "origin": "own",
1163
+ "values": [
1164
+ "sm",
1165
+ "md",
1166
+ "lg",
1167
+ "mini"
1168
+ ]
1169
+ },
1170
+ "max": {
1171
+ "type": "number | undefined",
1172
+ "required": false,
1173
+ "origin": "own",
1174
+ "description": "Cap on avatars shown; the remainder becomes a `+N` overflow avatar."
1175
+ },
1176
+ "shape": {
1177
+ "type": "AvatarShape | undefined",
1178
+ "required": false,
1179
+ "origin": "own",
1180
+ "values": [
1181
+ "circle",
1182
+ "square"
1183
+ ]
1184
+ },
1185
+ "children": {
1186
+ "type": "React.ReactNode",
1187
+ "required": false,
1188
+ "origin": "own"
1189
+ }
1190
+ },
1191
+ "propCounts": {
1192
+ "own": 4,
1193
+ "aria": 0,
1194
+ "dom": 277,
1195
+ "other": 0
1196
+ }
1197
+ },
1198
+ "Badge": {
1199
+ "name": "Badge",
1200
+ "source": "src/components/Badge.tsx",
1201
+ "propsType": "BadgeProps",
1202
+ "description": "Badge is presentational — no role, no interaction. React Aria has nothing to\noffer here, so this is a plain span rather than a hook wrapper for its own\nsake.\n\nIt carries no `status` or `alert` role on purpose: a badge is a label on\nsomething else, and announcing it as a live region would interrupt screen\nreader users on every render. Wrap it yourself if the value genuinely changes\nand matters.",
1203
+ "import": "import { Badge } from 'ionbase-ui';",
1204
+ "status": "stable",
1205
+ "summary": "A small, non-interactive label that classifies the thing next to it. Presentational — no role, no live region.",
1206
+ "useWhen": [
1207
+ "labelling a record's status, type, count or category",
1208
+ "the label belongs to something else on the page and is read as part of it"
1209
+ ],
1210
+ "useInstead": [
1211
+ {
1212
+ "when": "the message is a standalone statement the user must read",
1213
+ "use": "Alert",
1214
+ "why": "Badge carries no role and is not announced on its own"
1215
+ },
1216
+ {
1217
+ "when": "the label should be clickable — a filter chip, a removable tag",
1218
+ "use": "Button",
1219
+ "why": "Badge takes no focus and fires nothing"
1220
+ }
1221
+ ],
1222
+ "variants": {
1223
+ "intent": {
1224
+ "neutral": {
1225
+ "use": "the default — a category with no judgement attached"
1226
+ },
1227
+ "primary": {
1228
+ "use": "brand emphasis: Beta, New, Pro"
1229
+ },
1230
+ "success": {
1231
+ "use": "a terminal good state: Active, Paid, Passed"
1232
+ },
1233
+ "warning": {
1234
+ "use": "needs attention but is not broken: Pending, Expiring"
1235
+ },
1236
+ "error": {
1237
+ "use": "a failed or blocked state: Failed, Overdue, Rejected"
1238
+ },
1239
+ "information": {
1240
+ "use": "a neutral fact worth marking: Draft, Scheduled"
1241
+ }
1242
+ }
1243
+ },
1244
+ "slots": {
1245
+ "icon": {
1246
+ "accepts": "Icon",
1247
+ "note": "mutually exclusive with `dot` — `dot` wins and `icon` is dropped when both are set"
1248
+ }
1249
+ },
1250
+ "a11y": {
1251
+ "guarantees": [
1252
+ "the dot marker is `aria-hidden`, so the badge announces its text only",
1253
+ "no `status` or `alert` role, so a re-render never interrupts a screen reader"
1254
+ ],
1255
+ "notes": [
1256
+ "If the value genuinely changes and the change matters, wrap the Badge in your own live region — the component will not do it for you."
1257
+ ]
1258
+ },
1259
+ "antiPatterns": [
1260
+ {
1261
+ "dont": "relying on intent colour alone to carry the meaning",
1262
+ "why": "colour is not a label (WCAG 1.4.1); the text has to say Failed, not just be red"
1263
+ },
1264
+ {
1265
+ "dont": "putting a Button or Link inside a Badge",
1266
+ "why": "it renders a plain <span>; interactive content there has no affordance and no focus style"
1267
+ }
1268
+ ],
1269
+ "stylesheet": "src/styles/badge.css",
1270
+ "tokens": [
1271
+ "--border-default",
1272
+ "--border-error-subtle",
1273
+ "--border-information-subtle",
1274
+ "--border-primary-subtle",
1275
+ "--border-success-subtle",
1276
+ "--border-warning-subtle",
1277
+ "--border-width-default",
1278
+ "--font-family-sans",
1279
+ "--font-weight-medium",
1280
+ "--icon-error",
1281
+ "--icon-information",
1282
+ "--icon-primary",
1283
+ "--icon-secondary",
1284
+ "--icon-success",
1285
+ "--icon-warning",
1286
+ "--radius-full",
1287
+ "--spacing-2",
1288
+ "--spacing-4",
1289
+ "--spacing-6",
1290
+ "--spacing-8",
1291
+ "--surface-error-subtle",
1292
+ "--surface-information-subtle",
1293
+ "--surface-muted",
1294
+ "--surface-primary-subtle",
1295
+ "--surface-success-subtle",
1296
+ "--surface-warning-subtle",
1297
+ "--text-error",
1298
+ "--text-information",
1299
+ "--text-link",
1300
+ "--text-secondary",
1301
+ "--text-success",
1302
+ "--text-warning",
1303
+ "--type-caption",
1304
+ "--type-caption-line-height"
1305
+ ],
1306
+ "props": {
1307
+ "intent": {
1308
+ "type": "BadgeIntent | undefined",
1309
+ "required": false,
1310
+ "origin": "own",
1311
+ "description": "Matches the Figma `Intent` variant.",
1312
+ "values": [
1313
+ "neutral",
1314
+ "primary",
1315
+ "success",
1316
+ "warning",
1317
+ "error",
1318
+ "information"
1319
+ ]
1320
+ },
1321
+ "dot": {
1322
+ "type": "boolean | undefined",
1323
+ "required": false,
1324
+ "origin": "own",
1325
+ "description": "Show the leading dot marker. Figma's `Show Dot` boolean.\nThe dot inherits the intent's foreground colour, so it needs no prop."
1326
+ },
1327
+ "icon": {
1328
+ "type": "React.ReactNode",
1329
+ "required": false,
1330
+ "origin": "own",
1331
+ "description": "Leading icon. Figma's `Show Icon` + `Icon` swap. Mutually exclusive with `dot`."
1332
+ },
1333
+ "children": {
1334
+ "type": "React.ReactNode",
1335
+ "required": false,
1336
+ "origin": "own"
1337
+ }
1338
+ },
1339
+ "propCounts": {
1340
+ "own": 4,
1341
+ "aria": 0,
1342
+ "dom": 277,
1343
+ "other": 0
1344
+ }
1345
+ },
1346
+ "Button": {
1347
+ "name": "Button",
1348
+ "source": "src/components/Button.tsx",
1349
+ "propsType": "ButtonProps",
1350
+ "import": "import { Button } from 'ionbase-ui';",
1351
+ "status": "stable",
1352
+ "summary": "Triggers an action in place. Never navigates.",
1353
+ "useWhen": [
1354
+ "the interaction performs an action — submit, save, delete, open a dialog",
1355
+ "the result stays on the current page"
1356
+ ],
1357
+ "useInstead": [
1358
+ {
1359
+ "when": "it navigates to a URL or changes route",
1360
+ "use": "Link",
1361
+ "why": "Link renders an <a>. Middle-click, cmd-click, copy-link-address and 'open in new tab' all work on an anchor and none of them work on a button."
1362
+ },
1363
+ {
1364
+ "when": "it toggles a persistent boolean setting",
1365
+ "use": "Toggle",
1366
+ "why": "a toggle announces its on/off state; a button announces only its name"
1367
+ }
1368
+ ],
1369
+ "variants": {
1370
+ "variant": {
1371
+ "primary-brand": {
1372
+ "use": "the single most important action in a view",
1373
+ "limit": "one per view or dialog"
1374
+ },
1375
+ "primary-neutral": {
1376
+ "use": "a primary action that should not carry brand colour"
1377
+ },
1378
+ "primary-soft": {
1379
+ "use": "a primary action on a dense or already-colourful surface"
1380
+ },
1381
+ "secondary": {
1382
+ "use": "supporting actions shown alongside a primary one"
1383
+ },
1384
+ "tertiary": {
1385
+ "use": "low-emphasis actions — Cancel, Back, inline row actions"
1386
+ },
1387
+ "destructive": {
1388
+ "use": "irreversible actions",
1389
+ "requires": "a Modal confirmation before the action runs"
1390
+ },
1391
+ "success": {
1392
+ "use": "confirms the end of a flow. Rarely the right answer — prefer primary-brand."
1393
+ }
1394
+ },
1395
+ "size": {
1396
+ "sm": {
1397
+ "use": "table rows, toolbars, dense layouts"
1398
+ },
1399
+ "md": {
1400
+ "use": "the default; forms and page-level actions"
1401
+ },
1402
+ "lg": {
1403
+ "use": "prominent standalone actions"
1404
+ },
1405
+ "xl": {
1406
+ "use": "marketing and empty-state calls to action"
1407
+ }
1408
+ }
1409
+ },
1410
+ "slots": {
1411
+ "startIcon": {
1412
+ "accepts": "Icon",
1413
+ "note": "omit Icon's `label` — the Button's own text already names it, and a labelled icon is announced twice"
1414
+ },
1415
+ "endIcon": {
1416
+ "accepts": "Icon",
1417
+ "note": "same as startIcon"
1418
+ }
1419
+ },
1420
+ "a11y": {
1421
+ "role": "button",
1422
+ "guarantees": [
1423
+ "the focus ring appears for keyboard focus and not for a mouse click",
1424
+ "hover does not latch on touch devices"
1425
+ ],
1426
+ "requires": [
1427
+ "an accessible name — `children`, or `aria-label` when the button is icon-only"
1428
+ ]
1429
+ },
1430
+ "antiPatterns": [
1431
+ {
1432
+ "dont": "<Button onPress={() => router.push('/settings')}>Settings</Button>",
1433
+ "do": "<Link href=\"/settings\">Settings</Link>",
1434
+ "why": "a button that navigates breaks middle-click, cmd-click and copy-link, and is announced as a button rather than a link"
1435
+ },
1436
+ {
1437
+ "dont": "two variant=\"primary-brand\" buttons in the same dialog",
1438
+ "do": "one primary-brand, the rest secondary or tertiary",
1439
+ "why": "two primaries is the same as none — nothing indicates the expected action"
1440
+ },
1441
+ {
1442
+ "dont": "<Button variant=\"destructive\" onPress={deleteAccount}>",
1443
+ "do": "open a Modal first and put the destructive Button in its footer",
1444
+ "why": "irreversible actions need a confirmation step"
1445
+ },
1446
+ {
1447
+ "dont": "<Button><Icon as={Trash} /></Button>",
1448
+ "do": "<Button aria-label=\"Delete\"><Icon as={Trash} /></Button>",
1449
+ "why": "an icon-only button with no aria-label is announced as just 'button'"
1450
+ },
1451
+ {
1452
+ "dont": "<Button onClick={...}>",
1453
+ "do": "<Button onPress={...}>",
1454
+ "why": "onPress is React Aria's unified pointer/keyboard/touch handler. onClick works but skips the touch and keyboard normalisation the rest of the system relies on."
1455
+ }
1456
+ ],
1457
+ "deprecated": [
1458
+ {
1459
+ "prop": "disabled",
1460
+ "replacement": "isDisabled",
1461
+ "note": "still type-checks, so this will not fail loudly"
1462
+ }
1463
+ ],
1464
+ "stylesheet": "src/styles/button.css",
1465
+ "tokens": [
1466
+ "--border-disabled",
1467
+ "--border-error-strong",
1468
+ "--border-inverse",
1469
+ "--border-primary",
1470
+ "--border-primary-strong",
1471
+ "--border-primary-subtle",
1472
+ "--border-strong",
1473
+ "--border-stronger",
1474
+ "--border-success-strong",
1475
+ "--border-width-default",
1476
+ "--font-family-sans",
1477
+ "--font-weight-medium",
1478
+ "--icon-size-lg",
1479
+ "--icon-size-md",
1480
+ "--icon-size-xs",
1481
+ "--ion-button-elevation",
1482
+ "--ion-button-font-size",
1483
+ "--ion-button-gap",
1484
+ "--ion-button-icon-size",
1485
+ "--ion-button-inset-flush",
1486
+ "--ion-button-inset-lifted",
1487
+ "--ion-button-line-height",
1488
+ "--ion-button-padding-x",
1489
+ "--ion-button-pressed",
1490
+ "--ion-button-raised",
1491
+ "--ion-button-ring",
1492
+ "--ion-button-size",
1493
+ "--ion-duration-base",
1494
+ "--ion-duration-fast",
1495
+ "--ion-ease-out",
1496
+ "--ion-shadow-focus-md",
1497
+ "--ion-shadow-inset-flush-lg",
1498
+ "--ion-shadow-inset-flush-sm",
1499
+ "--ion-shadow-inset-flush-xs",
1500
+ "--ion-shadow-inset-lifted-lg",
1501
+ "--ion-shadow-inset-lifted-sm",
1502
+ "--ion-shadow-inset-lifted-xs",
1503
+ "--ion-shadow-none",
1504
+ "--ion-shadow-raised-lifted-lg",
1505
+ "--ion-shadow-raised-lifted-sm",
1506
+ "--ion-shadow-raised-lifted-xs",
1507
+ "--radius-md",
1508
+ "--spacing-12",
1509
+ "--spacing-16",
1510
+ "--spacing-20",
1511
+ "--spacing-24",
1512
+ "--spacing-32",
1513
+ "--spacing-40",
1514
+ "--spacing-48",
1515
+ "--spacing-56",
1516
+ "--spacing-6",
1517
+ "--spacing-8",
1518
+ "--surface-default",
1519
+ "--surface-disabled",
1520
+ "--surface-error",
1521
+ "--surface-error-hover",
1522
+ "--surface-error-pressed",
1523
+ "--surface-inverse",
1524
+ "--surface-inverse-subtle",
1525
+ "--surface-muted",
1526
+ "--surface-page",
1527
+ "--surface-primary",
1528
+ "--surface-primary-hover",
1529
+ "--surface-primary-pressed",
1530
+ "--surface-primary-subtle",
1531
+ "--surface-primary-subtle-hover",
1532
+ "--surface-primary-tint",
1533
+ "--surface-success",
1534
+ "--surface-success-hover",
1535
+ "--surface-success-pressed",
1536
+ "--text-disabled",
1537
+ "--text-on-color",
1538
+ "--text-primary",
1539
+ "--text-secondary",
1540
+ "--type-body",
1541
+ "--type-body-lg",
1542
+ "--type-body-lg-line-height",
1543
+ "--type-body-line-height",
1544
+ "--type-body-md",
1545
+ "--type-body-md-line-height",
1546
+ "--type-body-sm",
1547
+ "--type-body-sm-line-height"
1548
+ ],
1549
+ "props": {
1550
+ "variant": {
1551
+ "type": "\"success\" | \"primary-brand\" | \"primary-neutral\" | \"primary-soft\" | \"secondary\" | \"tertiary\" | \"destructive\" | undefined",
1552
+ "required": false,
1553
+ "origin": "own",
1554
+ "description": "The visual style variant of the button.",
1555
+ "values": [
1556
+ "success",
1557
+ "primary-brand",
1558
+ "primary-neutral",
1559
+ "primary-soft",
1560
+ "secondary",
1561
+ "tertiary",
1562
+ "destructive"
1563
+ ],
1564
+ "default": "primary-brand"
1565
+ },
1566
+ "size": {
1567
+ "type": "\"sm\" | \"md\" | \"lg\" | \"xl\" | undefined",
1568
+ "required": false,
1569
+ "origin": "own",
1570
+ "description": "The size of the button.",
1571
+ "values": [
1572
+ "sm",
1573
+ "md",
1574
+ "lg",
1575
+ "xl"
1576
+ ],
1577
+ "default": "md"
1578
+ },
1579
+ "startIcon": {
1580
+ "type": "React.ReactNode",
1581
+ "required": false,
1582
+ "origin": "own",
1583
+ "description": "Optional icon to render before the label."
1584
+ },
1585
+ "endIcon": {
1586
+ "type": "React.ReactNode",
1587
+ "required": false,
1588
+ "origin": "own",
1589
+ "description": "Optional icon to render after the label."
1590
+ },
1591
+ "className": {
1592
+ "type": "string | undefined",
1593
+ "required": false,
1594
+ "origin": "own",
1595
+ "description": "Additional CSS class names."
1596
+ },
1597
+ "children": {
1598
+ "type": "React.ReactNode",
1599
+ "required": false,
1600
+ "origin": "own",
1601
+ "description": "Children element to render inside the button."
1602
+ },
1603
+ "disabled": {
1604
+ "type": "boolean | undefined",
1605
+ "required": false,
1606
+ "origin": "own",
1607
+ "tags": {
1608
+ "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
1609
+ }
1610
+ },
1611
+ "isDisabled": {
1612
+ "type": "boolean | undefined",
1613
+ "required": false,
1614
+ "origin": "aria",
1615
+ "description": "Whether the button is disabled."
1616
+ },
1617
+ "onPress": {
1618
+ "type": "((e: PressEvent) => void) | undefined",
1619
+ "required": false,
1620
+ "origin": "aria",
1621
+ "description": "Handler that is called when the press is released over the target."
1622
+ },
1623
+ "onPressStart": {
1624
+ "type": "((e: PressEvent) => void) | undefined",
1625
+ "required": false,
1626
+ "origin": "aria",
1627
+ "description": "Handler that is called when a press interaction starts."
1628
+ },
1629
+ "onPressEnd": {
1630
+ "type": "((e: PressEvent) => void) | undefined",
1631
+ "required": false,
1632
+ "origin": "aria",
1633
+ "description": "Handler that is called when a press interaction ends, either\nover the target or when the pointer leaves the target."
1634
+ },
1635
+ "onPressChange": {
1636
+ "type": "((isPressed: boolean) => void) | undefined",
1637
+ "required": false,
1638
+ "origin": "aria",
1639
+ "description": "Handler that is called when the press state changes."
1640
+ },
1641
+ "onPressUp": {
1642
+ "type": "((e: PressEvent) => void) | undefined",
1643
+ "required": false,
1644
+ "origin": "aria",
1645
+ "description": "Handler that is called when a press is released over the target, regardless of\nwhether it started on the target or not."
1646
+ },
1647
+ "onClick": {
1648
+ "type": "((e: MouseEvent<FocusableElement>) => void) | undefined",
1649
+ "required": false,
1650
+ "origin": "aria",
1651
+ "description": "**Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress`\nprovided for compatibility with other libraries. `onPress` provides\nadditional event details for non-mouse interactions."
1652
+ },
1653
+ "autoFocus": {
1654
+ "type": "boolean | undefined",
1655
+ "required": false,
1656
+ "origin": "aria",
1657
+ "description": "Whether the element should receive focus on render."
1658
+ },
1659
+ "onFocus": {
1660
+ "type": "((e: FocusEvent<Element, Element>) => void) | undefined",
1661
+ "required": false,
1662
+ "origin": "aria",
1663
+ "description": "Handler that is called when the element receives focus."
1664
+ },
1665
+ "onBlur": {
1666
+ "type": "((e: FocusEvent<Element, Element>) => void) | undefined",
1667
+ "required": false,
1668
+ "origin": "aria",
1669
+ "description": "Handler that is called when the element loses focus."
1670
+ },
1671
+ "onFocusChange": {
1672
+ "type": "((isFocused: boolean) => void) | undefined",
1673
+ "required": false,
1674
+ "origin": "aria",
1675
+ "description": "Handler that is called when the element's focus status changes."
1676
+ },
1677
+ "onKeyDown": {
1678
+ "type": "((e: KeyboardEvent) => void) | undefined",
1679
+ "required": false,
1680
+ "origin": "aria",
1681
+ "description": "Handler that is called when a key is pressed."
1682
+ },
1683
+ "onKeyUp": {
1684
+ "type": "((e: KeyboardEvent) => void) | undefined",
1685
+ "required": false,
1686
+ "origin": "aria",
1687
+ "description": "Handler that is called when a key is released."
1688
+ },
1689
+ "href": {
1690
+ "type": "string | undefined",
1691
+ "required": false,
1692
+ "origin": "aria",
1693
+ "description": "A URL to link to if elementType=\"a\"."
1694
+ },
1695
+ "target": {
1696
+ "type": "string | undefined",
1697
+ "required": false,
1698
+ "origin": "aria",
1699
+ "description": "The target window for the link."
1700
+ },
1701
+ "rel": {
1702
+ "type": "string | undefined",
1703
+ "required": false,
1704
+ "origin": "aria",
1705
+ "description": "The relationship between the linked resource and the current page. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel)."
1706
+ },
1707
+ "elementType": {
1708
+ "type": "JSXElementConstructor<any> | \"button\" | undefined",
1709
+ "required": false,
1710
+ "origin": "aria",
1711
+ "description": "The HTML element or React element used to render the button, e.g. 'div', 'a', or `RouterLink`.",
1712
+ "tags": {
1713
+ "default": "'button'"
1714
+ }
1715
+ },
1716
+ "aria-disabled": {
1717
+ "type": "boolean | \"true\" | \"false\" | undefined",
1718
+ "required": false,
1719
+ "origin": "aria",
1720
+ "description": "Indicates whether the element is disabled to users of assistive technology."
1721
+ },
1722
+ "aria-expanded": {
1723
+ "type": "boolean | \"true\" | \"false\" | undefined",
1724
+ "required": false,
1725
+ "origin": "aria",
1726
+ "description": "Indicates whether the element, or another grouping element it controls, is currently expanded\nor collapsed."
1727
+ },
1728
+ "aria-haspopup": {
1729
+ "type": "boolean | \"true\" | \"false\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\" | undefined",
1730
+ "required": false,
1731
+ "origin": "aria",
1732
+ "description": "Indicates the availability and type of interactive popup element, such as menu or dialog, that\ncan be triggered by an element."
1733
+ },
1734
+ "aria-controls": {
1735
+ "type": "string | undefined",
1736
+ "required": false,
1737
+ "origin": "aria",
1738
+ "description": "Identifies the element (or elements) whose contents or presence are controlled by the current\nelement."
1739
+ },
1740
+ "aria-pressed": {
1741
+ "type": "boolean | \"true\" | \"false\" | \"mixed\" | undefined",
1742
+ "required": false,
1743
+ "origin": "aria",
1744
+ "description": "Indicates the current \"pressed\" state of toggle buttons."
1745
+ },
1746
+ "aria-current": {
1747
+ "type": "boolean | \"true\" | \"false\" | \"page\" | \"step\" | \"location\" | \"date\" | \"time\" | undefined",
1748
+ "required": false,
1749
+ "origin": "aria",
1750
+ "description": "Indicates whether this element represents the current item within a container or set of related\nelements."
1751
+ },
1752
+ "type": {
1753
+ "type": "\"button\" | \"submit\" | \"reset\" | undefined",
1754
+ "required": false,
1755
+ "origin": "aria",
1756
+ "description": "The behavior of the button when used in an HTML form.",
1757
+ "values": [
1758
+ "button",
1759
+ "submit",
1760
+ "reset"
1761
+ ],
1762
+ "tags": {
1763
+ "default": "'button'"
1764
+ }
1765
+ },
1766
+ "preventFocusOnPress": {
1767
+ "type": "boolean | undefined",
1768
+ "required": false,
1769
+ "origin": "aria",
1770
+ "description": "Whether to prevent focus from moving to the button when pressing it.\n\nCaution, this can make the button inaccessible and should only be used when alternative\nkeyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's\nincrement/decrement control."
1771
+ },
1772
+ "form": {
1773
+ "type": "string | undefined",
1774
+ "required": false,
1775
+ "origin": "aria",
1776
+ "description": "The `<form>` element to associate the button with.\nThe value of this attribute must be the id of a `<form>` in the same document.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#form)."
1777
+ },
1778
+ "formAction": {
1779
+ "type": "string | ((formData: FormData) => void | Promise<void>) | undefined",
1780
+ "required": false,
1781
+ "origin": "aria",
1782
+ "description": "The URL that processes the information submitted by the button.\nOverrides the action attribute of the button's form owner."
1783
+ },
1784
+ "formEncType": {
1785
+ "type": "string | undefined",
1786
+ "required": false,
1787
+ "origin": "aria",
1788
+ "description": "Indicates how to encode the form data that is submitted."
1789
+ },
1790
+ "formMethod": {
1791
+ "type": "string | undefined",
1792
+ "required": false,
1793
+ "origin": "aria",
1794
+ "description": "Indicates the HTTP method used to submit the form."
1795
+ },
1796
+ "formNoValidate": {
1797
+ "type": "boolean | undefined",
1798
+ "required": false,
1799
+ "origin": "aria",
1800
+ "description": "Indicates that the form is not to be validated when it is submitted."
1801
+ },
1802
+ "formTarget": {
1803
+ "type": "string | undefined",
1804
+ "required": false,
1805
+ "origin": "aria",
1806
+ "description": "Overrides the target attribute of the button's form owner."
1807
+ },
1808
+ "name": {
1809
+ "type": "string | undefined",
1810
+ "required": false,
1811
+ "origin": "aria",
1812
+ "description": "Submitted as a pair with the button's value as part of the form data."
1813
+ },
1814
+ "value": {
1815
+ "type": "string | undefined",
1816
+ "required": false,
1817
+ "origin": "aria",
1818
+ "description": "The value associated with the button's name when it's submitted with the form data."
1819
+ },
1820
+ "excludeFromTabOrder": {
1821
+ "type": "boolean | undefined",
1822
+ "required": false,
1823
+ "origin": "aria",
1824
+ "description": "Whether to exclude the element from the sequential tab order. If true,\nthe element will not be focusable via the keyboard by tabbing. This should\nbe avoided except in rare scenarios where an alternative means of accessing\nthe element or its functionality via the keyboard is available."
1825
+ },
1826
+ "id": {
1827
+ "type": "string | undefined",
1828
+ "required": false,
1829
+ "origin": "aria",
1830
+ "description": "The element's unique identifier. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id)."
1831
+ },
1832
+ "aria-label": {
1833
+ "type": "string | undefined",
1097
1834
  "required": false,
1098
1835
  "origin": "aria",
1099
- "description": "Identifies the element (or elements) whose contents or presence are controlled by the current\nelement."
1836
+ "description": "Defines a string value that labels the current element."
1100
1837
  },
1101
- "aria-pressed": {
1102
- "type": "boolean | \"true\" | \"false\" | \"mixed\" | undefined",
1838
+ "aria-labelledby": {
1839
+ "type": "string | undefined",
1103
1840
  "required": false,
1104
1841
  "origin": "aria",
1105
- "description": "Indicates the current \"pressed\" state of toggle buttons."
1842
+ "description": "Identifies the element (or elements) that labels the current element."
1106
1843
  },
1107
- "aria-current": {
1108
- "type": "boolean | \"true\" | \"false\" | \"page\" | \"step\" | \"location\" | \"date\" | \"time\" | undefined",
1844
+ "aria-describedby": {
1845
+ "type": "string | undefined",
1109
1846
  "required": false,
1110
1847
  "origin": "aria",
1111
- "description": "Indicates whether this element represents the current item within a container or set of related\nelements."
1848
+ "description": "Identifies the element (or elements) that describes the object."
1112
1849
  },
1113
- "type": {
1114
- "type": "\"button\" | \"submit\" | \"reset\" | undefined",
1850
+ "aria-details": {
1851
+ "type": "string | undefined",
1115
1852
  "required": false,
1116
1853
  "origin": "aria",
1117
- "description": "The behavior of the button when used in an HTML form.",
1854
+ "description": "Identifies the element (or elements) that provide a detailed, extended description for the\nobject."
1855
+ }
1856
+ },
1857
+ "propCounts": {
1858
+ "own": 7,
1859
+ "aria": 39,
1860
+ "dom": 0,
1861
+ "other": 0
1862
+ }
1863
+ },
1864
+ "Checkbox": {
1865
+ "name": "Checkbox",
1866
+ "source": "src/components/Checkbox.tsx",
1867
+ "propsType": "CheckboxProps",
1868
+ "description": "Checkbox wraps a native `<input type=\"checkbox\">` and hides it.\n\nThe native input is kept rather than replaced by a div with `role=checkbox`\nbecause it brings form association, the indeterminate property, label\nclicking, and correct announcement — none of which are free to reimplement,\nand all of which are easy to get subtly wrong.\n\n`indeterminate` is the reason for the effect below: HTML has no\n`indeterminate` attribute, only a DOM property, so React cannot set it\ndeclaratively and it must be written after every render.",
1869
+ "import": "import { Checkbox } from 'ionbase-ui';",
1870
+ "status": "stable",
1871
+ "summary": "A native checkbox with the system's styling, supporting an indeterminate state.",
1872
+ "useWhen": [
1873
+ "a single on/off choice inside a form that is submitted",
1874
+ "selecting rows in a Table",
1875
+ "several independent options where more than one may be picked"
1876
+ ],
1877
+ "useInstead": [
1878
+ {
1879
+ "when": "the change takes effect immediately rather than on submit",
1880
+ "use": "Toggle",
1881
+ "why": "a switch reads as a setting taking effect now; a checkbox reads as a value being collected"
1882
+ },
1883
+ {
1884
+ "when": "the options are mutually exclusive",
1885
+ "use": "RadioGroup"
1886
+ },
1887
+ {
1888
+ "when": "there are many options and space is tight",
1889
+ "use": "Select"
1890
+ }
1891
+ ],
1892
+ "variants": {
1893
+ "size": {
1894
+ "sm": {
1895
+ "use": "dense forms and table rows"
1896
+ },
1897
+ "md": {
1898
+ "use": "the default"
1899
+ },
1900
+ "lg": {
1901
+ "use": "touch targets and prominent single choices"
1902
+ }
1903
+ },
1904
+ "intent": {
1905
+ "brand": {
1906
+ "use": "the default"
1907
+ },
1908
+ "neutral": {
1909
+ "use": "where the checked state is a fact rather than an approval — bulk-selection columns"
1910
+ },
1911
+ "danger": {
1912
+ "use": "consenting to something destructive or irreversible"
1913
+ }
1914
+ }
1915
+ },
1916
+ "slots": {
1917
+ "children": {
1918
+ "accepts": "text",
1919
+ "note": "the visible label; omit for a bare box and supply `aria-label` instead"
1920
+ }
1921
+ },
1922
+ "a11y": {
1923
+ "role": "checkbox",
1924
+ "guarantees": [
1925
+ "a real <input type=\"checkbox\">, so form association, label clicking, and correct announcement come from the platform",
1926
+ "`isIndeterminate` is written to the DOM property after every render — HTML has no such attribute, so React cannot set it declaratively"
1927
+ ],
1928
+ "requires": [
1929
+ "`children`, or `aria-label` when the box has no visible label — a row-selection checkbox always needs one"
1930
+ ]
1931
+ },
1932
+ "antiPatterns": [
1933
+ {
1934
+ "dont": "a bare checkbox with no `children` and no `aria-label`",
1935
+ "why": "every row announces an unnamed checkbox"
1936
+ },
1937
+ {
1938
+ "dont": "`isIndeterminate` on a leaf checkbox",
1939
+ "why": "indeterminate means \"some of my children are checked\"; on a leaf it is a state the user can never reach"
1940
+ },
1941
+ {
1942
+ "dont": "a div with `role=\"checkbox\"`",
1943
+ "why": "form association, the indeterminate property and label clicking are not free to reimplement"
1944
+ }
1945
+ ],
1946
+ "deprecated": [
1947
+ {
1948
+ "prop": "disabled",
1949
+ "replacement": "isDisabled"
1950
+ }
1951
+ ],
1952
+ "stylesheet": "src/styles/checkbox.css",
1953
+ "tokens": [
1954
+ "--border-disabled",
1955
+ "--border-error-strong",
1956
+ "--border-focus",
1957
+ "--border-inverse",
1958
+ "--border-primary-strong",
1959
+ "--border-stronger",
1960
+ "--border-width-default",
1961
+ "--border-width-thick",
1962
+ "--font-family-sans",
1963
+ "--icon-disabled",
1964
+ "--icon-on-color",
1965
+ "--ion-checkbox-border",
1966
+ "--ion-checkbox-fill",
1967
+ "--ion-checkbox-font-size",
1968
+ "--ion-checkbox-gap",
1969
+ "--ion-checkbox-line-height",
1970
+ "--ion-checkbox-mark",
1971
+ "--ion-checkbox-radius",
1972
+ "--ion-checkbox-raised",
1973
+ "--ion-checkbox-size",
1974
+ "--ion-duration-base",
1975
+ "--ion-ease-out",
1976
+ "--ion-shadow-raised-flush-lg",
1977
+ "--ion-shadow-raised-flush-sm",
1978
+ "--ion-shadow-raised-flush-xs",
1979
+ "--radius-sm",
1980
+ "--radius-xs",
1981
+ "--spacing-12",
1982
+ "--spacing-16",
1983
+ "--spacing-20",
1984
+ "--spacing-24",
1985
+ "--spacing-4",
1986
+ "--spacing-8",
1987
+ "--surface-default",
1988
+ "--surface-disabled",
1989
+ "--surface-error",
1990
+ "--surface-inverse",
1991
+ "--surface-primary",
1992
+ "--text-disabled",
1993
+ "--text-secondary",
1994
+ "--type-body",
1995
+ "--type-body-line-height",
1996
+ "--type-body-sm",
1997
+ "--type-body-sm-line-height"
1998
+ ],
1999
+ "props": {
2000
+ "size": {
2001
+ "type": "CheckboxSize | undefined",
2002
+ "required": false,
2003
+ "origin": "own",
2004
+ "description": "Matches the Figma `Size` variant: Small, Medium, Large.",
1118
2005
  "values": [
1119
- "button",
1120
- "submit",
1121
- "reset"
2006
+ "sm",
2007
+ "md",
2008
+ "lg"
1122
2009
  ],
1123
- "tags": {
1124
- "default": "'button'"
1125
- }
2010
+ "default": "md"
1126
2011
  },
1127
- "preventFocusOnPress": {
2012
+ "intent": {
2013
+ "type": "CheckboxIntent | undefined",
2014
+ "required": false,
2015
+ "origin": "own",
2016
+ "description": "Matches the Figma `Color` variant: Brand, Neutral, Danger.",
2017
+ "values": [
2018
+ "neutral",
2019
+ "brand",
2020
+ "danger"
2021
+ ],
2022
+ "default": "brand"
2023
+ },
2024
+ "isIndeterminate": {
1128
2025
  "type": "boolean | undefined",
1129
2026
  "required": false,
1130
- "origin": "aria",
1131
- "description": "Whether to prevent focus from moving to the button when pressing it.\n\nCaution, this can make the button inaccessible and should only be used when alternative\nkeyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's\nincrement/decrement control."
2027
+ "origin": "own",
2028
+ "description": "Figma's `Indeterminate` state. Not an HTML attribute — `indeterminate` is a\nDOM property only, so it has to be assigned after render.",
2029
+ "default": false
1132
2030
  },
1133
- "form": {
1134
- "type": "string | undefined",
2031
+ "isDisabled": {
2032
+ "type": "boolean | undefined",
1135
2033
  "required": false,
1136
- "origin": "aria",
1137
- "description": "The `<form>` element to associate the button with.\nThe value of this attribute must be the id of a `<form>` in the same document.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#form)."
2034
+ "origin": "own",
2035
+ "description": "Whether the checkbox is disabled."
1138
2036
  },
1139
- "formAction": {
1140
- "type": "string | ((formData: FormData) => void | Promise<void>) | undefined",
2037
+ "disabled": {
2038
+ "type": "boolean | undefined",
1141
2039
  "required": false,
1142
- "origin": "aria",
1143
- "description": "The URL that processes the information submitted by the button.\nOverrides the action attribute of the button's form owner."
2040
+ "origin": "own",
2041
+ "tags": {
2042
+ "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
2043
+ }
1144
2044
  },
1145
- "formEncType": {
1146
- "type": "string | undefined",
2045
+ "children": {
2046
+ "type": "React.ReactNode",
1147
2047
  "required": false,
1148
- "origin": "aria",
1149
- "description": "Indicates how to encode the form data that is submitted."
2048
+ "origin": "own",
2049
+ "description": "Figma's `Show Label` + `Label`. Omit for a bare box."
2050
+ }
2051
+ },
2052
+ "propCounts": {
2053
+ "own": 6,
2054
+ "aria": 0,
2055
+ "dom": 304,
2056
+ "other": 0
2057
+ }
2058
+ },
2059
+ "Citation": {
2060
+ "name": "Citation",
2061
+ "source": "src/components/Citation.tsx",
2062
+ "propsType": "CitationProps",
2063
+ "description": "Citation — the inline marker.\n\nTHE MARKER IS NOT THE NAME. Rendered naively, a superscript \"1\" announces as\n\"link, 1\", which tells a screen-reader user nothing about whether to follow\nit. The visible marker stays a number; the accessible name is\n\"Source 1: <source>\". The number is for the sighted reader's eye and the\nsentence is for everyone.\n\nNOT A TOOLTIP. A tooltip cannot be reached on touch and closes on the way to\nit; attribution has to survive both. It is a real link, or a real\nnon-interactive marker when there is nowhere to go.\n\nWITHOUT `href` IT IS NOT A LINK. A citation to a phone call or an internal\ndocument has no address, and rendering a dead anchor for it puts an\nunfollowable link in the page's link list. It becomes a plain marked-up\nreference instead.",
2064
+ "import": "import { Citation } from 'ionbase-ui';",
2065
+ "status": "stable",
2066
+ "summary": "The inline source marker. The visible number is for the eye; the accessible name is the sentence.",
2067
+ "useWhen": [
2068
+ "a claim in generated text is drawn from a specific source the reader can check",
2069
+ "attribution has to survive being read aloud, on touch, and in a links list"
2070
+ ],
2071
+ "useInstead": [
2072
+ {
2073
+ "when": "the source needs a full row with a passage",
2074
+ "use": "CitationListItem"
2075
+ },
2076
+ {
2077
+ "when": "it is an ordinary link in prose",
2078
+ "use": "Link"
2079
+ },
2080
+ {
2081
+ "when": "you were about to put the source in a hover-only hint",
2082
+ "use": "Popover",
2083
+ "why": "a tooltip cannot be reached on touch and closes on the way to it"
2084
+ }
2085
+ ],
2086
+ "a11y": {
2087
+ "role": "link when `href` is set, note otherwise",
2088
+ "guarantees": [
2089
+ "the accessible name is \"Source <index>: <source>\", never the bare number — a superscript 1 announces as \"link, 1\", which tells a screen-reader user nothing about whether to follow it",
2090
+ "without `href` it renders a marker rather than a dead anchor, so an unfollowable link never enters the page's link list"
2091
+ ],
2092
+ "requires": [
2093
+ "`source` naming what is being cited",
2094
+ "an `index` matching the footer row, if there is one"
2095
+ ]
2096
+ },
2097
+ "antiPatterns": [
2098
+ {
2099
+ "dont": "a bare superscript number with no accessible name",
2100
+ "why": "\"link, 1\" is not attribution"
1150
2101
  },
1151
- "formMethod": {
1152
- "type": "string | undefined",
1153
- "required": false,
1154
- "origin": "aria",
1155
- "description": "Indicates the HTTP method used to submit the form."
2102
+ {
2103
+ "dont": "an empty `href` for a source with no address",
2104
+ "do": "omit `href`",
2105
+ "why": "a dead anchor is announced as a link and goes nowhere"
1156
2106
  },
1157
- "formNoValidate": {
1158
- "type": "boolean | undefined",
1159
- "required": false,
1160
- "origin": "aria",
1161
- "description": "Indicates that the form is not to be validated when it is submitted."
2107
+ {
2108
+ "dont": "putting the source only in a tooltip",
2109
+ "why": "undiscoverable on touch, and gone before it can be read"
2110
+ }
2111
+ ],
2112
+ "stylesheet": "src/styles/citation.css",
2113
+ "tokens": [
2114
+ "--border-focus",
2115
+ "--border-width-thick",
2116
+ "--font-family-sans",
2117
+ "--font-weight-medium",
2118
+ "--font-weight-semibold",
2119
+ "--radius-xs",
2120
+ "--spacing-16",
2121
+ "--spacing-2",
2122
+ "--spacing-4",
2123
+ "--spacing-8",
2124
+ "--surface-primary-subtle",
2125
+ "--surface-primary-subtle-hover",
2126
+ "--text-link",
2127
+ "--text-tertiary",
2128
+ "--type-body-sm",
2129
+ "--type-body-sm-line-height",
2130
+ "--type-caption",
2131
+ "--type-caption-line-height"
2132
+ ],
2133
+ "props": {
2134
+ "index": {
2135
+ "type": "string | number",
2136
+ "required": true,
2137
+ "origin": "own",
2138
+ "description": "The marker shown inline — usually a number matching the footer list."
1162
2139
  },
1163
- "formTarget": {
1164
- "type": "string | undefined",
1165
- "required": false,
1166
- "origin": "aria",
1167
- "description": "Overrides the target attribute of the button's form owner."
2140
+ "source": {
2141
+ "type": "string",
2142
+ "required": true,
2143
+ "origin": "own",
2144
+ "description": "What is being cited. Required: it is the link's accessible name, and a\ncitation a reader cannot identify without following it is not attribution."
1168
2145
  },
1169
- "name": {
2146
+ "href": {
1170
2147
  "type": "string | undefined",
1171
2148
  "required": false,
1172
- "origin": "aria",
1173
- "description": "Submitted as a pair with the button's value as part of the form data."
2149
+ "origin": "own",
2150
+ "description": "Where it goes. Omit for a source with no address — a document, a call."
2151
+ }
2152
+ },
2153
+ "propCounts": {
2154
+ "own": 3,
2155
+ "aria": 0,
2156
+ "dom": 284,
2157
+ "other": 0
2158
+ }
2159
+ },
2160
+ "CitationList": {
2161
+ "name": "CitationList",
2162
+ "source": "src/components/Citation.tsx",
2163
+ "propsType": "CitationListProps",
2164
+ "description": "The footer list. An `<ol>` so the numbering is structural rather than typed\ninto each row, and so a screen reader can say how many sources there are\nbefore the reader commits to hearing them.",
2165
+ "import": "import { CitationList } from 'ionbase-ui';",
2166
+ "status": "stable",
2167
+ "summary": "The footer list of sources, as an ordered list so the count and order are structural.",
2168
+ "useWhen": [
2169
+ "several claims cite sources and the reader should be able to see them all at once"
2170
+ ],
2171
+ "useInstead": [
2172
+ {
2173
+ "when": "there is one source and it belongs in the sentence",
2174
+ "use": "Link"
1174
2175
  },
1175
- "value": {
1176
- "type": "string | undefined",
1177
- "required": false,
1178
- "origin": "aria",
1179
- "description": "The value associated with the button's name when it's submitted with the form data."
2176
+ {
2177
+ "when": "it is tabular data with more than a source per row",
2178
+ "use": "Table"
2179
+ }
2180
+ ],
2181
+ "composition": {
2182
+ "order": [
2183
+ "CitationList",
2184
+ "CitationListItem"
2185
+ ],
2186
+ "example": "<CitationList><CitationListItem index={1} source=\"Q3 filing\" href=\"/q3\">Revenue rose 4%.</CitationListItem></CitationList>"
2187
+ },
2188
+ "slots": {
2189
+ "label": {
2190
+ "accepts": "text",
2191
+ "note": "rendered as plain text, not a heading element — it should not enter the document outline as a section"
1180
2192
  },
1181
- "excludeFromTabOrder": {
1182
- "type": "boolean | undefined",
1183
- "required": false,
1184
- "origin": "aria",
1185
- "description": "Whether to exclude the element from the sequential tab order. If true,\nthe element will not be focusable via the keyboard by tabbing. This should\nbe avoided except in rare scenarios where an alternative means of accessing\nthe element or its functionality via the keyboard is available."
2193
+ "children": {
2194
+ "accepts": "CitationListItem"
2195
+ }
2196
+ },
2197
+ "a11y": {
2198
+ "guarantees": [
2199
+ "an <ol>, so a screen reader can say how many sources there are before the reader commits to hearing them",
2200
+ "list markers are hidden in CSS while the semantics stay, because the visible number must match the inline Citation's exactly"
2201
+ ]
2202
+ },
2203
+ "antiPatterns": [
2204
+ {
2205
+ "dont": "a <div> stack of sources",
2206
+ "why": "the count and the ordering are the useful part, and only a list carries them"
1186
2207
  },
1187
- "id": {
1188
- "type": "string | undefined",
2208
+ {
2209
+ "dont": "numbers that disagree with the inline markers",
2210
+ "why": "the marker is a promise about where to look"
2211
+ }
2212
+ ],
2213
+ "stylesheet": "src/styles/citation.css",
2214
+ "tokens": [
2215
+ "--border-focus",
2216
+ "--border-width-thick",
2217
+ "--font-family-sans",
2218
+ "--font-weight-medium",
2219
+ "--font-weight-semibold",
2220
+ "--radius-xs",
2221
+ "--spacing-16",
2222
+ "--spacing-2",
2223
+ "--spacing-4",
2224
+ "--spacing-8",
2225
+ "--surface-primary-subtle",
2226
+ "--surface-primary-subtle-hover",
2227
+ "--text-link",
2228
+ "--text-tertiary",
2229
+ "--type-body-sm",
2230
+ "--type-body-sm-line-height",
2231
+ "--type-caption",
2232
+ "--type-caption-line-height"
2233
+ ],
2234
+ "props": {
2235
+ "children": {
2236
+ "type": "React.ReactNode",
1189
2237
  "required": false,
1190
- "origin": "aria",
1191
- "description": "The element's unique identifier. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id)."
2238
+ "origin": "own"
1192
2239
  },
1193
- "aria-label": {
1194
- "type": "string | undefined",
2240
+ "label": {
2241
+ "type": "React.ReactNode",
1195
2242
  "required": false,
1196
- "origin": "aria",
1197
- "description": "Defines a string value that labels the current element."
2243
+ "origin": "own",
2244
+ "description": "Heading above the list. Rendered as plain text, not a heading element."
2245
+ }
2246
+ },
2247
+ "propCounts": {
2248
+ "own": 2,
2249
+ "aria": 0,
2250
+ "dom": 280,
2251
+ "other": 0
2252
+ }
2253
+ },
2254
+ "CitationListItem": {
2255
+ "name": "CitationListItem",
2256
+ "source": "src/components/Citation.tsx",
2257
+ "propsType": "CitationListItemProps",
2258
+ "import": "import { CitationListItem } from 'ionbase-ui';",
2259
+ "status": "stable",
2260
+ "summary": "One source row: its number, what it is, and the passage it supports.",
2261
+ "useWhen": [
2262
+ "a child of CitationList"
2263
+ ],
2264
+ "useInstead": [
2265
+ {
2266
+ "when": "it is the inline marker in the prose",
2267
+ "use": "Citation"
2268
+ }
2269
+ ],
2270
+ "slots": {
2271
+ "children": {
2272
+ "accepts": "text",
2273
+ "note": "the quoted or summarised passage this source supports"
2274
+ }
2275
+ },
2276
+ "a11y": {
2277
+ "guarantees": [
2278
+ "the visible number is `aria-hidden`; the row is named by its source text, which is what a reader needs to hear",
2279
+ "without `href` the source renders as text rather than a dead link"
2280
+ ],
2281
+ "requires": [
2282
+ "`source` naming what is being cited"
2283
+ ]
2284
+ },
2285
+ "antiPatterns": [
2286
+ {
2287
+ "dont": "a bare URL as the `source`",
2288
+ "why": "a screen reader spells it out character by character, and it names nothing"
2289
+ }
2290
+ ],
2291
+ "stylesheet": "src/styles/citation.css",
2292
+ "tokens": [
2293
+ "--border-focus",
2294
+ "--border-width-thick",
2295
+ "--font-family-sans",
2296
+ "--font-weight-medium",
2297
+ "--font-weight-semibold",
2298
+ "--radius-xs",
2299
+ "--spacing-16",
2300
+ "--spacing-2",
2301
+ "--spacing-4",
2302
+ "--spacing-8",
2303
+ "--surface-primary-subtle",
2304
+ "--surface-primary-subtle-hover",
2305
+ "--text-link",
2306
+ "--text-tertiary",
2307
+ "--type-body-sm",
2308
+ "--type-body-sm-line-height",
2309
+ "--type-caption",
2310
+ "--type-caption-line-height"
2311
+ ],
2312
+ "props": {
2313
+ "index": {
2314
+ "type": "string | number",
2315
+ "required": true,
2316
+ "origin": "own"
1198
2317
  },
1199
- "aria-labelledby": {
1200
- "type": "string | undefined",
1201
- "required": false,
1202
- "origin": "aria",
1203
- "description": "Identifies the element (or elements) that labels the current element."
2318
+ "source": {
2319
+ "type": "string",
2320
+ "required": true,
2321
+ "origin": "own"
1204
2322
  },
1205
- "aria-describedby": {
2323
+ "href": {
1206
2324
  "type": "string | undefined",
1207
2325
  "required": false,
1208
- "origin": "aria",
1209
- "description": "Identifies the element (or elements) that describes the object."
2326
+ "origin": "own"
1210
2327
  },
1211
- "aria-details": {
1212
- "type": "string | undefined",
2328
+ "children": {
2329
+ "type": "React.ReactNode",
1213
2330
  "required": false,
1214
- "origin": "aria",
1215
- "description": "Identifies the element (or elements) that provide a detailed, extended description for the\nobject."
2331
+ "origin": "own",
2332
+ "description": "The quoted or summarised passage this citation supports."
1216
2333
  }
1217
2334
  },
1218
2335
  "propCounts": {
1219
- "own": 7,
1220
- "aria": 39,
1221
- "dom": 0,
2336
+ "own": 4,
2337
+ "aria": 0,
2338
+ "dom": 278,
1222
2339
  "other": 0
1223
2340
  }
1224
2341
  },
1225
- "Checkbox": {
1226
- "name": "Checkbox",
1227
- "source": "src/components/Checkbox.tsx",
1228
- "propsType": "CheckboxProps",
1229
- "description": "Checkbox wraps a native `<input type=\"checkbox\">` and hides it.\n\nThe native input is kept rather than replaced by a div with `role=checkbox`\nbecause it brings form association, the indeterminate property, label\nclicking, and correct announcement — none of which are free to reimplement,\nand all of which are easy to get subtly wrong.\n\n`indeterminate` is the reason for the effect below: HTML has no\n`indeterminate` attribute, only a DOM property, so React cannot set it\ndeclaratively and it must be written after every render.",
1230
- "import": "import { Checkbox } from 'ionbase-ui';",
2342
+ "ConfidenceIndicator": {
2343
+ "name": "ConfidenceIndicator",
2344
+ "source": "src/components/ConfidenceIndicator.tsx",
2345
+ "propsType": "ConfidenceIndicatorProps",
2346
+ "description": "ConfidenceIndicator — how much to trust the thing next to it.\n\nTHERE IS NO PERCENTAGE PROP, AND THERE WILL NOT BE ONE\n\n\"87% confident\" reads as a measurement. Almost nowhere is it one: it is\nusually a softmax score, a heuristic, or a number a model produced about\nitself — none of which are calibrated probabilities, and all of which invite\na reader to treat two digits of precision as real. Three levels cannot\noverclaim in that way.\n\n`basis` IS REQUIRED FOR THE SAME REASON. A level with nothing behind it is\ndecoration that changes behaviour: people act on \"high confidence\" whether or\nnot anything justifies it. Making the justification a required prop is the\nonly enforcement available here, and it is a type error rather than a policy\n— which is the strongest kind this system can offer.\n\nIt renders as text plus a three-bar meter, never the meter alone. The bars\ndiffer in filled COUNT, not only in colour, so the reading survives greyscale\nand forced-colours mode.",
2347
+ "import": "import { ConfidenceIndicator } from 'ionbase-ui';",
1231
2348
  "status": "stable",
1232
- "summary": "A native checkbox with the system's styling, supporting an indeterminate state.",
2349
+ "summary": "How much to trust the thing next to it — three levels with a required basis, never a percentage.",
1233
2350
  "useWhen": [
1234
- "a single on/off choice inside a form that is submitted",
1235
- "selecting rows in a Table",
1236
- "several independent options where more than one may be picked"
2351
+ "a generated answer varies in reliability and the reader should weigh it",
2352
+ "you can say what the confidence rests on"
1237
2353
  ],
1238
2354
  "useInstead": [
1239
2355
  {
1240
- "when": "the change takes effect immediately rather than on submit",
1241
- "use": "Toggle",
1242
- "why": "a switch reads as a setting taking effect now; a checkbox reads as a value being collected"
2356
+ "when": "the answer is wrong or missing rather than uncertain",
2357
+ "use": "Alert"
1243
2358
  },
1244
2359
  {
1245
- "when": "the options are mutually exclusive",
1246
- "use": "RadioGroup"
2360
+ "when": "you want to show where the claim came from",
2361
+ "use": "Citation",
2362
+ "why": "a source is stronger than a confidence level, and often what the reader actually wanted"
1247
2363
  },
1248
2364
  {
1249
- "when": "there are many options and space is tight",
1250
- "use": "Select"
2365
+ "when": "it is a status rather than a reliability",
2366
+ "use": "Badge"
1251
2367
  }
1252
2368
  ],
1253
2369
  "variants": {
1254
- "size": {
1255
- "sm": {
1256
- "use": "dense forms and table rows"
1257
- },
1258
- "md": {
1259
- "use": "the default"
1260
- },
1261
- "lg": {
1262
- "use": "touch targets and prominent single choices"
1263
- }
1264
- },
1265
- "intent": {
1266
- "brand": {
1267
- "use": "the default"
2370
+ "level": {
2371
+ "low": {
2372
+ "use": "thin or conflicting evidence. One bar, warning colour"
1268
2373
  },
1269
- "neutral": {
1270
- "use": "where the checked state is a fact rather than an approval — bulk-selection columns"
2374
+ "medium": {
2375
+ "use": "reasonable support with real gaps. Two bars"
1271
2376
  },
1272
- "danger": {
1273
- "use": "consenting to something destructive or irreversible"
1274
- }
1275
- }
1276
- },
1277
- "slots": {
1278
- "children": {
1279
- "accepts": "text",
1280
- "note": "the visible label; omit for a bare box and supply `aria-label` instead"
2377
+ "high": {
2378
+ "use": "corroborated, and you can say by what. Three bars"
2379
+ }
1281
2380
  }
1282
2381
  },
1283
2382
  "a11y": {
1284
- "role": "checkbox",
1285
2383
  "guarantees": [
1286
- "a real <input type=\"checkbox\">, so form association, label clicking, and correct announcement come from the platform",
1287
- "`isIndeterminate` is written to the DOM property after every render — HTML has no such attribute, so React cannot set it declaratively"
2384
+ "the level is spelled out in text, so it never depends on the meter",
2385
+ "the meter differs in filled COUNT rather than only in colour, and in forced-colours mode filled bars are solid while empty ones are outlined"
1288
2386
  ],
1289
2387
  "requires": [
1290
- "`children`, or `aria-label` when the box has no visible label — a row-selection checkbox always needs one"
2388
+ "`basis` — a type error if omitted, and the reason this component exists rather than a coloured dot"
1291
2389
  ]
1292
2390
  },
1293
2391
  "antiPatterns": [
1294
2392
  {
1295
- "dont": "a bare checkbox with no `children` and no `aria-label`",
1296
- "why": "every row announces an unnamed checkbox"
2393
+ "dont": "asking for a percentage prop",
2394
+ "why": "\"87% confident\" reads as a measurement and almost never is one — usually a softmax score or a model's claim about itself, neither of which is a calibrated probability. Three levels cannot overclaim that way"
1297
2395
  },
1298
2396
  {
1299
- "dont": "`isIndeterminate` on a leaf checkbox",
1300
- "why": "indeterminate means \"some of my children are checked\"; on a leaf it is a state the user can never reach"
2397
+ "dont": "a vague basis like \"model output\"",
2398
+ "why": "it satisfies the type and defeats the purpose — say \"3 of 4 sources agree\" or \"single unverified source\""
1301
2399
  },
1302
2400
  {
1303
- "dont": "a div with `role=\"checkbox\"`",
1304
- "why": "form association, the indeterminate property and label clicking are not free to reimplement"
1305
- }
1306
- ],
1307
- "deprecated": [
1308
- {
1309
- "prop": "disabled",
1310
- "replacement": "isDisabled"
2401
+ "dont": "high confidence on every response",
2402
+ "why": "an indicator that never varies is decoration that still changes behaviour"
1311
2403
  }
1312
2404
  ],
1313
- "stylesheet": "src/styles/checkbox.css",
2405
+ "stylesheet": "src/styles/confidence-indicator.css",
1314
2406
  "tokens": [
1315
- "--border-disabled",
1316
- "--border-error-strong",
1317
- "--border-focus",
1318
- "--border-inverse",
1319
- "--border-primary-strong",
1320
- "--border-stronger",
1321
2407
  "--border-width-default",
1322
- "--border-width-thick",
1323
2408
  "--font-family-sans",
1324
- "--icon-disabled",
1325
- "--icon-on-color",
1326
- "--ion-checkbox-border",
1327
- "--ion-checkbox-fill",
1328
- "--ion-checkbox-font-size",
1329
- "--ion-checkbox-gap",
1330
- "--ion-checkbox-line-height",
1331
- "--ion-checkbox-mark",
1332
- "--ion-checkbox-radius",
1333
- "--ion-checkbox-raised",
1334
- "--ion-checkbox-size",
1335
- "--ion-duration-base",
1336
- "--ion-ease-out",
1337
- "--ion-shadow-raised-flush-lg",
1338
- "--ion-shadow-raised-flush-sm",
1339
- "--ion-shadow-raised-flush-xs",
1340
- "--radius-sm",
1341
- "--radius-xs",
2409
+ "--font-weight-medium",
2410
+ "--icon-information",
2411
+ "--icon-success",
2412
+ "--icon-tertiary",
2413
+ "--icon-warning",
2414
+ "--radius-2xs",
1342
2415
  "--spacing-12",
1343
2416
  "--spacing-16",
1344
- "--spacing-20",
1345
- "--spacing-24",
2417
+ "--spacing-2",
1346
2418
  "--spacing-4",
1347
2419
  "--spacing-8",
1348
- "--surface-default",
1349
- "--surface-disabled",
1350
- "--surface-error",
1351
- "--surface-inverse",
1352
- "--surface-primary",
1353
- "--text-disabled",
2420
+ "--text-default",
1354
2421
  "--text-secondary",
1355
- "--type-body",
1356
- "--type-body-line-height",
1357
- "--type-body-sm",
1358
- "--type-body-sm-line-height"
2422
+ "--text-tertiary",
2423
+ "--type-caption",
2424
+ "--type-caption-line-height"
1359
2425
  ],
1360
2426
  "props": {
1361
- "size": {
1362
- "type": "CheckboxSize | undefined",
1363
- "required": false,
1364
- "origin": "own",
1365
- "description": "Matches the Figma `Size` variant: Small, Medium, Large.",
1366
- "values": [
1367
- "sm",
1368
- "md",
1369
- "lg"
1370
- ],
1371
- "default": "md"
1372
- },
1373
- "intent": {
1374
- "type": "CheckboxIntent | undefined",
1375
- "required": false,
2427
+ "level": {
2428
+ "type": "ConfidenceLevel",
2429
+ "required": true,
1376
2430
  "origin": "own",
1377
- "description": "Matches the Figma `Color` variant: Brand, Neutral, Danger.",
1378
2431
  "values": [
1379
- "neutral",
1380
- "brand",
1381
- "danger"
1382
- ],
1383
- "default": "brand"
1384
- },
1385
- "isIndeterminate": {
1386
- "type": "boolean | undefined",
1387
- "required": false,
1388
- "origin": "own",
1389
- "description": "Figma's `Indeterminate` state. Not an HTML attribute — `indeterminate` is a\nDOM property only, so it has to be assigned after render.",
1390
- "default": false
1391
- },
1392
- "isDisabled": {
1393
- "type": "boolean | undefined",
1394
- "required": false,
1395
- "origin": "own",
1396
- "description": "Whether the checkbox is disabled."
2432
+ "low",
2433
+ "medium",
2434
+ "high"
2435
+ ]
1397
2436
  },
1398
- "disabled": {
1399
- "type": "boolean | undefined",
1400
- "required": false,
2437
+ "basis": {
2438
+ "type": "string",
2439
+ "required": true,
1401
2440
  "origin": "own",
1402
- "tags": {
1403
- "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
1404
- }
2441
+ "description": "What the level is based on. REQUIRED, and the reason this component exists\nrather than a coloured dot: a confidence with no stated basis is a number\nthe reader has no way to weigh. \"3 of 4 sources agree\", \"no matching\nrecords found\", \"single unverified source\"."
1405
2442
  },
1406
- "children": {
1407
- "type": "React.ReactNode",
2443
+ "label": {
2444
+ "type": "string | undefined",
1408
2445
  "required": false,
1409
2446
  "origin": "own",
1410
- "description": "Figma's `Show Label` + `Label`. Omit for a bare box."
2447
+ "description": "Override the level's word. Keep it a word, not a percentage."
1411
2448
  }
1412
2449
  },
1413
2450
  "propCounts": {
1414
- "own": 6,
2451
+ "own": 3,
1415
2452
  "aria": 0,
1416
- "dom": 304,
2453
+ "dom": 277,
1417
2454
  "other": 0
1418
2455
  }
1419
2456
  },
@@ -2133,13 +3170,13 @@
2133
3170
  "description": "Identifies the element (or elements) whose contents or presence are controlled by the current\nelement."
2134
3171
  },
2135
3172
  "enterKeyHint": {
2136
- "type": "\"enter\" | \"done\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\" | undefined",
3173
+ "type": "\"done\" | \"enter\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\" | undefined",
2137
3174
  "required": false,
2138
3175
  "origin": "aria",
2139
3176
  "description": "An enumerated attribute that defines what action label or icon to preset for the enter key on\nvirtual keyboards. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/enterkeyhint).",
2140
3177
  "values": [
2141
- "enter",
2142
3178
  "done",
3179
+ "enter",
2143
3180
  "go",
2144
3181
  "next",
2145
3182
  "previous",
@@ -3661,6 +4698,42 @@
3661
4698
  "origin": "own",
3662
4699
  "description": "Spread onto the dial-code `<button>`. This is how a country picker gets\nattached — see the note on scope below."
3663
4700
  },
4701
+ "type": {
4702
+ "type": "(string & {}) | \"search\" | \"text\" | \"url\" | \"tel\" | \"email\" | \"password\" | undefined",
4703
+ "required": false,
4704
+ "origin": "aria",
4705
+ "description": "The type of input to render. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdeftype).",
4706
+ "tags": {
4707
+ "default": "'text'"
4708
+ },
4709
+ "default": "tel"
4710
+ },
4711
+ "disabled": {
4712
+ "type": "boolean | undefined",
4713
+ "required": false,
4714
+ "origin": "own",
4715
+ "tags": {
4716
+ "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
4717
+ }
4718
+ },
4719
+ "form": {
4720
+ "type": "string | undefined",
4721
+ "required": false,
4722
+ "origin": "aria",
4723
+ "description": "The `<form>` element to associate the input with.\nThe value of this attribute must be the id of a `<form>` in the same document.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form)."
4724
+ },
4725
+ "name": {
4726
+ "type": "string | undefined",
4727
+ "required": false,
4728
+ "origin": "aria",
4729
+ "description": "The name of the input element, used when submitting an HTML form. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname)."
4730
+ },
4731
+ "value": {
4732
+ "type": "string | undefined",
4733
+ "required": false,
4734
+ "origin": "aria",
4735
+ "description": "The current value (controlled)."
4736
+ },
3664
4737
  "defaultValue": {
3665
4738
  "type": "string | undefined",
3666
4739
  "required": false,
@@ -3694,13 +4767,13 @@
3694
4767
  "description": "Class names for the control box (`.ion-input`). Always lands here —\nnever on the form-field wrapper. Use `wrapperClassName` for that."
3695
4768
  },
3696
4769
  "enterKeyHint": {
3697
- "type": "\"enter\" | \"done\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\" | undefined",
4770
+ "type": "\"done\" | \"enter\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\" | undefined",
3698
4771
  "required": false,
3699
4772
  "origin": "aria",
3700
4773
  "description": "An enumerated attribute that defines what action label or icon to preset for the enter key on\nvirtual keyboards. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/enterkeyhint).",
3701
4774
  "values": [
3702
- "enter",
3703
4775
  "done",
4776
+ "enter",
3704
4777
  "go",
3705
4778
  "next",
3706
4779
  "previous",
@@ -3898,36 +4971,12 @@
3898
4971
  ],
3899
4972
  "default": "md"
3900
4973
  },
3901
- "type": {
3902
- "type": "(string & {}) | \"search\" | \"text\" | \"url\" | \"tel\" | \"email\" | \"password\" | undefined",
3903
- "required": false,
3904
- "origin": "aria",
3905
- "description": "The type of input to render. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdeftype).",
3906
- "tags": {
3907
- "default": "'text'"
3908
- },
3909
- "default": "tel"
3910
- },
3911
4974
  "autoComplete": {
3912
4975
  "type": "string | undefined",
3913
4976
  "required": false,
3914
4977
  "origin": "aria",
3915
4978
  "description": "Describes the type of autocomplete functionality the input should provide if any. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefautocomplete)."
3916
4979
  },
3917
- "disabled": {
3918
- "type": "boolean | undefined",
3919
- "required": false,
3920
- "origin": "own",
3921
- "tags": {
3922
- "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
3923
- }
3924
- },
3925
- "form": {
3926
- "type": "string | undefined",
3927
- "required": false,
3928
- "origin": "aria",
3929
- "description": "The `<form>` element to associate the input with.\nThe value of this attribute must be the id of a `<form>` in the same document.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form)."
3930
- },
3931
4980
  "maxLength": {
3932
4981
  "type": "number | undefined",
3933
4982
  "required": false,
@@ -3940,12 +4989,6 @@
3940
4989
  "origin": "aria",
3941
4990
  "description": "The minimum number of characters required by the input. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefminlength)."
3942
4991
  },
3943
- "name": {
3944
- "type": "string | undefined",
3945
- "required": false,
3946
- "origin": "aria",
3947
- "description": "The name of the input element, used when submitting an HTML form. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname)."
3948
- },
3949
4992
  "pattern": {
3950
4993
  "type": "string | undefined",
3951
4994
  "required": false,
@@ -3958,12 +5001,6 @@
3958
5001
  "origin": "aria",
3959
5002
  "description": "Temporary text that occupies the text input when it is empty."
3960
5003
  },
3961
- "value": {
3962
- "type": "string | undefined",
3963
- "required": false,
3964
- "origin": "aria",
3965
- "description": "The current value (controlled)."
3966
- },
3967
5004
  "isDisabled": {
3968
5005
  "type": "boolean | undefined",
3969
5006
  "required": false,
@@ -4975,6 +6012,105 @@
4975
6012
  "other": 0
4976
6013
  }
4977
6014
  },
6015
+ "StreamingText": {
6016
+ "name": "StreamingText",
6017
+ "source": "src/components/StreamingText.tsx",
6018
+ "propsType": "StreamingTextProps",
6019
+ "description": "StreamingText — model output arriving a token at a time.\n\nIT IS NOT A LIVE REGION, AND THAT IS THE WHOLE DESIGN\n\nThe obvious implementation — `aria-live=\"polite\"` on the container — is the\none that makes a screen reader unusable. Every token mutation queues an\nannouncement, so the user hears the answer re-read, stuttered, dozens of\ntimes, and cannot get ahead of it. `aria-live=\"off\"` is not an oversight\nhere; it is the accessible choice.\n\nWhat it does instead: `aria-busy` while streaming, so assistive tech knows\nthe region is unsettled and can wait. The text is ordinary readable content\nthroughout — a screen-reader user navigates into it whenever they want,\nexactly like sighted users reading ahead of the cursor.\n\nANNOUNCING COMPLETION IS THE CALLER'S CALL, not this component's. Some\nsurfaces want \"response complete\"; a chat with ten turns on screen does not\nwant ten of them. Render your own `role=\"status\"` when you want it.\n\nTHE HEIGHT IS RESERVED, NOT ANIMATED. `minLines` holds space in `lh` units so\nthe content below stays still. A container that grows token by token drags\nthe whole page, which is worse for someone using magnification than the wait.\n\nThe cursor is CSS and `aria-hidden`. It stops blinking under\n`prefers-reduced-motion` — a blinking element is a WCAG 2.3.1 concern and a\ngenuine problem for some vestibular and attention conditions.",
6020
+ "import": "import { StreamingText } from 'ionbase-ui';",
6021
+ "status": "stable",
6022
+ "summary": "Model output arriving a token at a time, in a container that does not move. Deliberately not a live region.",
6023
+ "useWhen": [
6024
+ "rendering a response that streams in rather than arriving whole",
6025
+ "the reader should be able to start reading before generation finishes"
6026
+ ],
6027
+ "useInstead": [
6028
+ {
6029
+ "when": "the text is already complete",
6030
+ "use": "Alert",
6031
+ "why": "nothing about this component helps once there is nothing left to arrive"
6032
+ },
6033
+ {
6034
+ "when": "you are showing progress through steps rather than producing text",
6035
+ "use": "AgentActivity"
6036
+ }
6037
+ ],
6038
+ "a11y": {
6039
+ "role": "region when `label` is set, none otherwise",
6040
+ "guarantees": [
6041
+ "`aria-live=\"off\"`, deliberately — a polite live region re-announces the whole answer on every token, which makes a streaming response unusable with a screen reader",
6042
+ "`aria-busy` while streaming, so assistive tech knows the region is unsettled",
6043
+ "the text stays ordinary readable content throughout, so a screen-reader user can read ahead exactly as a sighted user does",
6044
+ "the cursor is `aria-hidden`, is a styled box rather than a text character, and stops blinking under `prefers-reduced-motion` (WCAG 2.3.1)"
6045
+ ],
6046
+ "requires": [
6047
+ "announcing completion yourself, with your own `role=\"status\"`, if the surface wants it — a chat with ten turns on screen does not want ten announcements",
6048
+ "`label` when the region needs a name of its own"
6049
+ ]
6050
+ },
6051
+ "antiPatterns": [
6052
+ {
6053
+ "dont": "wrapping it in `aria-live=\"polite\"`",
6054
+ "why": "every token queues an announcement; the user hears the answer stuttered dozens of times and cannot get ahead of it"
6055
+ },
6056
+ {
6057
+ "dont": "letting the container grow token by token with no reserved height",
6058
+ "do": "`minLines`",
6059
+ "why": "the content below climbs the screen while someone is reading it, which is worse under magnification than the wait"
6060
+ },
6061
+ {
6062
+ "dont": "a text-character cursor like ▌",
6063
+ "why": "some screen readers read it aloud even inside an aria-hidden span"
6064
+ }
6065
+ ],
6066
+ "stylesheet": "src/styles/streaming-text.css",
6067
+ "tokens": [
6068
+ "--font-family-sans",
6069
+ "--ion-streaming-min-lines",
6070
+ "--text-default",
6071
+ "--text-tertiary",
6072
+ "--type-body",
6073
+ "--type-body-line-height"
6074
+ ],
6075
+ "props": {
6076
+ "children": {
6077
+ "type": "React.ReactNode",
6078
+ "required": false,
6079
+ "origin": "own",
6080
+ "description": "The text so far. Re-render with more of it; this component appends nothing."
6081
+ },
6082
+ "isStreaming": {
6083
+ "type": "boolean | undefined",
6084
+ "required": false,
6085
+ "origin": "own",
6086
+ "description": "More is still arriving. Shows the cursor and marks the region busy."
6087
+ },
6088
+ "minLines": {
6089
+ "type": "number | undefined",
6090
+ "required": false,
6091
+ "origin": "own",
6092
+ "description": "Rows of height to hold while the text is short, so the page below does not\nclimb the screen as tokens arrive. Costs blank space at the start and buys\na layout that does not move under a reader."
6093
+ },
6094
+ "label": {
6095
+ "type": "string | undefined",
6096
+ "required": false,
6097
+ "origin": "own",
6098
+ "description": "Accessible name for the region."
6099
+ },
6100
+ "hideCursor": {
6101
+ "type": "boolean | undefined",
6102
+ "required": false,
6103
+ "origin": "own",
6104
+ "description": "Hide the trailing cursor. The text still marks itself busy."
6105
+ }
6106
+ },
6107
+ "propCounts": {
6108
+ "own": 5,
6109
+ "aria": 0,
6110
+ "dom": 277,
6111
+ "other": 0
6112
+ }
6113
+ },
4978
6114
  "TabItem": {
4979
6115
  "name": "TabItem",
4980
6116
  "source": "../../node_modules/.pnpm/react-stately@3.48.0_react@19.2.8/node_modules/react-stately/dist/types/src/collections/Item.d.ts",