ionbase-ui 0.18.1 → 0.19.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/dist/meta/Avatar.json +66 -0
- package/dist/meta/AvatarGroup.json +57 -0
- package/dist/meta/Badge.json +65 -0
- package/dist/meta/Checkbox.json +82 -0
- package/dist/meta/Divider.json +39 -0
- package/dist/meta/FullCard.json +77 -0
- package/dist/meta/Header.json +59 -0
- package/dist/meta/Icon.json +54 -0
- package/dist/meta/Link.json +67 -0
- package/dist/meta/Logo.json +49 -0
- package/dist/meta/LogoMark.json +38 -0
- package/dist/meta/Menu.json +54 -0
- package/dist/meta/MenuItem.json +51 -0
- package/dist/meta/NavItem.json +53 -0
- package/dist/meta/PhoneInput.json +60 -0
- package/dist/meta/Popover.json +92 -0
- package/dist/meta/Radio.json +77 -0
- package/dist/meta/RadioGroup.json +84 -0
- package/dist/meta/ScrollProgress.json +41 -0
- package/dist/meta/TabItem.json +42 -0
- package/dist/meta/TableBody.json +36 -0
- package/dist/meta/TableCell.json +83 -0
- package/dist/meta/TableHead.json +39 -0
- package/dist/meta/TableRow.json +45 -0
- package/dist/meta/Tabs.json +85 -0
- package/dist/meta/Toast.json +81 -0
- package/dist/meta/ToastProvider.json +57 -0
- package/dist/meta/Toggle.json +76 -0
- package/dist/meta/Tooltip.json +74 -0
- package/dist/meta/components.json +1859 -76
- package/dist/meta/index.json +88 -59
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "ionbase-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"generated": "by scripts/build-meta.mjs — do not edit; intent lives in meta/*.json",
|
|
5
5
|
"hooks": [
|
|
6
6
|
"useToast"
|
|
@@ -260,6 +260,72 @@
|
|
|
260
260
|
"propsType": "AvatarProps",
|
|
261
261
|
"description": "Avatar picks its content the way Figma's `Type` variant does, but by\nprecedence rather than by a prop: image, then initials, then icon. A caller\npassing `src` and `initials` gets the image with the initials as its alt\nfallback, which is what you want when the image 404s.\n\nA failed `src` falls through to initials (then icon) rather than leaving the\nbrowser's broken-image glyph — that is why initials exist alongside `src`.\n\nIt is a `<span>`, not a `<div>`, so it can sit inline beside text without the\ncaller fighting a block element.",
|
|
262
262
|
"import": "import { Avatar } from 'ionbase-ui';",
|
|
263
|
+
"status": "stable",
|
|
264
|
+
"summary": "A person or entity marker. Content is chosen by precedence — image, then initials, then icon — not by a type prop.",
|
|
265
|
+
"useWhen": [
|
|
266
|
+
"identifying who owns, authored or is assigned to something",
|
|
267
|
+
"a table cell, menu row or header needs a compact identity marker beside a name"
|
|
268
|
+
],
|
|
269
|
+
"useInstead": [
|
|
270
|
+
{
|
|
271
|
+
"when": "the thing being marked is a status, count or category rather than a person",
|
|
272
|
+
"use": "Badge"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"when": "you only need a decorative graphic with no identity behind it",
|
|
276
|
+
"use": "Icon"
|
|
277
|
+
}
|
|
278
|
+
],
|
|
279
|
+
"variants": {
|
|
280
|
+
"size": {
|
|
281
|
+
"mini": {
|
|
282
|
+
"use": "24px — inline beside body text, dense table rows"
|
|
283
|
+
},
|
|
284
|
+
"sm": {
|
|
285
|
+
"use": "32px — menu rows, compact lists"
|
|
286
|
+
},
|
|
287
|
+
"md": {
|
|
288
|
+
"use": "the default, 40px"
|
|
289
|
+
},
|
|
290
|
+
"lg": {
|
|
291
|
+
"use": "48px — profile headers and detail pages"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"shape": {
|
|
295
|
+
"circle": {
|
|
296
|
+
"use": "the default — people"
|
|
297
|
+
},
|
|
298
|
+
"square": {
|
|
299
|
+
"use": "organisations, projects, workspaces and other non-human entities"
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
"slots": {
|
|
304
|
+
"icon": {
|
|
305
|
+
"accepts": "Icon",
|
|
306
|
+
"note": "the last fallback — rendered only when there is neither `src` nor `initials`"
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
"a11y": {
|
|
310
|
+
"guarantees": [
|
|
311
|
+
"`alt` falls back to `initials`, so the avatar is never announced as an unlabelled image",
|
|
312
|
+
"a `src` that fails to load falls through to initials, then icon — never the browser's broken-image glyph"
|
|
313
|
+
],
|
|
314
|
+
"requires": [
|
|
315
|
+
"`alt` describing the person, not the picture — \"Ada Lovelace\", not \"profile photo\""
|
|
316
|
+
]
|
|
317
|
+
},
|
|
318
|
+
"antiPatterns": [
|
|
319
|
+
{
|
|
320
|
+
"dont": "passing `src` without `initials`",
|
|
321
|
+
"why": "a 404 leaves the fallback empty; initials are what make the failure invisible"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"dont": "using Avatar as a button",
|
|
325
|
+
"do": "put the Avatar inside a Button or Link",
|
|
326
|
+
"why": "Avatar renders a <span> with no role and takes no focus"
|
|
327
|
+
}
|
|
328
|
+
],
|
|
263
329
|
"stylesheet": "src/styles/avatar.css",
|
|
264
330
|
"tokens": [
|
|
265
331
|
"--border-strong",
|
|
@@ -357,6 +423,63 @@
|
|
|
357
423
|
"propsType": "AvatarGroupProps",
|
|
358
424
|
"description": "AvatarGroup overlaps its children by a quarter of the avatar size, matching\nFigma's -6 / -8 / -10 / -12 gaps at Mini / Small / Medium / Large.\n\nGroup defaults fill in `size` / `shape` only when a child Avatar has not set\nthem itself — an explicit child prop wins. Non-Avatar children are left\nalone so `size`/`shape` are never pushed onto arbitrary DOM nodes.\n\nChildren render in source order because that is Figma's stacking: each avatar\npaints over the one before it, so the `+N` overflow ends up on top. Later\nsiblings paint later, so this needs no z-index.",
|
|
359
425
|
"import": "import { AvatarGroup } from 'ionbase-ui';",
|
|
426
|
+
"status": "stable",
|
|
427
|
+
"summary": "Overlapping Avatars with a `+N` overflow, for showing several people in one row's width.",
|
|
428
|
+
"useWhen": [
|
|
429
|
+
"several people share one thing — assignees, participants, collaborators",
|
|
430
|
+
"the exact list matters less than the fact that there are several"
|
|
431
|
+
],
|
|
432
|
+
"useInstead": [
|
|
433
|
+
{
|
|
434
|
+
"when": "each person needs their own name, role or action visible",
|
|
435
|
+
"use": "Table",
|
|
436
|
+
"why": "an overlapping stack hides everything but the picture"
|
|
437
|
+
}
|
|
438
|
+
],
|
|
439
|
+
"variants": {
|
|
440
|
+
"size": {
|
|
441
|
+
"mini": {
|
|
442
|
+
"use": "24px — inline beside body text, dense table rows"
|
|
443
|
+
},
|
|
444
|
+
"sm": {
|
|
445
|
+
"use": "32px — menu rows, compact lists"
|
|
446
|
+
},
|
|
447
|
+
"md": {
|
|
448
|
+
"use": "the default, 40px"
|
|
449
|
+
},
|
|
450
|
+
"lg": {
|
|
451
|
+
"use": "48px — profile headers and detail pages"
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
"shape": {
|
|
455
|
+
"circle": {
|
|
456
|
+
"use": "the default — people"
|
|
457
|
+
},
|
|
458
|
+
"square": {
|
|
459
|
+
"use": "organisations, projects, workspaces"
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
"slots": {
|
|
464
|
+
"children": {
|
|
465
|
+
"accepts": "Avatar",
|
|
466
|
+
"note": "group `size`/`shape` fill in only where a child has not set its own; a child's explicit prop wins. Non-Avatar children are left untouched."
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
"a11y": {
|
|
470
|
+
"requires": [
|
|
471
|
+
"`alt` on every child Avatar — the overflow avatar is the only one the group names for you"
|
|
472
|
+
],
|
|
473
|
+
"notes": [
|
|
474
|
+
"Children paint in source order, so the `+N` overflow lands on top without any z-index."
|
|
475
|
+
]
|
|
476
|
+
},
|
|
477
|
+
"antiPatterns": [
|
|
478
|
+
{
|
|
479
|
+
"dont": "omitting `max` on an unbounded list",
|
|
480
|
+
"why": "twenty overlapping avatars is not a summary; it is a smear"
|
|
481
|
+
}
|
|
482
|
+
],
|
|
360
483
|
"stylesheet": "src/styles/avatar.css",
|
|
361
484
|
"tokens": [
|
|
362
485
|
"--border-strong",
|
|
@@ -439,6 +562,71 @@
|
|
|
439
562
|
"propsType": "BadgeProps",
|
|
440
563
|
"description": "Badge is presentational — no role, no interaction. React Aria has nothing to\noffer here, so this is a plain span rather than a hook wrapper for its own\nsake.\n\nIt carries no `status` or `alert` role on purpose: a badge is a label on\nsomething else, and announcing it as a live region would interrupt screen\nreader users on every render. Wrap it yourself if the value genuinely changes\nand matters.",
|
|
441
564
|
"import": "import { Badge } from 'ionbase-ui';",
|
|
565
|
+
"status": "stable",
|
|
566
|
+
"summary": "A small, non-interactive label that classifies the thing next to it. Presentational — no role, no live region.",
|
|
567
|
+
"useWhen": [
|
|
568
|
+
"labelling a record's status, type, count or category",
|
|
569
|
+
"the label belongs to something else on the page and is read as part of it"
|
|
570
|
+
],
|
|
571
|
+
"useInstead": [
|
|
572
|
+
{
|
|
573
|
+
"when": "the message is a standalone statement the user must read",
|
|
574
|
+
"use": "Alert",
|
|
575
|
+
"why": "Badge carries no role and is not announced on its own"
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
"when": "the label should be clickable — a filter chip, a removable tag",
|
|
579
|
+
"use": "Button",
|
|
580
|
+
"why": "Badge takes no focus and fires nothing"
|
|
581
|
+
}
|
|
582
|
+
],
|
|
583
|
+
"variants": {
|
|
584
|
+
"intent": {
|
|
585
|
+
"neutral": {
|
|
586
|
+
"use": "the default — a category with no judgement attached"
|
|
587
|
+
},
|
|
588
|
+
"primary": {
|
|
589
|
+
"use": "brand emphasis: Beta, New, Pro"
|
|
590
|
+
},
|
|
591
|
+
"success": {
|
|
592
|
+
"use": "a terminal good state: Active, Paid, Passed"
|
|
593
|
+
},
|
|
594
|
+
"warning": {
|
|
595
|
+
"use": "needs attention but is not broken: Pending, Expiring"
|
|
596
|
+
},
|
|
597
|
+
"error": {
|
|
598
|
+
"use": "a failed or blocked state: Failed, Overdue, Rejected"
|
|
599
|
+
},
|
|
600
|
+
"information": {
|
|
601
|
+
"use": "a neutral fact worth marking: Draft, Scheduled"
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
"slots": {
|
|
606
|
+
"icon": {
|
|
607
|
+
"accepts": "Icon",
|
|
608
|
+
"note": "mutually exclusive with `dot` — `dot` wins and `icon` is dropped when both are set"
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
"a11y": {
|
|
612
|
+
"guarantees": [
|
|
613
|
+
"the dot marker is `aria-hidden`, so the badge announces its text only",
|
|
614
|
+
"no `status` or `alert` role, so a re-render never interrupts a screen reader"
|
|
615
|
+
],
|
|
616
|
+
"notes": [
|
|
617
|
+
"If the value genuinely changes and the change matters, wrap the Badge in your own live region — the component will not do it for you."
|
|
618
|
+
]
|
|
619
|
+
},
|
|
620
|
+
"antiPatterns": [
|
|
621
|
+
{
|
|
622
|
+
"dont": "relying on intent colour alone to carry the meaning",
|
|
623
|
+
"why": "colour is not a label (WCAG 1.4.1); the text has to say Failed, not just be red"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
"dont": "putting a Button or Link inside a Badge",
|
|
627
|
+
"why": "it renders a plain <span>; interactive content there has no affordance and no focus style"
|
|
628
|
+
}
|
|
629
|
+
],
|
|
442
630
|
"stylesheet": "src/styles/badge.css",
|
|
443
631
|
"tokens": [
|
|
444
632
|
"--border-default",
|
|
@@ -1040,6 +1228,88 @@
|
|
|
1040
1228
|
"propsType": "CheckboxProps",
|
|
1041
1229
|
"description": "Checkbox wraps a native `<input type=\"checkbox\">` and hides it.\n\nThe native input is kept rather than replaced by a div with `role=checkbox`\nbecause it brings form association, the indeterminate property, label\nclicking, and correct announcement — none of which are free to reimplement,\nand all of which are easy to get subtly wrong.\n\n`indeterminate` is the reason for the effect below: HTML has no\n`indeterminate` attribute, only a DOM property, so React cannot set it\ndeclaratively and it must be written after every render.",
|
|
1042
1230
|
"import": "import { Checkbox } from 'ionbase-ui';",
|
|
1231
|
+
"status": "stable",
|
|
1232
|
+
"summary": "A native checkbox with the system's styling, supporting an indeterminate state.",
|
|
1233
|
+
"useWhen": [
|
|
1234
|
+
"a single on/off choice inside a form that is submitted",
|
|
1235
|
+
"selecting rows in a Table",
|
|
1236
|
+
"several independent options where more than one may be picked"
|
|
1237
|
+
],
|
|
1238
|
+
"useInstead": [
|
|
1239
|
+
{
|
|
1240
|
+
"when": "the change takes effect immediately rather than on submit",
|
|
1241
|
+
"use": "Toggle",
|
|
1242
|
+
"why": "a switch reads as a setting taking effect now; a checkbox reads as a value being collected"
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
"when": "the options are mutually exclusive",
|
|
1246
|
+
"use": "RadioGroup"
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
"when": "there are many options and space is tight",
|
|
1250
|
+
"use": "Select"
|
|
1251
|
+
}
|
|
1252
|
+
],
|
|
1253
|
+
"variants": {
|
|
1254
|
+
"size": {
|
|
1255
|
+
"sm": {
|
|
1256
|
+
"use": "dense forms and table rows"
|
|
1257
|
+
},
|
|
1258
|
+
"md": {
|
|
1259
|
+
"use": "the default"
|
|
1260
|
+
},
|
|
1261
|
+
"lg": {
|
|
1262
|
+
"use": "touch targets and prominent single choices"
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
"intent": {
|
|
1266
|
+
"brand": {
|
|
1267
|
+
"use": "the default"
|
|
1268
|
+
},
|
|
1269
|
+
"neutral": {
|
|
1270
|
+
"use": "where the checked state is a fact rather than an approval — bulk-selection columns"
|
|
1271
|
+
},
|
|
1272
|
+
"danger": {
|
|
1273
|
+
"use": "consenting to something destructive or irreversible"
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
"slots": {
|
|
1278
|
+
"children": {
|
|
1279
|
+
"accepts": "text",
|
|
1280
|
+
"note": "the visible label; omit for a bare box and supply `aria-label` instead"
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
"a11y": {
|
|
1284
|
+
"role": "checkbox",
|
|
1285
|
+
"guarantees": [
|
|
1286
|
+
"a real <input type=\"checkbox\">, so form association, label clicking, and correct announcement come from the platform",
|
|
1287
|
+
"`isIndeterminate` is written to the DOM property after every render — HTML has no such attribute, so React cannot set it declaratively"
|
|
1288
|
+
],
|
|
1289
|
+
"requires": [
|
|
1290
|
+
"`children`, or `aria-label` when the box has no visible label — a row-selection checkbox always needs one"
|
|
1291
|
+
]
|
|
1292
|
+
},
|
|
1293
|
+
"antiPatterns": [
|
|
1294
|
+
{
|
|
1295
|
+
"dont": "a bare checkbox with no `children` and no `aria-label`",
|
|
1296
|
+
"why": "every row announces an unnamed checkbox"
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
"dont": "`isIndeterminate` on a leaf checkbox",
|
|
1300
|
+
"why": "indeterminate means \"some of my children are checked\"; on a leaf it is a state the user can never reach"
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
"dont": "a div with `role=\"checkbox\"`",
|
|
1304
|
+
"why": "form association, the indeterminate property and label clicking are not free to reimplement"
|
|
1305
|
+
}
|
|
1306
|
+
],
|
|
1307
|
+
"deprecated": [
|
|
1308
|
+
{
|
|
1309
|
+
"prop": "disabled",
|
|
1310
|
+
"replacement": "isDisabled"
|
|
1311
|
+
}
|
|
1312
|
+
],
|
|
1043
1313
|
"stylesheet": "src/styles/checkbox.css",
|
|
1044
1314
|
"tokens": [
|
|
1045
1315
|
"--border-disabled",
|
|
@@ -1153,6 +1423,45 @@
|
|
|
1153
1423
|
"propsType": "DividerProps",
|
|
1154
1424
|
"description": "A single `<hr>`, matching Figma's `Border` (70:22153).\n\n`<hr>` rather than a styled `<div>`: it is a semantic thematic break, so a\nscreen reader announces it as one, and it needs no `role` to get there.\n`aria-orientation` is set for the vertical case, since a vertical rule\ninside a horizontal toolbar is the one shape a screen reader cannot infer\nfrom the element alone.",
|
|
1155
1425
|
"import": "import { Divider } from 'ionbase-ui';",
|
|
1426
|
+
"status": "stable",
|
|
1427
|
+
"summary": "A semantic thematic break, rendered as a real <hr>.",
|
|
1428
|
+
"useWhen": [
|
|
1429
|
+
"separating groups of content or controls that are related but distinct",
|
|
1430
|
+
"a menu, toolbar or form needs a visible section break"
|
|
1431
|
+
],
|
|
1432
|
+
"useInstead": [
|
|
1433
|
+
{
|
|
1434
|
+
"when": "you only want space between two things",
|
|
1435
|
+
"use": "CSS gap or margin",
|
|
1436
|
+
"why": "a rule announces a break to a screen reader; whitespace does not, and usually shouldn't"
|
|
1437
|
+
}
|
|
1438
|
+
],
|
|
1439
|
+
"variants": {
|
|
1440
|
+
"orientation": {
|
|
1441
|
+
"horizontal": {
|
|
1442
|
+
"use": "the default — between stacked sections"
|
|
1443
|
+
},
|
|
1444
|
+
"vertical": {
|
|
1445
|
+
"use": "inside a horizontal row: toolbars, button groups, header bars"
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
"a11y": {
|
|
1450
|
+
"guarantees": [
|
|
1451
|
+
"`<hr>` is a thematic break to assistive tech without needing a role",
|
|
1452
|
+
"`aria-orientation=\"vertical\"` is set for the vertical case — the one thing a screen reader cannot infer from the element"
|
|
1453
|
+
]
|
|
1454
|
+
},
|
|
1455
|
+
"antiPatterns": [
|
|
1456
|
+
{
|
|
1457
|
+
"dont": "a styled <div> for a rule",
|
|
1458
|
+
"why": "that is what this component exists to replace"
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
"dont": "a Divider between every row of a list",
|
|
1462
|
+
"why": "a break announced on every row is noise; use the list's own row borders"
|
|
1463
|
+
}
|
|
1464
|
+
],
|
|
1156
1465
|
"stylesheet": "src/styles/divider.css",
|
|
1157
1466
|
"tokens": [
|
|
1158
1467
|
"--border-strong",
|
|
@@ -1183,6 +1492,83 @@
|
|
|
1183
1492
|
"propsType": "FullCardProps",
|
|
1184
1493
|
"description": "Full Card — the full-bleed case study row from Figma `Full Card` (592:857).\n\nA text column beside a framed media panel, split down the middle, mirrored\nby the `Alignment` variant.\n\nThe split holds from 1080 and stacks below it, media above content in BOTH\nalignments — `Alignment` names a horizontal side and\nstops meaning anything once there is one column, so it does not get to\ndecide the vertical order too. Size is a media query, not a prop, the same\ncall Header makes about Device.\n\nWHY THE `show*` BOOLEANS ARE GONE\n\nFigma carries `Show Eyebrow`, `Show Description` and `Show Actions` beside\nthe slots they gate, because a Figma component instance always holds every\nlayer and needs a switch to hide one. React has no such constraint — an\nabsent prop is the switch. Badge made the same call with `Show Dot`, and\nkeeping both would have let `showActions` and `actions` disagree.\n\n`headline` is required, and it is the only required prop. The heading is\nwhat makes this a section rather than a decorated div, and every other part\nof the card is optional in Figma too.\n\nNo `'use client'`: nothing here is stateful or interactive. The interactive\nparts arrive through `actions`, and they carry their own boundary.",
|
|
1185
1494
|
"import": "import { FullCard } from 'ionbase-ui';",
|
|
1495
|
+
"status": "stable",
|
|
1496
|
+
"summary": "A full-bleed row: a text column beside a framed media panel, mirrored by `alignment`.",
|
|
1497
|
+
"useWhen": [
|
|
1498
|
+
"a marketing or overview section presents one thing with a screenshot beside it",
|
|
1499
|
+
"a case-study or feature row on a landing or product page"
|
|
1500
|
+
],
|
|
1501
|
+
"useInstead": [
|
|
1502
|
+
{
|
|
1503
|
+
"when": "you are listing records with comparable fields",
|
|
1504
|
+
"use": "Table"
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
"when": "it is a short status message rather than a content section",
|
|
1508
|
+
"use": "Alert"
|
|
1509
|
+
}
|
|
1510
|
+
],
|
|
1511
|
+
"slots": {
|
|
1512
|
+
"eyebrow": {
|
|
1513
|
+
"accepts": "Badge",
|
|
1514
|
+
"note": "any node works"
|
|
1515
|
+
},
|
|
1516
|
+
"headline": {
|
|
1517
|
+
"accepts": "text",
|
|
1518
|
+
"note": "required — the heading is what makes this a section rather than a decorated div"
|
|
1519
|
+
},
|
|
1520
|
+
"description": {
|
|
1521
|
+
"accepts": "text"
|
|
1522
|
+
},
|
|
1523
|
+
"actions": {
|
|
1524
|
+
"accepts": "Button",
|
|
1525
|
+
"note": "a secondary Button in the design"
|
|
1526
|
+
},
|
|
1527
|
+
"media": {
|
|
1528
|
+
"accepts": "img or video",
|
|
1529
|
+
"note": "rendered inside the framed screen holder — pass the screenshot or embed itself, not the frame"
|
|
1530
|
+
},
|
|
1531
|
+
"children": {
|
|
1532
|
+
"accepts": "any",
|
|
1533
|
+
"note": "extra content below the description"
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
"variants": {
|
|
1537
|
+
"alignment": {
|
|
1538
|
+
"left": {
|
|
1539
|
+
"use": "media on the left, text on the right"
|
|
1540
|
+
},
|
|
1541
|
+
"right": {
|
|
1542
|
+
"use": "media on the right — the variant names the media, not the text"
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
"a11y": {
|
|
1547
|
+
"guarantees": [
|
|
1548
|
+
"the headline renders as a real heading, so the card appears in the document outline"
|
|
1549
|
+
],
|
|
1550
|
+
"requires": [
|
|
1551
|
+
"`headingLevel` matching the surrounding outline — `h3` is the default because Figma applies Type/H3, but only the page knows the real level",
|
|
1552
|
+
"`alt` on whatever you pass as `media`"
|
|
1553
|
+
],
|
|
1554
|
+
"notes": [
|
|
1555
|
+
"`h1` is deliberately not available: a full card is a section within a page, never the page's own title."
|
|
1556
|
+
]
|
|
1557
|
+
},
|
|
1558
|
+
"antiPatterns": [
|
|
1559
|
+
{
|
|
1560
|
+
"dont": "looking for `showEyebrow` / `showDescription` / `showActions`",
|
|
1561
|
+
"why": "an absent prop is the switch — Figma needs the boolean because an instance always holds every layer; React does not"
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
"dont": "leaving `headingLevel` at the default inside a section that is already at h3",
|
|
1565
|
+
"why": "it produces a skipped or duplicated outline level"
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
"dont": "passing your own frame or border around `media`",
|
|
1569
|
+
"why": "the component already draws the screen holder"
|
|
1570
|
+
}
|
|
1571
|
+
],
|
|
1186
1572
|
"stylesheet": "src/styles/full-card.css",
|
|
1187
1573
|
"tokens": [
|
|
1188
1574
|
"--border-default",
|
|
@@ -1280,16 +1666,75 @@
|
|
|
1280
1666
|
"propsType": "HeaderProps",
|
|
1281
1667
|
"description": "Header renders Figma's four Device variants — Desktop, Tablet,\nMobile-Closed, Mobile-Open — from one DOM tree.\n\nTWO AXES, AND THEY ARE NOT THE SAME KIND OF THING\n\nFigma spells Device as a single four-way variant, but it is really two: a\n*breakpoint* (Desktop / Tablet / Mobile) and a *state* (Closed / Open). They\nare modelled differently here because they are known by different people.\n\n Breakpoint is a media query. It is the one variant axis in the system the\n browser already knows the answer to — a header is Mobile because the\n viewport is narrow — and having React duplicate that judgement is how the\n two drift apart. Breakpoints match the Breakpoint collection's container\n widths: Tablet below 1216, Mobile below 896.\n\n Open/Closed is a prop, because only the caller knows. It follows the\n controlled/uncontrolled pair the rest of the system uses.\n\nONE TREE, TWO LAYOUTS\n\n`center` and `end` sit inline in the bar on Desktop and Tablet, and inside\nthe dropped Menu-Container on Mobile-Open. They are rendered once, in one\nwrapper, which is `display: contents` above the mobile breakpoint and an\nabsolutely positioned panel below it. Rendering them twice — or moving them\nwith JavaScript — would mean the same nav link exists twice in the\naccessibility tree, and would reset any state a caller put in a slot every\ntime the viewport crossed 896px.\n\nThe centre slot is no longer hidden on Tablet. Figma's Tablet variant ships\nit populated; the previous release collapsed it, which was correct for the\nprevious design and is not for this one.\n\nA `<header>` element with no explicit role: it is a landmark already when it\nis a direct child of body, which is where a page header sits.",
|
|
1282
1668
|
"import": "import { Header } from 'ionbase-ui';",
|
|
1283
|
-
"
|
|
1284
|
-
"
|
|
1285
|
-
|
|
1286
|
-
"
|
|
1287
|
-
"
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1669
|
+
"status": "stable",
|
|
1670
|
+
"summary": "The page header bar. Renders Figma's Desktop, Tablet, Mobile-Closed and Mobile-Open from one DOM tree.",
|
|
1671
|
+
"useWhen": [
|
|
1672
|
+
"the app needs a top bar with a brand, navigation and actions",
|
|
1673
|
+
"that bar must collapse to a mobile menu below 896px"
|
|
1674
|
+
],
|
|
1675
|
+
"useInstead": [
|
|
1676
|
+
{
|
|
1677
|
+
"when": "it is a sidebar rather than a top bar",
|
|
1678
|
+
"use": "NavItem",
|
|
1679
|
+
"why": "Header owns a horizontal bar and its own breakpoint behaviour"
|
|
1680
|
+
}
|
|
1681
|
+
],
|
|
1682
|
+
"slots": {
|
|
1683
|
+
"brand": {
|
|
1684
|
+
"accepts": "LogoMark or Logo",
|
|
1685
|
+
"note": "rendered at the start; never shrinks"
|
|
1686
|
+
},
|
|
1687
|
+
"center": {
|
|
1688
|
+
"accepts": "NavItem",
|
|
1689
|
+
"note": "inline on Desktop and Tablet; moves into the mobile menu below 896px. Rendered once, so slot state survives crossing the breakpoint"
|
|
1690
|
+
},
|
|
1691
|
+
"end": {
|
|
1692
|
+
"accepts": "Button, Avatar",
|
|
1693
|
+
"note": "actions; same placement rules as `center`"
|
|
1694
|
+
},
|
|
1695
|
+
"children": {
|
|
1696
|
+
"accepts": "any",
|
|
1697
|
+
"note": "escape hatch — rendered in the bar after the menu wrapper and before the mobile toggle. Prefer the named slots"
|
|
1698
|
+
}
|
|
1699
|
+
},
|
|
1700
|
+
"a11y": {
|
|
1701
|
+
"role": "banner, from the <header> element when it is a direct child of body",
|
|
1702
|
+
"guarantees": [
|
|
1703
|
+
"the mobile toggle carries `aria-expanded` and `aria-controls` pointing at the menu container",
|
|
1704
|
+
"Escape closes the open mobile menu — and the listener sits on the header, so a header that never opens never listens",
|
|
1705
|
+
"the slots are rendered once, so no nav link exists twice in the accessibility tree"
|
|
1706
|
+
],
|
|
1707
|
+
"requires": [
|
|
1708
|
+
"`menuLabel` — the mobile toggle is icon-only and has no other name"
|
|
1709
|
+
],
|
|
1710
|
+
"notes": [
|
|
1711
|
+
"This is a disclosure, not a modal: no focus trap and no scroll lock, so Escape is the whole dismissal contract."
|
|
1712
|
+
]
|
|
1713
|
+
},
|
|
1714
|
+
"antiPatterns": [
|
|
1715
|
+
{
|
|
1716
|
+
"dont": "rendering a second copy of the nav for mobile",
|
|
1717
|
+
"why": "Header already moves the one copy; two copies duplicate every link in the accessibility tree"
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
"dont": "driving the mobile layout from JS state",
|
|
1721
|
+
"why": "the breakpoint is a media query — the browser already knows the answer, and duplicating that judgement is how the two drift"
|
|
1722
|
+
},
|
|
1723
|
+
{
|
|
1724
|
+
"dont": "passing `open` without `onOpenChange`",
|
|
1725
|
+
"why": "controlled with no handler means the toggle can never change it"
|
|
1726
|
+
}
|
|
1727
|
+
],
|
|
1728
|
+
"stylesheet": "src/styles/header.css",
|
|
1729
|
+
"tokens": [
|
|
1730
|
+
"--border-default",
|
|
1731
|
+
"--border-strong",
|
|
1732
|
+
"--border-width-default",
|
|
1733
|
+
"--border-width-thick",
|
|
1734
|
+
"--font-family-sans",
|
|
1735
|
+
"--icon-default",
|
|
1736
|
+
"--icon-size-sm",
|
|
1737
|
+
"--ion-header-center-gap",
|
|
1293
1738
|
"--ion-header-end-gap",
|
|
1294
1739
|
"--ion-header-height",
|
|
1295
1740
|
"--ion-header-padding-x",
|
|
@@ -1371,6 +1816,60 @@
|
|
|
1371
1816
|
"propsType": "IconProps",
|
|
1372
1817
|
"description": "Wrapper that applies the design system's icon sizing and accessibility\ndefaults to whatever icon component you hand it.\n\n import { Plus } from 'lucide-react';\n <Icon as={Plus} size=\"sm\" />\n <Icon as={Plus} label=\"Add item\" /> // meaningful, gets an a11y name\n\nTakes the icon as a prop rather than re-exporting a set: a barrel of a\nthousand-plus icons defeats tree-shaking in several bundlers, and pinning one\nicon library would force it on every consumer. You import the one icon you\nneed, from whichever library you use, and it is the only one bundled.",
|
|
1373
1818
|
"import": "import { Icon } from 'ionbase-ui';",
|
|
1819
|
+
"status": "stable",
|
|
1820
|
+
"summary": "Applies the system's icon sizing and accessibility defaults to any SVG component you hand it. The package ships no icon set.",
|
|
1821
|
+
"useWhen": [
|
|
1822
|
+
"rendering any icon inside this design system, from any library — lucide, heroicons, react-icons, your own SVG"
|
|
1823
|
+
],
|
|
1824
|
+
"useInstead": [
|
|
1825
|
+
{
|
|
1826
|
+
"when": "the graphic is a brand mark",
|
|
1827
|
+
"use": "Logo or LogoMark"
|
|
1828
|
+
},
|
|
1829
|
+
{
|
|
1830
|
+
"when": "you need a coloured status marker with text",
|
|
1831
|
+
"use": "Badge"
|
|
1832
|
+
}
|
|
1833
|
+
],
|
|
1834
|
+
"a11y": {
|
|
1835
|
+
"guarantees": [
|
|
1836
|
+
"with `label`, the icon gets `role=\"img\"` and that name",
|
|
1837
|
+
"without `label`, it is `aria-hidden` and `focusable=\"false\"` — decoration, never read twice"
|
|
1838
|
+
],
|
|
1839
|
+
"requires": [
|
|
1840
|
+
"`label` when the icon carries meaning on its own — an icon-only button, a standalone status marker"
|
|
1841
|
+
],
|
|
1842
|
+
"notes": [
|
|
1843
|
+
"Omit `label` for an icon beside a visible text label. The text already names it."
|
|
1844
|
+
]
|
|
1845
|
+
},
|
|
1846
|
+
"antiPatterns": [
|
|
1847
|
+
{
|
|
1848
|
+
"dont": "`<Button startIcon={<Icon as={Plus} label=\"Add\" />}>Add</Button>`",
|
|
1849
|
+
"do": "drop the `label`",
|
|
1850
|
+
"why": "the button's own text names it; the labelled icon makes it announce twice"
|
|
1851
|
+
},
|
|
1852
|
+
{
|
|
1853
|
+
"dont": "hardcoding `width`/`height` in px",
|
|
1854
|
+
"do": "a `size` rung, or omit `size` to inherit the surrounding font size",
|
|
1855
|
+
"why": "inheriting is what lets an icon sit correctly inside a Button without the Button knowing about it"
|
|
1856
|
+
},
|
|
1857
|
+
{
|
|
1858
|
+
"dont": "assuming `size=\"xs\"` is 12px",
|
|
1859
|
+
"why": "the ladder shifted in 2026-08: `xs` is 14, and `2xs` is the old 12. This raises no type error"
|
|
1860
|
+
}
|
|
1861
|
+
],
|
|
1862
|
+
"sizes": {
|
|
1863
|
+
"note": "The rungs map one-to-one onto the Semantics `icon-size/*` tokens. Any CSS length is also accepted for a one-off.",
|
|
1864
|
+
"ladder": {
|
|
1865
|
+
"2xs": 12,
|
|
1866
|
+
"xs": 14,
|
|
1867
|
+
"sm": 16,
|
|
1868
|
+
"md": 20,
|
|
1869
|
+
"lg": 24,
|
|
1870
|
+
"xl": 32
|
|
1871
|
+
}
|
|
1872
|
+
},
|
|
1374
1873
|
"stylesheet": "src/styles/icon.css",
|
|
1375
1874
|
"tokens": [],
|
|
1376
1875
|
"props": {
|
|
@@ -1964,6 +2463,73 @@
|
|
|
1964
2463
|
"propsType": "LinkProps",
|
|
1965
2464
|
"description": "Link — Figma `Link` (774:1516).\n\nRenders an `<a>` when given an `href` and a `<button>` otherwise, the same\njudgment `NavItem` makes: the element follows what the caller is actually\nbuilding rather than a separate `as` prop. A link that does not navigate is\na button, and shipping one as an anchor breaks middle-click, \"open in new\ntab\" and the screen-reader announcement all at once.\n\nNO SIZE PROP, DELIBERATELY. A link is an inline element and inherits its\ntype from the text around it — `font-size: inherit` in the stylesheet, and\nicons sized in `em` so they scale with it. Figma has to draw its variants at\nsome concrete size (16/24, `type/body`), but that is one sample of an\ninheriting element, not a specification. Do not add a size ladder to match\nthe drawing.",
|
|
1966
2465
|
"import": "import { Link } from 'ionbase-ui';",
|
|
2466
|
+
"status": "stable",
|
|
2467
|
+
"summary": "Navigation to somewhere else. Renders an <a> when given `href`, a <button> otherwise.",
|
|
2468
|
+
"useWhen": [
|
|
2469
|
+
"the interaction takes the user to a different page, view or resource",
|
|
2470
|
+
"the destination should support middle-click, open-in-new-tab and copy-link"
|
|
2471
|
+
],
|
|
2472
|
+
"useInstead": [
|
|
2473
|
+
{
|
|
2474
|
+
"when": "the interaction changes something rather than going somewhere",
|
|
2475
|
+
"use": "Button",
|
|
2476
|
+
"why": "a link that does not navigate breaks middle-click, open-in-new-tab and the screen-reader announcement all at once"
|
|
2477
|
+
},
|
|
2478
|
+
{
|
|
2479
|
+
"when": "it is a primary navigation destination in a header or sidebar",
|
|
2480
|
+
"use": "NavItem"
|
|
2481
|
+
}
|
|
2482
|
+
],
|
|
2483
|
+
"variants": {
|
|
2484
|
+
"variant": {
|
|
2485
|
+
"inline": {
|
|
2486
|
+
"use": "the default — a link inside a paragraph. Underlined in every state, because a link in body copy must not rely on colour alone (WCAG 1.4.1)"
|
|
2487
|
+
},
|
|
2488
|
+
"standalone": {
|
|
2489
|
+
"use": "a link that already reads as interactive from its position — a card footer, an action row. Underlines on hover only"
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
},
|
|
2493
|
+
"slots": {
|
|
2494
|
+
"startIcon": {
|
|
2495
|
+
"accepts": "Icon",
|
|
2496
|
+
"note": "omit Icon's `label` — the link text already names it"
|
|
2497
|
+
},
|
|
2498
|
+
"endIcon": {
|
|
2499
|
+
"accepts": "Icon",
|
|
2500
|
+
"note": "the external-link or arrow affordance; same rule about `label`"
|
|
2501
|
+
}
|
|
2502
|
+
},
|
|
2503
|
+
"a11y": {
|
|
2504
|
+
"role": "link when `href` is set, button when it is not",
|
|
2505
|
+
"guarantees": [
|
|
2506
|
+
"the rendered element follows what the caller is actually building, so the announcement is always honest",
|
|
2507
|
+
"`inline` stays underlined in every state, so it never depends on colour alone"
|
|
2508
|
+
],
|
|
2509
|
+
"requires": [
|
|
2510
|
+
"link text that makes sense read on its own — screen-reader users navigate by a list of links"
|
|
2511
|
+
]
|
|
2512
|
+
},
|
|
2513
|
+
"antiPatterns": [
|
|
2514
|
+
{
|
|
2515
|
+
"dont": "\"click here\" or \"read more\" as the whole link text",
|
|
2516
|
+
"why": "out of context in a links list it names nothing"
|
|
2517
|
+
},
|
|
2518
|
+
{
|
|
2519
|
+
"dont": "adding a size prop or wrapper to scale it",
|
|
2520
|
+
"why": "a link is inline and inherits its type from the text around it, by design — icons are sized in `em` to follow"
|
|
2521
|
+
},
|
|
2522
|
+
{
|
|
2523
|
+
"dont": "`target=\"_blank\"` with no warning in the text or endIcon",
|
|
2524
|
+
"why": "an unannounced new tab disorients screen-reader and low-vision users"
|
|
2525
|
+
}
|
|
2526
|
+
],
|
|
2527
|
+
"deprecated": [
|
|
2528
|
+
{
|
|
2529
|
+
"prop": "disabled",
|
|
2530
|
+
"replacement": "isDisabled"
|
|
2531
|
+
}
|
|
2532
|
+
],
|
|
1967
2533
|
"stylesheet": "src/styles/link.css",
|
|
1968
2534
|
"tokens": [
|
|
1969
2535
|
"--border-width-thick",
|
|
@@ -2203,6 +2769,55 @@
|
|
|
2203
2769
|
"propsType": "LogoProps",
|
|
2204
2770
|
"description": "The full lockup: mark + wordmark, matching Figma's `Logo-Ionbase`\n(52:21369).\n\nFigma also has a `Property=Name` axis — wordmark only, no mark — but both\nof its variants currently render the placeholder text \"raza\" rather than\n\"IonBase\", in an unbound raw colour. That is leftover debug content, not a\nreal asset, so it is not implemented here. Reproducing it would ship a\nstranger's name into a design system; extending `LogoMark` alone already\ncovers \"no wordmark\" for the cases that are real.",
|
|
2205
2771
|
"import": "import { Logo } from 'ionbase-ui';",
|
|
2772
|
+
"status": "stable",
|
|
2773
|
+
"summary": "The full IonBase lockup — mark plus wordmark.",
|
|
2774
|
+
"useWhen": [
|
|
2775
|
+
"the product needs to identify itself: header, sign-in page, empty state, marketing surface"
|
|
2776
|
+
],
|
|
2777
|
+
"useInstead": [
|
|
2778
|
+
{
|
|
2779
|
+
"when": "space is tight, or a wordmark already appears nearby",
|
|
2780
|
+
"use": "LogoMark"
|
|
2781
|
+
},
|
|
2782
|
+
{
|
|
2783
|
+
"when": "you need any graphic that is not the brand",
|
|
2784
|
+
"use": "Icon"
|
|
2785
|
+
}
|
|
2786
|
+
],
|
|
2787
|
+
"variants": {
|
|
2788
|
+
"size": {
|
|
2789
|
+
"sm": {
|
|
2790
|
+
"use": "the default — a 24px mark, which is what a header bar wants"
|
|
2791
|
+
},
|
|
2792
|
+
"lg": {
|
|
2793
|
+
"use": "a 32px mark — sign-in, splash, or anywhere the brand is the subject"
|
|
2794
|
+
}
|
|
2795
|
+
},
|
|
2796
|
+
"wordmark": {
|
|
2797
|
+
"vector": {
|
|
2798
|
+
"use": "the default and the safe choice — Figma's serif logotype as frozen artwork, immune to font substitution and visually distinct from the UI type"
|
|
2799
|
+
},
|
|
2800
|
+
"text": {
|
|
2801
|
+
"use": "live, selectable Host Grotesk at `text/tertiary` — for surfaces where the name should behave like text (copyable, searchable) rather than like a mark"
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
},
|
|
2805
|
+
"a11y": {
|
|
2806
|
+
"guarantees": [
|
|
2807
|
+
"the wordmark carries the accessible name, so the mark beside it is not announced twice",
|
|
2808
|
+
"artwork uses `currentColor` rather than Figma's baked hex, so it themes with the rest of the icon layer"
|
|
2809
|
+
]
|
|
2810
|
+
},
|
|
2811
|
+
"antiPatterns": [
|
|
2812
|
+
{
|
|
2813
|
+
"dont": "recolouring the lockup with your own fill",
|
|
2814
|
+
"why": "it inherits `icon/default`; overriding it is how a brand mark ends up failing contrast in one theme"
|
|
2815
|
+
},
|
|
2816
|
+
{
|
|
2817
|
+
"dont": "putting the Logo inside a Link with its own text \"IonBase\"",
|
|
2818
|
+
"why": "the name is announced twice; label the link, not both"
|
|
2819
|
+
}
|
|
2820
|
+
],
|
|
2206
2821
|
"stylesheet": "src/styles/logo.css",
|
|
2207
2822
|
"tokens": [
|
|
2208
2823
|
"--font-family-sans",
|
|
@@ -2249,6 +2864,44 @@
|
|
|
2249
2864
|
"propsType": "LogoMarkProps",
|
|
2250
2865
|
"description": "The bare icon glyph, no wordmark — what `Header` actually uses.\n\nColour comes from the `.ion-logo__mark` class (`icon/default`), not the SVG\nitself, so it themes the same way every other icon in the system does.",
|
|
2251
2866
|
"import": "import { LogoMark } from 'ionbase-ui';",
|
|
2867
|
+
"status": "stable",
|
|
2868
|
+
"summary": "The bare mark, no wordmark — what Header uses.",
|
|
2869
|
+
"useWhen": [
|
|
2870
|
+
"a wordmark is already visible nearby, or the bar is too narrow for the lockup",
|
|
2871
|
+
"a favicon-scale identity marker inside the product"
|
|
2872
|
+
],
|
|
2873
|
+
"useInstead": [
|
|
2874
|
+
{
|
|
2875
|
+
"when": "this is the only place the product names itself",
|
|
2876
|
+
"use": "Logo",
|
|
2877
|
+
"why": "a mark alone identifies nothing to someone who has not learned it yet"
|
|
2878
|
+
}
|
|
2879
|
+
],
|
|
2880
|
+
"variants": {
|
|
2881
|
+
"size": {
|
|
2882
|
+
"sm": {
|
|
2883
|
+
"use": "the default, 24px — header bars"
|
|
2884
|
+
},
|
|
2885
|
+
"lg": {
|
|
2886
|
+
"use": "32px — sign-in and other brand-forward surfaces"
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
},
|
|
2890
|
+
"a11y": {
|
|
2891
|
+
"requires": [
|
|
2892
|
+
"`label` when the mark stands alone — it is the only thing naming it"
|
|
2893
|
+
],
|
|
2894
|
+
"notes": [
|
|
2895
|
+
"Omit `label` when a visible wordmark sits beside it; that text already carries the name."
|
|
2896
|
+
]
|
|
2897
|
+
},
|
|
2898
|
+
"antiPatterns": [
|
|
2899
|
+
{
|
|
2900
|
+
"dont": "`label=\"logo\"`",
|
|
2901
|
+
"do": "`label=\"IonBase\"`",
|
|
2902
|
+
"why": "the name of the brand is the useful announcement; the word \"logo\" is not"
|
|
2903
|
+
}
|
|
2904
|
+
],
|
|
2252
2905
|
"stylesheet": "src/styles/logo.css",
|
|
2253
2906
|
"tokens": [
|
|
2254
2907
|
"--font-family-sans",
|
|
@@ -2290,6 +2943,60 @@
|
|
|
2290
2943
|
"propsType": "MenuProps",
|
|
2291
2944
|
"description": "Menu is the list surface, not a popover.\n\nFigma models the list alone — there is no trigger, anchor or open state in\nthe design — so this renders the surface and nothing else. Positioning it\nagainst a button is the caller's job until Figma has a component that says\nhow. Building a popover here would be inventing design, and it would be the\nhard half to unpick later.\n\n`role=\"menu\"` is deliberately NOT set. A real ARIA menu owes the user\nroving-tabindex arrow navigation, typeahead and focus containment; claiming\nthe role without them is worse for a screen-reader user than an honest list,\nbecause it promises interactions that are not there. When the popover exists,\nthe role comes with it.",
|
|
2292
2945
|
"import": "import { Menu } from 'ionbase-ui';",
|
|
2946
|
+
"status": "stable",
|
|
2947
|
+
"summary": "The list surface only — no trigger, no anchor, no open state. Positioning it is the caller's job.",
|
|
2948
|
+
"useWhen": [
|
|
2949
|
+
"rendering a list of actions or options as a surface you have already positioned",
|
|
2950
|
+
"the rows need icons, a selected check, or two lines — more than a native <select> can hold"
|
|
2951
|
+
],
|
|
2952
|
+
"useInstead": [
|
|
2953
|
+
{
|
|
2954
|
+
"when": "you want a self-contained dropdown with a trigger and open state",
|
|
2955
|
+
"use": "Popover",
|
|
2956
|
+
"why": "Menu is the surface alone; Popover owns the trigger, positioning, focus containment and Escape"
|
|
2957
|
+
},
|
|
2958
|
+
{
|
|
2959
|
+
"when": "one value is picked from a modest list of plain text options",
|
|
2960
|
+
"use": "Select",
|
|
2961
|
+
"why": "the native picker brings keyboard handling, type-ahead and the mobile platform picker for free"
|
|
2962
|
+
},
|
|
2963
|
+
{
|
|
2964
|
+
"when": "it is primary navigation rather than a dropdown",
|
|
2965
|
+
"use": "NavItem"
|
|
2966
|
+
}
|
|
2967
|
+
],
|
|
2968
|
+
"composition": {
|
|
2969
|
+
"order": [
|
|
2970
|
+
"Menu",
|
|
2971
|
+
"MenuItem"
|
|
2972
|
+
],
|
|
2973
|
+
"example": "<Menu><MenuItem icon={<Icon as={Copy} />}>Duplicate</MenuItem><MenuItem isSelected>Archive</MenuItem></Menu>"
|
|
2974
|
+
},
|
|
2975
|
+
"slots": {
|
|
2976
|
+
"children": {
|
|
2977
|
+
"accepts": "MenuItem"
|
|
2978
|
+
}
|
|
2979
|
+
},
|
|
2980
|
+
"a11y": {
|
|
2981
|
+
"role": "none — it is a plain <ul>",
|
|
2982
|
+
"guarantees": [
|
|
2983
|
+
"no `role=\"menu\"`, deliberately: a real ARIA menu owes the user roving tabindex, arrow-key navigation and typeahead, and claiming the role without them promises interactions that are not there"
|
|
2984
|
+
],
|
|
2985
|
+
"notes": [
|
|
2986
|
+
"Each MenuItem is a real <button>, so Tab reaches every row. That is honest, and it is not the same as arrow-key menu navigation.",
|
|
2987
|
+
"The role arrives when the popover that owns it does."
|
|
2988
|
+
]
|
|
2989
|
+
},
|
|
2990
|
+
"antiPatterns": [
|
|
2991
|
+
{
|
|
2992
|
+
"dont": "adding `role=\"menu\"` yourself",
|
|
2993
|
+
"why": "the keyboard behaviour that role promises is not implemented; an honest list is better for a screen-reader user than a lying menu"
|
|
2994
|
+
},
|
|
2995
|
+
{
|
|
2996
|
+
"dont": "using Menu as a dropdown without positioning it",
|
|
2997
|
+
"why": "it renders in flow — it has no anchor logic of its own"
|
|
2998
|
+
}
|
|
2999
|
+
],
|
|
2293
3000
|
"stylesheet": "src/styles/menu.css",
|
|
2294
3001
|
"tokens": [
|
|
2295
3002
|
"--border-focus",
|
|
@@ -2339,6 +3046,57 @@
|
|
|
2339
3046
|
"source": "src/components/Menu.tsx",
|
|
2340
3047
|
"propsType": "MenuItemProps",
|
|
2341
3048
|
"import": "import { MenuItem } from 'ionbase-ui';",
|
|
3049
|
+
"status": "stable",
|
|
3050
|
+
"summary": "One row in a Menu. A real <button> inside an <li>, with an optional leading icon and a trailing selected check.",
|
|
3051
|
+
"useWhen": [
|
|
3052
|
+
"a row in a Menu surface"
|
|
3053
|
+
],
|
|
3054
|
+
"useInstead": [
|
|
3055
|
+
{
|
|
3056
|
+
"when": "it is a top-level navigation destination",
|
|
3057
|
+
"use": "NavItem"
|
|
3058
|
+
},
|
|
3059
|
+
{
|
|
3060
|
+
"when": "it is a standalone action outside a list",
|
|
3061
|
+
"use": "Button"
|
|
3062
|
+
}
|
|
3063
|
+
],
|
|
3064
|
+
"slots": {
|
|
3065
|
+
"icon": {
|
|
3066
|
+
"accepts": "Icon",
|
|
3067
|
+
"note": "leading icon; omit its `label` — the row's text names it"
|
|
3068
|
+
},
|
|
3069
|
+
"children": {
|
|
3070
|
+
"accepts": "text",
|
|
3071
|
+
"note": "the row label"
|
|
3072
|
+
}
|
|
3073
|
+
},
|
|
3074
|
+
"a11y": {
|
|
3075
|
+
"role": "button",
|
|
3076
|
+
"guarantees": [
|
|
3077
|
+
"`isSelected` sets `aria-pressed`, so the selected state is announced and not only drawn",
|
|
3078
|
+
"the check glyph always occupies its slot, so rows never reflow as the selection moves"
|
|
3079
|
+
],
|
|
3080
|
+
"requires": [
|
|
3081
|
+
"a text label in `children`"
|
|
3082
|
+
]
|
|
3083
|
+
},
|
|
3084
|
+
"antiPatterns": [
|
|
3085
|
+
{
|
|
3086
|
+
"dont": "using MenuItem outside a Menu",
|
|
3087
|
+
"why": "it renders an <li>, which is only valid inside a list"
|
|
3088
|
+
},
|
|
3089
|
+
{
|
|
3090
|
+
"dont": "an icon-only MenuItem with no children",
|
|
3091
|
+
"why": "the row announces nothing"
|
|
3092
|
+
}
|
|
3093
|
+
],
|
|
3094
|
+
"deprecated": [
|
|
3095
|
+
{
|
|
3096
|
+
"prop": "disabled",
|
|
3097
|
+
"replacement": "isDisabled"
|
|
3098
|
+
}
|
|
3099
|
+
],
|
|
2342
3100
|
"stylesheet": "src/styles/menu.css",
|
|
2343
3101
|
"tokens": [
|
|
2344
3102
|
"--border-focus",
|
|
@@ -2656,25 +3414,78 @@
|
|
|
2656
3414
|
"propsType": "NavItemProps",
|
|
2657
3415
|
"description": "Nav Item renders an `<a>` when given `href`, a `<button>` otherwise —\nchosen by what the caller is actually building, the same judgment call\nMenu and Select make. A primary nav bar is links; a nav item with\n`showChevron` that opens a menu (no `href` of its own) is a button.\n\nInteraction state comes from React Aria rather than CSS pseudo-classes,\nmatching Button: `useHover` is pointer-aware so a tap does not stay\n\"hovered\" until the next tap elsewhere, and `useFocusRing` shows the ring\nonly for keyboard navigation. The CSS keeps its own `:hover` /\n`:focus-visible` rules so the stylesheet still works without React.",
|
|
2658
3416
|
"import": "import { NavItem } from 'ionbase-ui';",
|
|
2659
|
-
"
|
|
2660
|
-
"
|
|
2661
|
-
|
|
2662
|
-
"
|
|
2663
|
-
"
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
"
|
|
2677
|
-
|
|
3417
|
+
"status": "stable",
|
|
3418
|
+
"summary": "A primary navigation destination. Renders an <a> when given `href`, a <button> otherwise.",
|
|
3419
|
+
"useWhen": [
|
|
3420
|
+
"building a header nav bar, a sidebar or a section switcher",
|
|
3421
|
+
"an item opens a menu rather than navigating — pass `showChevron` and no `href`"
|
|
3422
|
+
],
|
|
3423
|
+
"useInstead": [
|
|
3424
|
+
{
|
|
3425
|
+
"when": "the link sits inside body copy or a content block",
|
|
3426
|
+
"use": "Link"
|
|
3427
|
+
},
|
|
3428
|
+
{
|
|
3429
|
+
"when": "it is a row in a dropdown list rather than a top-level destination",
|
|
3430
|
+
"use": "MenuItem"
|
|
3431
|
+
}
|
|
3432
|
+
],
|
|
3433
|
+
"slots": {
|
|
3434
|
+
"icon": {
|
|
3435
|
+
"accepts": "Icon",
|
|
3436
|
+
"note": "leading icon; omit its `label` — the item's text names it"
|
|
3437
|
+
}
|
|
3438
|
+
},
|
|
3439
|
+
"a11y": {
|
|
3440
|
+
"role": "link when `href` is set, button when it is not",
|
|
3441
|
+
"guarantees": [
|
|
3442
|
+
"hover state comes from React Aria's pointer-aware `useHover`, so a tap does not stay hovered until the next tap elsewhere",
|
|
3443
|
+
"the focus ring shows for keyboard navigation only",
|
|
3444
|
+
"the CSS keeps its own `:hover` / `:focus-visible` rules, so the styling survives without React"
|
|
3445
|
+
],
|
|
3446
|
+
"requires": [
|
|
3447
|
+
"an accessible name — `children`, or `aria-label` on an icon-only item"
|
|
3448
|
+
],
|
|
3449
|
+
"notes": [
|
|
3450
|
+
"NavItem is one item, not the landmark. Wrap the set in a <nav> with its own accessible name."
|
|
3451
|
+
]
|
|
3452
|
+
},
|
|
3453
|
+
"antiPatterns": [
|
|
3454
|
+
{
|
|
3455
|
+
"dont": "`showChevron` on a plain link that navigates",
|
|
3456
|
+
"why": "the chevron promises a menu; nothing opens"
|
|
3457
|
+
},
|
|
3458
|
+
{
|
|
3459
|
+
"dont": "marking the current page with colour only",
|
|
3460
|
+
"do": "`aria-current=\"page\"`",
|
|
3461
|
+
"why": "a screen-reader user is otherwise never told where they are"
|
|
3462
|
+
}
|
|
3463
|
+
],
|
|
3464
|
+
"deprecated": [
|
|
3465
|
+
{
|
|
3466
|
+
"prop": "disabled",
|
|
3467
|
+
"replacement": "isDisabled"
|
|
3468
|
+
}
|
|
3469
|
+
],
|
|
3470
|
+
"stylesheet": "src/styles/nav-item.css",
|
|
3471
|
+
"tokens": [
|
|
3472
|
+
"--border-focus",
|
|
3473
|
+
"--border-width-thick",
|
|
3474
|
+
"--font-family-sans",
|
|
3475
|
+
"--font-weight-regular",
|
|
3476
|
+
"--icon-disabled",
|
|
3477
|
+
"--icon-interactive-hover",
|
|
3478
|
+
"--icon-size-sm",
|
|
3479
|
+
"--icon-tertiary",
|
|
3480
|
+
"--ion-duration-base",
|
|
3481
|
+
"--ion-ease-out",
|
|
3482
|
+
"--radius-sm",
|
|
3483
|
+
"--spacing-4",
|
|
3484
|
+
"--spacing-8",
|
|
3485
|
+
"--text-disabled",
|
|
3486
|
+
"--text-interactive-hover",
|
|
3487
|
+
"--text-secondary",
|
|
3488
|
+
"--type-body-sm",
|
|
2678
3489
|
"--type-body-sm-line-height"
|
|
2679
3490
|
],
|
|
2680
3491
|
"props": {
|
|
@@ -2732,6 +3543,66 @@
|
|
|
2732
3543
|
"propsType": "PhoneInputProps",
|
|
2733
3544
|
"description": "PhoneInput — Figma `Input/Phone` (80:372).\n\nA dial-code block butted against an Input, sharing one outline: the block\ncarries the left radii and the control the right, so the seam is square on\nboth sides and reads as a single control. Both keep their full 1px border,\nwhich is what Figma draws — the 2px seam is the two strokes meeting, not an\naccident.\n\nThree sizes, no State axis. Every interaction state is the Input's, reached\nthrough the ordinary props, because Figma composes this from the same `Input`\ninstance rather than redrawing it.\n\nWHAT THIS DELIBERATELY DOES NOT DO\n\nIt does not pick countries. The chevron implies a menu and Figma specifies no\nopen state for it — no list, no flags, no search, no selected state. Country\ndata and the picker are application concerns with real editorial weight (which\nterritories, which names, which order), and inventing them here would be\ndesigning rather than implementing.\n\nSo the trigger is a real `<button>` with an accessible name, and\n`countryButtonProps` is how you wire it to a `Menu`, a popover or your own\nlistbox — including the `aria-haspopup` and `aria-expanded` that only the\nthing owning the popup can set honestly.",
|
|
2734
3545
|
"import": "import { PhoneInput } from 'ionbase-ui';",
|
|
3546
|
+
"status": "stable",
|
|
3547
|
+
"summary": "A dial-code block butted against an Input, sharing one outline. It does not pick countries.",
|
|
3548
|
+
"useWhen": [
|
|
3549
|
+
"collecting a phone number where the country calling code matters"
|
|
3550
|
+
],
|
|
3551
|
+
"useInstead": [
|
|
3552
|
+
{
|
|
3553
|
+
"when": "the number is free-form and the code does not matter",
|
|
3554
|
+
"use": "Input"
|
|
3555
|
+
}
|
|
3556
|
+
],
|
|
3557
|
+
"variants": {
|
|
3558
|
+
"size": {
|
|
3559
|
+
"sm": {
|
|
3560
|
+
"use": "dense forms"
|
|
3561
|
+
},
|
|
3562
|
+
"md": {
|
|
3563
|
+
"use": "the default"
|
|
3564
|
+
},
|
|
3565
|
+
"lg": {
|
|
3566
|
+
"use": "prominent single-field forms"
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
},
|
|
3570
|
+
"slots": {
|
|
3571
|
+
"trailingIcon": {
|
|
3572
|
+
"accepts": "Icon",
|
|
3573
|
+
"note": "passed through to the Input"
|
|
3574
|
+
},
|
|
3575
|
+
"countryButtonProps": {
|
|
3576
|
+
"accepts": "button props",
|
|
3577
|
+
"note": "how a country picker is attached — spread your trigger's props here, including the `aria-haspopup` and `aria-expanded` only the thing owning the popup can set honestly"
|
|
3578
|
+
}
|
|
3579
|
+
},
|
|
3580
|
+
"a11y": {
|
|
3581
|
+
"guarantees": [
|
|
3582
|
+
"the dial-code trigger is a real <button> with an accessible name",
|
|
3583
|
+
"every interaction state is the Input's, reached through the ordinary Input props"
|
|
3584
|
+
],
|
|
3585
|
+
"requires": [
|
|
3586
|
+
"`label`, or `aria-label` when no visible label is rendered",
|
|
3587
|
+
"`countryLabel` if you change the default — the trigger shows only `+1`, which names a value rather than an action"
|
|
3588
|
+
]
|
|
3589
|
+
},
|
|
3590
|
+
"antiPatterns": [
|
|
3591
|
+
{
|
|
3592
|
+
"dont": "expecting the chevron to open a country list",
|
|
3593
|
+
"why": "the picker is deliberately not implemented — country data and ordering are editorial application decisions, and Figma specifies no open state. Wire your own through `countryButtonProps`"
|
|
3594
|
+
},
|
|
3595
|
+
{
|
|
3596
|
+
"dont": "wrapping it in your own bordered container",
|
|
3597
|
+
"why": "the block and the control already share one outline; a second border reads as two controls"
|
|
3598
|
+
}
|
|
3599
|
+
],
|
|
3600
|
+
"deprecated": [
|
|
3601
|
+
{
|
|
3602
|
+
"prop": "disabled",
|
|
3603
|
+
"replacement": "isDisabled"
|
|
3604
|
+
}
|
|
3605
|
+
],
|
|
2735
3606
|
"stylesheet": "src/styles/phone-input.css",
|
|
2736
3607
|
"tokens": [
|
|
2737
3608
|
"--border-default",
|
|
@@ -3204,6 +4075,98 @@
|
|
|
3204
4075
|
"propsType": "PopoverProps",
|
|
3205
4076
|
"description": "Popover — Figma `Popover` (825:1853).\n\nSITS BETWEEN TOOLTIP AND MODAL. A tooltip is a hint that cannot hold\nfocusable content; a modal is a task that takes over the page. A popover\nholds interactive content but stays attached to the control that opened it.\n\n`usePopover` contains focus, closes on Escape or an outside click, and hides\nthe rest of the page from assistive tech while open — the same guarantees\nModal gives. What differs is the framing, and it is deliberate: no visible\nscrim, anchored to its trigger, and `surface/raised` rather than Modal's\n`surface/overlay`. It reads as attached to the page rather than replacing it.",
|
|
3206
4077
|
"import": "import { Popover } from 'ionbase-ui';",
|
|
4078
|
+
"status": "stable",
|
|
4079
|
+
"summary": "An anchored panel that holds interactive content. Sits between Tooltip and Modal.",
|
|
4080
|
+
"useWhen": [
|
|
4081
|
+
"the content is interactive — a form, a filter, a list of actions — but should stay attached to the control that opened it",
|
|
4082
|
+
"the surrounding page should stay visible and in context"
|
|
4083
|
+
],
|
|
4084
|
+
"useInstead": [
|
|
4085
|
+
{
|
|
4086
|
+
"when": "the content is a short hint with nothing focusable in it",
|
|
4087
|
+
"use": "Tooltip",
|
|
4088
|
+
"why": "a tooltip cannot be focused or scrolled and closes when the pointer leaves"
|
|
4089
|
+
},
|
|
4090
|
+
{
|
|
4091
|
+
"when": "the task takes over the page or must be completed before anything else",
|
|
4092
|
+
"use": "Modal"
|
|
4093
|
+
},
|
|
4094
|
+
{
|
|
4095
|
+
"when": "you only need the list surface and will position it yourself",
|
|
4096
|
+
"use": "Menu"
|
|
4097
|
+
}
|
|
4098
|
+
],
|
|
4099
|
+
"variants": {
|
|
4100
|
+
"placement": {
|
|
4101
|
+
"top": {
|
|
4102
|
+
"use": "above the trigger"
|
|
4103
|
+
},
|
|
4104
|
+
"bottom": {
|
|
4105
|
+
"use": "below the trigger — the usual choice for a control in a header"
|
|
4106
|
+
},
|
|
4107
|
+
"left": {
|
|
4108
|
+
"use": "to the left of the trigger"
|
|
4109
|
+
},
|
|
4110
|
+
"right": {
|
|
4111
|
+
"use": "to the right of the trigger"
|
|
4112
|
+
}
|
|
4113
|
+
},
|
|
4114
|
+
"size": {
|
|
4115
|
+
"sm": {
|
|
4116
|
+
"use": "a couple of actions or one short field"
|
|
4117
|
+
},
|
|
4118
|
+
"md": {
|
|
4119
|
+
"use": "the default"
|
|
4120
|
+
},
|
|
4121
|
+
"lg": {
|
|
4122
|
+
"use": "a small form or a filter panel"
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
},
|
|
4126
|
+
"slots": {
|
|
4127
|
+
"children": {
|
|
4128
|
+
"accepts": "Button",
|
|
4129
|
+
"note": "the trigger. Must forward both a ref and DOM props — every component in this library does"
|
|
4130
|
+
},
|
|
4131
|
+
"content": {
|
|
4132
|
+
"accepts": "any"
|
|
4133
|
+
},
|
|
4134
|
+
"title": {
|
|
4135
|
+
"accepts": "text",
|
|
4136
|
+
"note": "also gives the dialog its accessible name"
|
|
4137
|
+
},
|
|
4138
|
+
"footer": {
|
|
4139
|
+
"accepts": "Button",
|
|
4140
|
+
"note": "action row, right-aligned"
|
|
4141
|
+
}
|
|
4142
|
+
},
|
|
4143
|
+
"a11y": {
|
|
4144
|
+
"role": "dialog",
|
|
4145
|
+
"guarantees": [
|
|
4146
|
+
"focus is contained while open, Escape and outside clicks close it, and the rest of the page is hidden from assistive tech",
|
|
4147
|
+
"`placement` is a preference, not a guarantee — it flips when there is no room, and the arrow follows"
|
|
4148
|
+
],
|
|
4149
|
+
"requires": [
|
|
4150
|
+
"`title`, or `aria-label` on the popover — a dialog with no name announces nothing about what opened"
|
|
4151
|
+
],
|
|
4152
|
+
"notes": [
|
|
4153
|
+
"`placement` names where the POPOVER sits, not where the arrow points."
|
|
4154
|
+
]
|
|
4155
|
+
},
|
|
4156
|
+
"antiPatterns": [
|
|
4157
|
+
{
|
|
4158
|
+
"dont": "a function-component trigger that drops its ref",
|
|
4159
|
+
"why": "it renders, but the popover has nothing to position against"
|
|
4160
|
+
},
|
|
4161
|
+
{
|
|
4162
|
+
"dont": "using a Popover where a Modal belongs",
|
|
4163
|
+
"why": "a popover closes on an outside click; a task that must be finished should not be dismissible by a stray click"
|
|
4164
|
+
},
|
|
4165
|
+
{
|
|
4166
|
+
"dont": "nesting a Popover inside another Popover",
|
|
4167
|
+
"why": "two overlapping focus containments, and dismissing the outer one takes the inner one with it"
|
|
4168
|
+
}
|
|
4169
|
+
],
|
|
3207
4170
|
"stylesheet": "src/styles/popover.css",
|
|
3208
4171
|
"tokens": [
|
|
3209
4172
|
"--border-subtle",
|
|
@@ -3327,6 +4290,83 @@
|
|
|
3327
4290
|
"source": "src/components/Radio.tsx",
|
|
3328
4291
|
"propsType": "RadioProps",
|
|
3329
4292
|
"import": "import { Radio } from 'ionbase-ui';",
|
|
4293
|
+
"status": "stable",
|
|
4294
|
+
"summary": "One option inside a RadioGroup. Meaningless on its own — the group owns the name and the selected value.",
|
|
4295
|
+
"useWhen": [
|
|
4296
|
+
"a child of RadioGroup"
|
|
4297
|
+
],
|
|
4298
|
+
"useInstead": [
|
|
4299
|
+
{
|
|
4300
|
+
"when": "the user may pick more than one",
|
|
4301
|
+
"use": "Checkbox"
|
|
4302
|
+
},
|
|
4303
|
+
{
|
|
4304
|
+
"when": "there are more than about five options",
|
|
4305
|
+
"use": "Select",
|
|
4306
|
+
"why": "a long radio list costs vertical space no one reads"
|
|
4307
|
+
}
|
|
4308
|
+
],
|
|
4309
|
+
"variants": {
|
|
4310
|
+
"size": {
|
|
4311
|
+
"sm": {
|
|
4312
|
+
"use": "dense forms"
|
|
4313
|
+
},
|
|
4314
|
+
"md": {
|
|
4315
|
+
"use": "the default"
|
|
4316
|
+
},
|
|
4317
|
+
"lg": {
|
|
4318
|
+
"use": "touch targets and prominent choices"
|
|
4319
|
+
}
|
|
4320
|
+
},
|
|
4321
|
+
"intent": {
|
|
4322
|
+
"brand": {
|
|
4323
|
+
"use": "the default"
|
|
4324
|
+
},
|
|
4325
|
+
"neutral": {
|
|
4326
|
+
"use": "a choice with no positive reading — sort order, display mode"
|
|
4327
|
+
},
|
|
4328
|
+
"danger": {
|
|
4329
|
+
"use": "selecting a destructive option"
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
},
|
|
4333
|
+
"slots": {
|
|
4334
|
+
"children": {
|
|
4335
|
+
"accepts": "text",
|
|
4336
|
+
"note": "the option's visible label"
|
|
4337
|
+
}
|
|
4338
|
+
},
|
|
4339
|
+
"a11y": {
|
|
4340
|
+
"role": "radio",
|
|
4341
|
+
"guarantees": [
|
|
4342
|
+
"`name` and the selected value come from the group's context, so no Radio has to repeat them",
|
|
4343
|
+
"`isDisabled` falls back to the group's"
|
|
4344
|
+
],
|
|
4345
|
+
"requires": [
|
|
4346
|
+
"a `value` unique within the group",
|
|
4347
|
+
"a text label in `children`"
|
|
4348
|
+
]
|
|
4349
|
+
},
|
|
4350
|
+
"antiPatterns": [
|
|
4351
|
+
{
|
|
4352
|
+
"dont": "a Radio outside a RadioGroup",
|
|
4353
|
+
"why": "it has no name and no siblings, so it can be selected but never unselected"
|
|
4354
|
+
},
|
|
4355
|
+
{
|
|
4356
|
+
"dont": "wiring `checked`/`onChange` on each Radio",
|
|
4357
|
+
"why": "the group owns selection; per-option wiring is what the context exists to remove"
|
|
4358
|
+
},
|
|
4359
|
+
{
|
|
4360
|
+
"dont": "a group of one radio",
|
|
4361
|
+
"why": "a single radio cannot be cleared once chosen — that is a Checkbox"
|
|
4362
|
+
}
|
|
4363
|
+
],
|
|
4364
|
+
"deprecated": [
|
|
4365
|
+
{
|
|
4366
|
+
"prop": "disabled",
|
|
4367
|
+
"replacement": "isDisabled"
|
|
4368
|
+
}
|
|
4369
|
+
],
|
|
3330
4370
|
"stylesheet": "src/styles/radio.css",
|
|
3331
4371
|
"tokens": [
|
|
3332
4372
|
"--border-disabled",
|
|
@@ -3432,53 +4472,137 @@
|
|
|
3432
4472
|
"propsType": "RadioGroupProps",
|
|
3433
4473
|
"description": "Renders a `<fieldset>` with a `<legend>` rather than a div with\n`role=\"radiogroup\"`. Both are announced correctly, but a fieldset also groups\nthe inputs for form submission and native validation, which the ARIA version\ndoes not.",
|
|
3434
4474
|
"import": "import { RadioGroup } from 'ionbase-ui';",
|
|
3435
|
-
"
|
|
3436
|
-
"
|
|
3437
|
-
|
|
3438
|
-
"
|
|
3439
|
-
"
|
|
3440
|
-
"--border-inverse",
|
|
3441
|
-
"--border-primary-strong",
|
|
3442
|
-
"--border-stronger",
|
|
3443
|
-
"--border-width-default",
|
|
3444
|
-
"--border-width-thick",
|
|
3445
|
-
"--font-family-sans",
|
|
3446
|
-
"--font-weight-medium",
|
|
3447
|
-
"--icon-disabled",
|
|
3448
|
-
"--icon-on-color",
|
|
3449
|
-
"--ion-duration-base",
|
|
3450
|
-
"--ion-ease-out",
|
|
3451
|
-
"--ion-radio-border",
|
|
3452
|
-
"--ion-radio-dot",
|
|
3453
|
-
"--ion-radio-fill",
|
|
3454
|
-
"--ion-radio-font-size",
|
|
3455
|
-
"--ion-radio-gap",
|
|
3456
|
-
"--ion-radio-line-height",
|
|
3457
|
-
"--ion-radio-raised",
|
|
3458
|
-
"--ion-radio-size",
|
|
3459
|
-
"--ion-shadow-raised-flush-lg",
|
|
3460
|
-
"--ion-shadow-raised-flush-sm",
|
|
3461
|
-
"--ion-shadow-raised-flush-xs",
|
|
3462
|
-
"--radius-full",
|
|
3463
|
-
"--spacing-12",
|
|
3464
|
-
"--spacing-16",
|
|
3465
|
-
"--spacing-2",
|
|
3466
|
-
"--spacing-20",
|
|
3467
|
-
"--spacing-24",
|
|
3468
|
-
"--spacing-8",
|
|
3469
|
-
"--surface-default",
|
|
3470
|
-
"--surface-disabled",
|
|
3471
|
-
"--surface-error",
|
|
3472
|
-
"--surface-inverse",
|
|
3473
|
-
"--surface-primary",
|
|
3474
|
-
"--text-disabled",
|
|
3475
|
-
"--text-secondary",
|
|
3476
|
-
"--type-body",
|
|
3477
|
-
"--type-body-line-height",
|
|
3478
|
-
"--type-body-sm",
|
|
3479
|
-
"--type-body-sm-line-height"
|
|
4475
|
+
"status": "stable",
|
|
4476
|
+
"summary": "A <fieldset>/<legend> group of mutually exclusive options. Owns the shared name and the selected value.",
|
|
4477
|
+
"useWhen": [
|
|
4478
|
+
"exactly one choice from a small set, all worth showing at once",
|
|
4479
|
+
"the options need explaining — each one can carry its own label text"
|
|
3480
4480
|
],
|
|
3481
|
-
"
|
|
4481
|
+
"useInstead": [
|
|
4482
|
+
{
|
|
4483
|
+
"when": "the list is long, or space is tight",
|
|
4484
|
+
"use": "Select"
|
|
4485
|
+
},
|
|
4486
|
+
{
|
|
4487
|
+
"when": "more than one may be picked",
|
|
4488
|
+
"use": "Checkbox"
|
|
4489
|
+
},
|
|
4490
|
+
{
|
|
4491
|
+
"when": "there are exactly two states and the change is immediate",
|
|
4492
|
+
"use": "Toggle"
|
|
4493
|
+
}
|
|
4494
|
+
],
|
|
4495
|
+
"variants": {
|
|
4496
|
+
"size": {
|
|
4497
|
+
"sm": {
|
|
4498
|
+
"use": "dense forms"
|
|
4499
|
+
},
|
|
4500
|
+
"md": {
|
|
4501
|
+
"use": "the default"
|
|
4502
|
+
},
|
|
4503
|
+
"lg": {
|
|
4504
|
+
"use": "touch targets and prominent choices"
|
|
4505
|
+
}
|
|
4506
|
+
},
|
|
4507
|
+
"intent": {
|
|
4508
|
+
"brand": {
|
|
4509
|
+
"use": "the default"
|
|
4510
|
+
},
|
|
4511
|
+
"neutral": {
|
|
4512
|
+
"use": "a choice with no positive reading — sort order, display mode"
|
|
4513
|
+
},
|
|
4514
|
+
"danger": {
|
|
4515
|
+
"use": "a set where the options are destructive"
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
},
|
|
4519
|
+
"slots": {
|
|
4520
|
+
"label": {
|
|
4521
|
+
"accepts": "text",
|
|
4522
|
+
"note": "renders as the <legend> — the question the options answer"
|
|
4523
|
+
},
|
|
4524
|
+
"children": {
|
|
4525
|
+
"accepts": "Radio"
|
|
4526
|
+
}
|
|
4527
|
+
},
|
|
4528
|
+
"a11y": {
|
|
4529
|
+
"role": "group, from <fieldset> and <legend>",
|
|
4530
|
+
"guarantees": [
|
|
4531
|
+
"a real fieldset rather than `role=\"radiogroup\"`: both announce correctly, but the fieldset also groups the inputs for form submission and native validation",
|
|
4532
|
+
"`name` is generated when omitted, so two groups on one page never collide",
|
|
4533
|
+
"`isDisabled` cascades to every Radio that has not set its own"
|
|
4534
|
+
],
|
|
4535
|
+
"requires": [
|
|
4536
|
+
"`label`, or `aria-label` when the question is already visible elsewhere"
|
|
4537
|
+
]
|
|
4538
|
+
},
|
|
4539
|
+
"antiPatterns": [
|
|
4540
|
+
{
|
|
4541
|
+
"dont": "an unlabelled group",
|
|
4542
|
+
"why": "the options are announced with no question attached"
|
|
4543
|
+
},
|
|
4544
|
+
{
|
|
4545
|
+
"dont": "passing both `value` and `defaultValue`",
|
|
4546
|
+
"why": "controlled and uncontrolled at once; the controlled value wins and `defaultValue` silently does nothing"
|
|
4547
|
+
},
|
|
4548
|
+
{
|
|
4549
|
+
"dont": "no option selected by default with no \"none\" option",
|
|
4550
|
+
"why": "a radio group cannot be returned to empty once touched"
|
|
4551
|
+
}
|
|
4552
|
+
],
|
|
4553
|
+
"deprecated": [
|
|
4554
|
+
{
|
|
4555
|
+
"prop": "disabled",
|
|
4556
|
+
"replacement": "isDisabled"
|
|
4557
|
+
}
|
|
4558
|
+
],
|
|
4559
|
+
"stylesheet": "src/styles/radio.css",
|
|
4560
|
+
"tokens": [
|
|
4561
|
+
"--border-disabled",
|
|
4562
|
+
"--border-error-strong",
|
|
4563
|
+
"--border-focus",
|
|
4564
|
+
"--border-inverse",
|
|
4565
|
+
"--border-primary-strong",
|
|
4566
|
+
"--border-stronger",
|
|
4567
|
+
"--border-width-default",
|
|
4568
|
+
"--border-width-thick",
|
|
4569
|
+
"--font-family-sans",
|
|
4570
|
+
"--font-weight-medium",
|
|
4571
|
+
"--icon-disabled",
|
|
4572
|
+
"--icon-on-color",
|
|
4573
|
+
"--ion-duration-base",
|
|
4574
|
+
"--ion-ease-out",
|
|
4575
|
+
"--ion-radio-border",
|
|
4576
|
+
"--ion-radio-dot",
|
|
4577
|
+
"--ion-radio-fill",
|
|
4578
|
+
"--ion-radio-font-size",
|
|
4579
|
+
"--ion-radio-gap",
|
|
4580
|
+
"--ion-radio-line-height",
|
|
4581
|
+
"--ion-radio-raised",
|
|
4582
|
+
"--ion-radio-size",
|
|
4583
|
+
"--ion-shadow-raised-flush-lg",
|
|
4584
|
+
"--ion-shadow-raised-flush-sm",
|
|
4585
|
+
"--ion-shadow-raised-flush-xs",
|
|
4586
|
+
"--radius-full",
|
|
4587
|
+
"--spacing-12",
|
|
4588
|
+
"--spacing-16",
|
|
4589
|
+
"--spacing-2",
|
|
4590
|
+
"--spacing-20",
|
|
4591
|
+
"--spacing-24",
|
|
4592
|
+
"--spacing-8",
|
|
4593
|
+
"--surface-default",
|
|
4594
|
+
"--surface-disabled",
|
|
4595
|
+
"--surface-error",
|
|
4596
|
+
"--surface-inverse",
|
|
4597
|
+
"--surface-primary",
|
|
4598
|
+
"--text-disabled",
|
|
4599
|
+
"--text-secondary",
|
|
4600
|
+
"--type-body",
|
|
4601
|
+
"--type-body-line-height",
|
|
4602
|
+
"--type-body-sm",
|
|
4603
|
+
"--type-body-sm-line-height"
|
|
4604
|
+
],
|
|
4605
|
+
"props": {
|
|
3482
4606
|
"name": {
|
|
3483
4607
|
"type": "string | undefined",
|
|
3484
4608
|
"required": false,
|
|
@@ -3558,6 +4682,47 @@
|
|
|
3558
4682
|
"propsType": "ScrollProgressProps",
|
|
3559
4683
|
"description": "The compact rail is the disclosure trigger. It is a real `<button>`, so a\nkeyboard user reaches it by Tab and opens it the same way as any button —\nEnter or Space — no `:focus-within` trick required; a mouse user can also\njust hover it. Built as a WAI-ARIA Disclosure (`aria-expanded` +\n`aria-controls`) rather than a menu: see the CSS header for why\n`role=\"menu\"` is deliberately not used here.\n\nCloses on outside pointerdown and on Escape — the minimum a disclosure\nneeds to not trap the page once opened, not a full popover/focus-trap\nimplementation.",
|
|
3560
4684
|
"import": "import { ScrollProgress } from 'ionbase-ui';",
|
|
4685
|
+
"status": "stable",
|
|
4686
|
+
"summary": "A compact progress rail that expands into a section list. A disclosure, not a menu — and it computes nothing itself.",
|
|
4687
|
+
"useWhen": [
|
|
4688
|
+
"a long document or page needs an in-page section index with reading progress",
|
|
4689
|
+
"the caller already knows the scroll fraction and the active section"
|
|
4690
|
+
],
|
|
4691
|
+
"useInstead": [
|
|
4692
|
+
{
|
|
4693
|
+
"when": "the sections are peer views rather than positions in one document",
|
|
4694
|
+
"use": "Tabs"
|
|
4695
|
+
},
|
|
4696
|
+
{
|
|
4697
|
+
"when": "it is site navigation rather than in-page position",
|
|
4698
|
+
"use": "NavItem"
|
|
4699
|
+
}
|
|
4700
|
+
],
|
|
4701
|
+
"a11y": {
|
|
4702
|
+
"role": "disclosure — a real <button> with aria-expanded and aria-controls",
|
|
4703
|
+
"guarantees": [
|
|
4704
|
+
"the rail is reachable by Tab and opens with Enter or Space, not just hover",
|
|
4705
|
+
"closes on outside pointerdown and on Escape"
|
|
4706
|
+
],
|
|
4707
|
+
"requires": [
|
|
4708
|
+
"`progress` as 0–100 and `activeId` — the component has no opinion on where they come from",
|
|
4709
|
+
"handling `onSelect` yourself: it reports the choice and does not scroll anywhere"
|
|
4710
|
+
],
|
|
4711
|
+
"notes": [
|
|
4712
|
+
"Deliberately not `role=\"menu\"` — see the stylesheet header. It is a disclosure with a list inside it, not a menu with roving focus.",
|
|
4713
|
+
"Not a focus trap: it is the minimum a disclosure needs to not trap the page once opened."
|
|
4714
|
+
]
|
|
4715
|
+
},
|
|
4716
|
+
"antiPatterns": [
|
|
4717
|
+
{
|
|
4718
|
+
"dont": "expecting it to scroll the page when a section is chosen",
|
|
4719
|
+
"why": "it reports the choice, matching Menu — what \"select this section\" means is the page's decision"
|
|
4720
|
+
},
|
|
4721
|
+
{
|
|
4722
|
+
"dont": "deriving `progress` from a scroll listener without throttling",
|
|
4723
|
+
"why": "this re-renders the rail on every scroll frame"
|
|
4724
|
+
}
|
|
4725
|
+
],
|
|
3561
4726
|
"stylesheet": "src/styles/scroll-progress.css",
|
|
3562
4727
|
"tokens": [
|
|
3563
4728
|
"--border-default",
|
|
@@ -3815,6 +4980,48 @@
|
|
|
3815
4980
|
"source": "../../node_modules/.pnpm/react-stately@3.48.0_react@19.2.8/node_modules/react-stately/dist/types/src/collections/Item.d.ts",
|
|
3816
4981
|
"propsType": null,
|
|
3817
4982
|
"import": "import { TabItem } from 'ionbase-ui';",
|
|
4983
|
+
"status": "stable",
|
|
4984
|
+
"summary": "One tab and its panel. Re-exported from react-stately's collection Item — `title` is the tab label, children are the panel.",
|
|
4985
|
+
"useWhen": [
|
|
4986
|
+
"a child of Tabs — it is not usable anywhere else"
|
|
4987
|
+
],
|
|
4988
|
+
"useInstead": [
|
|
4989
|
+
{
|
|
4990
|
+
"when": "it is a row in a dropdown list",
|
|
4991
|
+
"use": "MenuItem"
|
|
4992
|
+
},
|
|
4993
|
+
{
|
|
4994
|
+
"when": "it is a navigation destination",
|
|
4995
|
+
"use": "NavItem"
|
|
4996
|
+
}
|
|
4997
|
+
],
|
|
4998
|
+
"composition": {
|
|
4999
|
+
"order": [
|
|
5000
|
+
"Tabs",
|
|
5001
|
+
"TabItem"
|
|
5002
|
+
],
|
|
5003
|
+
"note": "A collection item, not a rendered component: Tabs reads it to build the collection, so it never appears in the DOM itself. That is why it exposes no props of its own here — `key` and `title` are the collection's API.",
|
|
5004
|
+
"example": "<TabItem key=\"billing\" title=\"Billing\">Billing panel content</TabItem>"
|
|
5005
|
+
},
|
|
5006
|
+
"a11y": {
|
|
5007
|
+
"guarantees": [
|
|
5008
|
+
"the tab/panel roles, ids and aria-controls pairing are supplied by Tabs, not by this element"
|
|
5009
|
+
],
|
|
5010
|
+
"requires": [
|
|
5011
|
+
"a stable `key` — it identifies the tab across renders and is what selection is expressed in",
|
|
5012
|
+
"a `title` that reads as a label on its own"
|
|
5013
|
+
]
|
|
5014
|
+
},
|
|
5015
|
+
"antiPatterns": [
|
|
5016
|
+
{
|
|
5017
|
+
"dont": "wrapping TabItem in a <div> or a fragment inside Tabs",
|
|
5018
|
+
"why": "Tabs reads the collection from its direct children; a wrapper makes the item invisible to it"
|
|
5019
|
+
},
|
|
5020
|
+
{
|
|
5021
|
+
"dont": "conditionally rendering TabItems with array indexes as keys",
|
|
5022
|
+
"why": "selection is tracked by key, so the selected tab silently becomes a different tab"
|
|
5023
|
+
}
|
|
5024
|
+
],
|
|
3818
5025
|
"stylesheet": null,
|
|
3819
5026
|
"tokens": [],
|
|
3820
5027
|
"props": {},
|
|
@@ -3965,6 +5172,42 @@
|
|
|
3965
5172
|
"source": "src/components/Table.tsx",
|
|
3966
5173
|
"propsType": null,
|
|
3967
5174
|
"import": "import { TableBody } from 'ionbase-ui';",
|
|
5175
|
+
"status": "stable",
|
|
5176
|
+
"summary": "The <tbody> section. Takes no props — it places the data rows and gives their cells body context.",
|
|
5177
|
+
"useWhen": [
|
|
5178
|
+
"every Table — the data rows go here"
|
|
5179
|
+
],
|
|
5180
|
+
"useInstead": [
|
|
5181
|
+
{
|
|
5182
|
+
"when": "the row names the columns",
|
|
5183
|
+
"use": "TableHead"
|
|
5184
|
+
}
|
|
5185
|
+
],
|
|
5186
|
+
"composition": {
|
|
5187
|
+
"order": [
|
|
5188
|
+
"Table",
|
|
5189
|
+
"TableBody",
|
|
5190
|
+
"TableRow",
|
|
5191
|
+
"TableCell"
|
|
5192
|
+
],
|
|
5193
|
+
"note": "Zebra striping is read from each row's position inside this section rather than a prop repeated on every TableRow.",
|
|
5194
|
+
"example": "<TableBody><TableRow><TableCell>Ada</TableCell></TableRow></TableBody>"
|
|
5195
|
+
},
|
|
5196
|
+
"a11y": {
|
|
5197
|
+
"guarantees": [
|
|
5198
|
+
"cells inside it default to <td>, and `scope` is inferred as `row` when a cell is marked `header`"
|
|
5199
|
+
]
|
|
5200
|
+
},
|
|
5201
|
+
"antiPatterns": [
|
|
5202
|
+
{
|
|
5203
|
+
"dont": "rendering rows directly under Table with no TableBody",
|
|
5204
|
+
"why": "the browser inserts a tbody anyway, but the head-or-body context does not exist, so cells cannot infer their scope"
|
|
5205
|
+
},
|
|
5206
|
+
{
|
|
5207
|
+
"dont": "an empty TableBody with no empty state",
|
|
5208
|
+
"why": "an empty table looks like a broken one — Table has no empty state of its own, so it is the caller's"
|
|
5209
|
+
}
|
|
5210
|
+
],
|
|
3968
5211
|
"stylesheet": "src/styles/table.css",
|
|
3969
5212
|
"tokens": [
|
|
3970
5213
|
"--border-default",
|
|
@@ -4009,6 +5252,89 @@
|
|
|
4009
5252
|
"propsType": "TableCellProps",
|
|
4010
5253
|
"description": "One component covers Figma's `Table Cell` + `Cell Text`: the two are never\nused apart in the design (`Table Cell` always wraps exactly one `Cell\nText`), so splitting them into two exported components would only add API\nsurface for a composition nothing ever varies independently.\n\n`header` decides `<th>` vs `<td>` directly rather than a `type` prop that\ncould disagree with where the cell actually sits — a `<th>` rendered inside\n`<tbody>` is still a header cell to the browser and to CSS either way.",
|
|
4011
5254
|
"import": "import { TableCell } from 'ionbase-ui';",
|
|
5255
|
+
"status": "stable",
|
|
5256
|
+
"summary": "One cell, covering Figma's Table Cell and Cell Text together. `header` decides <th> versus <td>.",
|
|
5257
|
+
"useWhen": [
|
|
5258
|
+
"every cell in a Table, header or data"
|
|
5259
|
+
],
|
|
5260
|
+
"useInstead": [
|
|
5261
|
+
{
|
|
5262
|
+
"when": "the content is a status label",
|
|
5263
|
+
"use": "Badge",
|
|
5264
|
+
"why": "put it inside the cell — the cell is the container, not the label"
|
|
5265
|
+
}
|
|
5266
|
+
],
|
|
5267
|
+
"variants": {
|
|
5268
|
+
"scope": {
|
|
5269
|
+
"col": {
|
|
5270
|
+
"use": "a column header — inferred inside TableHead"
|
|
5271
|
+
},
|
|
5272
|
+
"row": {
|
|
5273
|
+
"use": "a row header, e.g. the name column — inferred for a `header` cell in TableBody"
|
|
5274
|
+
},
|
|
5275
|
+
"colgroup": {
|
|
5276
|
+
"use": "a header spanning several columns, with colspan"
|
|
5277
|
+
},
|
|
5278
|
+
"rowgroup": {
|
|
5279
|
+
"use": "a header spanning several rows, with rowspan"
|
|
5280
|
+
}
|
|
5281
|
+
},
|
|
5282
|
+
"align": {
|
|
5283
|
+
"leading": {
|
|
5284
|
+
"use": "the default — text, names, identifiers"
|
|
5285
|
+
},
|
|
5286
|
+
"trailing": {
|
|
5287
|
+
"use": "numbers, currency and dates, so digits line up column-wise"
|
|
5288
|
+
},
|
|
5289
|
+
"center": {
|
|
5290
|
+
"use": "icons, short status marks, checkboxes"
|
|
5291
|
+
}
|
|
5292
|
+
},
|
|
5293
|
+
"variant": {
|
|
5294
|
+
"default": {
|
|
5295
|
+
"use": "the default — body text and icon colours"
|
|
5296
|
+
},
|
|
5297
|
+
"link": {
|
|
5298
|
+
"use": "recolours the content to `text/link` / `icon/primary` for a cell whose content navigates"
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
},
|
|
5302
|
+
"slots": {
|
|
5303
|
+
"icon": {
|
|
5304
|
+
"accepts": "Icon",
|
|
5305
|
+
"note": "leading icon; omit its `label`"
|
|
5306
|
+
},
|
|
5307
|
+
"trailingIcon": {
|
|
5308
|
+
"accepts": "Icon",
|
|
5309
|
+
"note": "trailing icon; same rule"
|
|
5310
|
+
},
|
|
5311
|
+
"children": {
|
|
5312
|
+
"accepts": "any"
|
|
5313
|
+
}
|
|
5314
|
+
},
|
|
5315
|
+
"a11y": {
|
|
5316
|
+
"guarantees": [
|
|
5317
|
+
"`scope` is inferred when omitted — `col` in TableHead, `row` in TableBody — and can be set explicitly when the inference is wrong",
|
|
5318
|
+
"`header` decides the element directly rather than a `type` prop that could disagree with where the cell sits"
|
|
5319
|
+
],
|
|
5320
|
+
"requires": [
|
|
5321
|
+
"`scope=\"col\"` on header cells — inferred inside TableHead, but explicit for a header rendered outside it"
|
|
5322
|
+
]
|
|
5323
|
+
},
|
|
5324
|
+
"antiPatterns": [
|
|
5325
|
+
{
|
|
5326
|
+
"dont": "`variant=\"link\"` on a cell with no link in it",
|
|
5327
|
+
"why": "link-coloured text that does nothing is the clearest possible false affordance"
|
|
5328
|
+
},
|
|
5329
|
+
{
|
|
5330
|
+
"dont": "leading-aligning a numeric column",
|
|
5331
|
+
"why": "digits stop lining up, so the column can no longer be scanned for magnitude"
|
|
5332
|
+
},
|
|
5333
|
+
{
|
|
5334
|
+
"dont": "`showDivider` on every cell",
|
|
5335
|
+
"why": "it is a column rule, not a row rule — on every cell it becomes a grid"
|
|
5336
|
+
}
|
|
5337
|
+
],
|
|
4012
5338
|
"stylesheet": "src/styles/table.css",
|
|
4013
5339
|
"tokens": [
|
|
4014
5340
|
"--border-default",
|
|
@@ -4113,6 +5439,45 @@
|
|
|
4113
5439
|
"source": "src/components/Table.tsx",
|
|
4114
5440
|
"propsType": null,
|
|
4115
5441
|
"import": "import { TableHead } from 'ionbase-ui';",
|
|
5442
|
+
"status": "stable",
|
|
5443
|
+
"summary": "The <thead> section. Takes no props — it places the header row and gives its cells their head-or-body context.",
|
|
5444
|
+
"useWhen": [
|
|
5445
|
+
"every Table with column headers, which is every data table"
|
|
5446
|
+
],
|
|
5447
|
+
"useInstead": [
|
|
5448
|
+
{
|
|
5449
|
+
"when": "the rows are data",
|
|
5450
|
+
"use": "TableBody"
|
|
5451
|
+
}
|
|
5452
|
+
],
|
|
5453
|
+
"composition": {
|
|
5454
|
+
"order": [
|
|
5455
|
+
"Table",
|
|
5456
|
+
"TableHead",
|
|
5457
|
+
"TableRow",
|
|
5458
|
+
"TableCell"
|
|
5459
|
+
],
|
|
5460
|
+
"note": "Exists so TableRow and TableCell can tell head from body through context, rather than the caller repeating a prop that has to stay in sync with where the row actually sits.",
|
|
5461
|
+
"example": "<TableHead><TableRow><TableCell scope=\"col\">Name</TableCell></TableRow></TableHead>"
|
|
5462
|
+
},
|
|
5463
|
+
"a11y": {
|
|
5464
|
+
"guarantees": [
|
|
5465
|
+
"cells inside it default to <th scope=\"col\">, and a row-selection cell becomes the select-all header"
|
|
5466
|
+
],
|
|
5467
|
+
"requires": [
|
|
5468
|
+
"a header row for every data table — without one, no data cell has a column to be associated with"
|
|
5469
|
+
]
|
|
5470
|
+
},
|
|
5471
|
+
"antiPatterns": [
|
|
5472
|
+
{
|
|
5473
|
+
"dont": "a data table with no TableHead",
|
|
5474
|
+
"why": "a screen-reader user hears values with no field names"
|
|
5475
|
+
},
|
|
5476
|
+
{
|
|
5477
|
+
"dont": "styling a first TableBody row to look like a header",
|
|
5478
|
+
"why": "it looks like a header and announces as data"
|
|
5479
|
+
}
|
|
5480
|
+
],
|
|
4116
5481
|
"stylesheet": "src/styles/table.css",
|
|
4117
5482
|
"tokens": [
|
|
4118
5483
|
"--border-default",
|
|
@@ -4157,6 +5522,51 @@
|
|
|
4157
5522
|
"propsType": "TableRowProps",
|
|
4158
5523
|
"description": "A plain `<tr>`. Hover is CSS-only (`:hover` plus a `data-hovered` escape\nhatch, matching the rest of the system) rather than React Aria's\n`useHover`: a row is not itself an interactive element — nothing about it\ntakes focus or fires a click — so there is no keyboard-vs-pointer\ndistinction to track. A clickable row is a link or button inside a cell,\nthe same accessible pattern Menu and Table Cell's own link variant use;\nnesting an interactive role on `<tr>` itself is not valid HTML.",
|
|
4159
5524
|
"import": "import { TableRow } from 'ionbase-ui';",
|
|
5525
|
+
"status": "stable",
|
|
5526
|
+
"summary": "A plain <tr>. Hover is CSS-only; a row is not itself interactive.",
|
|
5527
|
+
"useWhen": [
|
|
5528
|
+
"every row, in either TableHead or TableBody"
|
|
5529
|
+
],
|
|
5530
|
+
"useInstead": [
|
|
5531
|
+
{
|
|
5532
|
+
"when": "the whole row should be clickable",
|
|
5533
|
+
"use": "Link",
|
|
5534
|
+
"why": "put the link or Button in a cell — nesting an interactive role on <tr> is not valid HTML"
|
|
5535
|
+
}
|
|
5536
|
+
],
|
|
5537
|
+
"slots": {
|
|
5538
|
+
"selection": {
|
|
5539
|
+
"accepts": "Checkbox props",
|
|
5540
|
+
"note": "renders a leading Checkbox cell — a <th scope=\"col\"> select-all inside TableHead, a <td> inside TableBody. Takes the checkbox's own props rather than a boolean, since a selectable row needs `checked`/`onChange` wiring"
|
|
5541
|
+
}
|
|
5542
|
+
},
|
|
5543
|
+
"a11y": {
|
|
5544
|
+
"guarantees": [
|
|
5545
|
+
"hover comes from CSS `:hover` plus a `data-hovered` escape hatch, not React Aria — a row takes no focus and fires no click, so there is no keyboard-versus-pointer distinction to track",
|
|
5546
|
+
"the selection cell's element follows the section it sits in"
|
|
5547
|
+
],
|
|
5548
|
+
"requires": [
|
|
5549
|
+
"every row-selection Checkbox to be labelled — those boxes have no visible label by design, so see Checkbox's own contract",
|
|
5550
|
+
"`isSelected` alongside a checked selection checkbox, so the row's state is not carried by colour alone"
|
|
5551
|
+
],
|
|
5552
|
+
"notes": [
|
|
5553
|
+
"TableRow itself is never named — a <tr> is not an interactive element and takes no accessible name. The naming requirement above belongs to the Checkbox inside it."
|
|
5554
|
+
]
|
|
5555
|
+
},
|
|
5556
|
+
"antiPatterns": [
|
|
5557
|
+
{
|
|
5558
|
+
"dont": "`onClick` on TableRow",
|
|
5559
|
+
"why": "it takes no focus, so a keyboard user can never fire it"
|
|
5560
|
+
},
|
|
5561
|
+
{
|
|
5562
|
+
"dont": "`selection={{ checked }}` with no `onChange`",
|
|
5563
|
+
"why": "a checkbox that cannot be changed reads as broken rather than as disabled"
|
|
5564
|
+
},
|
|
5565
|
+
{
|
|
5566
|
+
"dont": "`isSelected` without a selection checkbox",
|
|
5567
|
+
"why": "the row is tinted with nothing announcing why"
|
|
5568
|
+
}
|
|
5569
|
+
],
|
|
4160
5570
|
"stylesheet": "src/styles/table.css",
|
|
4161
5571
|
"tokens": [
|
|
4162
5572
|
"--border-default",
|
|
@@ -4212,6 +5622,91 @@
|
|
|
4212
5622
|
"source": "src/components/Tabs.tsx",
|
|
4213
5623
|
"propsType": "TabsProps",
|
|
4214
5624
|
"import": "import { Tabs } from 'ionbase-ui';",
|
|
5625
|
+
"status": "stable",
|
|
5626
|
+
"summary": "A tab list and its panels, built on react-aria's tab collection. Children are TabItem.",
|
|
5627
|
+
"useWhen": [
|
|
5628
|
+
"one region of the page shows one of several peer views, and only one at a time",
|
|
5629
|
+
"the views are peers — no ordering, no progression between them"
|
|
5630
|
+
],
|
|
5631
|
+
"useInstead": [
|
|
5632
|
+
{
|
|
5633
|
+
"when": "the steps must be completed in order",
|
|
5634
|
+
"use": "Button",
|
|
5635
|
+
"why": "tabs imply peers you may visit in any order; a wizard is a sequence"
|
|
5636
|
+
},
|
|
5637
|
+
{
|
|
5638
|
+
"when": "the sections should all be readable at once, or printable",
|
|
5639
|
+
"use": "Divider",
|
|
5640
|
+
"why": "tabs hide everything but the active panel, including from Ctrl-F"
|
|
5641
|
+
},
|
|
5642
|
+
{
|
|
5643
|
+
"when": "it is navigation between pages rather than panels within one",
|
|
5644
|
+
"use": "NavItem"
|
|
5645
|
+
}
|
|
5646
|
+
],
|
|
5647
|
+
"composition": {
|
|
5648
|
+
"order": [
|
|
5649
|
+
"Tabs",
|
|
5650
|
+
"TabItem"
|
|
5651
|
+
],
|
|
5652
|
+
"note": "TabItem comes from react-stately's collection Item — it takes no props of its own beyond `title` (the tab label) and `key`; its children are the panel.",
|
|
5653
|
+
"example": "<Tabs aria-label=\"Account\"><TabItem key=\"profile\" title=\"Profile\">…</TabItem><TabItem key=\"billing\" title=\"Billing\">…</TabItem></Tabs>"
|
|
5654
|
+
},
|
|
5655
|
+
"variants": {
|
|
5656
|
+
"type": {
|
|
5657
|
+
"pill": {
|
|
5658
|
+
"use": "the default — a filled track, for switching views inside a panel or card"
|
|
5659
|
+
},
|
|
5660
|
+
"underline": {
|
|
5661
|
+
"use": "a rule under the active tab, for page-level sections"
|
|
5662
|
+
}
|
|
5663
|
+
},
|
|
5664
|
+
"size": {
|
|
5665
|
+
"sm": {
|
|
5666
|
+
"use": "dense panels and toolbars"
|
|
5667
|
+
},
|
|
5668
|
+
"md": {
|
|
5669
|
+
"use": "the default"
|
|
5670
|
+
},
|
|
5671
|
+
"lg": {
|
|
5672
|
+
"use": "page-level section switching"
|
|
5673
|
+
}
|
|
5674
|
+
},
|
|
5675
|
+
"orientation": {
|
|
5676
|
+
"horizontal": {
|
|
5677
|
+
"use": "the default"
|
|
5678
|
+
},
|
|
5679
|
+
"vertical": {
|
|
5680
|
+
"use": "a stacked track with up/down arrow keys",
|
|
5681
|
+
"caveat": "keyboard and ARIA are complete, but the decoration is not — the underline rule and the pill track's padding are still written for the horizontal axis. Figma has not specified the vertical drawing"
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
},
|
|
5685
|
+
"a11y": {
|
|
5686
|
+
"role": "tablist / tab / tabpanel",
|
|
5687
|
+
"guarantees": [
|
|
5688
|
+
"roving tabindex, arrow-key navigation and the aria-controls/aria-labelledby pairing between each tab and its panel come from react-aria",
|
|
5689
|
+
"the focus ring shows for keyboard focus only, and hover does not latch on touch"
|
|
5690
|
+
],
|
|
5691
|
+
"requires": [
|
|
5692
|
+
"`aria-label` or `aria-labelledby` on Tabs — the tab list is a named region",
|
|
5693
|
+
"a stable `key` on every TabItem"
|
|
5694
|
+
]
|
|
5695
|
+
},
|
|
5696
|
+
"antiPatterns": [
|
|
5697
|
+
{
|
|
5698
|
+
"dont": "tabs whose panels the user must read in order",
|
|
5699
|
+
"why": "the role announces peers, and nothing stops a user landing on the last one first"
|
|
5700
|
+
},
|
|
5701
|
+
{
|
|
5702
|
+
"dont": "hiding required form fields behind a tab",
|
|
5703
|
+
"why": "a validation error in an inactive panel is invisible; the user is told the form is invalid with nothing on screen to fix"
|
|
5704
|
+
},
|
|
5705
|
+
{
|
|
5706
|
+
"dont": "one tab",
|
|
5707
|
+
"why": "a tablist with a single tab is chrome with no choice in it"
|
|
5708
|
+
}
|
|
5709
|
+
],
|
|
4215
5710
|
"stylesheet": "src/styles/tabs.css",
|
|
4216
5711
|
"tokens": [
|
|
4217
5712
|
"--border-default",
|
|
@@ -4399,6 +5894,87 @@
|
|
|
4399
5894
|
"propsType": "ToastProps",
|
|
4400
5895
|
"description": "Toast — Figma `Toast` (820:1655).\n\nNeutral chrome on `surface/raised`; the intent is carried by the icon alone.\nA tinted panel floating over unknown content competes with whatever is\nbehind it, and makes Toast and Alert indistinguishable at a glance.\n\nAUTO-DISMISS PAUSES ON HOVER AND FOCUS. A toast that keeps counting down\nwhile being read or while its action has keyboard focus takes the action\naway mid-reach — WCAG 2.2.1 asks for exactly this. The timer restarts rather\nthan resumes, which is the forgiving direction.",
|
|
4401
5896
|
"import": "import { Toast } from 'ionbase-ui';",
|
|
5897
|
+
"status": "stable",
|
|
5898
|
+
"summary": "A transient notification on neutral chrome. Intent is carried by the icon, not by a tinted panel.",
|
|
5899
|
+
"useWhen": [
|
|
5900
|
+
"confirming that something the user just did succeeded or failed",
|
|
5901
|
+
"the message is short, transient, and not required to complete a task"
|
|
5902
|
+
],
|
|
5903
|
+
"useInstead": [
|
|
5904
|
+
{
|
|
5905
|
+
"when": "the message must stay until the condition is resolved",
|
|
5906
|
+
"use": "Alert",
|
|
5907
|
+
"why": "a toast disappears, and anything the user must act on should not"
|
|
5908
|
+
},
|
|
5909
|
+
{
|
|
5910
|
+
"when": "the message belongs to one field",
|
|
5911
|
+
"use": "Input",
|
|
5912
|
+
"why": "Input's error text sits with the field it is about"
|
|
5913
|
+
},
|
|
5914
|
+
{
|
|
5915
|
+
"when": "you need a decision from the user",
|
|
5916
|
+
"use": "Modal",
|
|
5917
|
+
"why": "a toast offers one action and takes it away on a timer"
|
|
5918
|
+
}
|
|
5919
|
+
],
|
|
5920
|
+
"variants": {
|
|
5921
|
+
"intent": {
|
|
5922
|
+
"neutral": {
|
|
5923
|
+
"use": "a plain fact with no status attached"
|
|
5924
|
+
},
|
|
5925
|
+
"primary": {
|
|
5926
|
+
"use": "brand-flavoured confirmations"
|
|
5927
|
+
},
|
|
5928
|
+
"success": {
|
|
5929
|
+
"use": "the action completed"
|
|
5930
|
+
},
|
|
5931
|
+
"warning": {
|
|
5932
|
+
"use": "it completed with a caveat worth knowing"
|
|
5933
|
+
},
|
|
5934
|
+
"error": {
|
|
5935
|
+
"use": "it failed — pair with a way to retry"
|
|
5936
|
+
},
|
|
5937
|
+
"information": {
|
|
5938
|
+
"use": "the default — a neutral update"
|
|
5939
|
+
}
|
|
5940
|
+
}
|
|
5941
|
+
},
|
|
5942
|
+
"slots": {
|
|
5943
|
+
"title": {
|
|
5944
|
+
"accepts": "text"
|
|
5945
|
+
},
|
|
5946
|
+
"message": {
|
|
5947
|
+
"accepts": "text"
|
|
5948
|
+
},
|
|
5949
|
+
"action": {
|
|
5950
|
+
"accepts": "{ label, onPress }",
|
|
5951
|
+
"note": "exactly one action. A second choice belongs in a Modal, which will still be there"
|
|
5952
|
+
}
|
|
5953
|
+
},
|
|
5954
|
+
"a11y": {
|
|
5955
|
+
"guarantees": [
|
|
5956
|
+
"auto-dismiss pauses on hover and on focus, and restarts rather than resumes — WCAG 2.2.1, and the forgiving direction",
|
|
5957
|
+
"announcement comes from ToastProvider's live region, which is always present rather than appearing with its content"
|
|
5958
|
+
],
|
|
5959
|
+
"requires": [
|
|
5960
|
+
"rendering inside a ToastProvider — a Toast on its own is not in a live region",
|
|
5961
|
+
"`duration={null}` when the message carries an action the user genuinely needs"
|
|
5962
|
+
]
|
|
5963
|
+
},
|
|
5964
|
+
"antiPatterns": [
|
|
5965
|
+
{
|
|
5966
|
+
"dont": "a toast for an error the user must fix",
|
|
5967
|
+
"why": "it vanishes; the fix does not"
|
|
5968
|
+
},
|
|
5969
|
+
{
|
|
5970
|
+
"dont": "two actions in one toast",
|
|
5971
|
+
"why": "a timed choice is not a choice"
|
|
5972
|
+
},
|
|
5973
|
+
{
|
|
5974
|
+
"dont": "stacking several toasts for one operation",
|
|
5975
|
+
"why": "the stack covers the page and the last one is the only one read"
|
|
5976
|
+
}
|
|
5977
|
+
],
|
|
4402
5978
|
"stylesheet": "src/styles/toast.css",
|
|
4403
5979
|
"tokens": [
|
|
4404
5980
|
"--border-subtle",
|
|
@@ -4504,6 +6080,63 @@
|
|
|
4504
6080
|
"propsType": "ToastProviderProps",
|
|
4505
6081
|
"description": "Renders the queue and supplies `useToast`.\n\nThe live region is the CONTAINER, declared once and always present. A region\nthat appears at the same moment as its content is not reliably announced —\nassistive tech has to be watching the node before the text lands in it.",
|
|
4506
6082
|
"import": "import { ToastProvider } from 'ionbase-ui';",
|
|
6083
|
+
"status": "stable",
|
|
6084
|
+
"summary": "Renders the toast queue and supplies the `useToast` hook. Mount once, near the app root.",
|
|
6085
|
+
"useWhen": [
|
|
6086
|
+
"the app shows toasts anywhere — this is the only way to get `useToast`"
|
|
6087
|
+
],
|
|
6088
|
+
"useInstead": [
|
|
6089
|
+
{
|
|
6090
|
+
"when": "you only need one persistent in-page message",
|
|
6091
|
+
"use": "Alert",
|
|
6092
|
+
"why": "no provider, no queue, and it stays put"
|
|
6093
|
+
}
|
|
6094
|
+
],
|
|
6095
|
+
"variants": {
|
|
6096
|
+
"placement": {
|
|
6097
|
+
"top-left": {
|
|
6098
|
+
"use": "rare — for RTL layouts or where the top-right is occupied"
|
|
6099
|
+
},
|
|
6100
|
+
"top-right": {
|
|
6101
|
+
"use": "the common desktop choice, out of the way of primary content"
|
|
6102
|
+
},
|
|
6103
|
+
"bottom-left": {
|
|
6104
|
+
"use": "keeps toasts clear of a right-hand panel"
|
|
6105
|
+
},
|
|
6106
|
+
"bottom-right": {
|
|
6107
|
+
"use": "close to where a save or submit action usually sits"
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
},
|
|
6111
|
+
"slots": {
|
|
6112
|
+
"children": {
|
|
6113
|
+
"accepts": "any",
|
|
6114
|
+
"note": "your app — the provider wraps it"
|
|
6115
|
+
}
|
|
6116
|
+
},
|
|
6117
|
+
"a11y": {
|
|
6118
|
+
"guarantees": [
|
|
6119
|
+
"the live region is the container, declared once and always present — a region that appears at the same moment as its content is not reliably announced",
|
|
6120
|
+
"`limit` drops the oldest toasts, so the stack can never cover the page"
|
|
6121
|
+
],
|
|
6122
|
+
"requires": [
|
|
6123
|
+
"mounting exactly one ToastProvider — a second live region competes with the first"
|
|
6124
|
+
]
|
|
6125
|
+
},
|
|
6126
|
+
"antiPatterns": [
|
|
6127
|
+
{
|
|
6128
|
+
"dont": "mounting a ToastProvider per route or per page",
|
|
6129
|
+
"why": "toasts queued before the switch are unmounted mid-announcement, and two live regions announce over each other"
|
|
6130
|
+
},
|
|
6131
|
+
{
|
|
6132
|
+
"dont": "placing it inside a Modal",
|
|
6133
|
+
"why": "the toasts are removed from the tree when the modal closes, and hidden from assistive tech while it is open"
|
|
6134
|
+
},
|
|
6135
|
+
{
|
|
6136
|
+
"dont": "raising `limit` to keep everything",
|
|
6137
|
+
"why": "an unbounded stack covers the content the toasts are about"
|
|
6138
|
+
}
|
|
6139
|
+
],
|
|
4507
6140
|
"stylesheet": "src/styles/toast.css",
|
|
4508
6141
|
"tokens": [
|
|
4509
6142
|
"--border-subtle",
|
|
@@ -4579,6 +6212,82 @@
|
|
|
4579
6212
|
"propsType": "ToggleProps",
|
|
4580
6213
|
"description": "Toggle is a checkbox with `role=\"switch\"`.\n\nNot a button with aria-pressed: a switch is a form value, and the checkbox\ngives form association and `:checked` for free. `role=\"switch\"` changes only\nhow it is announced — \"on/off\" rather than \"checked/unchecked\" — which is\nwhat Figma's On/Off states describe.\n\nThe visual difference from Checkbox is entirely in CSS; this component and\nCheckbox are near-identical by design rather than by accident, and are kept\napart because Figma models them as separate components with separate size\nramps.",
|
|
4581
6214
|
"import": "import { Toggle } from 'ionbase-ui';",
|
|
6215
|
+
"status": "stable",
|
|
6216
|
+
"summary": "A switch — a checkbox with role=\"switch\", announced as on/off.",
|
|
6217
|
+
"useWhen": [
|
|
6218
|
+
"the change takes effect immediately: a setting, a feature flag, a preference"
|
|
6219
|
+
],
|
|
6220
|
+
"useInstead": [
|
|
6221
|
+
{
|
|
6222
|
+
"when": "the value is collected and submitted with a form",
|
|
6223
|
+
"use": "Checkbox",
|
|
6224
|
+
"why": "a switch reads as taking effect now; a checkbox reads as a value being gathered"
|
|
6225
|
+
},
|
|
6226
|
+
{
|
|
6227
|
+
"when": "there are more than two states",
|
|
6228
|
+
"use": "RadioGroup"
|
|
6229
|
+
}
|
|
6230
|
+
],
|
|
6231
|
+
"variants": {
|
|
6232
|
+
"size": {
|
|
6233
|
+
"sm": {
|
|
6234
|
+
"use": "dense settings rows and table filters"
|
|
6235
|
+
},
|
|
6236
|
+
"md": {
|
|
6237
|
+
"use": "the default"
|
|
6238
|
+
},
|
|
6239
|
+
"lg": {
|
|
6240
|
+
"use": "touch targets and prominent single settings"
|
|
6241
|
+
}
|
|
6242
|
+
},
|
|
6243
|
+
"intent": {
|
|
6244
|
+
"brand": {
|
|
6245
|
+
"use": "the default"
|
|
6246
|
+
},
|
|
6247
|
+
"neutral": {
|
|
6248
|
+
"use": "a setting with no positive or negative reading — display and layout preferences"
|
|
6249
|
+
},
|
|
6250
|
+
"danger": {
|
|
6251
|
+
"use": "turning on something destructive or hard to undo"
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
},
|
|
6255
|
+
"slots": {
|
|
6256
|
+
"children": {
|
|
6257
|
+
"accepts": "text",
|
|
6258
|
+
"note": "the visible label; omit for a bare switch and supply `aria-label`"
|
|
6259
|
+
}
|
|
6260
|
+
},
|
|
6261
|
+
"a11y": {
|
|
6262
|
+
"role": "switch",
|
|
6263
|
+
"guarantees": [
|
|
6264
|
+
"a real <input type=\"checkbox\"> underneath, so form association, label clicking and `:checked` come from the platform",
|
|
6265
|
+
"`role=\"switch\"` changes only the announcement — \"on/off\" rather than \"checked/unchecked\""
|
|
6266
|
+
],
|
|
6267
|
+
"requires": [
|
|
6268
|
+
"`children`, or `aria-label` when there is no visible label"
|
|
6269
|
+
]
|
|
6270
|
+
},
|
|
6271
|
+
"antiPatterns": [
|
|
6272
|
+
{
|
|
6273
|
+
"dont": "a Toggle with a Save button beside it",
|
|
6274
|
+
"why": "a switch says the change is already applied; if it needs saving it is a Checkbox"
|
|
6275
|
+
},
|
|
6276
|
+
{
|
|
6277
|
+
"dont": "labelling a Toggle \"Enable dark mode?\" as a question",
|
|
6278
|
+
"why": "a switch label names the thing being switched, not a question about it"
|
|
6279
|
+
},
|
|
6280
|
+
{
|
|
6281
|
+
"dont": "a button with `aria-pressed` for a setting",
|
|
6282
|
+
"why": "a switch is a form value; the checkbox gives form association and `:checked` for free"
|
|
6283
|
+
}
|
|
6284
|
+
],
|
|
6285
|
+
"deprecated": [
|
|
6286
|
+
{
|
|
6287
|
+
"prop": "disabled",
|
|
6288
|
+
"replacement": "isDisabled"
|
|
6289
|
+
}
|
|
6290
|
+
],
|
|
4582
6291
|
"stylesheet": "src/styles/toggle.css",
|
|
4583
6292
|
"tokens": [
|
|
4584
6293
|
"--border-disabled",
|
|
@@ -4681,6 +6390,80 @@
|
|
|
4681
6390
|
"propsType": "TooltipProps",
|
|
4682
6391
|
"description": "Tooltip — Figma `Tooltip` (801:1568).\n\nWraps its trigger rather than taking a ref, so the common case is one\nelement deep: `<Tooltip label=\"...\"><Button/></Tooltip>`.\n\nFOCUS OPENS IT, NOT JUST HOVER. A hover-only tooltip is invisible to keyboard\nand switch users, and `useTooltipTrigger` wires both plus Escape to dismiss.\nIt also enforces one open tooltip at a time and a shared warmup, so moving\nalong a row of icon buttons does not flash a tooltip per button.\n\nThe trigger is cloned with the interaction props and a ref, which means it\nmust forward both. Every component in this library does; a bare `<div>` does\ntoo. A function component that drops its ref will render, but the tooltip\nwill have nothing to position against.",
|
|
4683
6392
|
"import": "import { Tooltip } from 'ionbase-ui';",
|
|
6393
|
+
"status": "stable",
|
|
6394
|
+
"summary": "A text-only hint attached to a trigger. Opens on hover and on focus.",
|
|
6395
|
+
"useWhen": [
|
|
6396
|
+
"naming an icon-only control",
|
|
6397
|
+
"adding a short clarification that is genuinely optional to read"
|
|
6398
|
+
],
|
|
6399
|
+
"useInstead": [
|
|
6400
|
+
{
|
|
6401
|
+
"when": "the content has anything focusable in it — a link, a button, a field",
|
|
6402
|
+
"use": "Popover",
|
|
6403
|
+
"why": "a tooltip cannot be focused, cannot be scrolled, and closes the moment the pointer leaves"
|
|
6404
|
+
},
|
|
6405
|
+
{
|
|
6406
|
+
"when": "the information is required to complete the field",
|
|
6407
|
+
"use": "Input",
|
|
6408
|
+
"why": "Input's helper text is always visible; a tooltip is not discoverable on touch"
|
|
6409
|
+
}
|
|
6410
|
+
],
|
|
6411
|
+
"variants": {
|
|
6412
|
+
"placement": {
|
|
6413
|
+
"top": {
|
|
6414
|
+
"use": "above the trigger — the default reading position"
|
|
6415
|
+
},
|
|
6416
|
+
"bottom": {
|
|
6417
|
+
"use": "below the trigger, for something near the top of the viewport"
|
|
6418
|
+
},
|
|
6419
|
+
"left": {
|
|
6420
|
+
"use": "to the left of the trigger"
|
|
6421
|
+
},
|
|
6422
|
+
"right": {
|
|
6423
|
+
"use": "to the right of the trigger"
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
},
|
|
6427
|
+
"slots": {
|
|
6428
|
+
"label": {
|
|
6429
|
+
"accepts": "text",
|
|
6430
|
+
"note": "the hint. Text only"
|
|
6431
|
+
},
|
|
6432
|
+
"title": {
|
|
6433
|
+
"accepts": "text",
|
|
6434
|
+
"note": "optional heading above the label"
|
|
6435
|
+
},
|
|
6436
|
+
"children": {
|
|
6437
|
+
"accepts": "Button",
|
|
6438
|
+
"note": "the trigger. Must forward both a ref and DOM props"
|
|
6439
|
+
}
|
|
6440
|
+
},
|
|
6441
|
+
"a11y": {
|
|
6442
|
+
"role": "tooltip",
|
|
6443
|
+
"guarantees": [
|
|
6444
|
+
"focus opens it, not only hover — a hover-only tooltip is invisible to keyboard and switch users",
|
|
6445
|
+
"Escape dismisses it",
|
|
6446
|
+
"one tooltip open at a time with a shared warmup, so moving along a row of icon buttons does not flash one per button",
|
|
6447
|
+
"`placement` flips to the opposite side when there is no room"
|
|
6448
|
+
],
|
|
6449
|
+
"requires": [
|
|
6450
|
+
"a trigger that is already labelled for icon-only controls — Tooltip supplies the hint, not the trigger's name"
|
|
6451
|
+
]
|
|
6452
|
+
},
|
|
6453
|
+
"antiPatterns": [
|
|
6454
|
+
{
|
|
6455
|
+
"dont": "putting a link or button inside `label`",
|
|
6456
|
+
"why": "it cannot be reached — the tooltip closes when the pointer leaves the trigger"
|
|
6457
|
+
},
|
|
6458
|
+
{
|
|
6459
|
+
"dont": "a tooltip as the only place information appears",
|
|
6460
|
+
"why": "it is undiscoverable on touch, where there is no hover"
|
|
6461
|
+
},
|
|
6462
|
+
{
|
|
6463
|
+
"dont": "a tooltip on a disabled Button",
|
|
6464
|
+
"why": "a disabled button takes no focus and often no pointer events, so the hint never appears for a keyboard user"
|
|
6465
|
+
}
|
|
6466
|
+
],
|
|
4684
6467
|
"stylesheet": "src/styles/tooltip.css",
|
|
4685
6468
|
"tokens": [
|
|
4686
6469
|
"--font-family-sans",
|