ionbase-ui 0.48.0 → 0.59.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/Accordion.d.ts +54 -0
  3. package/dist/components/Accordion.d.ts.map +1 -0
  4. package/dist/components/Accordion.js +68 -0
  5. package/dist/components/Accordion.js.map +1 -0
  6. package/dist/components/Breadcrumb.d.ts +45 -0
  7. package/dist/components/Breadcrumb.d.ts.map +1 -0
  8. package/dist/components/Breadcrumb.js +31 -0
  9. package/dist/components/Breadcrumb.js.map +1 -0
  10. package/dist/components/Checkbox.d.ts +2 -1
  11. package/dist/components/Checkbox.d.ts.map +1 -1
  12. package/dist/components/Checkbox.js +4 -2
  13. package/dist/components/Checkbox.js.map +1 -1
  14. package/dist/components/Combobox.d.ts +86 -0
  15. package/dist/components/Combobox.d.ts.map +1 -0
  16. package/dist/components/Combobox.js +261 -0
  17. package/dist/components/Combobox.js.map +1 -0
  18. package/dist/components/Drawer.d.ts +52 -0
  19. package/dist/components/Drawer.d.ts.map +1 -0
  20. package/dist/components/Drawer.js +66 -0
  21. package/dist/components/Drawer.js.map +1 -0
  22. package/dist/components/FileUpload.d.ts +79 -0
  23. package/dist/components/FileUpload.d.ts.map +1 -0
  24. package/dist/components/FileUpload.js +218 -0
  25. package/dist/components/FileUpload.js.map +1 -0
  26. package/dist/components/ProgressBar.d.ts +46 -0
  27. package/dist/components/ProgressBar.d.ts.map +1 -0
  28. package/dist/components/ProgressBar.js +52 -0
  29. package/dist/components/ProgressBar.js.map +1 -0
  30. package/dist/components/Select.d.ts +9 -2
  31. package/dist/components/Select.d.ts.map +1 -1
  32. package/dist/components/Select.js.map +1 -1
  33. package/dist/components/Skeleton.d.ts +39 -0
  34. package/dist/components/Skeleton.d.ts.map +1 -0
  35. package/dist/components/Skeleton.js +50 -0
  36. package/dist/components/Skeleton.js.map +1 -0
  37. package/dist/components/Spinner.d.ts +45 -0
  38. package/dist/components/Spinner.d.ts.map +1 -0
  39. package/dist/components/Spinner.js +44 -0
  40. package/dist/components/Spinner.js.map +1 -0
  41. package/dist/components/Textarea.d.ts +50 -0
  42. package/dist/components/Textarea.d.ts.map +1 -0
  43. package/dist/components/Textarea.js +48 -0
  44. package/dist/components/Textarea.js.map +1 -0
  45. package/dist/components/Toggle.d.ts +2 -1
  46. package/dist/components/Toggle.d.ts.map +1 -1
  47. package/dist/components/Toggle.js +4 -2
  48. package/dist/components/Toggle.js.map +1 -1
  49. package/dist/components/index.d.ts +18 -0
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/index.js +9 -0
  52. package/dist/components/index.js.map +1 -1
  53. package/dist/components/resolve-selection.d.ts +36 -0
  54. package/dist/components/resolve-selection.d.ts.map +1 -0
  55. package/dist/components/resolve-selection.js +14 -0
  56. package/dist/components/resolve-selection.js.map +1 -0
  57. package/dist/figma-descriptions.json +204 -144
  58. package/dist/figma-map.json +2311 -1643
  59. package/dist/meta/Accordion.json +123 -0
  60. package/dist/meta/AccordionItem.json +90 -0
  61. package/dist/meta/Breadcrumb.json +87 -0
  62. package/dist/meta/BreadcrumbItem.json +78 -0
  63. package/dist/meta/Checkbox.json +18 -1
  64. package/dist/meta/Combobox.json +265 -0
  65. package/dist/meta/Drawer.json +234 -0
  66. package/dist/meta/EmptyState.json +1 -1
  67. package/dist/meta/FileUpload.json +246 -0
  68. package/dist/meta/Input.json +0 -1
  69. package/dist/meta/PhoneInput.json +39 -39
  70. package/dist/meta/Popover.json +3 -3
  71. package/dist/meta/ProgressBar.json +166 -0
  72. package/dist/meta/Select.json +4 -3
  73. package/dist/meta/Skeleton.json +120 -0
  74. package/dist/meta/Spinner.json +123 -0
  75. package/dist/meta/Textarea.json +520 -0
  76. package/dist/meta/Toggle.json +18 -1
  77. package/dist/meta/Tooltip.json +3 -3
  78. package/dist/meta/components.json +2447 -361
  79. package/dist/meta/contrast.json +3130 -227
  80. package/dist/meta/index.json +149 -7
  81. package/dist/meta/patterns/index.json +1 -1
  82. package/dist/styles/accordion.css +102 -0
  83. package/dist/styles/breadcrumb.css +72 -0
  84. package/dist/styles/combobox.css +154 -0
  85. package/dist/styles/drawer.css +181 -0
  86. package/dist/styles/empty-state.css +16 -4
  87. package/dist/styles/file-upload.css +236 -0
  88. package/dist/styles/index.css +9 -0
  89. package/dist/styles/input.css +34 -15
  90. package/dist/styles/progress-bar.css +107 -0
  91. package/dist/styles/skeleton.css +76 -0
  92. package/dist/styles/spinner.css +70 -0
  93. package/dist/styles/textarea.css +178 -0
  94. package/dist/styles/tokens/base.css +0 -1
  95. package/dist/styles/tokens/theme-dark.css +0 -1
  96. package/dist/tokens/index.d.ts +0 -6
  97. package/dist/tokens/index.d.ts.map +1 -1
  98. package/dist/tokens/index.js +0 -5
  99. package/dist/tokens/index.js.map +1 -1
  100. package/llms.txt +2 -2
  101. package/package.json +19 -19
@@ -1,11 +1,224 @@
1
1
  {
2
2
  "package": "ionbase-ui",
3
- "version": "0.48.0",
3
+ "version": "0.59.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
+ "Accordion": {
10
+ "name": "Accordion",
11
+ "source": "src/components/Accordion.tsx",
12
+ "propsType": "AccordionProps",
13
+ "description": "Accordion — sections that collapse, with the heading structure intact.\n\nWHY THE TRIGGER IS A BUTTON INSIDE A HEADING\n\nBoth parts are load-bearing and the obvious implementations drop one of them.\nThe heading is how a screen-reader user navigates a long page — pressing `h`\nmoves between sections, and an accordion built from `div`s removes every one\nof those stops. The button is what makes the section operable by keyboard and\nannounced as expandable; a heading with a click handler is neither.\n\nSo the shape is `<h3><button aria-expanded aria-controls>`, which is the only\narrangement that keeps document structure AND operability.\n\n`headingLevel` is a prop with no safe default beyond 3, because the right\nlevel depends on the document around it and nothing here can see that. Two\naccordions at the wrong level produce a page whose outline is nonsense while\nlooking perfectly fine.\n\nTHE PANEL IS NOT UNMOUNTED\n\nCollapsed sections keep their DOM and are hidden with `hidden`. Unmounting\nwould lose form state in a collapsed section — the classic multi-step-form\nbug where answers vanish when a section is folded away — and would break\nin-page search, which cannot find text that is not there.",
14
+ "import": "import { Accordion } from 'ionbase-ui';",
15
+ "status": "stable",
16
+ "summary": "Sections that collapse, with the document's heading structure intact. Holds the open set and the heading level.",
17
+ "useWhen": [
18
+ "a page has several long sections and most users need one of them",
19
+ "optional or advanced settings should be present but not in the way",
20
+ "an FAQ or a reference list where scanning titles is the primary action"
21
+ ],
22
+ "useInstead": [
23
+ {
24
+ "when": "the sections are alternatives and only one is ever relevant",
25
+ "use": "Tabs",
26
+ "why": "tabs say \"pick one\"; an accordion says \"all of these exist, open what you need\""
27
+ },
28
+ {
29
+ "when": "hiding the content would hide something the user must act on",
30
+ "use": "render it directly",
31
+ "why": "a required field inside a collapsed section is a form that cannot be completed without exploration"
32
+ }
33
+ ],
34
+ "slots": {
35
+ "children": {
36
+ "accepts": "AccordionItem"
37
+ }
38
+ },
39
+ "a11y": {
40
+ "requires": [
41
+ "`headingLevel` that fits the surrounding document — the default of 3 is a guess this component cannot verify"
42
+ ],
43
+ "notes": [
44
+ "each section's trigger is a button inside a heading, which is the only arrangement that keeps both document structure and operability",
45
+ "collapsed panels are hidden rather than unmounted, so form state survives and in-page search still finds the text"
46
+ ]
47
+ },
48
+ "antiPatterns": [
49
+ {
50
+ "dont": "allowsMultiple on a long FAQ",
51
+ "do": "leave it off, so opening one section closes the last",
52
+ "why": "with every section openable at once the page has no maximum height, and a user who opens six is scrolling a wall of text looking for the one they wanted. Reach for it when sections are settings or reference material a user may need to compare side by side"
53
+ },
54
+ {
55
+ "dont": "headingLevel left at 3 inside a section already under an h4",
56
+ "do": "headingLevel={5}",
57
+ "why": "the page looks correct and its outline is nonsense — a heading-level jump is invisible to sighted users and disorienting to everyone navigating by headings"
58
+ },
59
+ {
60
+ "dont": "an accordion holding a single section",
61
+ "do": "a heading and the content",
62
+ "why": "one collapsible section is a control that exists to hide the only thing on screen"
63
+ }
64
+ ],
65
+ "stylesheet": "src/styles/accordion.css",
66
+ "tokens": [
67
+ "--border-focus",
68
+ "--border-subtle",
69
+ "--border-width-default",
70
+ "--border-width-thick",
71
+ "--font-family-sans",
72
+ "--font-weight-medium",
73
+ "--icon-disabled",
74
+ "--icon-secondary",
75
+ "--icon-size-md",
76
+ "--ion-duration-base",
77
+ "--ion-ease-out",
78
+ "--radius-xs",
79
+ "--spacing-12",
80
+ "--spacing-16",
81
+ "--spacing-4",
82
+ "--text-default",
83
+ "--text-disabled",
84
+ "--text-primary",
85
+ "--text-secondary",
86
+ "--type-body",
87
+ "--type-body-line-height"
88
+ ],
89
+ "props": {
90
+ "allowsMultiple": {
91
+ "type": "boolean | undefined",
92
+ "required": false,
93
+ "origin": "own",
94
+ "description": "Let more than one section be open at once."
95
+ },
96
+ "defaultExpandedKeys": {
97
+ "type": "string[] | undefined",
98
+ "required": false,
99
+ "origin": "own",
100
+ "description": "Uncontrolled: which sections start open."
101
+ },
102
+ "expandedKeys": {
103
+ "type": "string[] | undefined",
104
+ "required": false,
105
+ "origin": "own",
106
+ "description": "Controlled: which sections are open. Pass `onExpandedChange` with it."
107
+ },
108
+ "onExpandedChange": {
109
+ "type": "((keys: string[]) => void) | undefined",
110
+ "required": false,
111
+ "origin": "own"
112
+ },
113
+ "headingLevel": {
114
+ "type": "AccordionHeadingLevel | undefined",
115
+ "required": false,
116
+ "origin": "own",
117
+ "description": "The level each section's heading renders at. Must fit the surrounding\ndocument — a panel inside an `h2` section wants `3`."
118
+ },
119
+ "children": {
120
+ "type": "React.ReactNode",
121
+ "required": false,
122
+ "origin": "own"
123
+ }
124
+ },
125
+ "propCounts": {
126
+ "own": 6,
127
+ "aria": 0,
128
+ "dom": 276,
129
+ "other": 0
130
+ }
131
+ },
132
+ "AccordionItem": {
133
+ "name": "AccordionItem",
134
+ "source": "src/components/Accordion.tsx",
135
+ "propsType": "AccordionItemProps",
136
+ "import": "import { AccordionItem } from 'ionbase-ui';",
137
+ "status": "stable",
138
+ "summary": "One collapsible section: an always-visible title that toggles a region beneath it.",
139
+ "useWhen": [
140
+ "inside an Accordion, once per section"
141
+ ],
142
+ "slots": {
143
+ "title": {
144
+ "accepts": "text",
145
+ "note": "always visible, so it must say what is inside — \"Advanced\" tells the user nothing about whether to open it"
146
+ },
147
+ "children": {
148
+ "accepts": "any content"
149
+ }
150
+ },
151
+ "a11y": {
152
+ "requires": [
153
+ "a stable `id`, which is the section's identity in the open set — an index breaks the moment the list reorders"
154
+ ],
155
+ "notes": [
156
+ "the trigger carries aria-expanded and aria-controls; the panel is a region labelled by the trigger",
157
+ "the panel keeps its DOM when collapsed, so a half-filled form inside one does not lose its answers"
158
+ ]
159
+ },
160
+ "antiPatterns": [
161
+ {
162
+ "dont": "a required form field inside a collapsed section",
163
+ "do": "keep required fields outside, or expand the section when validation fails",
164
+ "why": "a user cannot fix an error they cannot see, and the browser cannot focus a hidden invalid field"
165
+ }
166
+ ],
167
+ "stylesheet": "src/styles/accordion.css",
168
+ "tokens": [
169
+ "--border-focus",
170
+ "--border-subtle",
171
+ "--border-width-default",
172
+ "--border-width-thick",
173
+ "--font-family-sans",
174
+ "--font-weight-medium",
175
+ "--icon-disabled",
176
+ "--icon-secondary",
177
+ "--icon-size-md",
178
+ "--ion-duration-base",
179
+ "--ion-ease-out",
180
+ "--radius-xs",
181
+ "--spacing-12",
182
+ "--spacing-16",
183
+ "--spacing-4",
184
+ "--text-default",
185
+ "--text-disabled",
186
+ "--text-primary",
187
+ "--text-secondary",
188
+ "--type-body",
189
+ "--type-body-line-height"
190
+ ],
191
+ "props": {
192
+ "id": {
193
+ "type": "string",
194
+ "required": true,
195
+ "origin": "own",
196
+ "description": "Stable identity for the open set. Required."
197
+ },
198
+ "title": {
199
+ "type": "React.ReactNode",
200
+ "required": true,
201
+ "origin": "own",
202
+ "description": "The always-visible label."
203
+ },
204
+ "isDisabled": {
205
+ "type": "boolean | undefined",
206
+ "required": false,
207
+ "origin": "own"
208
+ },
209
+ "children": {
210
+ "type": "React.ReactNode",
211
+ "required": false,
212
+ "origin": "own"
213
+ }
214
+ },
215
+ "propCounts": {
216
+ "own": 4,
217
+ "aria": 0,
218
+ "dom": 275,
219
+ "other": 0
220
+ }
221
+ },
9
222
  "AgentActivity": {
10
223
  "name": "AgentActivity",
11
224
  "source": "src/components/AgentActivity.tsx",
@@ -1751,127 +1964,292 @@
1751
1964
  "other": 0
1752
1965
  }
1753
1966
  },
1754
- "Button": {
1755
- "name": "Button",
1756
- "source": "src/components/Button.tsx",
1757
- "propsType": "ButtonProps",
1758
- "import": "import { Button } from 'ionbase-ui';",
1967
+ "Breadcrumb": {
1968
+ "name": "Breadcrumb",
1969
+ "source": "src/components/Breadcrumb.tsx",
1970
+ "propsType": "BreadcrumbProps",
1971
+ "description": "Breadcrumb — where this page sits, and how to get back up.\n\nWHY AN ORDERED LIST INSIDE A NAMED LANDMARK\n\nThe trail is a sequence, and `ol` is what says so: a screen reader announces\n\"list, 4 items\" and the position within it, which is the entire content of a\nbreadcrumb. A row of `div`s with slashes conveys none of that, and the\nslashes themselves get read out as punctuation.\n\nThe separator here is drawn by CSS `::before` on each item after the first,\nso it is decoration that never enters the accessibility tree. Putting a \"/\"\nin the markup is the usual version of this component and the usual defect.\n\nTHE LAST CRUMB IS NOT A LINK\n\n`isCurrent` renders text with `aria-current=\"page\"`. A link to the page you\nare already on is a dead control: it announces as a link, invites a click,\nand does nothing. This is the most common breadcrumb bug and the reason the\nprop exists rather than being inferred from position — a trail whose last\ncrumb IS a link to somewhere else is legitimate, and inferring would break it.",
1972
+ "import": "import { Breadcrumb } from 'ionbase-ui';",
1759
1973
  "status": "stable",
1760
- "summary": "Triggers an action in place. Never navigates.",
1974
+ "summary": "Where this page sits in the hierarchy, and how to get back up. An ordered list inside a named landmark.",
1761
1975
  "useWhen": [
1762
- "the interaction performs an action — submit, save, delete, open a dialog",
1763
- "the result stays on the current page"
1976
+ "a page sits more than one level deep and the parent is not otherwise reachable",
1977
+ "a record view needs its collection one click away",
1978
+ "the hierarchy itself is information the user needs — which project, which environment"
1764
1979
  ],
1765
1980
  "useInstead": [
1766
1981
  {
1767
- "when": "it navigates to a URL or changes route",
1768
- "use": "Link",
1769
- "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."
1982
+ "when": "the destinations are siblings rather than ancestors",
1983
+ "use": "Tabs",
1984
+ "why": "a breadcrumb describes depth; tabs describe alternatives at the same depth"
1770
1985
  },
1771
1986
  {
1772
- "when": "it toggles a persistent boolean setting",
1773
- "use": "Toggle",
1774
- "why": "a toggle announces its on/off state; a button announces only its name"
1987
+ "when": "the trail is the primary navigation for the whole app",
1988
+ "use": "NavItem inside the app shell",
1989
+ "why": "a breadcrumb supplements navigation, it does not replace it — it appears after a user has already gone somewhere"
1775
1990
  }
1776
1991
  ],
1777
- "variants": {
1778
- "variant": {
1779
- "primary-brand": {
1780
- "use": "the single most important action in a view",
1781
- "limit": "one per view or dialog"
1782
- },
1783
- "primary-neutral": {
1784
- "use": "a primary action that should not carry brand colour"
1785
- },
1786
- "primary-soft": {
1787
- "use": "a primary action on a dense or already-colourful surface"
1788
- },
1789
- "secondary": {
1790
- "use": "supporting actions shown alongside a primary one"
1791
- },
1792
- "tertiary": {
1793
- "use": "low-emphasis actions — Cancel, Back, inline row actions"
1794
- },
1795
- "destructive": {
1796
- "use": "irreversible actions",
1797
- "requires": "a Modal confirmation before the action runs"
1798
- },
1799
- "success": {
1800
- "use": "confirms the end of a flow. Rarely the right answer — prefer primary-brand."
1801
- }
1802
- },
1803
- "size": {
1804
- "sm": {
1805
- "use": "table rows, toolbars, dense layouts"
1806
- },
1807
- "md": {
1808
- "use": "the default; forms and page-level actions"
1809
- },
1810
- "lg": {
1811
- "use": "prominent standalone actions"
1812
- },
1813
- "xl": {
1814
- "use": "marketing and empty-state calls to action"
1815
- }
1816
- }
1817
- },
1818
1992
  "slots": {
1819
- "startIcon": {
1820
- "accepts": "Icon",
1821
- "note": "omit Icon's `label` — the Button's own text already names it, and a labelled icon is announced twice"
1822
- },
1823
- "endIcon": {
1824
- "accepts": "Icon",
1825
- "note": "same as startIcon"
1993
+ "children": {
1994
+ "accepts": "BreadcrumbItem",
1995
+ "note": "ancestor first, current page last. Anything else and the trail reads backwards"
1826
1996
  }
1827
1997
  },
1828
1998
  "a11y": {
1829
- "role": "button",
1830
- "guarantees": [
1831
- "the focus ring appears for keyboard focus and not for a mouse click",
1832
- "hover does not latch on touch devices"
1833
- ],
1834
1999
  "requires": [
1835
- "an accessible name — `children`, or `aria-label` when the button is icon-only"
2000
+ "`label` when a page has more than one nav landmark — it defaults to \"Breadcrumb\", which is what screen-reader users listen for"
2001
+ ],
2002
+ "notes": [
2003
+ "the separator is drawn by CSS ::before and never enters the accessibility tree; a \"/\" in the markup is announced as punctuation between every crumb",
2004
+ "the list is an `ol`, so the position and length are announced — which is the entire content of a breadcrumb"
1836
2005
  ]
1837
2006
  },
1838
2007
  "antiPatterns": [
1839
2008
  {
1840
- "dont": "<Button onPress={() => router.push('/settings')}>Settings</Button>",
1841
- "do": "<Link href=\"/settings\">Settings</Link>",
1842
- "why": "a button that navigates breaks middle-click, cmd-click and copy-link, and is announced as a button rather than a link"
1843
- },
1844
- {
1845
- "dont": "two variant=\"primary-brand\" buttons in the same dialog",
1846
- "do": "one primary-brand, the rest secondary or tertiary",
1847
- "why": "two primaries is the same as none — nothing indicates the expected action"
1848
- },
1849
- {
1850
- "dont": "<Button variant=\"destructive\" onPress={deleteAccount}>",
1851
- "do": "open a Modal first and put the destructive Button in its footer",
1852
- "why": "irreversible actions need a confirmation step"
1853
- },
1854
- {
1855
- "dont": "<Button><Icon as={Trash} /></Button>",
1856
- "do": "<Button aria-label=\"Delete\"><Icon as={Trash} /></Button>",
1857
- "why": "an icon-only button with no aria-label is announced as just 'button'"
2009
+ "dont": "<div>Home / Projects / Invoice 12</div>",
2010
+ "do": "<Breadcrumb><BreadcrumbItem href=\"/\">Home</BreadcrumbItem>…</Breadcrumb>",
2011
+ "why": "a row of divs conveys neither sequence nor position, and the slashes are read aloud"
1858
2012
  },
1859
2013
  {
1860
- "dont": "<Button onClick={...}>",
1861
- "do": "<Button onPress={...}>",
1862
- "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."
1863
- }
1864
- ],
1865
- "deprecated": [
1866
- {
1867
- "prop": "disabled",
1868
- "replacement": "isDisabled",
1869
- "note": "still type-checks, so this will not fail loudly"
2014
+ "dont": "a full trail on a top-level page",
2015
+ "do": "omit it",
2016
+ "why": "a breadcrumb with one crumb is furniture — it takes vertical space to say nothing"
1870
2017
  }
1871
2018
  ],
1872
- "stylesheet": "src/styles/button.css",
2019
+ "stylesheet": "src/styles/breadcrumb.css",
1873
2020
  "tokens": [
1874
- "--border-disabled",
2021
+ "--border-focus",
2022
+ "--border-width-thick",
2023
+ "--font-family-sans",
2024
+ "--font-weight-medium",
2025
+ "--radius-xs",
2026
+ "--spacing-8",
2027
+ "--text-default",
2028
+ "--text-secondary",
2029
+ "--text-tertiary",
2030
+ "--type-body-sm",
2031
+ "--type-body-sm-line-height"
2032
+ ],
2033
+ "props": {
2034
+ "label": {
2035
+ "type": "string | undefined",
2036
+ "required": false,
2037
+ "origin": "own",
2038
+ "description": "Names the landmark. A page with more than one `nav` needs them told apart,\nand \"Breadcrumb\" is what screen-reader users are listening for."
2039
+ },
2040
+ "children": {
2041
+ "type": "React.ReactNode",
2042
+ "required": false,
2043
+ "origin": "own",
2044
+ "description": "`BreadcrumbItem` elements, ancestor first."
2045
+ }
2046
+ },
2047
+ "propCounts": {
2048
+ "own": 2,
2049
+ "aria": 0,
2050
+ "dom": 277,
2051
+ "other": 0
2052
+ }
2053
+ },
2054
+ "BreadcrumbItem": {
2055
+ "name": "BreadcrumbItem",
2056
+ "source": "src/components/Breadcrumb.tsx",
2057
+ "propsType": "BreadcrumbItemProps",
2058
+ "import": "import { BreadcrumbItem } from 'ionbase-ui';",
2059
+ "status": "stable",
2060
+ "summary": "One crumb. A link to an ancestor, or the current page rendered as text.",
2061
+ "useWhen": [
2062
+ "inside a Breadcrumb, once per level"
2063
+ ],
2064
+ "useInstead": [
2065
+ {
2066
+ "when": "outside a breadcrumb trail",
2067
+ "use": "Link",
2068
+ "why": "the crumb's density, colour and truncation are tuned for a trail and look wrong on their own"
2069
+ }
2070
+ ],
2071
+ "slots": {
2072
+ "children": {
2073
+ "accepts": "text",
2074
+ "note": "the name of the level, not its path"
2075
+ }
2076
+ },
2077
+ "a11y": {
2078
+ "requires": [
2079
+ "`isCurrent` on the last crumb, which renders it as text with aria-current=\"page\" rather than as a link"
2080
+ ],
2081
+ "notes": [
2082
+ "isCurrent is a prop rather than inferred from position: a trail whose last crumb legitimately links elsewhere exists, and inferring would break it"
2083
+ ]
2084
+ },
2085
+ "antiPatterns": [
2086
+ {
2087
+ "dont": "<BreadcrumbItem href=\"/invoices/12\">Invoice 12</BreadcrumbItem> for the page you are on",
2088
+ "do": "<BreadcrumbItem isCurrent>Invoice 12</BreadcrumbItem>",
2089
+ "why": "a link to the current page is a dead control — it announces as a link, invites a click and does nothing"
2090
+ }
2091
+ ],
2092
+ "stylesheet": "src/styles/breadcrumb.css",
2093
+ "tokens": [
2094
+ "--border-focus",
2095
+ "--border-width-thick",
2096
+ "--font-family-sans",
2097
+ "--font-weight-medium",
2098
+ "--radius-xs",
2099
+ "--spacing-8",
2100
+ "--text-default",
2101
+ "--text-secondary",
2102
+ "--text-tertiary",
2103
+ "--type-body-sm",
2104
+ "--type-body-sm-line-height"
2105
+ ],
2106
+ "props": {
2107
+ "href": {
2108
+ "type": "string | undefined",
2109
+ "required": false,
2110
+ "origin": "own",
2111
+ "description": "Omit on the current page — the last crumb is not a link."
2112
+ },
2113
+ "isCurrent": {
2114
+ "type": "boolean | undefined",
2115
+ "required": false,
2116
+ "origin": "own",
2117
+ "description": "The page the user is on. Marked `aria-current=\"page\"` and rendered as text\nrather than a link."
2118
+ },
2119
+ "children": {
2120
+ "type": "React.ReactNode",
2121
+ "required": false,
2122
+ "origin": "own"
2123
+ }
2124
+ },
2125
+ "propCounts": {
2126
+ "own": 3,
2127
+ "aria": 0,
2128
+ "dom": 277,
2129
+ "other": 0
2130
+ }
2131
+ },
2132
+ "Button": {
2133
+ "name": "Button",
2134
+ "source": "src/components/Button.tsx",
2135
+ "propsType": "ButtonProps",
2136
+ "import": "import { Button } from 'ionbase-ui';",
2137
+ "status": "stable",
2138
+ "summary": "Triggers an action in place. Never navigates.",
2139
+ "useWhen": [
2140
+ "the interaction performs an action — submit, save, delete, open a dialog",
2141
+ "the result stays on the current page"
2142
+ ],
2143
+ "useInstead": [
2144
+ {
2145
+ "when": "it navigates to a URL or changes route",
2146
+ "use": "Link",
2147
+ "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."
2148
+ },
2149
+ {
2150
+ "when": "it toggles a persistent boolean setting",
2151
+ "use": "Toggle",
2152
+ "why": "a toggle announces its on/off state; a button announces only its name"
2153
+ }
2154
+ ],
2155
+ "variants": {
2156
+ "variant": {
2157
+ "primary-brand": {
2158
+ "use": "the single most important action in a view",
2159
+ "limit": "one per view or dialog"
2160
+ },
2161
+ "primary-neutral": {
2162
+ "use": "a primary action that should not carry brand colour"
2163
+ },
2164
+ "primary-soft": {
2165
+ "use": "a primary action on a dense or already-colourful surface"
2166
+ },
2167
+ "secondary": {
2168
+ "use": "supporting actions shown alongside a primary one"
2169
+ },
2170
+ "tertiary": {
2171
+ "use": "low-emphasis actions — Cancel, Back, inline row actions"
2172
+ },
2173
+ "destructive": {
2174
+ "use": "irreversible actions",
2175
+ "requires": "a Modal confirmation before the action runs"
2176
+ },
2177
+ "success": {
2178
+ "use": "confirms the end of a flow. Rarely the right answer — prefer primary-brand."
2179
+ }
2180
+ },
2181
+ "size": {
2182
+ "sm": {
2183
+ "use": "table rows, toolbars, dense layouts"
2184
+ },
2185
+ "md": {
2186
+ "use": "the default; forms and page-level actions"
2187
+ },
2188
+ "lg": {
2189
+ "use": "prominent standalone actions"
2190
+ },
2191
+ "xl": {
2192
+ "use": "marketing and empty-state calls to action"
2193
+ }
2194
+ }
2195
+ },
2196
+ "slots": {
2197
+ "startIcon": {
2198
+ "accepts": "Icon",
2199
+ "note": "omit Icon's `label` — the Button's own text already names it, and a labelled icon is announced twice"
2200
+ },
2201
+ "endIcon": {
2202
+ "accepts": "Icon",
2203
+ "note": "same as startIcon"
2204
+ }
2205
+ },
2206
+ "a11y": {
2207
+ "role": "button",
2208
+ "guarantees": [
2209
+ "the focus ring appears for keyboard focus and not for a mouse click",
2210
+ "hover does not latch on touch devices"
2211
+ ],
2212
+ "requires": [
2213
+ "an accessible name — `children`, or `aria-label` when the button is icon-only"
2214
+ ]
2215
+ },
2216
+ "antiPatterns": [
2217
+ {
2218
+ "dont": "<Button onPress={() => router.push('/settings')}>Settings</Button>",
2219
+ "do": "<Link href=\"/settings\">Settings</Link>",
2220
+ "why": "a button that navigates breaks middle-click, cmd-click and copy-link, and is announced as a button rather than a link"
2221
+ },
2222
+ {
2223
+ "dont": "two variant=\"primary-brand\" buttons in the same dialog",
2224
+ "do": "one primary-brand, the rest secondary or tertiary",
2225
+ "why": "two primaries is the same as none — nothing indicates the expected action"
2226
+ },
2227
+ {
2228
+ "dont": "<Button variant=\"destructive\" onPress={deleteAccount}>",
2229
+ "do": "open a Modal first and put the destructive Button in its footer",
2230
+ "why": "irreversible actions need a confirmation step"
2231
+ },
2232
+ {
2233
+ "dont": "<Button><Icon as={Trash} /></Button>",
2234
+ "do": "<Button aria-label=\"Delete\"><Icon as={Trash} /></Button>",
2235
+ "why": "an icon-only button with no aria-label is announced as just 'button'"
2236
+ },
2237
+ {
2238
+ "dont": "<Button onClick={...}>",
2239
+ "do": "<Button onPress={...}>",
2240
+ "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."
2241
+ }
2242
+ ],
2243
+ "deprecated": [
2244
+ {
2245
+ "prop": "disabled",
2246
+ "replacement": "isDisabled",
2247
+ "note": "still type-checks, so this will not fail loudly"
2248
+ }
2249
+ ],
2250
+ "stylesheet": "src/styles/button.css",
2251
+ "tokens": [
2252
+ "--border-disabled",
1875
2253
  "--border-error-strong",
1876
2254
  "--border-inverse",
1877
2255
  "--border-primary",
@@ -2354,6 +2732,11 @@
2354
2732
  {
2355
2733
  "dont": "a div with `role=\"checkbox\"`",
2356
2734
  "why": "form association, the indeterminate property and label clicking are not free to reimplement"
2735
+ },
2736
+ {
2737
+ "dont": "<Checkbox isSelected={on} onChange={(isSelected) => setOn(isSelected)} />",
2738
+ "do": "<Checkbox isSelected={on} onSelectionChange={setOn} />",
2739
+ "why": "both selection shapes work, but they pair: `isSelected` goes with `onSelectionChange`, which receives the boolean, and `checked` goes with `onChange`, which receives the change event. Passing the Aria selection prop with the DOM handler name is the one combination that does not type — `onChange` is still the event handler. There is no `CheckboxChange` type; a generated eval invented one."
2357
2740
  }
2358
2741
  ],
2359
2742
  "deprecated": [
@@ -2460,10 +2843,22 @@
2460
2843
  "required": false,
2461
2844
  "origin": "own",
2462
2845
  "description": "Figma's `Show Label` + `Label`. Omit for a bare box."
2846
+ },
2847
+ "isSelected": {
2848
+ "type": "boolean | undefined",
2849
+ "required": false,
2850
+ "origin": "own",
2851
+ "description": "React Aria's name for `checked`. Wins when both are passed."
2852
+ },
2853
+ "onSelectionChange": {
2854
+ "type": "((isSelected: boolean) => void) | undefined",
2855
+ "required": false,
2856
+ "origin": "own",
2857
+ "description": "Receives the new selection state rather than the change event."
2463
2858
  }
2464
2859
  },
2465
2860
  "propCounts": {
2466
- "own": 6,
2861
+ "own": 8,
2467
2862
  "aria": 0,
2468
2863
  "dom": 304,
2469
2864
  "other": 0
@@ -2752,40 +3147,305 @@
2752
3147
  "other": 0
2753
3148
  }
2754
3149
  },
2755
- "ConfidenceIndicator": {
2756
- "name": "ConfidenceIndicator",
2757
- "source": "src/components/ConfidenceIndicator.tsx",
2758
- "propsType": "ConfidenceIndicatorProps",
2759
- "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.",
2760
- "import": "import { ConfidenceIndicator } from 'ionbase-ui';",
3150
+ "Combobox": {
3151
+ "name": "Combobox",
3152
+ "source": "src/components/Combobox.tsx",
3153
+ "propsType": "ComboboxProps",
3154
+ "description": "Combobox — a text field that filters a list, with one selected value.\n\nDrawn in Figma as `Combobox` (1370:2359) — three sizes by seven states, the\nsame axes `Input` carries, because the field IS Input's box. The open list is\na second Figma component, `Combobox Menu`, with no React export behind it.\n\nWHY THIS IS NOT `Select` WITH A SEARCH BOX\n\n`Select` wraps a native `<select>`, whose list the browser owns: it cannot be\nfiltered, and its rows can only hold text. Everything above about twenty\noptions needs filtering, and `Select`'s own contract has pointed at \"a\ncombobox — not yet in this system\" since it was written. This is it.\n\nThe cost is that everything the native control gave away for free — keyboard\nhandling, the mobile picker, screen-reader semantics — now has to be built.\nReact Aria's `useComboBox` builds it: `role=\"combobox\"` with `aria-expanded`,\n`aria-controls` and `aria-activedescendant`, arrow keys that move a virtual\nfocus while the real focus stays in the input, and Escape that reverts.\n\nFILTERING IS LOCALE-AWARE, NOT `toLowerCase().includes()`\n\n`useFilter({ sensitivity: 'base' })` is `Intl.Collator` underneath, so\n\"resume\" matches \"résumé\" and Turkish dotted/dotless I behave the way a\nTurkish reader expects. The lowercase-and-includes version fails both, and\nfails them silently in exactly the locales least likely to be tested.",
3155
+ "import": "import { Combobox } from 'ionbase-ui';",
2761
3156
  "status": "stable",
2762
- "summary": "How much to trust the thing next to it — three levels with a required basis, never a percentage.",
3157
+ "summary": "A text field that filters a list of options down to one selected value.",
2763
3158
  "useWhen": [
2764
- "a generated answer varies in reliability and the reader should weigh it",
2765
- "you can say what the confidence rests on"
3159
+ "the list is long enough that scanning it is worse than typing",
3160
+ "the user picks exactly one value from a known set",
3161
+ "the options carry a second line of context the native <select> cannot render"
2766
3162
  ],
2767
3163
  "useInstead": [
2768
3164
  {
2769
- "when": "the answer is wrong or missing rather than uncertain",
2770
- "use": "Alert"
3165
+ "when": "the list is short and fully scannable",
3166
+ "use": "Select",
3167
+ "why": "the native control brings keyboard handling and the mobile platform picker for free; this rebuilds all of it"
2771
3168
  },
2772
3169
  {
2773
- "when": "you want to show where the claim came from",
2774
- "use": "Citation",
2775
- "why": "a source is stronger than a confidence level, and often what the reader actually wanted"
3170
+ "when": "there are two or three mutually exclusive choices worth showing at once",
3171
+ "use": "RadioGroup"
2776
3172
  },
2777
3173
  {
2778
- "when": "it is a status rather than a reliability",
2779
- "use": "Badge"
3174
+ "when": "the value is free text and the list is only a convenience",
3175
+ "use": "Input",
3176
+ "why": "or set `allowsCustomValue`, which is the same decision made explicitly"
3177
+ },
3178
+ {
3179
+ "when": "the rows are commands rather than values",
3180
+ "use": "Menu",
3181
+ "why": "options are selected and stay selected; menu items are invoked and the menu closes"
2780
3182
  }
2781
3183
  ],
2782
3184
  "variants": {
2783
- "level": {
2784
- "low": {
2785
- "use": "thin or conflicting evidence. One bar, warning colour"
2786
- },
2787
- "medium": {
2788
- "use": "reasonable support with real gaps. Two bars"
3185
+ "size": {
3186
+ "sm": {
3187
+ "use": "dense forms, table filters"
3188
+ },
3189
+ "md": {
3190
+ "use": "the default"
3191
+ },
3192
+ "lg": {
3193
+ "use": "prominent single-field forms"
3194
+ }
3195
+ }
3196
+ },
3197
+ "a11y": {
3198
+ "guarantees": [
3199
+ "role=combobox with aria-expanded, aria-controls and aria-activedescendant, from react-aria's useComboBox",
3200
+ "real focus never leaves the input; arrow keys move a virtual focus through the list, which is what keeps typing and browsing possible at once",
3201
+ "the popover is non-modal, so the page behind stays readable and focus stays in the field",
3202
+ "Escape reverts to the last committed value rather than clearing the field",
3203
+ "filtering is Intl.Collator-based, so accents and locale-specific casing behave correctly",
3204
+ "the empty message is a sibling of the listbox, not an option in it, so it is never counted, focused or selectable",
3205
+ "the menu matches the field's width, clamped to the space right of it so a full-bleed field cannot push the menu past the viewport edge"
3206
+ ],
3207
+ "requires": [
3208
+ "`label`, or an `aria-label` when no visible label is rendered",
3209
+ "`emptyLabel` wording that says why nothing is listed, if the default is too terse"
3210
+ ]
3211
+ },
3212
+ "antiPatterns": [
3213
+ {
3214
+ "dont": "giving the disclosure button a tab stop",
3215
+ "why": "the input already opens the list with ArrowDown. A second tab stop doubles the cost of tabbing past every combobox in a form and adds nothing"
3216
+ },
3217
+ {
3218
+ "dont": "styling the focused option with :hover or :focus",
3219
+ "why": "no option is ever either — focus is virtual. Use [data-focused], which react-aria sets for both pointer and keyboard"
3220
+ },
3221
+ {
3222
+ "dont": "posting the input's text in a form",
3223
+ "why": "the text is the label and the value is the key; pass `name` and the component posts the key in a hidden input"
3224
+ },
3225
+ {
3226
+ "dont": "filtering with toLowerCase().includes()",
3227
+ "why": "it fails on accents and on Turkish dotted/dotless I, silently, in the locales least likely to be tested"
3228
+ },
3229
+ {
3230
+ "dont": "writing a width onto the menu from a ResizeObserver or any effect that runs while it is open",
3231
+ "why": "a positioned popover that changes size feeds back into react-aria's positioning, and any scrollbar that results is read by useCloseOnScroll as a real scroll. The menu opens and closes within about a hundred milliseconds, which presents as a keyboard bug rather than a layout one"
3232
+ }
3233
+ ],
3234
+ "slots": {
3235
+ "emptyLabel": {
3236
+ "expects": "what to show when the filter matches nothing"
3237
+ }
3238
+ },
3239
+ "stylesheet": "src/styles/combobox.css",
3240
+ "tokens": [
3241
+ "--border-subtle",
3242
+ "--border-width-default",
3243
+ "--font-family-sans",
3244
+ "--font-weight-medium",
3245
+ "--icon-disabled",
3246
+ "--icon-secondary",
3247
+ "--ion-duration-base",
3248
+ "--ion-ease-out",
3249
+ "--ion-input-icon-size",
3250
+ "--ion-shadow-shadow-lg",
3251
+ "--radius-md",
3252
+ "--radius-sm",
3253
+ "--spacing-12",
3254
+ "--spacing-2",
3255
+ "--spacing-4",
3256
+ "--spacing-6",
3257
+ "--spacing-8",
3258
+ "--surface-hover",
3259
+ "--surface-primary-subtle",
3260
+ "--surface-raised",
3261
+ "--text-default",
3262
+ "--text-disabled",
3263
+ "--text-tertiary",
3264
+ "--type-body",
3265
+ "--type-body-line-height",
3266
+ "--type-body-sm",
3267
+ "--type-body-sm-line-height"
3268
+ ],
3269
+ "props": {
3270
+ "options": {
3271
+ "type": "readonly ComboboxOption[]",
3272
+ "required": true,
3273
+ "origin": "own",
3274
+ "description": "The full option list. Filtering happens here, against what is typed."
3275
+ },
3276
+ "label": {
3277
+ "type": "React.ReactNode",
3278
+ "required": false,
3279
+ "origin": "own",
3280
+ "description": "Field label. Required for a usable control — see `a11y.requires`."
3281
+ },
3282
+ "description": {
3283
+ "type": "React.ReactNode",
3284
+ "required": false,
3285
+ "origin": "own",
3286
+ "description": "Helper text below the field."
3287
+ },
3288
+ "errorMessage": {
3289
+ "type": "React.ReactNode",
3290
+ "required": false,
3291
+ "origin": "own",
3292
+ "description": "Replaces the helper text when `isInvalid` is set."
3293
+ },
3294
+ "isInvalid": {
3295
+ "type": "boolean | undefined",
3296
+ "required": false,
3297
+ "origin": "own"
3298
+ },
3299
+ "isDisabled": {
3300
+ "type": "boolean | undefined",
3301
+ "required": false,
3302
+ "origin": "own"
3303
+ },
3304
+ "isReadOnly": {
3305
+ "type": "boolean | undefined",
3306
+ "required": false,
3307
+ "origin": "own"
3308
+ },
3309
+ "size": {
3310
+ "type": "ComboboxSize | undefined",
3311
+ "required": false,
3312
+ "origin": "own",
3313
+ "description": "Matches Input's `Size` variant: Small, Medium, Large.",
3314
+ "values": [
3315
+ "sm",
3316
+ "md",
3317
+ "lg"
3318
+ ]
3319
+ },
3320
+ "placeholder": {
3321
+ "type": "string | undefined",
3322
+ "required": false,
3323
+ "origin": "own"
3324
+ },
3325
+ "selectedKey": {
3326
+ "type": "string | null | undefined",
3327
+ "required": false,
3328
+ "origin": "own",
3329
+ "description": "Controlled selection. `null` means nothing is selected."
3330
+ },
3331
+ "defaultSelectedKey": {
3332
+ "type": "string | undefined",
3333
+ "required": false,
3334
+ "origin": "own"
3335
+ },
3336
+ "onSelectionChange": {
3337
+ "type": "((key: string | null) => void) | undefined",
3338
+ "required": false,
3339
+ "origin": "own"
3340
+ },
3341
+ "inputValue": {
3342
+ "type": "string | undefined",
3343
+ "required": false,
3344
+ "origin": "own",
3345
+ "description": "Controlled text. Usually only needed for async/remote filtering."
3346
+ },
3347
+ "defaultInputValue": {
3348
+ "type": "string | undefined",
3349
+ "required": false,
3350
+ "origin": "own"
3351
+ },
3352
+ "onInputChange": {
3353
+ "type": "((value: string) => void) | undefined",
3354
+ "required": false,
3355
+ "origin": "own"
3356
+ },
3357
+ "allowsCustomValue": {
3358
+ "type": "boolean | undefined",
3359
+ "required": false,
3360
+ "origin": "own",
3361
+ "description": "Accept text that matches no option. Off by default: a combobox whose value\nis a free string is a text field with suggestions, and the caller should\nhave to say that is what they want."
3362
+ },
3363
+ "menuTrigger": {
3364
+ "type": "\"focus\" | \"input\" | \"manual\" | undefined",
3365
+ "required": false,
3366
+ "origin": "own",
3367
+ "description": "What opens the list. `input` — react-aria's default and this one — opens it\non the first keystroke; the chevron opens it on demand. `focus` opens it\nthe moment the field is tabbed into, which is loud in a long form.",
3368
+ "values": [
3369
+ "focus",
3370
+ "input",
3371
+ "manual"
3372
+ ]
3373
+ },
3374
+ "emptyLabel": {
3375
+ "type": "React.ReactNode",
3376
+ "required": false,
3377
+ "origin": "own",
3378
+ "description": "Shown in place of the list when nothing matches."
3379
+ },
3380
+ "buttonLabel": {
3381
+ "type": "string | undefined",
3382
+ "required": false,
3383
+ "origin": "own",
3384
+ "description": "Accessible label for the disclosure button."
3385
+ },
3386
+ "name": {
3387
+ "type": "string | undefined",
3388
+ "required": false,
3389
+ "origin": "own",
3390
+ "description": "Posts the selected value under this name, for an uncontrolled form."
3391
+ },
3392
+ "className": {
3393
+ "type": "string | undefined",
3394
+ "required": false,
3395
+ "origin": "own"
3396
+ },
3397
+ "wrapperClassName": {
3398
+ "type": "string | undefined",
3399
+ "required": false,
3400
+ "origin": "own"
3401
+ },
3402
+ "id": {
3403
+ "type": "string | undefined",
3404
+ "required": false,
3405
+ "origin": "own"
3406
+ }
3407
+ },
3408
+ "propCounts": {
3409
+ "own": 23,
3410
+ "aria": 0,
3411
+ "dom": 0,
3412
+ "other": 0
3413
+ }
3414
+ },
3415
+ "ConfidenceIndicator": {
3416
+ "name": "ConfidenceIndicator",
3417
+ "source": "src/components/ConfidenceIndicator.tsx",
3418
+ "propsType": "ConfidenceIndicatorProps",
3419
+ "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.",
3420
+ "import": "import { ConfidenceIndicator } from 'ionbase-ui';",
3421
+ "status": "stable",
3422
+ "summary": "How much to trust the thing next to it — three levels with a required basis, never a percentage.",
3423
+ "useWhen": [
3424
+ "a generated answer varies in reliability and the reader should weigh it",
3425
+ "you can say what the confidence rests on"
3426
+ ],
3427
+ "useInstead": [
3428
+ {
3429
+ "when": "the answer is wrong or missing rather than uncertain",
3430
+ "use": "Alert"
3431
+ },
3432
+ {
3433
+ "when": "you want to show where the claim came from",
3434
+ "use": "Citation",
3435
+ "why": "a source is stronger than a confidence level, and often what the reader actually wanted"
3436
+ },
3437
+ {
3438
+ "when": "it is a status rather than a reliability",
3439
+ "use": "Badge"
3440
+ }
3441
+ ],
3442
+ "variants": {
3443
+ "level": {
3444
+ "low": {
3445
+ "use": "thin or conflicting evidence. One bar, warning colour"
3446
+ },
3447
+ "medium": {
3448
+ "use": "reasonable support with real gaps. Two bars"
2789
3449
  },
2790
3450
  "high": {
2791
3451
  "use": "corroborated, and you can say by what. Three bars"
@@ -2936,41 +3596,275 @@
2936
3596
  "other": 0
2937
3597
  }
2938
3598
  },
2939
- "EmptyState": {
2940
- "name": "EmptyState",
2941
- "source": "src/components/EmptyState.tsx",
2942
- "propsType": "EmptyStateProps",
2943
- "description": "EmptyState — the state five of this system's nine patterns require and none\nof them could render.\n\n`DataTable`, `PageShell`, `SettingsPanel`, `AssistantAnswer` and\n`HumanApproval` all specify an empty state in their recipe. Until this\nexisted each consumer invented one, which is how a design system ends up\nwith four different ways to say \"nothing here\".\n\nREASON IS REQUIRED, AND HAS NO DEFAULT\n\nThe four reasons are genuinely different situations with different correct\nactions, and the failure mode is silent: an app that shows \"No invoices yet\n— Create your first invoice\" to a user whose filter excluded everything has\ntold them their records are gone. A default would make that the easy path,\nso there isn't one.\n\n`no-results` and `error` deliberately do NOT offer a create action in the\ncontract's guidance. The first needs the filter cleared; the second needs a\nretry, and offering \"Create\" over a failed fetch invites a duplicate.\n\nNOT A LIVE REGION\n\nAn empty state replaces content, so it is what the user reads next rather\nthan something announced over what they are reading. `role=\"status\"` here\nwould interrupt on every keystroke of a filter box. If a specific flow needs\nthe announcement, the caller owns that decision — the same reasoning\nStreamingText applies to `aria-live`.\n\n`error` is rendered as `role=\"alert\"` ONLY when the caller opts in via\n`role`, for the same reason: an error empty state arriving during typing is\nstill a read, not an interruption.\n\nNo `'use client'`: nothing here holds state. The buttons passed through\n`action` carry their own boundary.",
2944
- "import": "import { EmptyState } from 'ionbase-ui';",
3599
+ "Drawer": {
3600
+ "name": "Drawer",
3601
+ "source": "src/components/Drawer.tsx",
3602
+ "propsType": "DrawerProps",
3603
+ "description": "Drawer — a modal panel anchored to an edge.\n\nWHY IT IS A MODAL AND NOT A SIDEBAR\n\nIt takes focus, traps it, closes on Escape and on an outside click, and marks\nthe rest of the page inert — all of `Modal`'s behaviour, differing only in\nwhere the panel sits and how it enters. A persistent side panel that does not\ndo those things is layout, not a drawer, and belongs in the page rather than\nin an overlay.\n\nWHEN TO REACH FOR MODAL INSTEAD\n\nA drawer suits work that is long or list-shaped — filters, a record's detail,\na sequence of settings — because an edge panel can be tall without becoming a\nsquare that fights the viewport. A decision, especially a destructive one,\nbelongs in a Modal: it is centred, it is smaller, and it does not invite\nscrolling past the thing being confirmed.\n\nRENDERS NOTHING WHEN CLOSED, and portals when open, for the reasons recorded\non Modal: a closed overlay left in the tree is still keyboard-focusable, and\nan inline one inherits whatever `overflow` and stacking context surrounds it.",
3604
+ "import": "import { Drawer } from 'ionbase-ui';",
2945
3605
  "status": "stable",
2946
- "summary": "The state a region shows when it has nothing to show, and why — first run, no results, no access, or a failed fetch.",
3606
+ "summary": "A modal panel anchored to an edge. Modal behaviour — focus trap, Escape, scrim — with the shape of a side panel.",
2947
3607
  "useWhen": [
2948
- "a list, table, panel or route has no content to render",
2949
- "a filter or search returns nothing",
2950
- "the account is not permitted to see what would be here",
2951
- "the request that would have filled the region failed"
3608
+ "work that is long or list-shaped: filters, a record's detail, a run of settings",
3609
+ "the page behind should stay visible as context while a task is done beside it",
3610
+ "a form long enough that a centred dialog would scroll its own actions off screen"
2952
3611
  ],
2953
3612
  "useInstead": [
2954
3613
  {
2955
- "when": "content exists and something went wrong beside it",
2956
- "use": "Alert",
2957
- "why": "an Alert sits with the content; an EmptyState replaces it. If there is a table on screen, the message about it is an Alert"
3614
+ "when": "the user is making a decision, especially a destructive one",
3615
+ "use": "Modal",
3616
+ "why": "a modal is centred and small enough that the thing being confirmed cannot be scrolled past"
2958
3617
  },
2959
3618
  {
2960
- "when": "the message is transient and the content is unaffected",
2961
- "use": "Toast"
3619
+ "when": "the panel should stay open while the user works elsewhere",
3620
+ "use": "page layout",
3621
+ "why": "a drawer traps focus and marks the rest of the page inert. A panel that does not do those things is layout, not an overlay"
3622
+ },
3623
+ {
3624
+ "when": "the content is a short menu of actions",
3625
+ "use": "Popover or Menu"
2962
3626
  }
2963
3627
  ],
3628
+ "variants": {
3629
+ "placement": {
3630
+ "start": {
3631
+ "use": "navigation and hierarchy, which readers expect at the leading edge"
3632
+ },
3633
+ "end": {
3634
+ "use": "the default — detail and editing beside the record that opened it"
3635
+ },
3636
+ "top": {
3637
+ "use": "rare: a global search or command surface"
3638
+ },
3639
+ "bottom": {
3640
+ "use": "touch-first layouts, where the bottom edge is the reachable one"
3641
+ }
3642
+ },
3643
+ "size": {
3644
+ "sm": {
3645
+ "use": "a short form or a filter list"
3646
+ },
3647
+ "md": {
3648
+ "use": "the default"
3649
+ },
3650
+ "lg": {
3651
+ "use": "a record detail with its own sections"
3652
+ }
3653
+ }
3654
+ },
2964
3655
  "slots": {
2965
3656
  "title": {
2966
3657
  "accepts": "text",
2967
- "note": "required — it is the region's statement. Name the thing and the situation: \"No invoices match these filters\", not \"No results\""
3658
+ "note": "required — it is the dialog's accessible name"
2968
3659
  },
2969
3660
  "description": {
2970
- "accepts": "text",
2971
- "note": "one or two sentences: what would be here, and what puts it here"
3661
+ "accepts": "text"
2972
3662
  },
2973
- "action": {
3663
+ "children": {
3664
+ "accepts": "any content",
3665
+ "note": "the only scrolling region"
3666
+ },
3667
+ "footer": {
3668
+ "accepts": "Button",
3669
+ "note": "pinned to the bottom edge, so actions stay reachable in a long panel"
3670
+ }
3671
+ },
3672
+ "a11y": {
3673
+ "requires": [
3674
+ "`title`, which becomes the dialog's accessible name via aria-labelledby"
3675
+ ],
3676
+ "notes": [
3677
+ "start and end follow writing direction, so a right-hand drawer in English is a left-hand one in Arabic with no second variant",
3678
+ "renders nothing when closed and portals when open: a closed overlay left in the tree is still keyboard-focusable",
3679
+ "only the body scrolls, so the title stays on screen — the one piece of context a user needs while working down a long list"
3680
+ ]
3681
+ },
3682
+ "antiPatterns": [
3683
+ {
3684
+ "dont": "a Drawer that stays open while the user edits the page behind it",
3685
+ "do": "a layout panel",
3686
+ "why": "it traps focus and marks the rest inert; a keyboard user cannot reach the page behind it"
3687
+ },
3688
+ {
3689
+ "dont": "<Drawer> for a delete confirmation",
3690
+ "do": "<Modal>",
3691
+ "why": "an edge panel is tall, so the confirm button can sit below the fold — the one control that must never be missed"
3692
+ }
3693
+ ],
3694
+ "stylesheet": "src/styles/drawer.css",
3695
+ "tokens": [
3696
+ "--border-focus",
3697
+ "--border-subtle",
3698
+ "--border-width-default",
3699
+ "--border-width-thick",
3700
+ "--font-family-sans",
3701
+ "--font-weight-semibold",
3702
+ "--icon-secondary",
3703
+ "--icon-size-md",
3704
+ "--ion-drawer-size",
3705
+ "--radius-lg",
3706
+ "--radius-sm",
3707
+ "--spacing-12",
3708
+ "--spacing-24",
3709
+ "--spacing-32",
3710
+ "--spacing-4",
3711
+ "--spacing-8",
3712
+ "--surface-hover",
3713
+ "--surface-raised",
3714
+ "--surface-scrim",
3715
+ "--text-default",
3716
+ "--text-secondary",
3717
+ "--type-body",
3718
+ "--type-body-line-height",
3719
+ "--type-body-sm",
3720
+ "--type-body-sm-line-height",
3721
+ "--type-h5",
3722
+ "--type-h5-line-height"
3723
+ ],
3724
+ "props": {
3725
+ "isOpen": {
3726
+ "type": "boolean | undefined",
3727
+ "required": false,
3728
+ "origin": "own",
3729
+ "description": "Whether the drawer is shown."
3730
+ },
3731
+ "onOpenChange": {
3732
+ "type": "((isOpen: boolean) => void) | undefined",
3733
+ "required": false,
3734
+ "origin": "own",
3735
+ "description": "Called with the next open state — `false` when the drawer asks to close."
3736
+ },
3737
+ "placement": {
3738
+ "type": "DrawerPlacement | undefined",
3739
+ "required": false,
3740
+ "origin": "own",
3741
+ "description": "Which edge it enters from. `start` and `end` follow writing direction, so\na right-hand drawer in English is a left-hand one in Arabic without a\nsecond variant.",
3742
+ "values": [
3743
+ "start",
3744
+ "end",
3745
+ "top",
3746
+ "bottom"
3747
+ ],
3748
+ "default": "end"
3749
+ },
3750
+ "size": {
3751
+ "type": "DrawerSize | undefined",
3752
+ "required": false,
3753
+ "origin": "own",
3754
+ "values": [
3755
+ "sm",
3756
+ "md",
3757
+ "lg"
3758
+ ],
3759
+ "default": "md"
3760
+ },
3761
+ "title": {
3762
+ "type": "React.ReactNode",
3763
+ "required": true,
3764
+ "origin": "own",
3765
+ "description": "Required: the dialog's accessible name, wired via `aria-labelledby`."
3766
+ },
3767
+ "description": {
3768
+ "type": "React.ReactNode",
3769
+ "required": false,
3770
+ "origin": "own",
3771
+ "description": "Supporting copy under the title."
3772
+ },
3773
+ "footer": {
3774
+ "type": "React.ReactNode",
3775
+ "required": false,
3776
+ "origin": "own",
3777
+ "description": "Action row, pinned to the bottom edge."
3778
+ },
3779
+ "showClose": {
3780
+ "type": "boolean | undefined",
3781
+ "required": false,
3782
+ "origin": "own",
3783
+ "default": true
3784
+ },
3785
+ "closeLabel": {
3786
+ "type": "string | undefined",
3787
+ "required": false,
3788
+ "origin": "own",
3789
+ "default": "Close drawer"
3790
+ },
3791
+ "children": {
3792
+ "type": "React.ReactNode",
3793
+ "required": false,
3794
+ "origin": "own"
3795
+ },
3796
+ "className": {
3797
+ "type": "string | undefined",
3798
+ "required": false,
3799
+ "origin": "own"
3800
+ },
3801
+ "isDismissable": {
3802
+ "type": "boolean | undefined",
3803
+ "required": false,
3804
+ "origin": "aria",
3805
+ "description": "Whether to close the modal when the user interacts outside it.",
3806
+ "tags": {
3807
+ "default": "false"
3808
+ }
3809
+ },
3810
+ "isKeyboardDismissDisabled": {
3811
+ "type": "boolean | undefined",
3812
+ "required": false,
3813
+ "origin": "aria",
3814
+ "description": "Whether pressing the escape key to close the modal should be disabled.",
3815
+ "tags": {
3816
+ "default": "false"
3817
+ }
3818
+ },
3819
+ "shouldCloseOnInteractOutside": {
3820
+ "type": "((element: Element) => boolean) | undefined",
3821
+ "required": false,
3822
+ "origin": "aria",
3823
+ "description": "When user interacts with the argument element outside of the overlay ref,\nreturn true if onClose should be called. This gives you a chance to filter\nout interaction with elements that should not dismiss the overlay.\nBy default, onClose will always be called on interaction outside the overlay ref."
3824
+ }
3825
+ },
3826
+ "propCounts": {
3827
+ "own": 11,
3828
+ "aria": 3,
3829
+ "dom": 0,
3830
+ "other": 0
3831
+ }
3832
+ },
3833
+ "EmptyState": {
3834
+ "name": "EmptyState",
3835
+ "source": "src/components/EmptyState.tsx",
3836
+ "propsType": "EmptyStateProps",
3837
+ "description": "EmptyState — the state five of this system's nine patterns require and none\nof them could render.\n\n`DataTable`, `PageShell`, `SettingsPanel`, `AssistantAnswer` and\n`HumanApproval` all specify an empty state in their recipe. Until this\nexisted each consumer invented one, which is how a design system ends up\nwith four different ways to say \"nothing here\".\n\nREASON IS REQUIRED, AND HAS NO DEFAULT\n\nThe four reasons are genuinely different situations with different correct\nactions, and the failure mode is silent: an app that shows \"No invoices yet\n— Create your first invoice\" to a user whose filter excluded everything has\ntold them their records are gone. A default would make that the easy path,\nso there isn't one.\n\n`no-results` and `error` deliberately do NOT offer a create action in the\ncontract's guidance. The first needs the filter cleared; the second needs a\nretry, and offering \"Create\" over a failed fetch invites a duplicate.\n\nNOT A LIVE REGION\n\nAn empty state replaces content, so it is what the user reads next rather\nthan something announced over what they are reading. `role=\"status\"` here\nwould interrupt on every keystroke of a filter box. If a specific flow needs\nthe announcement, the caller owns that decision — the same reasoning\nStreamingText applies to `aria-live`.\n\n`error` is rendered as `role=\"alert\"` ONLY when the caller opts in via\n`role`, for the same reason: an error empty state arriving during typing is\nstill a read, not an interruption.\n\nNo `'use client'`: nothing here holds state. The buttons passed through\n`action` carry their own boundary.",
3838
+ "import": "import { EmptyState } from 'ionbase-ui';",
3839
+ "status": "stable",
3840
+ "summary": "The state a region shows when it has nothing to show, and why — first run, no results, no access, or a failed fetch.",
3841
+ "useWhen": [
3842
+ "a list, table, panel or route has no content to render",
3843
+ "a filter or search returns nothing",
3844
+ "the account is not permitted to see what would be here",
3845
+ "the request that would have filled the region failed"
3846
+ ],
3847
+ "useInstead": [
3848
+ {
3849
+ "when": "content exists and something went wrong beside it",
3850
+ "use": "Alert",
3851
+ "why": "an Alert sits with the content; an EmptyState replaces it. If there is a table on screen, the message about it is an Alert"
3852
+ },
3853
+ {
3854
+ "when": "the message is transient and the content is unaffected",
3855
+ "use": "Toast"
3856
+ }
3857
+ ],
3858
+ "slots": {
3859
+ "title": {
3860
+ "accepts": "text",
3861
+ "note": "required — it is the region's statement. Name the thing and the situation: \"No invoices match these filters\", not \"No results\""
3862
+ },
3863
+ "description": {
3864
+ "accepts": "text",
3865
+ "note": "one or two sentences: what would be here, and what puts it here"
3866
+ },
3867
+ "action": {
2974
3868
  "accepts": "Button",
2975
3869
  "note": "the action that RESOLVES this reason — clearing a filter for no-results, not creating a record"
2976
3870
  },
@@ -3068,7 +3962,7 @@
3068
3962
  "stylesheet": "src/styles/empty-state.css",
3069
3963
  "tokens": [
3070
3964
  "--font-family-sans",
3071
- "--font-weight-medium",
3965
+ "--font-weight-semibold",
3072
3966
  "--icon-error",
3073
3967
  "--icon-primary",
3074
3968
  "--icon-size-md",
@@ -3173,99 +4067,345 @@
3173
4067
  "other": 0
3174
4068
  }
3175
4069
  },
3176
- "FullCard": {
3177
- "name": "FullCard",
3178
- "source": "src/components/FullCard.tsx",
3179
- "propsType": "FullCardProps",
3180
- "description": "Full Card — the full-bleed case study row from Figma `Full Card` (592:857).\n\nA text column beside a framed media panel, split down the middle, mirrored\nby the `Alignment` variant.\n\nThe split holds from 1080 and stacks below it, media above content in BOTH\nalignments — `Alignment` names a horizontal side and\nstops meaning anything once there is one column, so it does not get to\ndecide the vertical order too. Size is a media query, not a prop, the same\ncall Header makes about Device.\n\nWHY THE `show*` BOOLEANS ARE GONE\n\nFigma carries `Show Eyebrow`, `Show Description` and `Show Actions` beside\nthe slots they gate, because a Figma component instance always holds every\nlayer and needs a switch to hide one. React has no such constraint — an\nabsent prop is the switch. Badge made the same call with `Show Dot`, and\nkeeping both would have let `showActions` and `actions` disagree.\n\n`headline` is required, and it is the only required prop. The heading is\nwhat makes this a section rather than a decorated div, and every other part\nof the card is optional in Figma too.\n\nNo `'use client'`: nothing here is stateful or interactive. The interactive\nparts arrive through `actions`, and they carry their own boundary.",
3181
- "import": "import { FullCard } from 'ionbase-ui';",
4070
+ "FileUpload": {
4071
+ "name": "FileUpload",
4072
+ "source": "src/components/FileUpload.tsx",
4073
+ "propsType": "FileUploadProps",
4074
+ "description": "FileUpload — a drop target wrapped around a real `<input type=\"file\">`.\n\nDrawn in Figma as `File Upload` (1367:2333) — two sizes by five states. The\nmeasurements came from `Input` and `EmptyState` rather than being invented:\nthe border, radius and disabled treatment are Input's; the centred\nicon-over-text stack is EmptyState's. Figma draws the drop zone alone, the\nsame split `Input` makes against `Form Field`.\n\nTHE INPUT IS THE CONTROL. THE DROP ZONE IS DECORATION.\n\nThe usual build of this component is a `<div>` with drag handlers and a\nclick that calls `input.click()`. That version cannot be reached by keyboard,\nhas no accessible name, does not participate in a form, and does not work in\nany environment without a pointer — which includes switch access, voice\ncontrol and most screen-reader browse modes.\n\nHere the file input is a real, focusable, labelled control that is visually\nhidden but NOT `display: none` — it keeps its place in the tab order and its\nlabel. Drag-and-drop is layered on top as an enhancement, and every path it\noffers is also reachable without it. That ordering is the whole component.\n\nVALIDATION IS ADVISORY, NOT SECURITY. `accept` and `maxSize` are checked here\nso the user finds out immediately instead of after an upload. A server that\ntrusts either one is trusting a value the client chose.",
4075
+ "import": "import { FileUpload } from 'ionbase-ui';",
3182
4076
  "status": "stable",
3183
- "summary": "A full-bleed row: a text column beside a framed media panel, mirrored by `alignment`.",
4077
+ "summary": "A drop target wrapped around a real file input, with client-side accept and size checks.",
3184
4078
  "useWhen": [
3185
- "a marketing or overview section presents one thing with a screenshot beside it",
3186
- "a case-study or feature row on a landing or product page"
4079
+ "the user attaches one or more files to a form or a message",
4080
+ "the constraints (types, size, count) should be stated before the upload is attempted rather than after"
3187
4081
  ],
3188
4082
  "useInstead": [
3189
4083
  {
3190
- "when": "you are listing records with comparable fields",
3191
- "use": "Table"
4084
+ "when": "a single file is picked and no drop target or file list is wanted",
4085
+ "use": "a plain <input type=\"file\">",
4086
+ "why": "this component adds a drop zone and a removable list; without either it is a heavier way to render the same control"
3192
4087
  },
3193
4088
  {
3194
- "when": "it is a short status message rather than a content section",
3195
- "use": "Alert"
4089
+ "when": "upload progress needs to be shown",
4090
+ "use": "ProgressBar",
4091
+ "why": "FileUpload selects files; it does not transfer them, and deliberately owns no network state"
3196
4092
  }
3197
4093
  ],
3198
- "slots": {
3199
- "eyebrow": {
3200
- "accepts": "Badge",
3201
- "note": "any node works"
3202
- },
3203
- "headline": {
3204
- "accepts": "text",
3205
- "note": "required — the heading is what makes this a section rather than a decorated div"
3206
- },
3207
- "description": {
3208
- "accepts": "text"
3209
- },
3210
- "actions": {
3211
- "accepts": "Button",
3212
- "note": "a secondary Button in the design"
3213
- },
3214
- "media": {
3215
- "accepts": "img or video",
3216
- "note": "rendered inside the framed screen holder — pass the screenshot or embed itself, not the frame"
3217
- },
3218
- "children": {
3219
- "accepts": "any",
3220
- "note": "extra content below the description"
3221
- }
3222
- },
3223
4094
  "variants": {
3224
- "alignment": {
3225
- "left": {
3226
- "use": "media on the left, text on the right"
4095
+ "size": {
4096
+ "sm": {
4097
+ "use": "inside a dense form, or beside other fields"
3227
4098
  },
3228
- "right": {
3229
- "use": "media on the right — the variant names the media, not the text"
4099
+ "md": {
4100
+ "use": "the default — a standalone attachment area"
3230
4101
  }
3231
4102
  }
3232
4103
  },
3233
4104
  "a11y": {
3234
4105
  "guarantees": [
3235
- "the headline renders as a real heading, so the card appears in the document outline"
4106
+ "the control is a real <input type=\"file\">, focusable and in the tab order — the drop zone is an enhancement layered on top, never the only way in",
4107
+ "the zone is a <label> for that input, so clicking anywhere opens the picker without any script",
4108
+ "the input is clipped to 1px rather than display:none, which would remove it from the tab order and the accessibility tree",
4109
+ "the focus ring is drawn on the zone, where the user is looking, not on the clipped input",
4110
+ "a polite live region announces the number of files held, because a drop changes nothing near the user's focus",
4111
+ "dragging is signalled by a solid border as well as a colour, so it is not conveyed by colour alone (WCAG 1.4.1)",
4112
+ "each remove button is a 24px target with a name that includes the file (WCAG 2.5.8)"
3236
4113
  ],
3237
4114
  "requires": [
3238
- "`headingLevel` matching the surrounding outline — `h3` is the default because Figma applies Type/H3, but only the page knows the real level",
3239
- "`alt` on whatever you pass as `media`"
3240
- ],
3241
- "notes": [
3242
- "`h1` is deliberately not available: a full card is a section within a page, never the page's own title."
4115
+ "`label`, or an `aria-label` — an unnamed file input announces only as \"button\"",
4116
+ "`hint` describing the accepted types and size limit, since `accept` itself is never announced"
3243
4117
  ]
3244
4118
  },
3245
4119
  "antiPatterns": [
3246
4120
  {
3247
- "dont": "looking for `showEyebrow` / `showDescription` / `showActions`",
3248
- "why": "an absent prop is the switch — Figma needs the boolean because an instance always holds every layer; React does not"
4121
+ "dont": "treating `accept` or `maxSize` as validation",
4122
+ "why": "both are client-side and advisory. A browser does not enforce `accept` on drop, and every platform picker offers an \"All files\" escape. The server must check again"
3249
4123
  },
3250
4124
  {
3251
- "dont": "leaving `headingLevel` at the default inside a section that is already at h3",
3252
- "why": "it produces a skipped or duplicated outline level"
4125
+ "dont": "rebuilding this as a div with onClick={() => inputRef.current.click()}",
4126
+ "why": "that version has no accessible name, no tab stop, no form participation and no keyboard path — it is the usual build of this component and the usual defect"
3253
4127
  },
3254
4128
  {
3255
- "dont": "passing your own frame or border around `media`",
3256
- "why": "the component already draws the screen holder"
4129
+ "dont": "putting the file list inside the drop zone",
4130
+ "why": "the zone is a label, so every row in it — including each remove button — would also open the file picker"
4131
+ },
4132
+ {
4133
+ "dont": "using `onChange` as a delta",
4134
+ "why": "it fires with the complete list after every add and every remove, never with what changed"
3257
4135
  }
3258
4136
  ],
3259
- "stylesheet": "src/styles/full-card.css",
4137
+ "slots": {
4138
+ "prompt": {
4139
+ "expects": "the call to action inside the zone"
4140
+ },
4141
+ "hint": {
4142
+ "expects": "the constraints in words — \"PNG or JPG, up to 5 MB\""
4143
+ }
4144
+ },
4145
+ "stylesheet": "src/styles/file-upload.css",
3260
4146
  "tokens": [
3261
4147
  "--border-default",
4148
+ "--border-disabled",
4149
+ "--border-error",
4150
+ "--border-focus",
4151
+ "--border-strong",
3262
4152
  "--border-width-default",
4153
+ "--border-width-thick",
3263
4154
  "--font-family-sans",
3264
- "--font-weight-regular",
3265
- "--font-weight-semibold",
3266
- "--ion-full-card-gap",
3267
- "--ion-full-card-media-gutter",
3268
- "--ion-full-card-media-min-height",
4155
+ "--font-weight-medium",
4156
+ "--icon-default",
4157
+ "--icon-disabled",
4158
+ "--icon-secondary",
4159
+ "--icon-size-lg",
4160
+ "--icon-size-md",
4161
+ "--icon-size-sm",
4162
+ "--ion-duration-base",
4163
+ "--ion-ease-out",
4164
+ "--ion-file-upload-gap",
4165
+ "--ion-file-upload-icon-size",
4166
+ "--ion-file-upload-padding",
4167
+ "--radius-md",
4168
+ "--radius-sm",
4169
+ "--ring-focus",
4170
+ "--spacing-12",
4171
+ "--spacing-20",
4172
+ "--spacing-24",
4173
+ "--spacing-32",
4174
+ "--spacing-6",
4175
+ "--spacing-8",
4176
+ "--surface-default",
4177
+ "--surface-disabled",
4178
+ "--surface-hover",
4179
+ "--surface-primary-subtle",
4180
+ "--surface-subtle",
4181
+ "--text-default",
4182
+ "--text-disabled",
4183
+ "--text-tertiary",
4184
+ "--type-body",
4185
+ "--type-body-line-height",
4186
+ "--type-body-sm",
4187
+ "--type-body-sm-line-height"
4188
+ ],
4189
+ "props": {
4190
+ "label": {
4191
+ "type": "React.ReactNode",
4192
+ "required": false,
4193
+ "origin": "own",
4194
+ "description": "Field label. Also the accessible name of the file input."
4195
+ },
4196
+ "description": {
4197
+ "type": "React.ReactNode",
4198
+ "required": false,
4199
+ "origin": "own",
4200
+ "description": "Helper text below the control."
4201
+ },
4202
+ "errorMessage": {
4203
+ "type": "React.ReactNode",
4204
+ "required": false,
4205
+ "origin": "own",
4206
+ "description": "Replaces the helper text when `isInvalid` is set."
4207
+ },
4208
+ "isInvalid": {
4209
+ "type": "boolean | undefined",
4210
+ "required": false,
4211
+ "origin": "own"
4212
+ },
4213
+ "isDisabled": {
4214
+ "type": "boolean | undefined",
4215
+ "required": false,
4216
+ "origin": "own"
4217
+ },
4218
+ "size": {
4219
+ "type": "FileUploadSize | undefined",
4220
+ "required": false,
4221
+ "origin": "own",
4222
+ "description": "Matches the Figma `Size` variant when drawn. Height of the drop target.",
4223
+ "values": [
4224
+ "sm",
4225
+ "md"
4226
+ ]
4227
+ },
4228
+ "accept": {
4229
+ "type": "string | undefined",
4230
+ "required": false,
4231
+ "origin": "own",
4232
+ "description": "Native `accept` — a comma-separated list of extensions or MIME types."
4233
+ },
4234
+ "multiple": {
4235
+ "type": "boolean | undefined",
4236
+ "required": false,
4237
+ "origin": "own",
4238
+ "description": "Allow more than one file."
4239
+ },
4240
+ "maxSize": {
4241
+ "type": "number | undefined",
4242
+ "required": false,
4243
+ "origin": "own",
4244
+ "description": "Reject anything larger, in bytes."
4245
+ },
4246
+ "maxFiles": {
4247
+ "type": "number | undefined",
4248
+ "required": false,
4249
+ "origin": "own",
4250
+ "description": "Reject once this many files are held. Only meaningful with `multiple`."
4251
+ },
4252
+ "files": {
4253
+ "type": "readonly File[] | undefined",
4254
+ "required": false,
4255
+ "origin": "own",
4256
+ "description": "The files held. Pass it to control the list; omit and the component keeps\nits own. A controlled list is the usual case, because the caller is the\nonly thing that knows which uploads have since succeeded or failed."
4257
+ },
4258
+ "onChange": {
4259
+ "type": "((files: File[]) => void) | undefined",
4260
+ "required": false,
4261
+ "origin": "own",
4262
+ "description": "Fires with the full list after every add or remove, never with a delta."
4263
+ },
4264
+ "onReject": {
4265
+ "type": "((rejected: RejectedFile[]) => void) | undefined",
4266
+ "required": false,
4267
+ "origin": "own",
4268
+ "description": "Fires with anything refused by `accept`, `maxSize` or `maxFiles`."
4269
+ },
4270
+ "prompt": {
4271
+ "type": "React.ReactNode",
4272
+ "required": false,
4273
+ "origin": "own",
4274
+ "description": "Call-to-action inside the drop target."
4275
+ },
4276
+ "hint": {
4277
+ "type": "React.ReactNode",
4278
+ "required": false,
4279
+ "origin": "own",
4280
+ "description": "Second line inside the drop target — the constraints, in words."
4281
+ },
4282
+ "removeLabel": {
4283
+ "type": "((name: string) => string) | undefined",
4284
+ "required": false,
4285
+ "origin": "own",
4286
+ "description": "Accessible label for each file's remove button. Receives the file name."
4287
+ },
4288
+ "className": {
4289
+ "type": "string | undefined",
4290
+ "required": false,
4291
+ "origin": "own"
4292
+ },
4293
+ "wrapperClassName": {
4294
+ "type": "string | undefined",
4295
+ "required": false,
4296
+ "origin": "own"
4297
+ },
4298
+ "id": {
4299
+ "type": "string | undefined",
4300
+ "required": false,
4301
+ "origin": "own"
4302
+ },
4303
+ "name": {
4304
+ "type": "string | undefined",
4305
+ "required": false,
4306
+ "origin": "own"
4307
+ }
4308
+ },
4309
+ "propCounts": {
4310
+ "own": 20,
4311
+ "aria": 0,
4312
+ "dom": 0,
4313
+ "other": 0
4314
+ }
4315
+ },
4316
+ "FullCard": {
4317
+ "name": "FullCard",
4318
+ "source": "src/components/FullCard.tsx",
4319
+ "propsType": "FullCardProps",
4320
+ "description": "Full Card — the full-bleed case study row from Figma `Full Card` (592:857).\n\nA text column beside a framed media panel, split down the middle, mirrored\nby the `Alignment` variant.\n\nThe split holds from 1080 and stacks below it, media above content in BOTH\nalignments — `Alignment` names a horizontal side and\nstops meaning anything once there is one column, so it does not get to\ndecide the vertical order too. Size is a media query, not a prop, the same\ncall Header makes about Device.\n\nWHY THE `show*` BOOLEANS ARE GONE\n\nFigma carries `Show Eyebrow`, `Show Description` and `Show Actions` beside\nthe slots they gate, because a Figma component instance always holds every\nlayer and needs a switch to hide one. React has no such constraint — an\nabsent prop is the switch. Badge made the same call with `Show Dot`, and\nkeeping both would have let `showActions` and `actions` disagree.\n\n`headline` is required, and it is the only required prop. The heading is\nwhat makes this a section rather than a decorated div, and every other part\nof the card is optional in Figma too.\n\nNo `'use client'`: nothing here is stateful or interactive. The interactive\nparts arrive through `actions`, and they carry their own boundary.",
4321
+ "import": "import { FullCard } from 'ionbase-ui';",
4322
+ "status": "stable",
4323
+ "summary": "A full-bleed row: a text column beside a framed media panel, mirrored by `alignment`.",
4324
+ "useWhen": [
4325
+ "a marketing or overview section presents one thing with a screenshot beside it",
4326
+ "a case-study or feature row on a landing or product page"
4327
+ ],
4328
+ "useInstead": [
4329
+ {
4330
+ "when": "you are listing records with comparable fields",
4331
+ "use": "Table"
4332
+ },
4333
+ {
4334
+ "when": "it is a short status message rather than a content section",
4335
+ "use": "Alert"
4336
+ }
4337
+ ],
4338
+ "slots": {
4339
+ "eyebrow": {
4340
+ "accepts": "Badge",
4341
+ "note": "any node works"
4342
+ },
4343
+ "headline": {
4344
+ "accepts": "text",
4345
+ "note": "required — the heading is what makes this a section rather than a decorated div"
4346
+ },
4347
+ "description": {
4348
+ "accepts": "text"
4349
+ },
4350
+ "actions": {
4351
+ "accepts": "Button",
4352
+ "note": "a secondary Button in the design"
4353
+ },
4354
+ "media": {
4355
+ "accepts": "img or video",
4356
+ "note": "rendered inside the framed screen holder — pass the screenshot or embed itself, not the frame"
4357
+ },
4358
+ "children": {
4359
+ "accepts": "any",
4360
+ "note": "extra content below the description"
4361
+ }
4362
+ },
4363
+ "variants": {
4364
+ "alignment": {
4365
+ "left": {
4366
+ "use": "media on the left, text on the right"
4367
+ },
4368
+ "right": {
4369
+ "use": "media on the right — the variant names the media, not the text"
4370
+ }
4371
+ }
4372
+ },
4373
+ "a11y": {
4374
+ "guarantees": [
4375
+ "the headline renders as a real heading, so the card appears in the document outline"
4376
+ ],
4377
+ "requires": [
4378
+ "`headingLevel` matching the surrounding outline — `h3` is the default because Figma applies Type/H3, but only the page knows the real level",
4379
+ "`alt` on whatever you pass as `media`"
4380
+ ],
4381
+ "notes": [
4382
+ "`h1` is deliberately not available: a full card is a section within a page, never the page's own title."
4383
+ ]
4384
+ },
4385
+ "antiPatterns": [
4386
+ {
4387
+ "dont": "looking for `showEyebrow` / `showDescription` / `showActions`",
4388
+ "why": "an absent prop is the switch — Figma needs the boolean because an instance always holds every layer; React does not"
4389
+ },
4390
+ {
4391
+ "dont": "leaving `headingLevel` at the default inside a section that is already at h3",
4392
+ "why": "it produces a skipped or duplicated outline level"
4393
+ },
4394
+ {
4395
+ "dont": "passing your own frame or border around `media`",
4396
+ "why": "the component already draws the screen holder"
4397
+ }
4398
+ ],
4399
+ "stylesheet": "src/styles/full-card.css",
4400
+ "tokens": [
4401
+ "--border-default",
4402
+ "--border-width-default",
4403
+ "--font-family-sans",
4404
+ "--font-weight-regular",
4405
+ "--font-weight-semibold",
4406
+ "--ion-full-card-gap",
4407
+ "--ion-full-card-media-gutter",
4408
+ "--ion-full-card-media-min-height",
3269
4409
  "--ion-full-card-media-width",
3270
4410
  "--ion-full-card-padding-x",
3271
4411
  "--ion-full-card-padding-y",
@@ -3684,7 +4824,6 @@
3684
4824
  "--border-strong",
3685
4825
  "--border-subtle",
3686
4826
  "--border-width-default",
3687
- "--border-width-thick",
3688
4827
  "--font-family-sans",
3689
4828
  "--font-weight-medium",
3690
4829
  "--font-weight-regular",
@@ -5548,50 +6687,20 @@
5548
6687
  "origin": "own",
5549
6688
  "description": "Spread onto the dial-code `<button>`. This is how a country picker gets\nattached — see the note on scope below."
5550
6689
  },
5551
- "type": {
5552
- "type": "(string & {}) | \"search\" | \"text\" | \"url\" | \"tel\" | \"email\" | \"password\" | undefined",
5553
- "required": false,
5554
- "origin": "aria",
5555
- "description": "The type of input to render. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdeftype).",
5556
- "tags": {
5557
- "default": "'text'"
5558
- },
5559
- "default": "tel"
5560
- },
5561
- "disabled": {
5562
- "type": "boolean | undefined",
5563
- "required": false,
5564
- "origin": "own",
5565
- "tags": {
5566
- "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
5567
- }
5568
- },
5569
- "form": {
5570
- "type": "string | undefined",
6690
+ "onChange": {
6691
+ "type": "((value: string) => void) | undefined",
5571
6692
  "required": false,
5572
6693
  "origin": "aria",
5573
- "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)."
6694
+ "description": "Handler that is called when the value changes."
5574
6695
  },
5575
- "name": {
6696
+ "defaultValue": {
5576
6697
  "type": "string | undefined",
5577
6698
  "required": false,
5578
6699
  "origin": "aria",
5579
- "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)."
6700
+ "description": "The default value (uncontrolled)."
5580
6701
  },
5581
- "value": {
5582
- "type": "string | undefined",
5583
- "required": false,
5584
- "origin": "aria",
5585
- "description": "The current value (controlled)."
5586
- },
5587
- "defaultValue": {
5588
- "type": "string | undefined",
5589
- "required": false,
5590
- "origin": "aria",
5591
- "description": "The default value (uncontrolled)."
5592
- },
5593
- "autoCapitalize": {
5594
- "type": "\"none\" | \"off\" | \"on\" | \"sentences\" | \"words\" | \"characters\" | undefined",
6702
+ "autoCapitalize": {
6703
+ "type": "\"none\" | \"off\" | \"on\" | \"sentences\" | \"words\" | \"characters\" | undefined",
5595
6704
  "required": false,
5596
6705
  "origin": "own",
5597
6706
  "description": "Narrower than React's own typing, which also allows any `string`.\n`autoCapitalize` is declared on `AriaTextFieldOptions` rather than\n`AriaTextFieldProps`, so `keyof AriaTextFieldProps` above cannot omit it —\nand the wide DOM version does not satisfy what `useTextField` accepts.",
@@ -5773,12 +6882,6 @@
5773
6882
  "origin": "aria",
5774
6883
  "description": "Handler that is called when the element loses focus."
5775
6884
  },
5776
- "onChange": {
5777
- "type": "((value: string) => void) | undefined",
5778
- "required": false,
5779
- "origin": "aria",
5780
- "description": "Handler that is called when the value changes."
5781
- },
5782
6885
  "onBeforeInput": {
5783
6886
  "type": "FormEventHandler<HTMLInputElement> | undefined",
5784
6887
  "required": false,
@@ -5809,6 +6912,42 @@
5809
6912
  "origin": "aria",
5810
6913
  "description": "Handler that is called when text in the input is selected. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/select_event)."
5811
6914
  },
6915
+ "type": {
6916
+ "type": "(string & {}) | \"search\" | \"text\" | \"url\" | \"tel\" | \"email\" | \"password\" | undefined",
6917
+ "required": false,
6918
+ "origin": "aria",
6919
+ "description": "The type of input to render. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdeftype).",
6920
+ "tags": {
6921
+ "default": "'text'"
6922
+ },
6923
+ "default": "tel"
6924
+ },
6925
+ "disabled": {
6926
+ "type": "boolean | undefined",
6927
+ "required": false,
6928
+ "origin": "own",
6929
+ "tags": {
6930
+ "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
6931
+ }
6932
+ },
6933
+ "form": {
6934
+ "type": "string | undefined",
6935
+ "required": false,
6936
+ "origin": "aria",
6937
+ "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)."
6938
+ },
6939
+ "name": {
6940
+ "type": "string | undefined",
6941
+ "required": false,
6942
+ "origin": "aria",
6943
+ "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)."
6944
+ },
6945
+ "value": {
6946
+ "type": "string | undefined",
6947
+ "required": false,
6948
+ "origin": "aria",
6949
+ "description": "The current value (controlled)."
6950
+ },
5812
6951
  "size": {
5813
6952
  "type": "InputSize | undefined",
5814
6953
  "required": false,
@@ -6115,10 +7254,10 @@
6115
7254
  "origin": "own",
6116
7255
  "description": "Matches the Figma `Placement` variant, and names where the POPOVER sits —\nnot where the arrow points. A preference, not a guarantee: it flips when\nthere is no room, and the arrow follows.",
6117
7256
  "values": [
6118
- "right",
6119
- "left",
6120
7257
  "top",
6121
- "bottom"
7258
+ "bottom",
7259
+ "right",
7260
+ "left"
6122
7261
  ]
6123
7262
  },
6124
7263
  "size": {
@@ -6172,6 +7311,172 @@
6172
7311
  "other": 0
6173
7312
  }
6174
7313
  },
7314
+ "ProgressBar": {
7315
+ "name": "ProgressBar",
7316
+ "source": "src/components/ProgressBar.tsx",
7317
+ "propsType": "ProgressBarProps",
7318
+ "description": "ProgressBar — a determinate or indeterminate measure of work.\n\nDETERMINATE AND INDETERMINATE ARE THE SAME COMPONENT ON PURPOSE\n\nWork that starts unmeasurable and becomes measurable is the common case — a\nupload that does not know its size until the first chunk lands. Splitting\nthem into two components would make that transition a swap, which remounts\nthe node and loses the live region with it.\n\nOmitting `value` is what makes it indeterminate. `aria-valuenow` is then\nomitted too, which is exactly what the ARIA spec asks for and is the detail\nhand-rolled progress bars get wrong most often — a bar reporting\n`aria-valuenow=\"0\"` forever tells a screen reader the work is stuck at zero,\nnot that it is unmeasured.\n\nWHY `label` IS REQUIRED\n\n`role=\"progressbar\"` announces a number. Without a name the user hears \"42\npercent\" with no indication of what is at 42 percent, which is worse than\nsilence because it sounds like information.",
7319
+ "import": "import { ProgressBar } from 'ionbase-ui';",
7320
+ "status": "stable",
7321
+ "summary": "A determinate or indeterminate measure of work, with the name of the work attached. Omitting `value` is what makes it indeterminate.",
7322
+ "useWhen": [
7323
+ "work reports a fraction complete — an upload, an import, a multi-step job",
7324
+ "work will become measurable but is not yet, and the bar should not remount when it does",
7325
+ "a quota or usage figure is better read as a bar than a number"
7326
+ ],
7327
+ "useInstead": [
7328
+ {
7329
+ "when": "the wait is short and unmeasurable",
7330
+ "use": "Spinner",
7331
+ "why": "an indeterminate bar occupies a block of layout to say what a spinner says inline"
7332
+ },
7333
+ {
7334
+ "when": "the region is being replaced by content",
7335
+ "use": "Skeleton"
7336
+ }
7337
+ ],
7338
+ "variants": {
7339
+ "intent": {
7340
+ "primary": {
7341
+ "use": "the default — work proceeding normally"
7342
+ },
7343
+ "success": {
7344
+ "use": "work that finished, where the bar stays on screen as a record"
7345
+ },
7346
+ "warning": {
7347
+ "use": "a quota approaching its limit",
7348
+ "note": "warning describes the VALUE, not a failure of the bar"
7349
+ },
7350
+ "error": {
7351
+ "use": "a quota exceeded, or a job that failed part-way",
7352
+ "note": "the bar is still reporting correctly — reserve this for the measured thing being bad"
7353
+ }
7354
+ },
7355
+ "size": {
7356
+ "sm": {
7357
+ "use": "inside a row or a dense table cell"
7358
+ },
7359
+ "md": {
7360
+ "use": "the default"
7361
+ }
7362
+ }
7363
+ },
7364
+ "a11y": {
7365
+ "requires": [
7366
+ "`label` — it is required, because role=\"progressbar\" announces a number and a number with no noun sounds like information while carrying none",
7367
+ "`valueText` when a percentage is not what a person wants read aloud — \"3 of 12 files\""
7368
+ ],
7369
+ "notes": [
7370
+ "indeterminate omits aria-valuenow entirely, as the ARIA spec asks; a bar reporting aria-valuenow=0 forever says the work is stuck rather than unmeasured",
7371
+ "value is clamped to 0..max, so a caller cannot render a bar past its own end"
7372
+ ]
7373
+ },
7374
+ "antiPatterns": [
7375
+ {
7376
+ "dont": "<ProgressBar label=\"Loading\" value={0} /> while the total is unknown",
7377
+ "do": "<ProgressBar label=\"Loading\" />",
7378
+ "why": "value={0} announces \"0 percent\" indefinitely. Omitting value is how you say \"not measurable yet\""
7379
+ },
7380
+ {
7381
+ "dont": "swapping a Spinner for a ProgressBar once the size is known",
7382
+ "do": "one ProgressBar, with `value` appearing when it becomes known",
7383
+ "why": "the swap remounts the node and takes the live region with it, so the transition is silent to a screen reader"
7384
+ },
7385
+ {
7386
+ "dont": "intent=\"error\" because the request failed",
7387
+ "do": "leave the bar and put the failure in an Alert",
7388
+ "why": "intent describes the value being measured. A failed request is not a red measurement, it is a different message"
7389
+ }
7390
+ ],
7391
+ "stylesheet": "src/styles/progress-bar.css",
7392
+ "tokens": [
7393
+ "--border-width-default",
7394
+ "--font-family-sans",
7395
+ "--ion-duration-base",
7396
+ "--ion-duration-slow",
7397
+ "--ion-ease-in-out",
7398
+ "--ion-ease-out",
7399
+ "--ion-progress-bar-fill",
7400
+ "--ion-progress-bar-height",
7401
+ "--ion-progress-bar-pct",
7402
+ "--radius-full",
7403
+ "--spacing-4",
7404
+ "--spacing-8",
7405
+ "--surface-error",
7406
+ "--surface-muted",
7407
+ "--surface-primary",
7408
+ "--surface-success",
7409
+ "--surface-warning",
7410
+ "--text-default",
7411
+ "--text-secondary",
7412
+ "--type-body-sm",
7413
+ "--type-body-sm-line-height"
7414
+ ],
7415
+ "props": {
7416
+ "label": {
7417
+ "type": "string",
7418
+ "required": true,
7419
+ "origin": "own",
7420
+ "description": "What is progressing. Required — a bare bar announces a number and no noun."
7421
+ },
7422
+ "value": {
7423
+ "type": "number | undefined",
7424
+ "required": false,
7425
+ "origin": "own",
7426
+ "description": "0 to `max`. Omit for an indeterminate bar."
7427
+ },
7428
+ "max": {
7429
+ "type": "number | undefined",
7430
+ "required": false,
7431
+ "origin": "own",
7432
+ "description": "Defaults to 100."
7433
+ },
7434
+ "intent": {
7435
+ "type": "ProgressBarIntent | undefined",
7436
+ "required": false,
7437
+ "origin": "own",
7438
+ "values": [
7439
+ "primary",
7440
+ "success",
7441
+ "warning",
7442
+ "error"
7443
+ ]
7444
+ },
7445
+ "size": {
7446
+ "type": "ProgressBarSize | undefined",
7447
+ "required": false,
7448
+ "origin": "own",
7449
+ "values": [
7450
+ "sm",
7451
+ "md"
7452
+ ]
7453
+ },
7454
+ "isLabelVisible": {
7455
+ "type": "boolean | undefined",
7456
+ "required": false,
7457
+ "origin": "own",
7458
+ "description": "Show the label as text above the track."
7459
+ },
7460
+ "isValueVisible": {
7461
+ "type": "boolean | undefined",
7462
+ "required": false,
7463
+ "origin": "own",
7464
+ "description": "Show the percentage beside the label. Determinate bars only."
7465
+ },
7466
+ "valueText": {
7467
+ "type": "string | undefined",
7468
+ "required": false,
7469
+ "origin": "own",
7470
+ "description": "Spoken instead of the percentage — \"3 of 12 files\". A percentage is rarely\nthe thing a person wants read aloud."
7471
+ }
7472
+ },
7473
+ "propCounts": {
7474
+ "own": 8,
7475
+ "aria": 0,
7476
+ "dom": 277,
7477
+ "other": 0
7478
+ }
7479
+ },
6175
7480
  "Radio": {
6176
7481
  "name": "Radio",
6177
7482
  "source": "src/components/Radio.tsx",
@@ -6692,7 +7997,8 @@
6692
7997
  "useInstead": [
6693
7998
  {
6694
7999
  "when": "the list is long enough to need search or type-ahead beyond the native behaviour",
6695
- "use": "a combobox — not yet in this system"
8000
+ "use": "Combobox",
8001
+ "why": "a native <select> list is the browser's, and cannot be filtered"
6696
8002
  },
6697
8003
  {
6698
8004
  "when": "the rows need icons, avatars or two lines of text",
@@ -6805,10 +8111,10 @@
6805
8111
  "default": "md"
6806
8112
  },
6807
8113
  "options": {
6808
- "type": "SelectOption[] | undefined",
8114
+ "type": "readonly SelectOption[] | undefined",
6809
8115
  "required": false,
6810
8116
  "origin": "own",
6811
- "description": "The options to render. Omit and pass `children` for grouped options."
8117
+ "description": "The options to render. Omit and pass `children` for grouped options.\n\n`readonly` because an options list is almost always a module-level constant,\nand `as const` on one produces a readonly array that a mutable parameter\nrejects. The component only ever maps over it, so demanding a mutable array\nbought nothing and cost callers a spread or a cast."
6812
8118
  },
6813
8119
  "placeholder": {
6814
8120
  "type": "string | undefined",
@@ -6872,107 +8178,350 @@
6872
8178
  "other": 0
6873
8179
  }
6874
8180
  },
6875
- "StreamingText": {
6876
- "name": "StreamingText",
6877
- "source": "src/components/StreamingText.tsx",
6878
- "propsType": "StreamingTextProps",
6879
- "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.",
6880
- "import": "import { StreamingText } from 'ionbase-ui';",
8181
+ "Skeleton": {
8182
+ "name": "Skeleton",
8183
+ "source": "src/components/Skeleton.tsx",
8184
+ "propsType": "SkeletonProps",
8185
+ "description": "Skeleton — the shape of content that has not arrived.\n\nIT IS HIDDEN FROM ASSISTIVE TECHNOLOGY, ALWAYS, AND THAT IS THE WHOLE POINT\n\nA skeleton is a picture of content, and a screen reader cannot use a picture\nof content. Announcing it produces a stream of empty boxes between the user\nand the thing they asked for. So every skeleton is `aria-hidden`, with no way\nto opt out — a prop for that would only ever be used by mistake.\n\nWHICH MEANS THE CALLER OWES THE ANNOUNCEMENT, AND THIS COMPONENT CANNOT DO IT\n\nHiding the placeholder is only half an answer: something still has to tell a\nscreen-reader user that the region is loading. That belongs on the region\nbeing replaced — `aria-busy=\"true\"` while it loads — because only the caller\nknows where that region starts and ends. `Spinner` is the other half when the\nwait deserves an announcement of its own.\n\n <section aria-busy={isLoading}>\n {isLoading ? <Skeleton lines={3} /> : <Rows data={data} />}\n </section>\n\nThis is the one component in the system whose correct use REQUIRES something\nof the caller that the type system cannot check, which is why it is stated\nhere, in the contract, and in the story.",
8186
+ "import": "import { Skeleton } from 'ionbase-ui';",
6881
8187
  "status": "stable",
6882
- "summary": "Model output arriving a token at a time, in a container that does not move. Deliberately not a live region.",
8188
+ "summary": "The shape of content that has not arrived. Always hidden from assistive technology — the caller owns the announcement.",
6883
8189
  "useWhen": [
6884
- "rendering a response that streams in rather than arriving whole",
6885
- "the reader should be able to start reading before generation finishes"
8190
+ "a region will be replaced by content whose shape is known in advance",
8191
+ "the layout would otherwise jump when the content lands",
8192
+ "several regions load independently and a single page spinner would hide that"
6886
8193
  ],
6887
8194
  "useInstead": [
6888
8195
  {
6889
- "when": "the text is already complete",
6890
- "use": "Alert",
6891
- "why": "nothing about this component helps once there is nothing left to arrive"
8196
+ "when": "the shape of what is coming is not known",
8197
+ "use": "Spinner",
8198
+ "why": "a skeleton that does not match what replaces it is a worse promise than no promise"
6892
8199
  },
6893
8200
  {
6894
- "when": "you are showing progress through steps rather than producing text",
6895
- "use": "AgentActivity"
8201
+ "when": "the region is empty because there is nothing to show",
8202
+ "use": "EmptyState",
8203
+ "why": "a skeleton that never resolves is how an empty list looks like a broken one"
6896
8204
  }
6897
8205
  ],
8206
+ "variants": {
8207
+ "variant": {
8208
+ "text": {
8209
+ "use": "lines of copy",
8210
+ "note": "height comes from the line box, so the block occupies what the real paragraph will. With `lines`, the last is drawn short because real paragraphs end mid-measure"
8211
+ },
8212
+ "circle": {
8213
+ "use": "an avatar or an icon slot"
8214
+ },
8215
+ "rect": {
8216
+ "use": "a card, a thumbnail, a chart"
8217
+ }
8218
+ }
8219
+ },
6898
8220
  "a11y": {
6899
- "role": "region when `label` is set, none otherwise",
6900
- "guarantees": [
6901
- "`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",
6902
- "`aria-busy` while streaming, so assistive tech knows the region is unsettled",
6903
- "the text stays ordinary readable content throughout, so a screen-reader user can read ahead exactly as a sighted user does",
6904
- "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)"
6905
- ],
6906
8221
  "requires": [
6907
- "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",
6908
- "`label` when the region needs a name of its own"
8222
+ "`aria-busy=\"true\"` on the region being replaced, set by the CALLER — this component cannot do it, because only the caller knows where the region starts and ends"
8223
+ ],
8224
+ "notes": [
8225
+ "every skeleton is aria-hidden with no opt-out: a screen reader cannot use a picture of content, and announcing it puts a stream of empty boxes between the user and what they asked for",
8226
+ "prefers-reduced-motion stops the pulse outright, unlike Spinner and ProgressBar — a skeleton conveys nothing a static block does not, so there is no signal to preserve"
6909
8227
  ]
6910
8228
  },
6911
8229
  "antiPatterns": [
6912
8230
  {
6913
- "dont": "wrapping it in `aria-live=\"polite\"`",
6914
- "why": "every token queues an announcement; the user hears the answer stuttered dozens of times and cannot get ahead of it"
8231
+ "dont": "<Skeleton /> inside a region with no aria-busy",
8232
+ "do": "<section aria-busy={isLoading}>{isLoading ? <Skeleton lines={3} /> : <Rows />}</section>",
8233
+ "why": "the skeleton is hidden, so without aria-busy a screen-reader user is told nothing at all — the region simply reads as empty"
6915
8234
  },
6916
8235
  {
6917
- "dont": "letting the container grow token by token with no reserved height",
6918
- "do": "`minLines`",
6919
- "why": "the content below climbs the screen while someone is reading it, which is worse under magnification than the wait"
8236
+ "dont": "a skeleton whose shape does not match the content that replaces it",
8237
+ "do": "match the line count and block sizes to the real thing",
8238
+ "why": "the entire benefit is that the layout does not move; a mismatched skeleton pays the cost and keeps the jump"
6920
8239
  },
6921
8240
  {
6922
- "dont": "a text-character cursor like ▌",
6923
- "why": "some screen readers read it aloud even inside an aria-hidden span"
8241
+ "dont": "leaving skeletons on screen when a request fails",
8242
+ "do": "render EmptyState with reason=\"error\"",
8243
+ "why": "a skeleton that never resolves is indistinguishable from a hung page"
6924
8244
  }
6925
8245
  ],
6926
- "stylesheet": "src/styles/streaming-text.css",
8246
+ "stylesheet": "src/styles/skeleton.css",
6927
8247
  "tokens": [
6928
- "--font-family-sans",
6929
- "--ion-streaming-min-lines",
6930
- "--text-default",
6931
- "--text-tertiary",
6932
- "--type-body",
8248
+ "--border-width-default",
8249
+ "--icon-size-lg",
8250
+ "--ion-duration-slow",
8251
+ "--ion-ease-in-out",
8252
+ "--ion-skeleton-height",
8253
+ "--ion-skeleton-radius",
8254
+ "--ion-skeleton-width",
8255
+ "--radius-full",
8256
+ "--radius-md",
8257
+ "--radius-sm",
8258
+ "--spacing-16",
8259
+ "--spacing-64",
8260
+ "--spacing-8",
8261
+ "--surface-muted",
6933
8262
  "--type-body-line-height"
6934
8263
  ],
6935
8264
  "props": {
6936
- "children": {
6937
- "type": "React.ReactNode",
6938
- "required": false,
6939
- "origin": "own",
6940
- "description": "The text so far. Re-render with more of it; this component appends nothing."
6941
- },
6942
- "isStreaming": {
6943
- "type": "boolean | undefined",
8265
+ "variant": {
8266
+ "type": "SkeletonVariant | undefined",
6944
8267
  "required": false,
6945
8268
  "origin": "own",
6946
- "description": "More is still arriving. Shows the cursor and marks the region busy."
8269
+ "values": [
8270
+ "circle",
8271
+ "text",
8272
+ "rect"
8273
+ ]
6947
8274
  },
6948
- "minLines": {
6949
- "type": "number | undefined",
8275
+ "width": {
8276
+ "type": "string | undefined",
6950
8277
  "required": false,
6951
8278
  "origin": "own",
6952
- "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."
8279
+ "description": "Any CSS length. Defaults to filling the width available."
6953
8280
  },
6954
- "label": {
8281
+ "height": {
6955
8282
  "type": "string | undefined",
6956
8283
  "required": false,
6957
8284
  "origin": "own",
6958
- "description": "Accessible name for the region."
8285
+ "description": "Any CSS length. `text` derives its height from the line box instead."
6959
8286
  },
6960
- "hideCursor": {
6961
- "type": "boolean | undefined",
8287
+ "lines": {
8288
+ "type": "number | undefined",
6962
8289
  "required": false,
6963
8290
  "origin": "own",
6964
- "description": "Hide the trailing cursor. The text still marks itself busy."
8291
+ "description": "`text` only: how many lines to draw. The last is drawn short."
6965
8292
  }
6966
8293
  },
6967
8294
  "propCounts": {
6968
- "own": 5,
8295
+ "own": 4,
6969
8296
  "aria": 0,
6970
8297
  "dom": 277,
6971
8298
  "other": 0
6972
8299
  }
6973
8300
  },
6974
- "TabItem": {
6975
- "name": "TabItem",
8301
+ "Spinner": {
8302
+ "name": "Spinner",
8303
+ "source": "src/components/Spinner.tsx",
8304
+ "propsType": "SpinnerProps",
8305
+ "description": "Spinner — an indeterminate wait, announced once.\n\nWHY THIS IS NOT A DIV WITH A CSS ANIMATION\n\nThe obvious implementation is inaccessible in a specific and common way: a\nspinning graphic with no text is silence to a screen reader, so the user is\ntold nothing happened. Generated code in this repo's own eval corpus reached\nfor a bare `role=\"status\"` in 48 files, which is the right instinct and only\nhalf the job — the role creates a live region, but an empty one announces\nnothing at all.\n\nSo the default carries a label. It is visually hidden, it lives inside the\nlive region, and it is what makes \"Loading\" reach the user who cannot see the\nring.\n\n`isDecorative` exists because the opposite mistake is just as easy. A spinner\ninside a button whose label already changed to \"Saving…\" would announce\ntwice, and two live regions racing is worse than one. Decorative renders no\nrole at all rather than `aria-hidden` on a live region, which browsers treat\ninconsistently.\n\nMOTION\n\n`prefers-reduced-motion` slows the rotation rather than stopping it. A\nstationary spinner is not a calmer spinner — it is a broken one, and it\nremoves the only signal a sighted user has that the wait is still live.",
8306
+ "import": "import { Spinner } from 'ionbase-ui';",
8307
+ "status": "stable",
8308
+ "summary": "An indeterminate wait, announced once. A ring plus a politely-announced label, sized off the icon ramp.",
8309
+ "useWhen": [
8310
+ "work is in flight and its duration cannot be measured",
8311
+ "a control has started something and must show it is working",
8312
+ "a small region is waiting and a skeleton would be more scaffolding than content"
8313
+ ],
8314
+ "useInstead": [
8315
+ {
8316
+ "when": "the work has a measurable fraction complete",
8317
+ "use": "ProgressBar",
8318
+ "why": "a spinner over measurable work discards information the user wants — a spinner and a 90%-complete upload look identical"
8319
+ },
8320
+ {
8321
+ "when": "a region is being replaced by content with a known shape",
8322
+ "use": "Skeleton",
8323
+ "why": "a skeleton holds the layout so it does not jump when content lands; a spinner leaves a hole that then reflows"
8324
+ }
8325
+ ],
8326
+ "variants": {
8327
+ "size": {
8328
+ "sm": {
8329
+ "use": "inside a control, or beside a line of text"
8330
+ },
8331
+ "md": {
8332
+ "use": "the default — a panel or a section"
8333
+ },
8334
+ "lg": {
8335
+ "use": "a whole route or a full-page wait"
8336
+ }
8337
+ }
8338
+ },
8339
+ "a11y": {
8340
+ "requires": [
8341
+ "a `label` naming what is loading — it is announced politely and is the only thing a screen-reader user receives",
8342
+ "`isDecorative` when something else already announces the wait, so two live regions do not race"
8343
+ ],
8344
+ "notes": [
8345
+ "the ring is aria-hidden; the label carries the meaning",
8346
+ "prefers-reduced-motion slows the rotation rather than stopping it — a stationary spinner is indistinguishable from a hung one"
8347
+ ]
8348
+ },
8349
+ "antiPatterns": [
8350
+ {
8351
+ "dont": "<div className=\"spinner\" role=\"status\" />",
8352
+ "do": "<Spinner label=\"Loading invoices\" />",
8353
+ "why": "role=\"status\" creates a live region; an EMPTY live region announces nothing. This exact half-measure appeared in 48 files of this repo's eval corpus"
8354
+ },
8355
+ {
8356
+ "dont": "<Button isDisabled>Saving… <Spinner /></Button>",
8357
+ "do": "<Button isDisabled>Saving… <Spinner isDecorative /></Button>",
8358
+ "why": "the button label already says it. Two live regions announcing the same wait talk over each other"
8359
+ },
8360
+ {
8361
+ "dont": "a Spinner for an upload that reports bytes transferred",
8362
+ "do": "ProgressBar with value",
8363
+ "why": "throwing away a known percentage makes a finite wait feel unbounded"
8364
+ }
8365
+ ],
8366
+ "stylesheet": "src/styles/spinner.css",
8367
+ "tokens": [
8368
+ "--border-primary-strong",
8369
+ "--border-subtle",
8370
+ "--border-width-thick",
8371
+ "--font-family-sans",
8372
+ "--icon-size-lg",
8373
+ "--icon-size-md",
8374
+ "--icon-size-sm",
8375
+ "--ion-duration-slow",
8376
+ "--ion-ease-linear",
8377
+ "--ion-spinner-head",
8378
+ "--ion-spinner-size",
8379
+ "--ion-spinner-track",
8380
+ "--radius-full",
8381
+ "--spacing-8",
8382
+ "--text-secondary",
8383
+ "--type-body-sm",
8384
+ "--type-body-sm-line-height"
8385
+ ],
8386
+ "props": {
8387
+ "size": {
8388
+ "type": "SpinnerSize | undefined",
8389
+ "required": false,
8390
+ "origin": "own",
8391
+ "description": "Matches the icon size ramp: sm, md, lg.",
8392
+ "values": [
8393
+ "sm",
8394
+ "md",
8395
+ "lg"
8396
+ ]
8397
+ },
8398
+ "label": {
8399
+ "type": "string | undefined",
8400
+ "required": false,
8401
+ "origin": "own",
8402
+ "description": "What is loading. Announced politely; visible only with `isLabelVisible`."
8403
+ },
8404
+ "isLabelVisible": {
8405
+ "type": "boolean | undefined",
8406
+ "required": false,
8407
+ "origin": "own",
8408
+ "description": "Render the label as text beside the ring as well as announcing it."
8409
+ },
8410
+ "isDecorative": {
8411
+ "type": "boolean | undefined",
8412
+ "required": false,
8413
+ "origin": "own",
8414
+ "description": "The spinner conveys nothing on its own — something else already says the\nregion is busy. Renders inert: no role, no live region, hidden from AT."
8415
+ }
8416
+ },
8417
+ "propCounts": {
8418
+ "own": 4,
8419
+ "aria": 0,
8420
+ "dom": 277,
8421
+ "other": 0
8422
+ }
8423
+ },
8424
+ "StreamingText": {
8425
+ "name": "StreamingText",
8426
+ "source": "src/components/StreamingText.tsx",
8427
+ "propsType": "StreamingTextProps",
8428
+ "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.",
8429
+ "import": "import { StreamingText } from 'ionbase-ui';",
8430
+ "status": "stable",
8431
+ "summary": "Model output arriving a token at a time, in a container that does not move. Deliberately not a live region.",
8432
+ "useWhen": [
8433
+ "rendering a response that streams in rather than arriving whole",
8434
+ "the reader should be able to start reading before generation finishes"
8435
+ ],
8436
+ "useInstead": [
8437
+ {
8438
+ "when": "the text is already complete",
8439
+ "use": "Alert",
8440
+ "why": "nothing about this component helps once there is nothing left to arrive"
8441
+ },
8442
+ {
8443
+ "when": "you are showing progress through steps rather than producing text",
8444
+ "use": "AgentActivity"
8445
+ }
8446
+ ],
8447
+ "a11y": {
8448
+ "role": "region when `label` is set, none otherwise",
8449
+ "guarantees": [
8450
+ "`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",
8451
+ "`aria-busy` while streaming, so assistive tech knows the region is unsettled",
8452
+ "the text stays ordinary readable content throughout, so a screen-reader user can read ahead exactly as a sighted user does",
8453
+ "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)"
8454
+ ],
8455
+ "requires": [
8456
+ "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",
8457
+ "`label` when the region needs a name of its own"
8458
+ ]
8459
+ },
8460
+ "antiPatterns": [
8461
+ {
8462
+ "dont": "wrapping it in `aria-live=\"polite\"`",
8463
+ "why": "every token queues an announcement; the user hears the answer stuttered dozens of times and cannot get ahead of it"
8464
+ },
8465
+ {
8466
+ "dont": "letting the container grow token by token with no reserved height",
8467
+ "do": "`minLines`",
8468
+ "why": "the content below climbs the screen while someone is reading it, which is worse under magnification than the wait"
8469
+ },
8470
+ {
8471
+ "dont": "a text-character cursor like ▌",
8472
+ "why": "some screen readers read it aloud even inside an aria-hidden span"
8473
+ }
8474
+ ],
8475
+ "stylesheet": "src/styles/streaming-text.css",
8476
+ "tokens": [
8477
+ "--font-family-sans",
8478
+ "--ion-streaming-min-lines",
8479
+ "--text-default",
8480
+ "--text-tertiary",
8481
+ "--type-body",
8482
+ "--type-body-line-height"
8483
+ ],
8484
+ "props": {
8485
+ "children": {
8486
+ "type": "React.ReactNode",
8487
+ "required": false,
8488
+ "origin": "own",
8489
+ "description": "The text so far. Re-render with more of it; this component appends nothing."
8490
+ },
8491
+ "isStreaming": {
8492
+ "type": "boolean | undefined",
8493
+ "required": false,
8494
+ "origin": "own",
8495
+ "description": "More is still arriving. Shows the cursor and marks the region busy."
8496
+ },
8497
+ "minLines": {
8498
+ "type": "number | undefined",
8499
+ "required": false,
8500
+ "origin": "own",
8501
+ "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."
8502
+ },
8503
+ "label": {
8504
+ "type": "string | undefined",
8505
+ "required": false,
8506
+ "origin": "own",
8507
+ "description": "Accessible name for the region."
8508
+ },
8509
+ "hideCursor": {
8510
+ "type": "boolean | undefined",
8511
+ "required": false,
8512
+ "origin": "own",
8513
+ "description": "Hide the trailing cursor. The text still marks itself busy."
8514
+ }
8515
+ },
8516
+ "propCounts": {
8517
+ "own": 5,
8518
+ "aria": 0,
8519
+ "dom": 277,
8520
+ "other": 0
8521
+ }
8522
+ },
8523
+ "TabItem": {
8524
+ "name": "TabItem",
6976
8525
  "source": "../../node_modules/.pnpm/react-stately@3.48.0_react@19.2.8/node_modules/react-stately/dist/types/src/collections/Item.d.ts",
6977
8526
  "propsType": null,
6978
8527
  "import": "import { TabItem } from 'ionbase-ui';",
@@ -7778,79 +9327,454 @@
7778
9327
  "origin": "own",
7779
9328
  "description": "Drives both the arrow-key axis and the track layout.\n\n`vertical` is keyboard- and ARIA-complete — up/down move between tabs, and\nthe track stacks — but its *decoration* is not: the underline rule and the\npill track's padding are still written for the horizontal axis only. Those\nare Figma's to specify, not this file's to invent. See tabs.css.",
7780
9329
  "values": [
7781
- "horizontal",
7782
- "vertical"
9330
+ "horizontal",
9331
+ "vertical"
9332
+ ],
9333
+ "tags": {
9334
+ "default": "'horizontal'"
9335
+ },
9336
+ "default": "horizontal"
9337
+ },
9338
+ "className": {
9339
+ "type": "string | undefined",
9340
+ "required": false,
9341
+ "origin": "own"
9342
+ },
9343
+ "keyboardActivation": {
9344
+ "type": "\"manual\" | \"automatic\" | undefined",
9345
+ "required": false,
9346
+ "origin": "aria",
9347
+ "description": "Whether tabs are activated automatically on focus or manually.",
9348
+ "values": [
9349
+ "manual",
9350
+ "automatic"
9351
+ ],
9352
+ "tags": {
9353
+ "default": "'automatic'"
9354
+ }
9355
+ },
9356
+ "isDisabled": {
9357
+ "type": "boolean | undefined",
9358
+ "required": false,
9359
+ "origin": "other",
9360
+ "description": "Whether the TabList is disabled.\nShows that a selection exists, but is not available in that circumstance."
9361
+ },
9362
+ "selectedKey": {
9363
+ "type": "Key | undefined",
9364
+ "required": false,
9365
+ "origin": "other",
9366
+ "description": "The currently selected key in the collection (controlled)."
9367
+ },
9368
+ "defaultSelectedKey": {
9369
+ "type": "Key | undefined",
9370
+ "required": false,
9371
+ "origin": "other",
9372
+ "description": "The initial selected keys in the collection (uncontrolled)."
9373
+ },
9374
+ "onSelectionChange": {
9375
+ "type": "((key: Key) => void) | undefined",
9376
+ "required": false,
9377
+ "origin": "other",
9378
+ "description": "Handler that is called when the selection changes."
9379
+ },
9380
+ "children": {
9381
+ "type": "CollectionChildren<T>",
9382
+ "required": true,
9383
+ "origin": "aria",
9384
+ "description": "The contents of the collection."
9385
+ },
9386
+ "items": {
9387
+ "type": "Iterable<T> | undefined",
9388
+ "required": false,
9389
+ "origin": "aria",
9390
+ "description": "Item objects in the collection."
9391
+ },
9392
+ "disabledKeys": {
9393
+ "type": "Iterable<Key> | undefined",
9394
+ "required": false,
9395
+ "origin": "aria",
9396
+ "description": "The item keys that are disabled. These items cannot be selected, focused, or otherwise\ninteracted with."
9397
+ },
9398
+ "id": {
9399
+ "type": "string | undefined",
9400
+ "required": false,
9401
+ "origin": "aria",
9402
+ "description": "The element's unique identifier. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id)."
9403
+ },
9404
+ "aria-label": {
9405
+ "type": "string | undefined",
9406
+ "required": false,
9407
+ "origin": "aria",
9408
+ "description": "Defines a string value that labels the current element."
9409
+ },
9410
+ "aria-labelledby": {
9411
+ "type": "string | undefined",
9412
+ "required": false,
9413
+ "origin": "aria",
9414
+ "description": "Identifies the element (or elements) that labels the current element."
9415
+ },
9416
+ "aria-describedby": {
9417
+ "type": "string | undefined",
9418
+ "required": false,
9419
+ "origin": "aria",
9420
+ "description": "Identifies the element (or elements) that describes the object."
9421
+ },
9422
+ "aria-details": {
9423
+ "type": "string | undefined",
9424
+ "required": false,
9425
+ "origin": "aria",
9426
+ "description": "Identifies the element (or elements) that provide a detailed, extended description for the\nobject."
9427
+ }
9428
+ },
9429
+ "propCounts": {
9430
+ "own": 4,
9431
+ "aria": 9,
9432
+ "dom": 0,
9433
+ "other": 4
9434
+ }
9435
+ },
9436
+ "Textarea": {
9437
+ "name": "Textarea",
9438
+ "source": "src/components/Textarea.tsx",
9439
+ "propsType": "TextareaProps",
9440
+ "description": "A multi-line text field.\n\nGeometry from the Figma `Textarea` (1301:334). Every size and state is\nInput's — see textarea.css for why that is the point rather than a shortcut.",
9441
+ "import": "import { Textarea } from 'ionbase-ui';",
9442
+ "status": "stable",
9443
+ "summary": "A multi-line text field. Every size and state is Input's; it wraps and it resizes vertically.",
9444
+ "useWhen": [
9445
+ "the answer is prose — a description, a note, a message — and may run past one line",
9446
+ "the user should be able to see several lines of what they wrote at once"
9447
+ ],
9448
+ "useInstead": [
9449
+ {
9450
+ "when": "the value is a single short string — a name, an email, a search term",
9451
+ "use": "Input",
9452
+ "why": "a one-line answer in a three-line box tells the user to write more than you want"
9453
+ },
9454
+ {
9455
+ "when": "the answer is one of a known set",
9456
+ "use": "Select",
9457
+ "why": "free text you intend to parse into categories is a Select you have not built yet"
9458
+ },
9459
+ {
9460
+ "when": "the field needs formatting controls or renders markup",
9461
+ "use": "Input",
9462
+ "why": "a rich-text editor is not in this library, and a textarea styled to look like one will not behave like one"
9463
+ }
9464
+ ],
9465
+ "variants": {
9466
+ "size": {
9467
+ "sm": {
9468
+ "use": "dense forms and side panels"
9469
+ },
9470
+ "md": {
9471
+ "use": "the default"
9472
+ },
9473
+ "lg": {
9474
+ "use": "roomy layouts, and the primary field on a page"
9475
+ }
9476
+ }
9477
+ },
9478
+ "a11y": {
9479
+ "role": "textbox (multiline)",
9480
+ "guarantees": [
9481
+ "the label, helper text and error message are wired to the control by React Aria's useTextField, so the accessible name and description are correct without the caller doing anything",
9482
+ "`errorMessage` replaces the helper text only when `isInvalid` is set, and is announced as the field's description",
9483
+ "the focus ring shows for keyboard focus only, and hover does not latch on touch",
9484
+ "resize is disabled when the field is, matching what browsers do with a disabled textarea"
9485
+ ],
9486
+ "requires": [
9487
+ "`label`, or an `aria-label` when the field is labelled by something on the page — an unlabelled textarea is announced only as \"edit text\"",
9488
+ "a visible error cue that is not the border. THE INVALID BORDER IS 1px AS OF 0.49.0 and differs from a default field by hue alone, so pass `errorMessage` — WCAG 1.4.1 is not satisfied by the border on its own"
9489
+ ]
9490
+ },
9491
+ "deprecated": [
9492
+ {
9493
+ "prop": "disabled",
9494
+ "replacement": "isDisabled"
9495
+ }
9496
+ ],
9497
+ "antiPatterns": [
9498
+ {
9499
+ "avoid": "setting a fixed `height` in CSS to size the box",
9500
+ "instead": "use `rows`. The Figma height is derived from three lines of a bound line-height, so a fixed height pins the box while the text inside it keeps scaling with the type ramp"
9501
+ },
9502
+ {
9503
+ "avoid": "drawing a custom resize grip",
9504
+ "instead": "the browser paints one for `resize: vertical` and it cannot be styled or suppressed independently, so a custom grip sits beside it and the field gets two"
9505
+ },
9506
+ {
9507
+ "avoid": "relying on `isInvalid` alone to communicate an error",
9508
+ "instead": "pass `errorMessage` too. The border is 1px and the same width as every other state, so colour is the only difference without it"
9509
+ }
9510
+ ],
9511
+ "stylesheet": "src/styles/textarea.css",
9512
+ "tokens": [
9513
+ "--border-default",
9514
+ "--border-disabled",
9515
+ "--border-error-strong",
9516
+ "--border-focus",
9517
+ "--border-strong",
9518
+ "--border-subtle",
9519
+ "--border-width-default",
9520
+ "--font-family-sans",
9521
+ "--font-weight-regular",
9522
+ "--ion-duration-base",
9523
+ "--ion-ease-out",
9524
+ "--ion-textarea-border-width",
9525
+ "--ion-textarea-font-size",
9526
+ "--ion-textarea-line-height",
9527
+ "--ion-textarea-padding-x",
9528
+ "--ion-textarea-padding-y",
9529
+ "--ion-textarea-radius",
9530
+ "--radius-md",
9531
+ "--radius-sm",
9532
+ "--spacing-12",
9533
+ "--spacing-16",
9534
+ "--spacing-6",
9535
+ "--spacing-8",
9536
+ "--surface-default",
9537
+ "--surface-disabled",
9538
+ "--surface-page",
9539
+ "--text-disabled",
9540
+ "--text-secondary",
9541
+ "--text-tertiary",
9542
+ "--type-body",
9543
+ "--type-body-line-height",
9544
+ "--type-body-sm",
9545
+ "--type-body-sm-line-height"
9546
+ ],
9547
+ "props": {
9548
+ "size": {
9549
+ "type": "TextareaSize | undefined",
9550
+ "required": false,
9551
+ "origin": "own",
9552
+ "description": "Matches the Figma `Size` variant: Small, Medium, Large.",
9553
+ "values": [
9554
+ "sm",
9555
+ "md",
9556
+ "lg"
9557
+ ],
9558
+ "default": "md"
9559
+ },
9560
+ "autoCapitalize": {
9561
+ "type": "\"none\" | \"off\" | \"on\" | \"sentences\" | \"words\" | \"characters\" | undefined",
9562
+ "required": false,
9563
+ "origin": "own",
9564
+ "description": "Narrower than React's own typing, which also allows any `string` — the same\ncorrection Input carries. `autoCapitalize` is declared on\n`AriaTextFieldOptions` rather than `AriaTextFieldProps`, so the `keyof`\nomission above cannot reach it.",
9565
+ "values": [
9566
+ "none",
9567
+ "off",
9568
+ "on",
9569
+ "sentences",
9570
+ "words",
9571
+ "characters"
9572
+ ]
9573
+ },
9574
+ "rows": {
9575
+ "type": "number | undefined",
9576
+ "required": false,
9577
+ "origin": "own",
9578
+ "description": "Visible rows. Three matches the Figma frame, whose height is derived from\nthree lines of bound line-height rather than a fixed number — so this stays\ncorrect if the type ramp moves.",
9579
+ "default": 3
9580
+ },
9581
+ "label": {
9582
+ "type": "React.ReactNode",
9583
+ "required": false,
9584
+ "origin": "own",
9585
+ "description": "Field label. Renders the same `.ion-field` wrapper Input uses."
9586
+ },
9587
+ "description": {
9588
+ "type": "React.ReactNode",
9589
+ "required": false,
9590
+ "origin": "own",
9591
+ "description": "Helper text below the field."
9592
+ },
9593
+ "errorMessage": {
9594
+ "type": "React.ReactNode",
9595
+ "required": false,
9596
+ "origin": "own",
9597
+ "description": "Replaces the helper text when `isInvalid` is set."
9598
+ },
9599
+ "disabled": {
9600
+ "type": "boolean | undefined",
9601
+ "required": false,
9602
+ "origin": "own",
9603
+ "tags": {
9604
+ "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
9605
+ }
9606
+ },
9607
+ "className": {
9608
+ "type": "string | undefined",
9609
+ "required": false,
9610
+ "origin": "own",
9611
+ "description": "Class names for the control itself (`.ion-textarea`)."
9612
+ },
9613
+ "wrapperClassName": {
9614
+ "type": "string | undefined",
9615
+ "required": false,
9616
+ "origin": "own",
9617
+ "description": "Class names for the `.ion-field` wrapper when a label or helper is shown."
9618
+ },
9619
+ "aria-activedescendant": {
9620
+ "type": "string | undefined",
9621
+ "required": false,
9622
+ "origin": "aria",
9623
+ "description": "Identifies the currently active element when DOM focus is on a composite widget, textbox,\ngroup, or application."
9624
+ },
9625
+ "aria-autocomplete": {
9626
+ "type": "\"inline\" | \"list\" | \"none\" | \"both\" | undefined",
9627
+ "required": false,
9628
+ "origin": "aria",
9629
+ "description": "Indicates whether inputting text could trigger display of one or more predictions of the user's\nintended value for an input and specifies how predictions would be presented if they are made.",
9630
+ "values": [
9631
+ "inline",
9632
+ "list",
9633
+ "none",
9634
+ "both"
9635
+ ]
9636
+ },
9637
+ "aria-haspopup": {
9638
+ "type": "boolean | \"true\" | \"false\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\" | undefined",
9639
+ "required": false,
9640
+ "origin": "aria",
9641
+ "description": "Indicates the availability and type of interactive popup element, such as menu or dialog, that\ncan be triggered by an element."
9642
+ },
9643
+ "aria-controls": {
9644
+ "type": "string | undefined",
9645
+ "required": false,
9646
+ "origin": "aria",
9647
+ "description": "Identifies the element (or elements) whose contents or presence are controlled by the current\nelement."
9648
+ },
9649
+ "enterKeyHint": {
9650
+ "type": "\"done\" | \"enter\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\" | undefined",
9651
+ "required": false,
9652
+ "origin": "aria",
9653
+ "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).",
9654
+ "values": [
9655
+ "done",
9656
+ "enter",
9657
+ "go",
9658
+ "next",
9659
+ "previous",
9660
+ "search",
9661
+ "send"
9662
+ ]
9663
+ },
9664
+ "isDisabled": {
9665
+ "type": "boolean | undefined",
9666
+ "required": false,
9667
+ "origin": "aria",
9668
+ "description": "Whether the input is disabled."
9669
+ },
9670
+ "isReadOnly": {
9671
+ "type": "boolean | undefined",
9672
+ "required": false,
9673
+ "origin": "aria",
9674
+ "description": "Whether the input can be selected but not changed by the user."
9675
+ },
9676
+ "isRequired": {
9677
+ "type": "boolean | undefined",
9678
+ "required": false,
9679
+ "origin": "aria",
9680
+ "description": "Whether user input is required on the input before form submission."
9681
+ },
9682
+ "isInvalid": {
9683
+ "type": "boolean | undefined",
9684
+ "required": false,
9685
+ "origin": "aria",
9686
+ "description": "Whether the input value is invalid."
9687
+ },
9688
+ "validationState": {
9689
+ "type": "ValidationState | undefined",
9690
+ "required": false,
9691
+ "origin": "aria",
9692
+ "values": [
9693
+ "valid",
9694
+ "invalid"
7783
9695
  ],
7784
9696
  "tags": {
7785
- "default": "'horizontal'"
7786
- },
7787
- "default": "horizontal"
7788
- },
7789
- "className": {
7790
- "type": "string | undefined",
7791
- "required": false,
7792
- "origin": "own"
9697
+ "deprecated": "Use `isInvalid` instead."
9698
+ }
7793
9699
  },
7794
- "keyboardActivation": {
7795
- "type": "\"manual\" | \"automatic\" | undefined",
9700
+ "validationBehavior": {
9701
+ "type": "\"aria\" | \"native\" | undefined",
7796
9702
  "required": false,
7797
9703
  "origin": "aria",
7798
- "description": "Whether tabs are activated automatically on focus or manually.",
9704
+ "description": "Whether to use native HTML form validation to prevent form submission\nwhen the value is missing or invalid, or mark the field as required\nor invalid via ARIA.",
7799
9705
  "values": [
7800
- "manual",
7801
- "automatic"
9706
+ "aria",
9707
+ "native"
7802
9708
  ],
7803
9709
  "tags": {
7804
- "default": "'automatic'"
9710
+ "default": "'aria'"
7805
9711
  }
7806
9712
  },
7807
- "isDisabled": {
9713
+ "validate": {
9714
+ "type": "((value: string) => ValidationError | true | null | undefined) | undefined",
9715
+ "required": false,
9716
+ "origin": "aria",
9717
+ "description": "A function that returns an error message if a given value is invalid.\nValidation errors are displayed to the user when the form is submitted\nif `validationBehavior=\"native\"`. For realtime validation, use the `isInvalid`\nprop instead."
9718
+ },
9719
+ "autoFocus": {
7808
9720
  "type": "boolean | undefined",
7809
9721
  "required": false,
7810
- "origin": "other",
7811
- "description": "Whether the TabList is disabled.\nShows that a selection exists, but is not available in that circumstance."
9722
+ "origin": "aria",
9723
+ "description": "Whether the element should receive focus on render."
7812
9724
  },
7813
- "selectedKey": {
7814
- "type": "Key | undefined",
9725
+ "onFocus": {
9726
+ "type": "((e: FocusEvent<HTMLTextAreaElement, Element>) => void) | undefined",
7815
9727
  "required": false,
7816
- "origin": "other",
7817
- "description": "The currently selected key in the collection (controlled)."
9728
+ "origin": "aria",
9729
+ "description": "Handler that is called when the element receives focus."
7818
9730
  },
7819
- "defaultSelectedKey": {
7820
- "type": "Key | undefined",
9731
+ "onBlur": {
9732
+ "type": "((e: FocusEvent<HTMLTextAreaElement, Element>) => void) | undefined",
7821
9733
  "required": false,
7822
- "origin": "other",
7823
- "description": "The initial selected keys in the collection (uncontrolled)."
9734
+ "origin": "aria",
9735
+ "description": "Handler that is called when the element loses focus."
7824
9736
  },
7825
- "onSelectionChange": {
7826
- "type": "((key: Key) => void) | undefined",
9737
+ "onFocusChange": {
9738
+ "type": "((isFocused: boolean) => void) | undefined",
7827
9739
  "required": false,
7828
- "origin": "other",
7829
- "description": "Handler that is called when the selection changes."
9740
+ "origin": "aria",
9741
+ "description": "Handler that is called when the element's focus status changes."
7830
9742
  },
7831
- "children": {
7832
- "type": "CollectionChildren<T>",
7833
- "required": true,
9743
+ "onKeyDown": {
9744
+ "type": "((e: KeyboardEvent) => void) | undefined",
9745
+ "required": false,
7834
9746
  "origin": "aria",
7835
- "description": "The contents of the collection."
9747
+ "description": "Handler that is called when a key is pressed."
7836
9748
  },
7837
- "items": {
7838
- "type": "Iterable<T> | undefined",
9749
+ "onKeyUp": {
9750
+ "type": "((e: KeyboardEvent) => void) | undefined",
7839
9751
  "required": false,
7840
9752
  "origin": "aria",
7841
- "description": "Item objects in the collection."
9753
+ "description": "Handler that is called when a key is released."
7842
9754
  },
7843
- "disabledKeys": {
7844
- "type": "Iterable<Key> | undefined",
9755
+ "placeholder": {
9756
+ "type": "string | undefined",
7845
9757
  "required": false,
7846
9758
  "origin": "aria",
7847
- "description": "The item keys that are disabled. These items cannot be selected, focused, or otherwise\ninteracted with."
9759
+ "description": "Temporary text that occupies the text input when it is empty."
7848
9760
  },
7849
- "id": {
9761
+ "value": {
7850
9762
  "type": "string | undefined",
7851
9763
  "required": false,
7852
9764
  "origin": "aria",
7853
- "description": "The element's unique identifier. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id)."
9765
+ "description": "The current value (controlled)."
9766
+ },
9767
+ "defaultValue": {
9768
+ "type": "string | undefined",
9769
+ "required": false,
9770
+ "origin": "aria",
9771
+ "description": "The default value (uncontrolled)."
9772
+ },
9773
+ "onChange": {
9774
+ "type": "((value: string) => void) | undefined",
9775
+ "required": false,
9776
+ "origin": "aria",
9777
+ "description": "Handler that is called when the value changes."
7854
9778
  },
7855
9779
  "aria-label": {
7856
9780
  "type": "string | undefined",
@@ -7875,13 +9799,158 @@
7875
9799
  "required": false,
7876
9800
  "origin": "aria",
7877
9801
  "description": "Identifies the element (or elements) that provide a detailed, extended description for the\nobject."
9802
+ },
9803
+ "excludeFromTabOrder": {
9804
+ "type": "boolean | undefined",
9805
+ "required": false,
9806
+ "origin": "aria",
9807
+ "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."
9808
+ },
9809
+ "id": {
9810
+ "type": "string | undefined",
9811
+ "required": false,
9812
+ "origin": "aria",
9813
+ "description": "The element's unique identifier. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id)."
9814
+ },
9815
+ "autoComplete": {
9816
+ "type": "string | undefined",
9817
+ "required": false,
9818
+ "origin": "aria",
9819
+ "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)."
9820
+ },
9821
+ "maxLength": {
9822
+ "type": "number | undefined",
9823
+ "required": false,
9824
+ "origin": "aria",
9825
+ "description": "The maximum number of characters supported by the input. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefmaxlength)."
9826
+ },
9827
+ "minLength": {
9828
+ "type": "number | undefined",
9829
+ "required": false,
9830
+ "origin": "aria",
9831
+ "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)."
9832
+ },
9833
+ "pattern": {
9834
+ "type": "string | undefined",
9835
+ "required": false,
9836
+ "origin": "aria",
9837
+ "description": "Regex pattern that the value of the input must match to be valid. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefpattern)."
9838
+ },
9839
+ "type": {
9840
+ "type": "(string & {}) | \"search\" | \"text\" | \"url\" | \"tel\" | \"email\" | \"password\" | undefined",
9841
+ "required": false,
9842
+ "origin": "aria",
9843
+ "description": "The type of input to render. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdeftype).",
9844
+ "tags": {
9845
+ "default": "'text'"
9846
+ }
9847
+ },
9848
+ "inputMode": {
9849
+ "type": "\"none\" | \"search\" | \"text\" | \"url\" | \"tel\" | \"email\" | \"numeric\" | \"decimal\" | undefined",
9850
+ "required": false,
9851
+ "origin": "aria",
9852
+ "description": "Hints at the type of data that might be entered by the user while editing the element or its\ncontents. See\n[MDN](https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute).",
9853
+ "values": [
9854
+ "none",
9855
+ "search",
9856
+ "text",
9857
+ "url",
9858
+ "tel",
9859
+ "email",
9860
+ "numeric",
9861
+ "decimal"
9862
+ ]
9863
+ },
9864
+ "autoCorrect": {
9865
+ "type": "string | undefined",
9866
+ "required": false,
9867
+ "origin": "aria",
9868
+ "description": "An attribute that takes as its value a space-separated string that describes what, if any, type\nof autocomplete functionality the input should provide. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#autocomplete)."
9869
+ },
9870
+ "spellCheck": {
9871
+ "type": "string | undefined",
9872
+ "required": false,
9873
+ "origin": "aria",
9874
+ "description": "An enumerated attribute that defines whether the element may be checked for spelling errors.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/spellcheck)."
9875
+ },
9876
+ "name": {
9877
+ "type": "string | undefined",
9878
+ "required": false,
9879
+ "origin": "aria",
9880
+ "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)."
9881
+ },
9882
+ "form": {
9883
+ "type": "string | undefined",
9884
+ "required": false,
9885
+ "origin": "aria",
9886
+ "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)."
9887
+ },
9888
+ "onCopy": {
9889
+ "type": "ClipboardEventHandler<HTMLTextAreaElement> | undefined",
9890
+ "required": false,
9891
+ "origin": "aria",
9892
+ "description": "Handler that is called when the user copies text. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/oncopy)."
9893
+ },
9894
+ "onCut": {
9895
+ "type": "ClipboardEventHandler<HTMLTextAreaElement> | undefined",
9896
+ "required": false,
9897
+ "origin": "aria",
9898
+ "description": "Handler that is called when the user cuts text. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/oncut)."
9899
+ },
9900
+ "onPaste": {
9901
+ "type": "ClipboardEventHandler<HTMLTextAreaElement> | undefined",
9902
+ "required": false,
9903
+ "origin": "aria",
9904
+ "description": "Handler that is called when the user pastes text. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/onpaste)."
9905
+ },
9906
+ "onCompositionStart": {
9907
+ "type": "CompositionEventHandler<HTMLTextAreaElement> | undefined",
9908
+ "required": false,
9909
+ "origin": "aria",
9910
+ "description": "Handler that is called when a text composition system starts a new text composition session.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event)."
9911
+ },
9912
+ "onCompositionEnd": {
9913
+ "type": "CompositionEventHandler<HTMLTextAreaElement> | undefined",
9914
+ "required": false,
9915
+ "origin": "aria",
9916
+ "description": "Handler that is called when a text composition system completes or cancels the current text\ncomposition session. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionend_event)."
9917
+ },
9918
+ "onCompositionUpdate": {
9919
+ "type": "CompositionEventHandler<HTMLTextAreaElement> | undefined",
9920
+ "required": false,
9921
+ "origin": "aria",
9922
+ "description": "Handler that is called when a new character is received in the current text composition\nsession. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionupdate_event)."
9923
+ },
9924
+ "onSelect": {
9925
+ "type": "ReactEventHandler<HTMLTextAreaElement> | undefined",
9926
+ "required": false,
9927
+ "origin": "aria",
9928
+ "description": "Handler that is called when text in the input is selected. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/select_event)."
9929
+ },
9930
+ "onBeforeInput": {
9931
+ "type": "FormEventHandler<HTMLTextAreaElement> | undefined",
9932
+ "required": false,
9933
+ "origin": "aria",
9934
+ "description": "Handler that is called when the input value is about to be modified. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event)."
9935
+ },
9936
+ "onInput": {
9937
+ "type": "FormEventHandler<HTMLTextAreaElement> | undefined",
9938
+ "required": false,
9939
+ "origin": "aria",
9940
+ "description": "Handler that is called when the input value is modified. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event)."
9941
+ },
9942
+ "aria-errormessage": {
9943
+ "type": "string | undefined",
9944
+ "required": false,
9945
+ "origin": "aria",
9946
+ "description": "Identifies the element that provides an error message for the object."
7878
9947
  }
7879
9948
  },
7880
9949
  "propCounts": {
7881
- "own": 4,
7882
- "aria": 9,
7883
- "dom": 0,
7884
- "other": 4
9950
+ "own": 9,
9951
+ "aria": 48,
9952
+ "dom": 251,
9953
+ "other": 0
7885
9954
  }
7886
9955
  },
7887
9956
  "Toast": {
@@ -8276,6 +10345,11 @@
8276
10345
  {
8277
10346
  "dont": "a button with `aria-pressed` for a setting",
8278
10347
  "why": "a switch is a form value; the checkbox gives form association and `:checked` for free"
10348
+ },
10349
+ {
10350
+ "dont": "<Toggle isSelected={on} onChange={(isSelected) => setOn(isSelected)} />",
10351
+ "do": "<Toggle isSelected={on} onSelectionChange={setOn} />",
10352
+ "why": "as Checkbox: `isSelected` pairs with `onSelectionChange` and `checked` pairs with `onChange(event)`. Toggle extends the input element, so `onChange` keeps the DOM signature it has always had."
8279
10353
  }
8280
10354
  ],
8281
10355
  "deprecated": [
@@ -8371,10 +10445,22 @@
8371
10445
  "type": "React.ReactNode",
8372
10446
  "required": false,
8373
10447
  "origin": "own"
10448
+ },
10449
+ "isSelected": {
10450
+ "type": "boolean | undefined",
10451
+ "required": false,
10452
+ "origin": "own",
10453
+ "description": "React Aria's name for `checked`. Wins when both are passed."
10454
+ },
10455
+ "onSelectionChange": {
10456
+ "type": "((isSelected: boolean) => void) | undefined",
10457
+ "required": false,
10458
+ "origin": "own",
10459
+ "description": "Receives the new selection state rather than the change event."
8374
10460
  }
8375
10461
  },
8376
10462
  "propCounts": {
8377
- "own": 5,
10463
+ "own": 7,
8378
10464
  "aria": 0,
8379
10465
  "dom": 304,
8380
10466
  "other": 0
@@ -8495,10 +10581,10 @@
8495
10581
  "origin": "own",
8496
10582
  "description": "Matches the Figma `Placement` variant, and names where the TOOLTIP sits —\nnot where the arrow points. `top` is above the trigger, with the arrow on\nthe tooltip's bottom edge.\n\nTreated as a preference, not a guarantee: it flips to the opposite side\nwhen there is no room, which is the behaviour Figma cannot draw.",
8497
10583
  "values": [
8498
- "right",
8499
- "left",
8500
10584
  "top",
8501
- "bottom"
10585
+ "bottom",
10586
+ "right",
10587
+ "left"
8502
10588
  ]
8503
10589
  },
8504
10590
  "delay": {