ionbase-ui 0.51.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.
- package/LICENSE +21 -0
- package/dist/components/Accordion.d.ts +54 -0
- package/dist/components/Accordion.d.ts.map +1 -0
- package/dist/components/Accordion.js +68 -0
- package/dist/components/Accordion.js.map +1 -0
- package/dist/components/Breadcrumb.d.ts +45 -0
- package/dist/components/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb.js +31 -0
- package/dist/components/Breadcrumb.js.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -1
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox.js +4 -2
- package/dist/components/Checkbox.js.map +1 -1
- package/dist/components/Combobox.d.ts +86 -0
- package/dist/components/Combobox.d.ts.map +1 -0
- package/dist/components/Combobox.js +261 -0
- package/dist/components/Combobox.js.map +1 -0
- package/dist/components/Drawer.d.ts +52 -0
- package/dist/components/Drawer.d.ts.map +1 -0
- package/dist/components/Drawer.js +66 -0
- package/dist/components/Drawer.js.map +1 -0
- package/dist/components/FileUpload.d.ts +79 -0
- package/dist/components/FileUpload.d.ts.map +1 -0
- package/dist/components/FileUpload.js +218 -0
- package/dist/components/FileUpload.js.map +1 -0
- package/dist/components/ProgressBar.d.ts +46 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +52 -0
- package/dist/components/ProgressBar.js.map +1 -0
- package/dist/components/Select.d.ts +9 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Select.js.map +1 -1
- package/dist/components/Skeleton.d.ts +39 -0
- package/dist/components/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton.js +50 -0
- package/dist/components/Skeleton.js.map +1 -0
- package/dist/components/Spinner.d.ts +45 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +44 -0
- package/dist/components/Spinner.js.map +1 -0
- package/dist/components/Toggle.d.ts +2 -1
- package/dist/components/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle.js +4 -2
- package/dist/components/Toggle.js.map +1 -1
- package/dist/components/index.d.ts +16 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +8 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/resolve-selection.d.ts +36 -0
- package/dist/components/resolve-selection.d.ts.map +1 -0
- package/dist/components/resolve-selection.js +14 -0
- package/dist/components/resolve-selection.js.map +1 -0
- package/dist/figma-descriptions.json +201 -146
- package/dist/figma-map.json +2296 -1684
- package/dist/meta/Accordion.json +123 -0
- package/dist/meta/AccordionItem.json +90 -0
- package/dist/meta/Breadcrumb.json +87 -0
- package/dist/meta/BreadcrumbItem.json +78 -0
- package/dist/meta/Checkbox.json +18 -1
- package/dist/meta/Combobox.json +265 -0
- package/dist/meta/Drawer.json +234 -0
- package/dist/meta/EmptyState.json +1 -1
- package/dist/meta/FileUpload.json +246 -0
- package/dist/meta/PhoneInput.json +39 -39
- package/dist/meta/Popover.json +3 -3
- package/dist/meta/ProgressBar.json +166 -0
- package/dist/meta/Select.json +4 -3
- package/dist/meta/Skeleton.json +120 -0
- package/dist/meta/Spinner.json +123 -0
- package/dist/meta/Toggle.json +18 -1
- package/dist/meta/Tooltip.json +3 -3
- package/dist/meta/components.json +1837 -270
- package/dist/meta/contrast.json +2873 -194
- package/dist/meta/index.json +128 -7
- package/dist/meta/patterns/index.json +1 -1
- package/dist/styles/accordion.css +102 -0
- package/dist/styles/breadcrumb.css +72 -0
- package/dist/styles/combobox.css +154 -0
- package/dist/styles/drawer.css +181 -0
- package/dist/styles/empty-state.css +16 -4
- package/dist/styles/file-upload.css +236 -0
- package/dist/styles/index.css +8 -0
- package/dist/styles/progress-bar.css +107 -0
- package/dist/styles/skeleton.css +76 -0
- package/dist/styles/spinner.css +70 -0
- package/llms.txt +2 -2
- package/package.json +19 -19
|
@@ -1,11 +1,224 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "ionbase-ui",
|
|
3
|
-
"version": "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
|
-
"
|
|
1755
|
-
"name": "
|
|
1756
|
-
"source": "src/components/
|
|
1757
|
-
"propsType": "
|
|
1758
|
-
"
|
|
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": "
|
|
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
|
-
"
|
|
1763
|
-
"
|
|
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": "
|
|
1768
|
-
"use": "
|
|
1769
|
-
"why": "
|
|
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": "
|
|
1773
|
-
"use": "
|
|
1774
|
-
"why": "a
|
|
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
|
-
"
|
|
1820
|
-
"accepts": "
|
|
1821
|
-
"note": "
|
|
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
|
-
"
|
|
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": "<
|
|
1841
|
-
"do": "<
|
|
1842
|
-
"why": "a
|
|
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": "
|
|
1861
|
-
"do": "
|
|
1862
|
-
"why": "
|
|
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/
|
|
2019
|
+
"stylesheet": "src/styles/breadcrumb.css",
|
|
1873
2020
|
"tokens": [
|
|
1874
|
-
"--border-
|
|
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":
|
|
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
|
-
"
|
|
2756
|
-
"name": "
|
|
2757
|
-
"source": "src/components/
|
|
2758
|
-
"propsType": "
|
|
2759
|
-
"description": "
|
|
2760
|
-
"import": "import {
|
|
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": "
|
|
3157
|
+
"summary": "A text field that filters a list of options down to one selected value.",
|
|
2763
3158
|
"useWhen": [
|
|
2764
|
-
"
|
|
2765
|
-
"
|
|
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
|
|
2770
|
-
"use": "
|
|
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": "
|
|
2774
|
-
"use": "
|
|
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": "
|
|
2779
|
-
"use": "
|
|
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
|
-
"
|
|
2784
|
-
"
|
|
2785
|
-
"use": "
|
|
2786
|
-
},
|
|
2787
|
-
"
|
|
2788
|
-
"use": "
|
|
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"
|
|
@@ -2907,32 +3567,266 @@
|
|
|
2907
3567
|
"dont": "a styled <div> for a rule",
|
|
2908
3568
|
"why": "that is what this component exists to replace"
|
|
2909
3569
|
},
|
|
2910
|
-
{
|
|
2911
|
-
"dont": "a Divider between every row of a list",
|
|
2912
|
-
"why": "a break announced on every row is noise; use the list's own row borders"
|
|
2913
|
-
}
|
|
2914
|
-
],
|
|
2915
|
-
"stylesheet": "src/styles/divider.css",
|
|
2916
|
-
"tokens": [
|
|
2917
|
-
"--border-strong",
|
|
2918
|
-
"--border-width-default"
|
|
2919
|
-
],
|
|
2920
|
-
"props": {
|
|
2921
|
-
"orientation": {
|
|
2922
|
-
"type": "DividerOrientation | undefined",
|
|
3570
|
+
{
|
|
3571
|
+
"dont": "a Divider between every row of a list",
|
|
3572
|
+
"why": "a break announced on every row is noise; use the list's own row borders"
|
|
3573
|
+
}
|
|
3574
|
+
],
|
|
3575
|
+
"stylesheet": "src/styles/divider.css",
|
|
3576
|
+
"tokens": [
|
|
3577
|
+
"--border-strong",
|
|
3578
|
+
"--border-width-default"
|
|
3579
|
+
],
|
|
3580
|
+
"props": {
|
|
3581
|
+
"orientation": {
|
|
3582
|
+
"type": "DividerOrientation | undefined",
|
|
3583
|
+
"required": false,
|
|
3584
|
+
"origin": "own",
|
|
3585
|
+
"description": "Matches Figma's `Style` variant.",
|
|
3586
|
+
"values": [
|
|
3587
|
+
"horizontal",
|
|
3588
|
+
"vertical"
|
|
3589
|
+
]
|
|
3590
|
+
}
|
|
3591
|
+
},
|
|
3592
|
+
"propCounts": {
|
|
3593
|
+
"own": 1,
|
|
3594
|
+
"aria": 0,
|
|
3595
|
+
"dom": 278,
|
|
3596
|
+
"other": 0
|
|
3597
|
+
}
|
|
3598
|
+
},
|
|
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';",
|
|
3605
|
+
"status": "stable",
|
|
3606
|
+
"summary": "A modal panel anchored to an edge. Modal behaviour — focus trap, Escape, scrim — with the shape of a side panel.",
|
|
3607
|
+
"useWhen": [
|
|
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"
|
|
3611
|
+
],
|
|
3612
|
+
"useInstead": [
|
|
3613
|
+
{
|
|
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"
|
|
3617
|
+
},
|
|
3618
|
+
{
|
|
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"
|
|
3626
|
+
}
|
|
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
|
+
},
|
|
3655
|
+
"slots": {
|
|
3656
|
+
"title": {
|
|
3657
|
+
"accepts": "text",
|
|
3658
|
+
"note": "required — it is the dialog's accessible name"
|
|
3659
|
+
},
|
|
3660
|
+
"description": {
|
|
3661
|
+
"accepts": "text"
|
|
3662
|
+
},
|
|
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",
|
|
2923
3775
|
"required": false,
|
|
2924
3776
|
"origin": "own",
|
|
2925
|
-
"description": "
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
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."
|
|
2930
3824
|
}
|
|
2931
3825
|
},
|
|
2932
3826
|
"propCounts": {
|
|
2933
|
-
"own":
|
|
2934
|
-
"aria":
|
|
2935
|
-
"dom":
|
|
3827
|
+
"own": 11,
|
|
3828
|
+
"aria": 3,
|
|
3829
|
+
"dom": 0,
|
|
2936
3830
|
"other": 0
|
|
2937
3831
|
}
|
|
2938
3832
|
},
|
|
@@ -3068,7 +3962,7 @@
|
|
|
3068
3962
|
"stylesheet": "src/styles/empty-state.css",
|
|
3069
3963
|
"tokens": [
|
|
3070
3964
|
"--font-family-sans",
|
|
3071
|
-
"--font-weight-
|
|
3965
|
+
"--font-weight-semibold",
|
|
3072
3966
|
"--icon-error",
|
|
3073
3967
|
"--icon-primary",
|
|
3074
3968
|
"--icon-size-md",
|
|
@@ -3146,30 +4040,276 @@
|
|
|
3146
4040
|
"size": {
|
|
3147
4041
|
"type": "EmptyStateSize | undefined",
|
|
3148
4042
|
"required": false,
|
|
3149
|
-
"origin": "own",
|
|
3150
|
-
"description": "Defaults to `panel`.",
|
|
3151
|
-
"values": [
|
|
3152
|
-
"inline",
|
|
3153
|
-
"page",
|
|
3154
|
-
"panel"
|
|
3155
|
-
]
|
|
4043
|
+
"origin": "own",
|
|
4044
|
+
"description": "Defaults to `panel`.",
|
|
4045
|
+
"values": [
|
|
4046
|
+
"inline",
|
|
4047
|
+
"page",
|
|
4048
|
+
"panel"
|
|
4049
|
+
]
|
|
4050
|
+
},
|
|
4051
|
+
"headingLevel": {
|
|
4052
|
+
"type": "EmptyStateHeadingLevel | undefined",
|
|
4053
|
+
"required": false,
|
|
4054
|
+
"origin": "own",
|
|
4055
|
+
"description": "Heading level for the title. Defaults to `h3`; only the page knows its own\noutline, so it is a prop rather than a constant."
|
|
4056
|
+
},
|
|
4057
|
+
"children": {
|
|
4058
|
+
"type": "React.ReactNode",
|
|
4059
|
+
"required": false,
|
|
4060
|
+
"origin": "own"
|
|
4061
|
+
}
|
|
4062
|
+
},
|
|
4063
|
+
"propCounts": {
|
|
4064
|
+
"own": 9,
|
|
4065
|
+
"aria": 0,
|
|
4066
|
+
"dom": 276,
|
|
4067
|
+
"other": 0
|
|
4068
|
+
}
|
|
4069
|
+
},
|
|
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';",
|
|
4076
|
+
"status": "stable",
|
|
4077
|
+
"summary": "A drop target wrapped around a real file input, with client-side accept and size checks.",
|
|
4078
|
+
"useWhen": [
|
|
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"
|
|
4081
|
+
],
|
|
4082
|
+
"useInstead": [
|
|
4083
|
+
{
|
|
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"
|
|
4087
|
+
},
|
|
4088
|
+
{
|
|
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"
|
|
4092
|
+
}
|
|
4093
|
+
],
|
|
4094
|
+
"variants": {
|
|
4095
|
+
"size": {
|
|
4096
|
+
"sm": {
|
|
4097
|
+
"use": "inside a dense form, or beside other fields"
|
|
4098
|
+
},
|
|
4099
|
+
"md": {
|
|
4100
|
+
"use": "the default — a standalone attachment area"
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
},
|
|
4104
|
+
"a11y": {
|
|
4105
|
+
"guarantees": [
|
|
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)"
|
|
4113
|
+
],
|
|
4114
|
+
"requires": [
|
|
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"
|
|
4117
|
+
]
|
|
4118
|
+
},
|
|
4119
|
+
"antiPatterns": [
|
|
4120
|
+
{
|
|
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"
|
|
4123
|
+
},
|
|
4124
|
+
{
|
|
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"
|
|
4127
|
+
},
|
|
4128
|
+
{
|
|
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"
|
|
4135
|
+
}
|
|
4136
|
+
],
|
|
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",
|
|
4146
|
+
"tokens": [
|
|
4147
|
+
"--border-default",
|
|
4148
|
+
"--border-disabled",
|
|
4149
|
+
"--border-error",
|
|
4150
|
+
"--border-focus",
|
|
4151
|
+
"--border-strong",
|
|
4152
|
+
"--border-width-default",
|
|
4153
|
+
"--border-width-thick",
|
|
4154
|
+
"--font-family-sans",
|
|
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"
|
|
3156
4297
|
},
|
|
3157
|
-
"
|
|
3158
|
-
"type": "
|
|
4298
|
+
"id": {
|
|
4299
|
+
"type": "string | undefined",
|
|
3159
4300
|
"required": false,
|
|
3160
|
-
"origin": "own"
|
|
3161
|
-
"description": "Heading level for the title. Defaults to `h3`; only the page knows its own\noutline, so it is a prop rather than a constant."
|
|
4301
|
+
"origin": "own"
|
|
3162
4302
|
},
|
|
3163
|
-
"
|
|
3164
|
-
"type": "
|
|
4303
|
+
"name": {
|
|
4304
|
+
"type": "string | undefined",
|
|
3165
4305
|
"required": false,
|
|
3166
4306
|
"origin": "own"
|
|
3167
4307
|
}
|
|
3168
4308
|
},
|
|
3169
4309
|
"propCounts": {
|
|
3170
|
-
"own":
|
|
4310
|
+
"own": 20,
|
|
3171
4311
|
"aria": 0,
|
|
3172
|
-
"dom":
|
|
4312
|
+
"dom": 0,
|
|
3173
4313
|
"other": 0
|
|
3174
4314
|
}
|
|
3175
4315
|
},
|
|
@@ -5547,41 +6687,11 @@
|
|
|
5547
6687
|
"origin": "own",
|
|
5548
6688
|
"description": "Spread onto the dial-code `<button>`. This is how a country picker gets\nattached — see the note on scope below."
|
|
5549
6689
|
},
|
|
5550
|
-
"
|
|
5551
|
-
"type": "(string
|
|
5552
|
-
"required": false,
|
|
5553
|
-
"origin": "aria",
|
|
5554
|
-
"description": "The type of input to render. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdeftype).",
|
|
5555
|
-
"tags": {
|
|
5556
|
-
"default": "'text'"
|
|
5557
|
-
},
|
|
5558
|
-
"default": "tel"
|
|
5559
|
-
},
|
|
5560
|
-
"disabled": {
|
|
5561
|
-
"type": "boolean | undefined",
|
|
5562
|
-
"required": false,
|
|
5563
|
-
"origin": "own",
|
|
5564
|
-
"tags": {
|
|
5565
|
-
"deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
|
|
5566
|
-
}
|
|
5567
|
-
},
|
|
5568
|
-
"form": {
|
|
5569
|
-
"type": "string | undefined",
|
|
5570
|
-
"required": false,
|
|
5571
|
-
"origin": "aria",
|
|
5572
|
-
"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)."
|
|
5573
|
-
},
|
|
5574
|
-
"name": {
|
|
5575
|
-
"type": "string | undefined",
|
|
5576
|
-
"required": false,
|
|
5577
|
-
"origin": "aria",
|
|
5578
|
-
"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)."
|
|
5579
|
-
},
|
|
5580
|
-
"value": {
|
|
5581
|
-
"type": "string | undefined",
|
|
6690
|
+
"onChange": {
|
|
6691
|
+
"type": "((value: string) => void) | undefined",
|
|
5582
6692
|
"required": false,
|
|
5583
6693
|
"origin": "aria",
|
|
5584
|
-
"description": "
|
|
6694
|
+
"description": "Handler that is called when the value changes."
|
|
5585
6695
|
},
|
|
5586
6696
|
"defaultValue": {
|
|
5587
6697
|
"type": "string | undefined",
|
|
@@ -5772,12 +6882,6 @@
|
|
|
5772
6882
|
"origin": "aria",
|
|
5773
6883
|
"description": "Handler that is called when the element loses focus."
|
|
5774
6884
|
},
|
|
5775
|
-
"onChange": {
|
|
5776
|
-
"type": "((value: string) => void) | undefined",
|
|
5777
|
-
"required": false,
|
|
5778
|
-
"origin": "aria",
|
|
5779
|
-
"description": "Handler that is called when the value changes."
|
|
5780
|
-
},
|
|
5781
6885
|
"onBeforeInput": {
|
|
5782
6886
|
"type": "FormEventHandler<HTMLInputElement> | undefined",
|
|
5783
6887
|
"required": false,
|
|
@@ -5808,6 +6912,42 @@
|
|
|
5808
6912
|
"origin": "aria",
|
|
5809
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)."
|
|
5810
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
|
+
},
|
|
5811
6951
|
"size": {
|
|
5812
6952
|
"type": "InputSize | undefined",
|
|
5813
6953
|
"required": false,
|
|
@@ -6059,115 +7199,281 @@
|
|
|
6059
7199
|
"--border-width-default",
|
|
6060
7200
|
"--border-width-thick",
|
|
6061
7201
|
"--font-family-sans",
|
|
6062
|
-
"--font-weight-medium",
|
|
6063
|
-
"--font-weight-regular",
|
|
6064
|
-
"--icon-secondary",
|
|
6065
|
-
"--icon-size-md",
|
|
7202
|
+
"--font-weight-medium",
|
|
7203
|
+
"--font-weight-regular",
|
|
7204
|
+
"--icon-secondary",
|
|
7205
|
+
"--icon-size-md",
|
|
7206
|
+
"--ion-duration-base",
|
|
7207
|
+
"--ion-ease-out",
|
|
7208
|
+
"--ion-popover-width",
|
|
7209
|
+
"--ion-shadow-shadow-lg",
|
|
7210
|
+
"--radius-md",
|
|
7211
|
+
"--radius-sm",
|
|
7212
|
+
"--ring-focus",
|
|
7213
|
+
"--spacing-12",
|
|
7214
|
+
"--spacing-16",
|
|
7215
|
+
"--spacing-32",
|
|
7216
|
+
"--spacing-8",
|
|
7217
|
+
"--surface-hover",
|
|
7218
|
+
"--surface-raised",
|
|
7219
|
+
"--text-default",
|
|
7220
|
+
"--text-secondary",
|
|
7221
|
+
"--type-body",
|
|
7222
|
+
"--type-body-line-height",
|
|
7223
|
+
"--type-body-sm",
|
|
7224
|
+
"--type-body-sm-line-height"
|
|
7225
|
+
],
|
|
7226
|
+
"props": {
|
|
7227
|
+
"children": {
|
|
7228
|
+
"type": "React.ReactElement<unknown, string | React.JSXElementConstructor<any>>",
|
|
7229
|
+
"required": true,
|
|
7230
|
+
"origin": "own",
|
|
7231
|
+
"description": "The element that opens it. Must accept a ref and DOM props."
|
|
7232
|
+
},
|
|
7233
|
+
"content": {
|
|
7234
|
+
"type": "React.ReactNode",
|
|
7235
|
+
"required": false,
|
|
7236
|
+
"origin": "own",
|
|
7237
|
+
"description": "Panel contents."
|
|
7238
|
+
},
|
|
7239
|
+
"title": {
|
|
7240
|
+
"type": "React.ReactNode",
|
|
7241
|
+
"required": false,
|
|
7242
|
+
"origin": "own",
|
|
7243
|
+
"description": "Heading. Also gives the dialog its accessible name."
|
|
7244
|
+
},
|
|
7245
|
+
"footer": {
|
|
7246
|
+
"type": "React.ReactNode",
|
|
7247
|
+
"required": false,
|
|
7248
|
+
"origin": "own",
|
|
7249
|
+
"description": "Action row, right-aligned."
|
|
7250
|
+
},
|
|
7251
|
+
"placement": {
|
|
7252
|
+
"type": "PopoverPlacement | undefined",
|
|
7253
|
+
"required": false,
|
|
7254
|
+
"origin": "own",
|
|
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.",
|
|
7256
|
+
"values": [
|
|
7257
|
+
"top",
|
|
7258
|
+
"bottom",
|
|
7259
|
+
"right",
|
|
7260
|
+
"left"
|
|
7261
|
+
]
|
|
7262
|
+
},
|
|
7263
|
+
"size": {
|
|
7264
|
+
"type": "PopoverSize | undefined",
|
|
7265
|
+
"required": false,
|
|
7266
|
+
"origin": "own",
|
|
7267
|
+
"description": "Matches the Figma `Size` variant. Widths only; height always hugs.",
|
|
7268
|
+
"values": [
|
|
7269
|
+
"sm",
|
|
7270
|
+
"md",
|
|
7271
|
+
"lg"
|
|
7272
|
+
]
|
|
7273
|
+
},
|
|
7274
|
+
"showClose": {
|
|
7275
|
+
"type": "boolean | undefined",
|
|
7276
|
+
"required": false,
|
|
7277
|
+
"origin": "own",
|
|
7278
|
+
"description": "Show the close button. Defaults to `true`."
|
|
7279
|
+
},
|
|
7280
|
+
"hideArrow": {
|
|
7281
|
+
"type": "boolean | undefined",
|
|
7282
|
+
"required": false,
|
|
7283
|
+
"origin": "own",
|
|
7284
|
+
"description": "Hide the caret."
|
|
7285
|
+
},
|
|
7286
|
+
"closeLabel": {
|
|
7287
|
+
"type": "string | undefined",
|
|
7288
|
+
"required": false,
|
|
7289
|
+
"origin": "own"
|
|
7290
|
+
},
|
|
7291
|
+
"isOpen": {
|
|
7292
|
+
"type": "boolean | undefined",
|
|
7293
|
+
"required": false,
|
|
7294
|
+
"origin": "own"
|
|
7295
|
+
},
|
|
7296
|
+
"onOpenChange": {
|
|
7297
|
+
"type": "((isOpen: boolean) => void) | undefined",
|
|
7298
|
+
"required": false,
|
|
7299
|
+
"origin": "own"
|
|
7300
|
+
},
|
|
7301
|
+
"className": {
|
|
7302
|
+
"type": "string | undefined",
|
|
7303
|
+
"required": false,
|
|
7304
|
+
"origin": "own"
|
|
7305
|
+
}
|
|
7306
|
+
},
|
|
7307
|
+
"propCounts": {
|
|
7308
|
+
"own": 12,
|
|
7309
|
+
"aria": 0,
|
|
7310
|
+
"dom": 0,
|
|
7311
|
+
"other": 0
|
|
7312
|
+
}
|
|
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",
|
|
6066
7395
|
"--ion-duration-base",
|
|
7396
|
+
"--ion-duration-slow",
|
|
7397
|
+
"--ion-ease-in-out",
|
|
6067
7398
|
"--ion-ease-out",
|
|
6068
|
-
"--ion-
|
|
6069
|
-
"--ion-
|
|
6070
|
-
"--
|
|
6071
|
-
"--radius-
|
|
6072
|
-
"--
|
|
6073
|
-
"--spacing-12",
|
|
6074
|
-
"--spacing-16",
|
|
6075
|
-
"--spacing-32",
|
|
7399
|
+
"--ion-progress-bar-fill",
|
|
7400
|
+
"--ion-progress-bar-height",
|
|
7401
|
+
"--ion-progress-bar-pct",
|
|
7402
|
+
"--radius-full",
|
|
7403
|
+
"--spacing-4",
|
|
6076
7404
|
"--spacing-8",
|
|
6077
|
-
"--surface-
|
|
6078
|
-
"--surface-
|
|
7405
|
+
"--surface-error",
|
|
7406
|
+
"--surface-muted",
|
|
7407
|
+
"--surface-primary",
|
|
7408
|
+
"--surface-success",
|
|
7409
|
+
"--surface-warning",
|
|
6079
7410
|
"--text-default",
|
|
6080
7411
|
"--text-secondary",
|
|
6081
|
-
"--type-body",
|
|
6082
|
-
"--type-body-line-height",
|
|
6083
7412
|
"--type-body-sm",
|
|
6084
7413
|
"--type-body-sm-line-height"
|
|
6085
7414
|
],
|
|
6086
7415
|
"props": {
|
|
6087
|
-
"
|
|
6088
|
-
"type": "
|
|
7416
|
+
"label": {
|
|
7417
|
+
"type": "string",
|
|
6089
7418
|
"required": true,
|
|
6090
7419
|
"origin": "own",
|
|
6091
|
-
"description": "
|
|
6092
|
-
},
|
|
6093
|
-
"content": {
|
|
6094
|
-
"type": "React.ReactNode",
|
|
6095
|
-
"required": false,
|
|
6096
|
-
"origin": "own",
|
|
6097
|
-
"description": "Panel contents."
|
|
7420
|
+
"description": "What is progressing. Required — a bare bar announces a number and no noun."
|
|
6098
7421
|
},
|
|
6099
|
-
"
|
|
6100
|
-
"type": "
|
|
7422
|
+
"value": {
|
|
7423
|
+
"type": "number | undefined",
|
|
6101
7424
|
"required": false,
|
|
6102
7425
|
"origin": "own",
|
|
6103
|
-
"description": "
|
|
7426
|
+
"description": "0 to `max`. Omit for an indeterminate bar."
|
|
6104
7427
|
},
|
|
6105
|
-
"
|
|
6106
|
-
"type": "
|
|
7428
|
+
"max": {
|
|
7429
|
+
"type": "number | undefined",
|
|
6107
7430
|
"required": false,
|
|
6108
7431
|
"origin": "own",
|
|
6109
|
-
"description": "
|
|
7432
|
+
"description": "Defaults to 100."
|
|
6110
7433
|
},
|
|
6111
|
-
"
|
|
6112
|
-
"type": "
|
|
7434
|
+
"intent": {
|
|
7435
|
+
"type": "ProgressBarIntent | undefined",
|
|
6113
7436
|
"required": false,
|
|
6114
7437
|
"origin": "own",
|
|
6115
|
-
"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.",
|
|
6116
7438
|
"values": [
|
|
6117
|
-
"
|
|
6118
|
-
"
|
|
6119
|
-
"
|
|
6120
|
-
"
|
|
7439
|
+
"primary",
|
|
7440
|
+
"success",
|
|
7441
|
+
"warning",
|
|
7442
|
+
"error"
|
|
6121
7443
|
]
|
|
6122
7444
|
},
|
|
6123
7445
|
"size": {
|
|
6124
|
-
"type": "
|
|
7446
|
+
"type": "ProgressBarSize | undefined",
|
|
6125
7447
|
"required": false,
|
|
6126
7448
|
"origin": "own",
|
|
6127
|
-
"description": "Matches the Figma `Size` variant. Widths only; height always hugs.",
|
|
6128
7449
|
"values": [
|
|
6129
7450
|
"sm",
|
|
6130
|
-
"md"
|
|
6131
|
-
"lg"
|
|
7451
|
+
"md"
|
|
6132
7452
|
]
|
|
6133
7453
|
},
|
|
6134
|
-
"
|
|
7454
|
+
"isLabelVisible": {
|
|
6135
7455
|
"type": "boolean | undefined",
|
|
6136
7456
|
"required": false,
|
|
6137
7457
|
"origin": "own",
|
|
6138
|
-
"description": "Show the
|
|
7458
|
+
"description": "Show the label as text above the track."
|
|
6139
7459
|
},
|
|
6140
|
-
"
|
|
7460
|
+
"isValueVisible": {
|
|
6141
7461
|
"type": "boolean | undefined",
|
|
6142
7462
|
"required": false,
|
|
6143
7463
|
"origin": "own",
|
|
6144
|
-
"description": "
|
|
7464
|
+
"description": "Show the percentage beside the label. Determinate bars only."
|
|
6145
7465
|
},
|
|
6146
|
-
"
|
|
6147
|
-
"type": "string | undefined",
|
|
6148
|
-
"required": false,
|
|
6149
|
-
"origin": "own"
|
|
6150
|
-
},
|
|
6151
|
-
"isOpen": {
|
|
6152
|
-
"type": "boolean | undefined",
|
|
6153
|
-
"required": false,
|
|
6154
|
-
"origin": "own"
|
|
6155
|
-
},
|
|
6156
|
-
"onOpenChange": {
|
|
6157
|
-
"type": "((isOpen: boolean) => void) | undefined",
|
|
6158
|
-
"required": false,
|
|
6159
|
-
"origin": "own"
|
|
6160
|
-
},
|
|
6161
|
-
"className": {
|
|
7466
|
+
"valueText": {
|
|
6162
7467
|
"type": "string | undefined",
|
|
6163
7468
|
"required": false,
|
|
6164
|
-
"origin": "own"
|
|
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."
|
|
6165
7471
|
}
|
|
6166
7472
|
},
|
|
6167
7473
|
"propCounts": {
|
|
6168
|
-
"own":
|
|
7474
|
+
"own": 8,
|
|
6169
7475
|
"aria": 0,
|
|
6170
|
-
"dom":
|
|
7476
|
+
"dom": 277,
|
|
6171
7477
|
"other": 0
|
|
6172
7478
|
}
|
|
6173
7479
|
},
|
|
@@ -6691,7 +7997,8 @@
|
|
|
6691
7997
|
"useInstead": [
|
|
6692
7998
|
{
|
|
6693
7999
|
"when": "the list is long enough to need search or type-ahead beyond the native behaviour",
|
|
6694
|
-
"use": "
|
|
8000
|
+
"use": "Combobox",
|
|
8001
|
+
"why": "a native <select> list is the browser's, and cannot be filtered"
|
|
6695
8002
|
},
|
|
6696
8003
|
{
|
|
6697
8004
|
"when": "the rows need icons, avatars or two lines of text",
|
|
@@ -6804,10 +8111,10 @@
|
|
|
6804
8111
|
"default": "md"
|
|
6805
8112
|
},
|
|
6806
8113
|
"options": {
|
|
6807
|
-
"type": "SelectOption[] | undefined",
|
|
8114
|
+
"type": "readonly SelectOption[] | undefined",
|
|
6808
8115
|
"required": false,
|
|
6809
8116
|
"origin": "own",
|
|
6810
|
-
"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."
|
|
6811
8118
|
},
|
|
6812
8119
|
"placeholder": {
|
|
6813
8120
|
"type": "string | undefined",
|
|
@@ -6871,6 +8178,249 @@
|
|
|
6871
8178
|
"other": 0
|
|
6872
8179
|
}
|
|
6873
8180
|
},
|
|
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';",
|
|
8187
|
+
"status": "stable",
|
|
8188
|
+
"summary": "The shape of content that has not arrived. Always hidden from assistive technology — the caller owns the announcement.",
|
|
8189
|
+
"useWhen": [
|
|
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"
|
|
8193
|
+
],
|
|
8194
|
+
"useInstead": [
|
|
8195
|
+
{
|
|
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"
|
|
8199
|
+
},
|
|
8200
|
+
{
|
|
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"
|
|
8204
|
+
}
|
|
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
|
+
},
|
|
8220
|
+
"a11y": {
|
|
8221
|
+
"requires": [
|
|
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"
|
|
8227
|
+
]
|
|
8228
|
+
},
|
|
8229
|
+
"antiPatterns": [
|
|
8230
|
+
{
|
|
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"
|
|
8234
|
+
},
|
|
8235
|
+
{
|
|
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"
|
|
8239
|
+
},
|
|
8240
|
+
{
|
|
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"
|
|
8244
|
+
}
|
|
8245
|
+
],
|
|
8246
|
+
"stylesheet": "src/styles/skeleton.css",
|
|
8247
|
+
"tokens": [
|
|
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",
|
|
8262
|
+
"--type-body-line-height"
|
|
8263
|
+
],
|
|
8264
|
+
"props": {
|
|
8265
|
+
"variant": {
|
|
8266
|
+
"type": "SkeletonVariant | undefined",
|
|
8267
|
+
"required": false,
|
|
8268
|
+
"origin": "own",
|
|
8269
|
+
"values": [
|
|
8270
|
+
"circle",
|
|
8271
|
+
"text",
|
|
8272
|
+
"rect"
|
|
8273
|
+
]
|
|
8274
|
+
},
|
|
8275
|
+
"width": {
|
|
8276
|
+
"type": "string | undefined",
|
|
8277
|
+
"required": false,
|
|
8278
|
+
"origin": "own",
|
|
8279
|
+
"description": "Any CSS length. Defaults to filling the width available."
|
|
8280
|
+
},
|
|
8281
|
+
"height": {
|
|
8282
|
+
"type": "string | undefined",
|
|
8283
|
+
"required": false,
|
|
8284
|
+
"origin": "own",
|
|
8285
|
+
"description": "Any CSS length. `text` derives its height from the line box instead."
|
|
8286
|
+
},
|
|
8287
|
+
"lines": {
|
|
8288
|
+
"type": "number | undefined",
|
|
8289
|
+
"required": false,
|
|
8290
|
+
"origin": "own",
|
|
8291
|
+
"description": "`text` only: how many lines to draw. The last is drawn short."
|
|
8292
|
+
}
|
|
8293
|
+
},
|
|
8294
|
+
"propCounts": {
|
|
8295
|
+
"own": 4,
|
|
8296
|
+
"aria": 0,
|
|
8297
|
+
"dom": 277,
|
|
8298
|
+
"other": 0
|
|
8299
|
+
}
|
|
8300
|
+
},
|
|
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
|
+
},
|
|
6874
8424
|
"StreamingText": {
|
|
6875
8425
|
"name": "StreamingText",
|
|
6876
8426
|
"source": "src/components/StreamingText.tsx",
|
|
@@ -8795,6 +10345,11 @@
|
|
|
8795
10345
|
{
|
|
8796
10346
|
"dont": "a button with `aria-pressed` for a setting",
|
|
8797
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."
|
|
8798
10353
|
}
|
|
8799
10354
|
],
|
|
8800
10355
|
"deprecated": [
|
|
@@ -8890,10 +10445,22 @@
|
|
|
8890
10445
|
"type": "React.ReactNode",
|
|
8891
10446
|
"required": false,
|
|
8892
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."
|
|
8893
10460
|
}
|
|
8894
10461
|
},
|
|
8895
10462
|
"propCounts": {
|
|
8896
|
-
"own":
|
|
10463
|
+
"own": 7,
|
|
8897
10464
|
"aria": 0,
|
|
8898
10465
|
"dom": 304,
|
|
8899
10466
|
"other": 0
|
|
@@ -9014,10 +10581,10 @@
|
|
|
9014
10581
|
"origin": "own",
|
|
9015
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.",
|
|
9016
10583
|
"values": [
|
|
9017
|
-
"right",
|
|
9018
|
-
"left",
|
|
9019
10584
|
"top",
|
|
9020
|
-
"bottom"
|
|
10585
|
+
"bottom",
|
|
10586
|
+
"right",
|
|
10587
|
+
"left"
|
|
9021
10588
|
]
|
|
9022
10589
|
},
|
|
9023
10590
|
"delay": {
|