ionbase-ui 0.84.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/components/Checkbox.d.ts +39 -0
  2. package/dist/components/Checkbox.d.ts.map +1 -1
  3. package/dist/components/Checkbox.js +77 -4
  4. package/dist/components/Checkbox.js.map +1 -1
  5. package/dist/components/Fieldset.d.ts +62 -0
  6. package/dist/components/Fieldset.d.ts.map +1 -0
  7. package/dist/components/Fieldset.js +58 -0
  8. package/dist/components/Fieldset.js.map +1 -0
  9. package/dist/components/MultiSelect.d.ts +90 -0
  10. package/dist/components/MultiSelect.d.ts.map +1 -0
  11. package/dist/components/MultiSelect.js +189 -0
  12. package/dist/components/MultiSelect.js.map +1 -0
  13. package/dist/components/Radio.d.ts +14 -0
  14. package/dist/components/Radio.d.ts.map +1 -1
  15. package/dist/components/Radio.js +15 -4
  16. package/dist/components/Radio.js.map +1 -1
  17. package/dist/components/SearchField.d.ts +36 -0
  18. package/dist/components/SearchField.d.ts.map +1 -0
  19. package/dist/components/SearchField.js +76 -0
  20. package/dist/components/SearchField.js.map +1 -0
  21. package/dist/components/Toolbar.d.ts +35 -0
  22. package/dist/components/Toolbar.d.ts.map +1 -0
  23. package/dist/components/Toolbar.js +81 -0
  24. package/dist/components/Toolbar.js.map +1 -0
  25. package/dist/components/index.d.ts +10 -2
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/components/index.js +5 -1
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/figma-descriptions.json +97 -77
  30. package/dist/figma-map.json +228 -4
  31. package/dist/meta/Checkbox.json +8 -6
  32. package/dist/meta/CheckboxGroup.json +268 -0
  33. package/dist/meta/Combobox.json +8 -0
  34. package/dist/meta/Divider.json +2 -2
  35. package/dist/meta/Fieldset.json +162 -0
  36. package/dist/meta/MultiSelect.json +269 -0
  37. package/dist/meta/Radio.json +0 -1
  38. package/dist/meta/RadioGroup.json +59 -7
  39. package/dist/meta/SearchField.json +487 -0
  40. package/dist/meta/Select.json +2 -1
  41. package/dist/meta/Stepper.json +2 -2
  42. package/dist/meta/Tabs.json +3 -3
  43. package/dist/meta/Toolbar.json +130 -0
  44. package/dist/meta/components.json +1616 -238
  45. package/dist/meta/contrast.json +398 -74
  46. package/dist/meta/index.json +84 -8
  47. package/dist/meta/patterns/DataTable.json +33 -6
  48. package/dist/meta/patterns/Form.json +27 -2
  49. package/dist/meta/patterns/index.json +6 -2
  50. package/dist/styles/fieldset.css +65 -0
  51. package/dist/styles/index.css +4 -0
  52. package/dist/styles/multi-select.css +104 -0
  53. package/dist/styles/radio.css +0 -20
  54. package/dist/styles/search-field.css +70 -0
  55. package/dist/styles/toolbar.css +36 -0
  56. package/llms.txt +2 -2
  57. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "ionbase-ui",
3
- "version": "0.84.0",
3
+ "version": "0.88.0",
4
4
  "generated": "by scripts/build-meta.mjs — do not edit; intent lives in meta/*.json",
5
5
  "hooks": [
6
6
  "useAgentRun",
@@ -3080,8 +3080,7 @@
3080
3080
  "summary": "A native checkbox with the system's styling, supporting an indeterminate state.",
3081
3081
  "useWhen": [
3082
3082
  "a single on/off choice inside a form that is submitted",
3083
- "selecting rows in a Table",
3084
- "several independent options where more than one may be picked"
3083
+ "selecting rows in a Table"
3085
3084
  ],
3086
3085
  "useInstead": [
3087
3086
  {
@@ -3089,6 +3088,11 @@
3089
3088
  "use": "Toggle",
3090
3089
  "why": "a switch reads as a setting taking effect now; a checkbox reads as a value being collected"
3091
3090
  },
3091
+ {
3092
+ "when": "several options answer one question",
3093
+ "use": "CheckboxGroup",
3094
+ "why": "it announces the question with each option, carries the group's error, and can require at least one"
3095
+ },
3092
3096
  {
3093
3097
  "when": "the options are mutually exclusive",
3094
3098
  "use": "RadioGroup"
@@ -3220,8 +3224,7 @@
3220
3224
  "sm",
3221
3225
  "md",
3222
3226
  "lg"
3223
- ],
3224
- "default": "md"
3227
+ ]
3225
3228
  },
3226
3229
  "intent": {
3227
3230
  "type": "CheckboxIntent | undefined",
@@ -3232,8 +3235,7 @@
3232
3235
  "neutral",
3233
3236
  "danger",
3234
3237
  "brand"
3235
- ],
3236
- "default": "brand"
3238
+ ]
3237
3239
  },
3238
3240
  "isIndeterminate": {
3239
3241
  "type": "boolean | undefined",
@@ -3282,6 +3284,274 @@
3282
3284
  "other": 0
3283
3285
  }
3284
3286
  },
3287
+ "CheckboxGroup": {
3288
+ "name": "CheckboxGroup",
3289
+ "source": "src/components/Checkbox.tsx",
3290
+ "propsType": "CheckboxGroupProps",
3291
+ "description": "A set of checkboxes that answers one question — \"notify me when…\", \"which\nregions\". Owns the selected values, the group's label, help text and error,\nand \"select at least one\".\n\nA row of loose Checkboxes can do none of that accessibly: the question is\nnot announced with the options, an error has nothing to attach to, and\n\"at least one\" has no native expression at all. See the `required` note in\nCheckbox for how that last one is done.",
3292
+ "import": "import { CheckboxGroup } from 'ionbase-ui';",
3293
+ "status": "stable",
3294
+ "summary": "A set of checkboxes answering one question. Owns the selected values, the group's label, help and error, and \"select at least one\".",
3295
+ "useWhen": [
3296
+ "several options may be picked and they answer one question — \"notify me when\", \"regions\", \"columns to show\"",
3297
+ "the group needs a rule of its own — at least one, or an error that belongs to the set rather than to an option"
3298
+ ],
3299
+ "useInstead": [
3300
+ {
3301
+ "when": "only one option may be picked",
3302
+ "use": "RadioGroup"
3303
+ },
3304
+ {
3305
+ "when": "the options are many, or space is tight",
3306
+ "use": "MultiSelect",
3307
+ "why": "past about seven options a list of boxes is longer than the form around it; MultiSelect filters them and shows the chosen ones as tags"
3308
+ },
3309
+ {
3310
+ "when": "there is one on/off choice",
3311
+ "use": "Checkbox",
3312
+ "why": "a group of one announces a question and then its only answer"
3313
+ },
3314
+ {
3315
+ "when": "each option takes effect immediately",
3316
+ "use": "Toggle"
3317
+ }
3318
+ ],
3319
+ "composition": {
3320
+ "order": [
3321
+ "CheckboxGroup",
3322
+ "Checkbox"
3323
+ ],
3324
+ "note": "Give every Checkbox a `value`; the group's `onChange` receives the array of ticked values. Selection lives on the group — do not also set `isSelected` on the boxes.",
3325
+ "example": "<CheckboxGroup label=\"Notify the team when\" isRequired value={notifyOn} onChange={setNotifyOn} isInvalid={!!error} errorMessage={error}><Checkbox value=\"failed\">A run fails</Checkbox><Checkbox value=\"approval\">A run needs approval</Checkbox></CheckboxGroup>"
3326
+ },
3327
+ "variants": {
3328
+ "size": {
3329
+ "sm": {
3330
+ "use": "dense forms and filter panels"
3331
+ },
3332
+ "md": {
3333
+ "use": "the default"
3334
+ },
3335
+ "lg": {
3336
+ "use": "touch targets and prominent choices"
3337
+ }
3338
+ },
3339
+ "intent": {
3340
+ "brand": {
3341
+ "use": "the default"
3342
+ },
3343
+ "neutral": {
3344
+ "use": "facts rather than approvals — the columns shown, the filters applied"
3345
+ },
3346
+ "danger": {
3347
+ "use": "a set of destructive consents"
3348
+ }
3349
+ },
3350
+ "orientation": {
3351
+ "vertical": {
3352
+ "use": "the default, and always when a label runs past a few words"
3353
+ },
3354
+ "horizontal": {
3355
+ "use": "two to four one-word options — weekdays, regions. Wraps rather than overflows"
3356
+ }
3357
+ }
3358
+ },
3359
+ "slots": {
3360
+ "label": {
3361
+ "accepts": "text",
3362
+ "note": "renders as the <legend> — the question the options answer"
3363
+ },
3364
+ "description": {
3365
+ "accepts": "text",
3366
+ "note": "help beneath the options; replaced by `errorMessage` while `isInvalid` is set"
3367
+ },
3368
+ "errorMessage": {
3369
+ "accepts": "text",
3370
+ "note": "shown only while `isInvalid` is set"
3371
+ },
3372
+ "children": {
3373
+ "accepts": "Checkbox"
3374
+ }
3375
+ },
3376
+ "a11y": {
3377
+ "role": "group, from <fieldset> and <legend>",
3378
+ "guarantees": [
3379
+ "the legend is announced as the group's name when focus enters any box",
3380
+ "the description or error is on every box's `aria-describedby` as well as the fieldset's, so it is read on the box that takes focus",
3381
+ "`isInvalid` sets `aria-invalid` on every box",
3382
+ "`isRequired` sets native `required` on every box while none is ticked and on none once one is: the browser blocks the submit in its own language, and each box announces \"required\" exactly while the rule is unmet",
3383
+ "`isDisabled` cascades to every Checkbox that has not set its own"
3384
+ ],
3385
+ "requires": [
3386
+ "`label`, or `aria-label` when the question is already visible elsewhere",
3387
+ "a `value` on every Checkbox — the group tracks the selection by it"
3388
+ ]
3389
+ },
3390
+ "antiPatterns": [
3391
+ {
3392
+ "dont": "loose Checkboxes under a text label",
3393
+ "why": "the question is not announced with the options, an error has nothing to attach to, and \"at least one\" cannot be expressed"
3394
+ },
3395
+ {
3396
+ "dont": "`isSelected` or `checked` on a Checkbox inside a group",
3397
+ "why": "the group owns the selection; the box's own value is ignored"
3398
+ },
3399
+ {
3400
+ "dont": "`isRequired` on every Checkbox in the group",
3401
+ "why": "that means every box must be ticked; `isRequired` on the group means at least one"
3402
+ }
3403
+ ],
3404
+ "stylesheet": "src/styles/checkbox.css",
3405
+ "tokens": [
3406
+ "--border-disabled",
3407
+ "--border-error-strong",
3408
+ "--border-focus",
3409
+ "--border-inverse",
3410
+ "--border-primary-strong",
3411
+ "--border-stronger",
3412
+ "--border-width-default",
3413
+ "--border-width-thick",
3414
+ "--font-family-sans",
3415
+ "--icon-disabled",
3416
+ "--icon-on-color",
3417
+ "--ion-checkbox-border",
3418
+ "--ion-checkbox-fill",
3419
+ "--ion-checkbox-font-size",
3420
+ "--ion-checkbox-gap",
3421
+ "--ion-checkbox-line-height",
3422
+ "--ion-checkbox-mark",
3423
+ "--ion-checkbox-radius",
3424
+ "--ion-checkbox-raised",
3425
+ "--ion-checkbox-size",
3426
+ "--ion-duration-base",
3427
+ "--ion-ease-out",
3428
+ "--ion-shadow-raised-flush-lg",
3429
+ "--ion-shadow-raised-flush-sm",
3430
+ "--ion-shadow-raised-flush-xs",
3431
+ "--radius-sm",
3432
+ "--radius-xs",
3433
+ "--spacing-12",
3434
+ "--spacing-16",
3435
+ "--spacing-20",
3436
+ "--spacing-24",
3437
+ "--spacing-4",
3438
+ "--spacing-8",
3439
+ "--surface-default",
3440
+ "--surface-disabled",
3441
+ "--surface-error",
3442
+ "--surface-inverse",
3443
+ "--surface-primary",
3444
+ "--text-disabled",
3445
+ "--text-secondary",
3446
+ "--type-body",
3447
+ "--type-body-line-height",
3448
+ "--type-body-sm",
3449
+ "--type-body-sm-line-height"
3450
+ ],
3451
+ "props": {
3452
+ "label": {
3453
+ "type": "React.ReactNode",
3454
+ "required": false,
3455
+ "origin": "own",
3456
+ "description": "The question the options answer. Renders as the `<legend>`."
3457
+ },
3458
+ "description": {
3459
+ "type": "React.ReactNode",
3460
+ "required": false,
3461
+ "origin": "own",
3462
+ "description": "Help text beneath the options. Replaced by `errorMessage` while invalid."
3463
+ },
3464
+ "errorMessage": {
3465
+ "type": "React.ReactNode",
3466
+ "required": false,
3467
+ "origin": "own",
3468
+ "description": "Shown in the description's place while `isInvalid` is set."
3469
+ },
3470
+ "isInvalid": {
3471
+ "type": "boolean | undefined",
3472
+ "required": false,
3473
+ "origin": "own",
3474
+ "description": "Marks every box invalid and shows `errorMessage`."
3475
+ },
3476
+ "isRequired": {
3477
+ "type": "boolean | undefined",
3478
+ "required": false,
3479
+ "origin": "own",
3480
+ "description": "At least one option must be selected — enforced by native validation."
3481
+ },
3482
+ "value": {
3483
+ "type": "readonly string[] | undefined",
3484
+ "required": false,
3485
+ "origin": "own",
3486
+ "description": "The selected values (controlled)."
3487
+ },
3488
+ "defaultValue": {
3489
+ "type": "readonly string[] | undefined",
3490
+ "required": false,
3491
+ "origin": "own",
3492
+ "description": "The initially selected values (uncontrolled)."
3493
+ },
3494
+ "onChange": {
3495
+ "type": "((value: string[]) => void) | undefined",
3496
+ "required": false,
3497
+ "origin": "own",
3498
+ "description": "Receives the whole new selection, in the order the options were ticked."
3499
+ },
3500
+ "name": {
3501
+ "type": "string | undefined",
3502
+ "required": false,
3503
+ "origin": "own",
3504
+ "description": "Shared input name, so a form submits every ticked value under it."
3505
+ },
3506
+ "size": {
3507
+ "type": "CheckboxSize | undefined",
3508
+ "required": false,
3509
+ "origin": "own",
3510
+ "values": [
3511
+ "sm",
3512
+ "md",
3513
+ "lg"
3514
+ ]
3515
+ },
3516
+ "intent": {
3517
+ "type": "CheckboxIntent | undefined",
3518
+ "required": false,
3519
+ "origin": "own",
3520
+ "values": [
3521
+ "neutral",
3522
+ "danger",
3523
+ "brand"
3524
+ ]
3525
+ },
3526
+ "isDisabled": {
3527
+ "type": "boolean | undefined",
3528
+ "required": false,
3529
+ "origin": "own",
3530
+ "description": "Whether every checkbox in the group is disabled."
3531
+ },
3532
+ "orientation": {
3533
+ "type": "FieldsetOrientation | undefined",
3534
+ "required": false,
3535
+ "origin": "own",
3536
+ "values": [
3537
+ "vertical",
3538
+ "horizontal"
3539
+ ]
3540
+ },
3541
+ "children": {
3542
+ "type": "React.ReactNode",
3543
+ "required": false,
3544
+ "origin": "own",
3545
+ "description": "Checkboxes, each with a `value`."
3546
+ }
3547
+ },
3548
+ "propCounts": {
3549
+ "own": 14,
3550
+ "aria": 0,
3551
+ "dom": 276,
3552
+ "other": 0
3553
+ }
3554
+ },
3285
3555
  "Citation": {
3286
3556
  "name": "Citation",
3287
3557
  "source": "src/components/Citation.tsx",
@@ -3584,6 +3854,11 @@
3584
3854
  "use": "Select",
3585
3855
  "why": "the native control brings keyboard handling and the mobile platform picker for free; this rebuilds all of it"
3586
3856
  },
3857
+ {
3858
+ "when": "more than one value may be picked",
3859
+ "use": "MultiSelect",
3860
+ "why": "its input holds only the filter text, the list stays open between picks, and the chosen values show as tags"
3861
+ },
3587
3862
  {
3588
3863
  "when": "there are two or three mutually exclusive choices worth showing at once",
3589
3864
  "use": "RadioGroup"
@@ -3625,6 +3900,9 @@
3625
3900
  "requires": [
3626
3901
  "`label`, or an `aria-label` when no visible label is rendered",
3627
3902
  "`emptyLabel` wording that says why nothing is listed, if the default is too terse"
3903
+ ],
3904
+ "notes": [
3905
+ "While the list is open, React Aria hides everything outside the field and the list from assistive technology. That is the platform pattern for a combobox; a whole-page accessibility scan taken with the list open reports the hidden page, not this control."
3628
3906
  ]
3629
3907
  },
3630
3908
  "antiPatterns": [
@@ -4736,8 +5014,8 @@
4736
5014
  "origin": "own",
4737
5015
  "description": "Matches Figma's `Style` variant.",
4738
5016
  "values": [
4739
- "horizontal",
4740
- "vertical"
5017
+ "vertical",
5018
+ "horizontal"
4741
5019
  ]
4742
5020
  }
4743
5021
  },
@@ -5219,37 +5497,199 @@
5219
5497
  "other": 0
5220
5498
  }
5221
5499
  },
5222
- "FileUpload": {
5223
- "name": "FileUpload",
5224
- "source": "src/components/FileUpload.tsx",
5225
- "propsType": "FileUploadProps",
5226
- "description": "FileUpload — a drop target wrapped around a real `<input type=\"file\">`.\n\nDrawn in Figma as `File Upload` (1367:2333) — two sizes by five states. The\nmeasurements came from `Input` and `EmptyState` rather than being invented:\nthe border, radius and disabled treatment are Input's; the centred\nicon-over-text stack is EmptyState's. Figma draws the drop zone alone, the\nsame split `Input` makes against `Form Field`.\n\nTHE INPUT IS THE CONTROL. THE DROP ZONE IS DECORATION.\n\nThe usual build of this component is a `<div>` with drag handlers and a\nclick that calls `input.click()`. That version cannot be reached by keyboard,\nhas no accessible name, does not participate in a form, and does not work in\nany environment without a pointer — which includes switch access, voice\ncontrol and most screen-reader browse modes.\n\nHere the file input is a real, focusable, labelled control that is visually\nhidden but NOT `display: none` — it keeps its place in the tab order and its\nlabel. Drag-and-drop is layered on top as an enhancement, and every path it\noffers is also reachable without it. That ordering is the whole component.\n\nVALIDATION IS ADVISORY, NOT SECURITY. `accept` and `maxSize` are checked here\nso the user finds out immediately instead of after an upload. A server that\ntrusts either one is trusting a value the client chose.",
5227
- "import": "import { FileUpload } from 'ionbase-ui';",
5500
+ "Fieldset": {
5501
+ "name": "Fieldset",
5502
+ "source": "src/components/Fieldset.tsx",
5503
+ "propsType": "FieldsetProps",
5504
+ "description": "Groups related fields under one label, with one description and one error —\nan address, a date range typed as two fields, a set of limits.\n\nFor checkboxes use CheckboxGroup and for radios RadioGroup: both render this\nsame shell, and add the selection state it deliberately does not own.\n\nThere is no `isDisabled`. A native `disabled` fieldset does disable every\ncontrol inside, but Input, Select and the rest draw their disabled state from\ntheir own prop, so the fields would stop working while still looking live.\nDisable the fields themselves; the choice groups can cascade because their\ncheckboxes and radios style off `:disabled`.",
5505
+ "import": "import { Fieldset } from 'ionbase-ui';",
5228
5506
  "status": "stable",
5229
- "summary": "A drop target wrapped around a real file input, with client-side accept and size checks.",
5507
+ "summary": "A <fieldset>/<legend> that groups related fields under one label, one description and one error — an address, a pair of limits, a date typed as two fields.",
5230
5508
  "useWhen": [
5231
- "the user attaches one or more files to a form or a message",
5232
- "the constraints (types, size, count) should be stated before the upload is attempted rather than after"
5509
+ "several fields answer one question and a reader must hear the question with each of them — street, city and postcode under \"Billing address\"",
5510
+ "one error belongs to the group rather than to a field — \"the minimum must be below the maximum\""
5233
5511
  ],
5234
5512
  "useInstead": [
5235
5513
  {
5236
- "when": "a single file is picked and no drop target or file list is wanted",
5237
- "use": "a plain <input type=\"file\">",
5238
- "why": "this component adds a drop zone and a removable list; without either it is a heavier way to render the same control"
5514
+ "when": "the fields are checkboxes",
5515
+ "use": "CheckboxGroup",
5516
+ "why": "it renders this same shell and adds the selected values and \"select at least one\""
5239
5517
  },
5240
5518
  {
5241
- "when": "upload progress needs to be shown",
5242
- "use": "ProgressBar",
5243
- "why": "FileUpload selects files; it does not transfer them, and deliberately owns no network state"
5519
+ "when": "the fields are radios",
5520
+ "use": "RadioGroup",
5521
+ "why": "it renders this same shell and adds the shared name and the selected value"
5522
+ },
5523
+ {
5524
+ "when": "a single field needs a label and help text",
5525
+ "use": "Input",
5526
+ "why": "every field already renders its own label, description and error; a fieldset around one field announces the label twice"
5527
+ },
5528
+ {
5529
+ "when": "the grouping is visual — a titled section of a settings page",
5530
+ "use": "Card",
5531
+ "why": "a fieldset is a form grouping that assistive technology announces on entry; a section of unrelated settings is not one question"
5244
5532
  }
5245
5533
  ],
5534
+ "composition": {
5535
+ "order": [
5536
+ "Fieldset",
5537
+ "Input"
5538
+ ],
5539
+ "note": "Each field keeps its own label — the legend names the group, not the fields. Put a group-level error in `errorMessage` with `isInvalid`, and keep field-level errors on the fields.",
5540
+ "example": "<Fieldset label=\"Run limits\" description=\"Runs stop at whichever comes first.\" orientation=\"horizontal\"><NumberInput label=\"Max steps\" /><NumberInput label=\"Max minutes\" /></Fieldset>"
5541
+ },
5246
5542
  "variants": {
5247
- "size": {
5248
- "sm": {
5249
- "use": "inside a dense form, or beside other fields"
5543
+ "orientation": {
5544
+ "vertical": {
5545
+ "use": "the default — fields stacked, as a form reads"
5250
5546
  },
5251
- "md": {
5252
- "use": "the default — a standalone attachment area"
5547
+ "horizontal": {
5548
+ "use": "two or three short fields that read as one value — a range, a size, a city and postcode. Wraps rather than overflows"
5549
+ }
5550
+ }
5551
+ },
5552
+ "slots": {
5553
+ "label": {
5554
+ "accepts": "text",
5555
+ "note": "renders as the <legend> — the question the fields answer"
5556
+ },
5557
+ "description": {
5558
+ "accepts": "text",
5559
+ "note": "help beneath the fields; replaced by `errorMessage` while `isInvalid` is set, as Input's is"
5560
+ },
5561
+ "errorMessage": {
5562
+ "accepts": "text",
5563
+ "note": "the group's error, shown only while `isInvalid` is set"
5564
+ },
5565
+ "children": {
5566
+ "accepts": "Input, Select, NumberInput, DatePicker and other fields"
5567
+ }
5568
+ },
5569
+ "a11y": {
5570
+ "role": "group, from <fieldset> and <legend>",
5571
+ "guarantees": [
5572
+ "the legend is announced as the group's name when focus enters any field inside",
5573
+ "`aria-describedby` on the fieldset points at whichever of the description or the error is showing"
5574
+ ],
5575
+ "requires": [
5576
+ "`label`, or `aria-label` when the question is already visible as a heading"
5577
+ ],
5578
+ "notes": [
5579
+ "There is no `isDisabled`. A native disabled fieldset disables every control inside, but Input, Select and the rest draw their disabled look from their own prop — the fields would stop working while still looking live. Disable the fields themselves."
5580
+ ]
5581
+ },
5582
+ "antiPatterns": [
5583
+ {
5584
+ "dont": "a Fieldset around one field",
5585
+ "why": "the field's label and the legend both announce, saying the same thing twice"
5586
+ },
5587
+ {
5588
+ "dont": "a Fieldset of Checkboxes",
5589
+ "do": "<CheckboxGroup label=\"Notify me when\">…</CheckboxGroup>",
5590
+ "why": "it looks the same, but nothing owns the selected values or \"select at least one\""
5591
+ },
5592
+ {
5593
+ "dont": "<fieldset disabled> around Inputs",
5594
+ "why": "the fields stop accepting input while still drawn as enabled"
5595
+ }
5596
+ ],
5597
+ "stylesheet": "src/styles/fieldset.css",
5598
+ "tokens": [
5599
+ "--font-family-sans",
5600
+ "--font-weight-medium",
5601
+ "--font-weight-regular",
5602
+ "--ion-fieldset-gap",
5603
+ "--ion-fieldset-inline-gap",
5604
+ "--spacing-16",
5605
+ "--spacing-6",
5606
+ "--spacing-8",
5607
+ "--text-error",
5608
+ "--text-secondary",
5609
+ "--text-tertiary",
5610
+ "--type-body-sm",
5611
+ "--type-body-sm-line-height"
5612
+ ],
5613
+ "props": {
5614
+ "label": {
5615
+ "type": "React.ReactNode",
5616
+ "required": false,
5617
+ "origin": "own",
5618
+ "description": "The question the fields answer. Renders as the `<legend>`."
5619
+ },
5620
+ "description": {
5621
+ "type": "React.ReactNode",
5622
+ "required": false,
5623
+ "origin": "own",
5624
+ "description": "Help text beneath the fields. Replaced by `errorMessage` while invalid."
5625
+ },
5626
+ "errorMessage": {
5627
+ "type": "React.ReactNode",
5628
+ "required": false,
5629
+ "origin": "own",
5630
+ "description": "Shown in the description's place while `isInvalid` is set."
5631
+ },
5632
+ "isInvalid": {
5633
+ "type": "boolean | undefined",
5634
+ "required": false,
5635
+ "origin": "own",
5636
+ "description": "Whether the group as a whole fails validation."
5637
+ },
5638
+ "orientation": {
5639
+ "type": "FieldsetOrientation | undefined",
5640
+ "required": false,
5641
+ "origin": "own",
5642
+ "description": "How the fields flow. Horizontal wraps rather than overflowing.",
5643
+ "values": [
5644
+ "vertical",
5645
+ "horizontal"
5646
+ ],
5647
+ "default": "vertical"
5648
+ },
5649
+ "children": {
5650
+ "type": "React.ReactNode",
5651
+ "required": false,
5652
+ "origin": "own"
5653
+ }
5654
+ },
5655
+ "propCounts": {
5656
+ "own": 6,
5657
+ "aria": 0,
5658
+ "dom": 279,
5659
+ "other": 0
5660
+ }
5661
+ },
5662
+ "FileUpload": {
5663
+ "name": "FileUpload",
5664
+ "source": "src/components/FileUpload.tsx",
5665
+ "propsType": "FileUploadProps",
5666
+ "description": "FileUpload — a drop target wrapped around a real `<input type=\"file\">`.\n\nDrawn in Figma as `File Upload` (1367:2333) — two sizes by five states. The\nmeasurements came from `Input` and `EmptyState` rather than being invented:\nthe border, radius and disabled treatment are Input's; the centred\nicon-over-text stack is EmptyState's. Figma draws the drop zone alone, the\nsame split `Input` makes against `Form Field`.\n\nTHE INPUT IS THE CONTROL. THE DROP ZONE IS DECORATION.\n\nThe usual build of this component is a `<div>` with drag handlers and a\nclick that calls `input.click()`. That version cannot be reached by keyboard,\nhas no accessible name, does not participate in a form, and does not work in\nany environment without a pointer — which includes switch access, voice\ncontrol and most screen-reader browse modes.\n\nHere the file input is a real, focusable, labelled control that is visually\nhidden but NOT `display: none` — it keeps its place in the tab order and its\nlabel. Drag-and-drop is layered on top as an enhancement, and every path it\noffers is also reachable without it. That ordering is the whole component.\n\nVALIDATION IS ADVISORY, NOT SECURITY. `accept` and `maxSize` are checked here\nso the user finds out immediately instead of after an upload. A server that\ntrusts either one is trusting a value the client chose.",
5667
+ "import": "import { FileUpload } from 'ionbase-ui';",
5668
+ "status": "stable",
5669
+ "summary": "A drop target wrapped around a real file input, with client-side accept and size checks.",
5670
+ "useWhen": [
5671
+ "the user attaches one or more files to a form or a message",
5672
+ "the constraints (types, size, count) should be stated before the upload is attempted rather than after"
5673
+ ],
5674
+ "useInstead": [
5675
+ {
5676
+ "when": "a single file is picked and no drop target or file list is wanted",
5677
+ "use": "a plain <input type=\"file\">",
5678
+ "why": "this component adds a drop zone and a removable list; without either it is a heavier way to render the same control"
5679
+ },
5680
+ {
5681
+ "when": "upload progress needs to be shown",
5682
+ "use": "ProgressBar",
5683
+ "why": "FileUpload selects files; it does not transfer them, and deliberately owns no network state"
5684
+ }
5685
+ ],
5686
+ "variants": {
5687
+ "size": {
5688
+ "sm": {
5689
+ "use": "inside a dense form, or beside other fields"
5690
+ },
5691
+ "md": {
5692
+ "use": "the default — a standalone attachment area"
5253
5693
  }
5254
5694
  }
5255
5695
  },
@@ -8074,128 +8514,397 @@
8074
8514
  "other": 0
8075
8515
  }
8076
8516
  },
8077
- "NavItem": {
8078
- "name": "NavItem",
8079
- "source": "src/components/NavItem.tsx",
8080
- "propsType": "NavItemProps",
8081
- "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.",
8082
- "import": "import { NavItem } from 'ionbase-ui';",
8517
+ "MultiSelect": {
8518
+ "name": "MultiSelect",
8519
+ "source": "src/components/MultiSelect.tsx",
8520
+ "propsType": "MultiSelectProps",
8521
+ "description": "MultiSelect — a text field that filters a list, with any number of selected\nvalues shown as removable tags beneath it.\n\nWHY IT IS ITS OWN COMPONENT, NOT `Combobox selectionMode=\"multiple\"`\n\nThe two differ in what the field holds. Combobox's input shows the chosen\nlabel — typing edits the value. Here the input only ever holds the filter\ntext, the value lives in the tags, and choosing an option leaves the list\nopen for the next one. One prop switching the meaning of the text in the\nbox is the kind of contract agents get wrong in generated code.\n\nWHAT REACT ARIA GIVES IT\n\n`useComboBoxState` and `useComboBox` in `selectionMode: 'multiple'`: the\nlistbox is `aria-multiselectable`, Enter and click toggle an option without\nclosing the list, the filter text clears after each pick, and native\n`required` is set only while nothing is chosen — \"at least one\", the same\nrule CheckboxGroup enforces.\n\nWHAT IT ADDS\n\n - The tags. A TagGroup under the field, named by the field's label, one\n tab stop, Delete or the × to remove. The selection must be visible with\n the list closed, and \"3 selected\" hides which three.\n - The value is announced with the field. React Aria points the input's\n `aria-describedby` at a value element; this fills it with the chosen\n labels joined by `Intl.ListFormat`, so \"Billing, Legal and Ops\" is read\n in the user's language with no string shipped for it.\n - Backspace in an empty field removes the last value, which is what\n every tag input on the web has taught people to expect.\n - Focus lands back in the field when the last tag is removed, instead of\n falling to <body> with the TagGroup that held it.",
8522
+ "import": "import { MultiSelect } from 'ionbase-ui';",
8083
8523
  "status": "stable",
8084
- "summary": "A primary navigation destination. Renders an <a> when given `href`, a <button> otherwise.",
8524
+ "summary": "A text field that filters a list, with any number of values chosen. The choices show as removable tags beneath the field and are read with it.",
8085
8525
  "useWhen": [
8086
- "building a header nav bar, a sidebar or a section switcher",
8087
- "an item opens a menu rather than navigating — pass `showChevron` and no `href`"
8526
+ "the user picks several values from a list too long to show as checkboxes — teams, regions, labels, assignees",
8527
+ "a table filter can match more than one value — \"Status is Failing or Paused\""
8088
8528
  ],
8089
8529
  "useInstead": [
8090
8530
  {
8091
- "when": "the link sits inside body copy or a content block",
8092
- "use": "Link"
8531
+ "when": "exactly one value is picked",
8532
+ "use": "Combobox",
8533
+ "why": "Combobox's input shows the chosen label; here the input only ever holds the filter text, and the list stays open for the next pick"
8093
8534
  },
8094
8535
  {
8095
- "when": "it is a row in a dropdown list rather than a top-level destination",
8096
- "use": "MenuItem"
8536
+ "when": "there are about seven options or fewer",
8537
+ "use": "CheckboxGroup",
8538
+ "why": "every option visible at once beats typing to find one, and nothing is hidden behind a popover"
8539
+ },
8540
+ {
8541
+ "when": "the values are free text the user invents, not options from a list",
8542
+ "use": "TagGroup",
8543
+ "why": "MultiSelect only chooses from `options`; a free-text tag input is a TagGroup beside an Input"
8097
8544
  }
8098
8545
  ],
8546
+ "composition": {
8547
+ "order": [
8548
+ "MultiSelect"
8549
+ ],
8550
+ "note": "Pass the full `options` list; filtering, the tags and the announcement are the component's. In a table's filter bar, give it `aria-label` and `hideTags`, and list its values in the table's active-filters TagGroup — one removable tag per value, not the same tags twice.",
8551
+ "example": "<MultiSelect label=\"Notify teams\" options={teams} value={teamIds} onChange={setTeamIds} placeholder=\"Search teams\" />"
8552
+ },
8553
+ "variants": {
8554
+ "size": {
8555
+ "sm": {
8556
+ "use": "dense forms, table filters"
8557
+ },
8558
+ "md": {
8559
+ "use": "the default"
8560
+ },
8561
+ "lg": {
8562
+ "use": "prominent single-field forms"
8563
+ }
8564
+ }
8565
+ },
8099
8566
  "slots": {
8100
- "icon": {
8101
- "accepts": "Icon",
8102
- "note": "leading icon; omit its `label` — the item's text names it"
8567
+ "label": {
8568
+ "accepts": "text",
8569
+ "note": "names the field and the tags beneath it"
8570
+ },
8571
+ "description": {
8572
+ "accepts": "text",
8573
+ "note": "replaced by `errorMessage` while `isInvalid` is set"
8574
+ },
8575
+ "errorMessage": {
8576
+ "accepts": "text",
8577
+ "note": "shown only while `isInvalid` is set"
8578
+ },
8579
+ "emptyLabel": {
8580
+ "accepts": "text",
8581
+ "note": "shown in the list when the filter matches nothing"
8103
8582
  }
8104
8583
  },
8105
8584
  "a11y": {
8106
- "role": "link when `href` is set, button when it is not",
8585
+ "role": "combobox, controlling a listbox that is aria-multiselectable",
8107
8586
  "guarantees": [
8108
- "hover state comes from React Aria's pointer-aware `useHover`, so a tap does not stay hovered until the next tap elsewhere",
8109
- "the focus ring shows for keyboard navigation only",
8110
- "the CSS keeps its own `:hover` / `:focus-visible` rules, so the styling survives without React",
8111
- "`isCurrent` sets `aria-current=\"page\"`, and the current item is marked by an underline as well as colour, which survives forced-colours mode"
8587
+ "React Aria's multi-select combobox: arrow keys move through the list while focus stays in the input, Enter or click toggles an option, and the list stays open for the next pick",
8588
+ "the chosen labels are on the input's `aria-describedby`, joined by `Intl.ListFormat` — read with the field in the user's language, with no string shipped for it",
8589
+ "the chosen values are a TagGroup named by the field's label: one tab stop, arrow keys between tags, Delete or the × to remove",
8590
+ "Backspace in an empty field removes the last value; with text in the field it only edits the text",
8591
+ "removing the last tag returns focus to the field instead of dropping it on <body>",
8592
+ "`isRequired` marks the field required only while nothing is chosen — at least one"
8112
8593
  ],
8113
8594
  "requires": [
8114
- "an accessible name — `children`, or `aria-label` on an icon-only item"
8595
+ "`label`, or `aria-label` when there is no visible label — it names the tags as well as the field"
8115
8596
  ],
8116
8597
  "notes": [
8117
- "NavItem is one item, not the landmark. Wrap the set in a <nav> with its own accessible name."
8598
+ "While the list is open, React Aria hides everything outside the field and the list from assistive technology, the tags included. That is the platform pattern for a combobox, not a defect: the chosen options are marked selected in the list itself."
8118
8599
  ]
8119
8600
  },
8120
8601
  "antiPatterns": [
8121
8602
  {
8122
- "dont": "`showChevron` on a plain link that navigates",
8123
- "why": "the chevron promises a menu; nothing opens"
8603
+ "dont": "a Combobox that appends each pick to a TagGroup by hand",
8604
+ "why": "the list closes after every pick, the input shows the last label, and nothing announces what is chosen"
8124
8605
  },
8125
8606
  {
8126
- "dont": "marking the current page with colour only",
8127
- "do": "`aria-current=\"page\"`",
8128
- "why": "a screen-reader user is otherwise never told where they are"
8607
+ "dont": "a native <select multiple>",
8608
+ "why": "it needs Ctrl or Cmd to pick a second option, which most people never discover, and it cannot filter"
8129
8609
  },
8130
8610
  {
8131
- "dont": "marking the current page with a className or colour of your own instead of `isCurrent`",
8132
- "why": "a screen reader hears nothing without `aria-current`, and a colour-only marker fails anyone who cannot see the difference"
8133
- }
8134
- ],
8135
- "deprecated": [
8611
+ "dont": "a count in place of the values — \"3 selected\"",
8612
+ "why": "it hides which three; the tags show them and the field reads them"
8613
+ },
8136
8614
  {
8137
- "prop": "disabled",
8138
- "replacement": "isDisabled"
8615
+ "dont": "`hideTags` with nowhere else showing the values",
8616
+ "why": "the field shows only the filter text, so the choice is invisible until the list is opened"
8139
8617
  }
8140
8618
  ],
8141
- "stylesheet": "src/styles/nav-item.css",
8619
+ "stylesheet": "src/styles/multi-select.css",
8142
8620
  "tokens": [
8143
- "--border-focus",
8144
- "--border-primary",
8621
+ "--border-disabled",
8622
+ "--border-primary-strong",
8623
+ "--border-stronger",
8624
+ "--border-width-default",
8145
8625
  "--border-width-thick",
8146
- "--font-family-sans",
8147
- "--font-weight-medium",
8148
- "--icon-disabled",
8149
- "--icon-interactive-hover",
8150
- "--icon-primary",
8151
- "--icon-size-sm",
8152
- "--icon-tertiary",
8153
- "--ion-duration-base",
8154
- "--ion-ease-out",
8155
- "--radius-sm",
8626
+ "--font-weight-regular",
8627
+ "--icon-on-color",
8628
+ "--ion-shadow-raised-flush-xs",
8629
+ "--radius-xs",
8630
+ "--spacing-16",
8631
+ "--spacing-2",
8156
8632
  "--spacing-4",
8157
8633
  "--spacing-8",
8158
- "--text-default",
8159
- "--text-disabled",
8160
- "--text-interactive-hover",
8161
- "--text-secondary",
8162
- "--type-body-sm",
8163
- "--type-body-sm-line-height"
8634
+ "--surface-default",
8635
+ "--surface-disabled",
8636
+ "--surface-hover",
8637
+ "--surface-primary",
8638
+ "--type-body-line-height"
8164
8639
  ],
8165
8640
  "props": {
8166
- "icon": {
8641
+ "options": {
8642
+ "type": "readonly ComboboxOption[]",
8643
+ "required": true,
8644
+ "origin": "own",
8645
+ "description": "The full option list. Filtering happens here, against what is typed."
8646
+ },
8647
+ "label": {
8167
8648
  "type": "React.ReactNode",
8168
8649
  "required": false,
8169
8650
  "origin": "own",
8170
- "description": "Figma's `Icon` slot — an optional leading icon."
8651
+ "description": "Field label. Required for a usable control — see `a11y.requires`."
8171
8652
  },
8172
- "showChevron": {
8173
- "type": "boolean | undefined",
8653
+ "aria-label": {
8654
+ "type": "string | undefined",
8174
8655
  "required": false,
8175
8656
  "origin": "own",
8176
- "description": "Figma's `Show Chevron`. Set on a nav item that opens a menu, not a plain link."
8657
+ "description": "Names the field, and its tags, when there is no visible `label`."
8177
8658
  },
8178
- "isCurrent": {
8179
- "type": "boolean | undefined",
8659
+ "description": {
8660
+ "type": "React.ReactNode",
8180
8661
  "required": false,
8181
8662
  "origin": "own",
8182
- "description": "The page the user is on. Marks the item `aria-current=\"page\"` and draws\nthe same indicator Tabs' underline does — so a section switcher built from\nNavItems says where you are, in words for a screen reader and in more than\ncolour for everyone else."
8663
+ "description": "Helper text below the field."
8183
8664
  },
8184
- "isDisabled": {
8185
- "type": "boolean | undefined",
8665
+ "errorMessage": {
8666
+ "type": "React.ReactNode",
8186
8667
  "required": false,
8187
- "origin": "own"
8668
+ "origin": "own",
8669
+ "description": "Replaces the helper text when `isInvalid` is set."
8188
8670
  },
8189
- "disabled": {
8671
+ "isInvalid": {
8190
8672
  "type": "boolean | undefined",
8191
8673
  "required": false,
8192
- "origin": "own",
8193
- "tags": {
8194
- "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
8195
- }
8674
+ "origin": "own"
8196
8675
  },
8197
- "children": {
8198
- "type": "React.ReactNode",
8676
+ "isDisabled": {
8677
+ "type": "boolean | undefined",
8678
+ "required": false,
8679
+ "origin": "own"
8680
+ },
8681
+ "isReadOnly": {
8682
+ "type": "boolean | undefined",
8683
+ "required": false,
8684
+ "origin": "own"
8685
+ },
8686
+ "isRequired": {
8687
+ "type": "boolean | undefined",
8688
+ "required": false,
8689
+ "origin": "own",
8690
+ "description": "At least one value must be chosen."
8691
+ },
8692
+ "size": {
8693
+ "type": "MultiSelectSize | undefined",
8694
+ "required": false,
8695
+ "origin": "own",
8696
+ "description": "Matches Input's `Size` variant: Small, Medium, Large.",
8697
+ "values": [
8698
+ "sm",
8699
+ "md",
8700
+ "lg"
8701
+ ]
8702
+ },
8703
+ "placeholder": {
8704
+ "type": "string | undefined",
8705
+ "required": false,
8706
+ "origin": "own"
8707
+ },
8708
+ "value": {
8709
+ "type": "readonly string[] | undefined",
8710
+ "required": false,
8711
+ "origin": "own",
8712
+ "description": "The selected values (controlled), in the order they were chosen."
8713
+ },
8714
+ "defaultValue": {
8715
+ "type": "readonly string[] | undefined",
8716
+ "required": false,
8717
+ "origin": "own",
8718
+ "description": "The initially selected values (uncontrolled)."
8719
+ },
8720
+ "onChange": {
8721
+ "type": "((value: string[]) => void) | undefined",
8722
+ "required": false,
8723
+ "origin": "own",
8724
+ "description": "Receives the whole new selection."
8725
+ },
8726
+ "inputValue": {
8727
+ "type": "string | undefined",
8728
+ "required": false,
8729
+ "origin": "own",
8730
+ "description": "Controlled filter text. Usually only needed for remote filtering."
8731
+ },
8732
+ "onInputChange": {
8733
+ "type": "((value: string) => void) | undefined",
8734
+ "required": false,
8735
+ "origin": "own"
8736
+ },
8737
+ "hideTags": {
8738
+ "type": "boolean | undefined",
8739
+ "required": false,
8740
+ "origin": "own",
8741
+ "description": "Leave the tags out, when the chosen values are already on screen as\nremovable tags — a table's active-filters row. The values are still read\nwith the field, and still marked in the list."
8742
+ },
8743
+ "emptyLabel": {
8744
+ "type": "React.ReactNode",
8745
+ "required": false,
8746
+ "origin": "own",
8747
+ "description": "Shown in place of the list when nothing matches."
8748
+ },
8749
+ "buttonLabel": {
8750
+ "type": "string | undefined",
8751
+ "required": false,
8752
+ "origin": "own",
8753
+ "description": "Accessible label for the disclosure button."
8754
+ },
8755
+ "name": {
8756
+ "type": "string | undefined",
8757
+ "required": false,
8758
+ "origin": "own",
8759
+ "description": "Posts every selected value under this name, for an uncontrolled form."
8760
+ },
8761
+ "className": {
8762
+ "type": "string | undefined",
8763
+ "required": false,
8764
+ "origin": "own",
8765
+ "description": "Class names for the field box (`.ion-input`)."
8766
+ },
8767
+ "wrapperClassName": {
8768
+ "type": "string | undefined",
8769
+ "required": false,
8770
+ "origin": "own",
8771
+ "description": "Class names for the `.ion-field` wrapper."
8772
+ },
8773
+ "id": {
8774
+ "type": "string | undefined",
8775
+ "required": false,
8776
+ "origin": "own"
8777
+ }
8778
+ },
8779
+ "propCounts": {
8780
+ "own": 23,
8781
+ "aria": 0,
8782
+ "dom": 0,
8783
+ "other": 0
8784
+ }
8785
+ },
8786
+ "NavItem": {
8787
+ "name": "NavItem",
8788
+ "source": "src/components/NavItem.tsx",
8789
+ "propsType": "NavItemProps",
8790
+ "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.",
8791
+ "import": "import { NavItem } from 'ionbase-ui';",
8792
+ "status": "stable",
8793
+ "summary": "A primary navigation destination. Renders an <a> when given `href`, a <button> otherwise.",
8794
+ "useWhen": [
8795
+ "building a header nav bar, a sidebar or a section switcher",
8796
+ "an item opens a menu rather than navigating — pass `showChevron` and no `href`"
8797
+ ],
8798
+ "useInstead": [
8799
+ {
8800
+ "when": "the link sits inside body copy or a content block",
8801
+ "use": "Link"
8802
+ },
8803
+ {
8804
+ "when": "it is a row in a dropdown list rather than a top-level destination",
8805
+ "use": "MenuItem"
8806
+ }
8807
+ ],
8808
+ "slots": {
8809
+ "icon": {
8810
+ "accepts": "Icon",
8811
+ "note": "leading icon; omit its `label` — the item's text names it"
8812
+ }
8813
+ },
8814
+ "a11y": {
8815
+ "role": "link when `href` is set, button when it is not",
8816
+ "guarantees": [
8817
+ "hover state comes from React Aria's pointer-aware `useHover`, so a tap does not stay hovered until the next tap elsewhere",
8818
+ "the focus ring shows for keyboard navigation only",
8819
+ "the CSS keeps its own `:hover` / `:focus-visible` rules, so the styling survives without React",
8820
+ "`isCurrent` sets `aria-current=\"page\"`, and the current item is marked by an underline as well as colour, which survives forced-colours mode"
8821
+ ],
8822
+ "requires": [
8823
+ "an accessible name — `children`, or `aria-label` on an icon-only item"
8824
+ ],
8825
+ "notes": [
8826
+ "NavItem is one item, not the landmark. Wrap the set in a <nav> with its own accessible name."
8827
+ ]
8828
+ },
8829
+ "antiPatterns": [
8830
+ {
8831
+ "dont": "`showChevron` on a plain link that navigates",
8832
+ "why": "the chevron promises a menu; nothing opens"
8833
+ },
8834
+ {
8835
+ "dont": "marking the current page with colour only",
8836
+ "do": "`aria-current=\"page\"`",
8837
+ "why": "a screen-reader user is otherwise never told where they are"
8838
+ },
8839
+ {
8840
+ "dont": "marking the current page with a className or colour of your own instead of `isCurrent`",
8841
+ "why": "a screen reader hears nothing without `aria-current`, and a colour-only marker fails anyone who cannot see the difference"
8842
+ }
8843
+ ],
8844
+ "deprecated": [
8845
+ {
8846
+ "prop": "disabled",
8847
+ "replacement": "isDisabled"
8848
+ }
8849
+ ],
8850
+ "stylesheet": "src/styles/nav-item.css",
8851
+ "tokens": [
8852
+ "--border-focus",
8853
+ "--border-primary",
8854
+ "--border-width-thick",
8855
+ "--font-family-sans",
8856
+ "--font-weight-medium",
8857
+ "--icon-disabled",
8858
+ "--icon-interactive-hover",
8859
+ "--icon-primary",
8860
+ "--icon-size-sm",
8861
+ "--icon-tertiary",
8862
+ "--ion-duration-base",
8863
+ "--ion-ease-out",
8864
+ "--radius-sm",
8865
+ "--spacing-4",
8866
+ "--spacing-8",
8867
+ "--text-default",
8868
+ "--text-disabled",
8869
+ "--text-interactive-hover",
8870
+ "--text-secondary",
8871
+ "--type-body-sm",
8872
+ "--type-body-sm-line-height"
8873
+ ],
8874
+ "props": {
8875
+ "icon": {
8876
+ "type": "React.ReactNode",
8877
+ "required": false,
8878
+ "origin": "own",
8879
+ "description": "Figma's `Icon` slot — an optional leading icon."
8880
+ },
8881
+ "showChevron": {
8882
+ "type": "boolean | undefined",
8883
+ "required": false,
8884
+ "origin": "own",
8885
+ "description": "Figma's `Show Chevron`. Set on a nav item that opens a menu, not a plain link."
8886
+ },
8887
+ "isCurrent": {
8888
+ "type": "boolean | undefined",
8889
+ "required": false,
8890
+ "origin": "own",
8891
+ "description": "The page the user is on. Marks the item `aria-current=\"page\"` and draws\nthe same indicator Tabs' underline does — so a section switcher built from\nNavItems says where you are, in words for a screen reader and in more than\ncolour for everyone else."
8892
+ },
8893
+ "isDisabled": {
8894
+ "type": "boolean | undefined",
8895
+ "required": false,
8896
+ "origin": "own"
8897
+ },
8898
+ "disabled": {
8899
+ "type": "boolean | undefined",
8900
+ "required": false,
8901
+ "origin": "own",
8902
+ "tags": {
8903
+ "deprecated": "Use `isDisabled`. Accepted as an alias for one minor version."
8904
+ }
8905
+ },
8906
+ "children": {
8907
+ "type": "React.ReactNode",
8199
8908
  "required": false,
8200
8909
  "origin": "own"
8201
8910
  },
@@ -10212,7 +10921,6 @@
10212
10921
  "--border-width-default",
10213
10922
  "--border-width-thick",
10214
10923
  "--font-family-sans",
10215
- "--font-weight-medium",
10216
10924
  "--icon-disabled",
10217
10925
  "--icon-on-color",
10218
10926
  "--ion-duration-base",
@@ -10304,10 +11012,10 @@
10304
11012
  "name": "RadioGroup",
10305
11013
  "source": "src/components/Radio.tsx",
10306
11014
  "propsType": "RadioGroupProps",
10307
- "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.",
11015
+ "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.\n\nThe fieldset itself is Fieldset's shell, shared with CheckboxGroup, so the\ntwo choice groups take the same label, help, error and orientation props.",
10308
11016
  "import": "import { RadioGroup } from 'ionbase-ui';",
10309
11017
  "status": "stable",
10310
- "summary": "A <fieldset>/<legend> group of mutually exclusive options. Owns the shared name and the selected value.",
11018
+ "summary": "A <fieldset>/<legend> group of mutually exclusive options. Owns the shared name and the selected value, and the group's label, help and error.",
10311
11019
  "useWhen": [
10312
11020
  "exactly one choice from a small set, all worth showing at once",
10313
11021
  "the options need explaining — each one can carry its own label text"
@@ -10319,7 +11027,7 @@
10319
11027
  },
10320
11028
  {
10321
11029
  "when": "more than one may be picked",
10322
- "use": "Checkbox"
11030
+ "use": "CheckboxGroup"
10323
11031
  },
10324
11032
  {
10325
11033
  "when": "there are exactly two states and the change is immediate",
@@ -10348,6 +11056,14 @@
10348
11056
  "danger": {
10349
11057
  "use": "a set where the options are destructive"
10350
11058
  }
11059
+ },
11060
+ "orientation": {
11061
+ "vertical": {
11062
+ "use": "the default, and always when a label runs past a few words"
11063
+ },
11064
+ "horizontal": {
11065
+ "use": "two to four one-word options. Wraps rather than overflows"
11066
+ }
10351
11067
  }
10352
11068
  },
10353
11069
  "slots": {
@@ -10355,6 +11071,14 @@
10355
11071
  "accepts": "text",
10356
11072
  "note": "renders as the <legend> — the question the options answer"
10357
11073
  },
11074
+ "description": {
11075
+ "accepts": "text",
11076
+ "note": "help beneath the options; replaced by `errorMessage` while `isInvalid` is set"
11077
+ },
11078
+ "errorMessage": {
11079
+ "accepts": "text",
11080
+ "note": "shown only while `isInvalid` is set"
11081
+ },
10358
11082
  "children": {
10359
11083
  "accepts": "Radio"
10360
11084
  }
@@ -10364,7 +11088,10 @@
10364
11088
  "guarantees": [
10365
11089
  "a real fieldset rather than `role=\"radiogroup\"`: both announce correctly, but the fieldset also groups the inputs for form submission and native validation",
10366
11090
  "`name` is generated when omitted, so two groups on one page never collide",
10367
- "`isDisabled` cascades to every Radio that has not set its own"
11091
+ "`isDisabled` cascades to every Radio that has not set its own",
11092
+ "the fieldset is Fieldset's shell, shared with CheckboxGroup — the same label, help, error and orientation props",
11093
+ "the description or error is on every radio's `aria-describedby` as well as the fieldset's, so it is read on the radio that takes focus",
11094
+ "`isRequired` is native `required` on the radios, which the platform already reads as \"one of this name\""
10368
11095
  ],
10369
11096
  "requires": [
10370
11097
  "`label`, or `aria-label` when the question is already visible elsewhere"
@@ -10401,7 +11128,6 @@
10401
11128
  "--border-width-default",
10402
11129
  "--border-width-thick",
10403
11130
  "--font-family-sans",
10404
- "--font-weight-medium",
10405
11131
  "--icon-disabled",
10406
11132
  "--icon-on-color",
10407
11133
  "--ion-duration-base",
@@ -10461,35 +11187,69 @@
10461
11187
  "label": {
10462
11188
  "type": "React.ReactNode",
10463
11189
  "required": false,
10464
- "origin": "own"
11190
+ "origin": "own",
11191
+ "description": "The question the options answer. Renders as the `<legend>`."
10465
11192
  },
10466
- "size": {
10467
- "type": "RadioSize | undefined",
11193
+ "description": {
11194
+ "type": "React.ReactNode",
10468
11195
  "required": false,
10469
11196
  "origin": "own",
10470
- "values": [
10471
- "sm",
10472
- "md",
10473
- "lg"
10474
- ]
11197
+ "description": "Help text beneath the options. Replaced by `errorMessage` while invalid."
10475
11198
  },
10476
- "intent": {
10477
- "type": "RadioIntent | undefined",
11199
+ "errorMessage": {
11200
+ "type": "React.ReactNode",
10478
11201
  "required": false,
10479
11202
  "origin": "own",
10480
- "values": [
10481
- "neutral",
10482
- "danger",
10483
- "brand"
10484
- ]
11203
+ "description": "Shown in the description's place while `isInvalid` is set."
10485
11204
  },
10486
- "isDisabled": {
11205
+ "isInvalid": {
10487
11206
  "type": "boolean | undefined",
10488
11207
  "required": false,
10489
11208
  "origin": "own",
10490
- "description": "Whether every radio in the group is disabled."
11209
+ "description": "Shows `errorMessage` in the description's place."
10491
11210
  },
10492
- "disabled": {
11211
+ "isRequired": {
11212
+ "type": "boolean | undefined",
11213
+ "required": false,
11214
+ "origin": "own",
11215
+ "description": "One option must be chosen before the form submits."
11216
+ },
11217
+ "orientation": {
11218
+ "type": "FieldsetOrientation | undefined",
11219
+ "required": false,
11220
+ "origin": "own",
11221
+ "values": [
11222
+ "vertical",
11223
+ "horizontal"
11224
+ ]
11225
+ },
11226
+ "size": {
11227
+ "type": "RadioSize | undefined",
11228
+ "required": false,
11229
+ "origin": "own",
11230
+ "values": [
11231
+ "sm",
11232
+ "md",
11233
+ "lg"
11234
+ ]
11235
+ },
11236
+ "intent": {
11237
+ "type": "RadioIntent | undefined",
11238
+ "required": false,
11239
+ "origin": "own",
11240
+ "values": [
11241
+ "neutral",
11242
+ "danger",
11243
+ "brand"
11244
+ ]
11245
+ },
11246
+ "isDisabled": {
11247
+ "type": "boolean | undefined",
11248
+ "required": false,
11249
+ "origin": "own",
11250
+ "description": "Whether every radio in the group is disabled."
11251
+ },
11252
+ "disabled": {
10493
11253
  "type": "boolean | undefined",
10494
11254
  "required": false,
10495
11255
  "origin": "own",
@@ -10504,7 +11264,7 @@
10504
11264
  }
10505
11265
  },
10506
11266
  "propCounts": {
10507
- "own": 10,
11267
+ "own": 15,
10508
11268
  "aria": 0,
10509
11269
  "dom": 276,
10510
11270
  "other": 0
@@ -10527,126 +11287,613 @@
10527
11287
  "when": "the sections are peer views rather than positions in one document",
10528
11288
  "use": "Tabs"
10529
11289
  },
10530
- {
10531
- "when": "it is site navigation rather than in-page position",
10532
- "use": "NavItem"
10533
- }
10534
- ],
10535
- "variants": {
10536
- "placement": {
10537
- "right": {
10538
- "use": "the default — a rail on the left of the content, opening across it"
10539
- },
10540
- "left": {
10541
- "use": "a rail against the right edge of the page; `right` would open the list off the screen"
10542
- }
10543
- }
10544
- },
10545
- "a11y": {
10546
- "role": "disclosure — a real <button> with aria-expanded and aria-controls",
10547
- "guarantees": [
10548
- "the rail is reachable by Tab and opens with Enter or Space, not just hover",
10549
- "closes on outside pointerdown and on Escape"
10550
- ],
10551
- "requires": [
10552
- "`progress` as 0–100 and `activeId` — the component has no opinion on where they come from",
10553
- "handling `onSelect` yourself: it reports the choice and does not scroll anywhere"
10554
- ],
10555
- "notes": [
10556
- "Deliberately not `role=\"menu\"` — see the stylesheet header. It is a disclosure with a list inside it, not a menu with roving focus.",
10557
- "Not a focus trap: it is the minimum a disclosure needs to not trap the page once opened."
10558
- ]
10559
- },
10560
- "antiPatterns": [
10561
- {
10562
- "dont": "expecting it to scroll the page when a section is chosen",
10563
- "why": "it reports the choice, matching Menu — what \"select this section\" means is the page's decision"
11290
+ {
11291
+ "when": "it is site navigation rather than in-page position",
11292
+ "use": "NavItem"
11293
+ }
11294
+ ],
11295
+ "variants": {
11296
+ "placement": {
11297
+ "right": {
11298
+ "use": "the default — a rail on the left of the content, opening across it"
11299
+ },
11300
+ "left": {
11301
+ "use": "a rail against the right edge of the page; `right` would open the list off the screen"
11302
+ }
11303
+ }
11304
+ },
11305
+ "a11y": {
11306
+ "role": "disclosure — a real <button> with aria-expanded and aria-controls",
11307
+ "guarantees": [
11308
+ "the rail is reachable by Tab and opens with Enter or Space, not just hover",
11309
+ "closes on outside pointerdown and on Escape"
11310
+ ],
11311
+ "requires": [
11312
+ "`progress` as 0–100 and `activeId` — the component has no opinion on where they come from",
11313
+ "handling `onSelect` yourself: it reports the choice and does not scroll anywhere"
11314
+ ],
11315
+ "notes": [
11316
+ "Deliberately not `role=\"menu\"` — see the stylesheet header. It is a disclosure with a list inside it, not a menu with roving focus.",
11317
+ "Not a focus trap: it is the minimum a disclosure needs to not trap the page once opened."
11318
+ ]
11319
+ },
11320
+ "antiPatterns": [
11321
+ {
11322
+ "dont": "expecting it to scroll the page when a section is chosen",
11323
+ "why": "it reports the choice, matching Menu — what \"select this section\" means is the page's decision"
11324
+ },
11325
+ {
11326
+ "dont": "deriving `progress` from a scroll listener without throttling",
11327
+ "why": "this re-renders the rail on every scroll frame"
11328
+ },
11329
+ {
11330
+ "dont": "expecting every section to be visible at once",
11331
+ "why": "the panel caps at 60vh and scrolls — a document-length index would otherwise grow off both edges of the viewport, taking the rows nearest the active one with it"
11332
+ },
11333
+ {
11334
+ "dont": "leaving `placement` at `right` for a rail on the page's right edge",
11335
+ "why": "the open list runs past the viewport; `left` opens it back over the content"
11336
+ }
11337
+ ],
11338
+ "stylesheet": "src/styles/scroll-progress.css",
11339
+ "tokens": [
11340
+ "--border-default",
11341
+ "--border-focus",
11342
+ "--border-strong",
11343
+ "--border-stronger",
11344
+ "--border-width-default",
11345
+ "--border-width-thick",
11346
+ "--font-family-sans",
11347
+ "--font-weight-regular",
11348
+ "--ion-duration-base",
11349
+ "--ion-ease-out",
11350
+ "--ion-scroll-progress-slide",
11351
+ "--ion-shadow-shadow-md",
11352
+ "--radius-full",
11353
+ "--radius-md",
11354
+ "--radius-sm",
11355
+ "--spacing-24",
11356
+ "--spacing-32",
11357
+ "--spacing-4",
11358
+ "--spacing-6",
11359
+ "--spacing-8",
11360
+ "--surface-default",
11361
+ "--surface-muted",
11362
+ "--text-default",
11363
+ "--text-secondary",
11364
+ "--text-tertiary",
11365
+ "--type-body-sm",
11366
+ "--type-body-sm-line-height",
11367
+ "--type-caption",
11368
+ "--type-caption-line-height"
11369
+ ],
11370
+ "props": {
11371
+ "progress": {
11372
+ "type": "number",
11373
+ "required": true,
11374
+ "origin": "own",
11375
+ "description": "0-100. The caller computes this from whatever it is tracking — a\nwindow scroll fraction, a custom scroll container, a reading-time\nestimate. This component has no opinion on the source."
11376
+ },
11377
+ "sections": {
11378
+ "type": "ScrollProgressSection[]",
11379
+ "required": true,
11380
+ "origin": "own"
11381
+ },
11382
+ "activeId": {
11383
+ "type": "string | undefined",
11384
+ "required": false,
11385
+ "origin": "own",
11386
+ "description": "The section the rail's heavier tick and the panel's selected row point\nto. Also the caller's to compute — see `progress`."
11387
+ },
11388
+ "onSelect": {
11389
+ "type": "((id: string) => void) | undefined",
11390
+ "required": false,
11391
+ "origin": "own",
11392
+ "description": "Fired when a row is chosen. The component does not scroll anywhere\nitself; matching Menu, it reports the choice and lets the caller decide\nwhat \"select this section\" means for their page."
11393
+ },
11394
+ "placement": {
11395
+ "type": "ScrollProgressPlacement | undefined",
11396
+ "required": false,
11397
+ "origin": "own",
11398
+ "description": "Which way the section list opens. `right` suits a rail on the left of the\ncontent; a rail against the right edge of the page needs `left`, or the\nopen list runs off the screen.",
11399
+ "values": [
11400
+ "right",
11401
+ "left"
11402
+ ]
11403
+ }
11404
+ },
11405
+ "propCounts": {
11406
+ "own": 5,
11407
+ "aria": 0,
11408
+ "dom": 277,
11409
+ "other": 0
11410
+ }
11411
+ },
11412
+ "SearchField": {
11413
+ "name": "SearchField",
11414
+ "source": "src/components/SearchField.tsx",
11415
+ "propsType": "SearchFieldProps",
11416
+ "description": "SearchField — a text field for a search query.\n\nNOT `<Input type=\"search\">`. That gets the right input type and nothing\nelse. `useSearchField` adds what a search box owes its user:\n\n `role=\"searchbox\"`, so a screen reader announces a search field, not a\n text field. Enter calls `onSubmit` with the query. Escape clears it — a\n second Escape then reaches whatever the field sits in, so a search inside\n a dialog clears first and closes second. A clear button appears once there\n is something to clear, named in the user's language by React Aria.\n\nThe clear button is out of the tab order on purpose, as React Aria sets it:\nEscape is the keyboard's way to clear, and an extra tab stop in every search\nbox costs every keyboard user a keystroke. Pressing it puts focus back in the\nfield.\n\nThe box IS Input's — the component renders `.ion-input` and its size and\nstate classes, so a search field beside an Input in a toolbar matches it\nexactly and cannot drift. Same arrangement as NumberInput.",
11417
+ "import": "import { SearchField } from 'ionbase-ui';",
11418
+ "status": "stable",
11419
+ "summary": "A text field for a search query: role=\"searchbox\", Enter submits, Escape clears, and a clear button once there is something to clear. Input's box and sizes.",
11420
+ "useWhen": [
11421
+ "the user types to search or filter a list, a table or a page — the search above a DataTable, the filter at the top of a Sidebar",
11422
+ "the query is free text that narrows what is shown, not a value chosen from a list"
11423
+ ],
11424
+ "useInstead": [
11425
+ {
11426
+ "when": "the typed text picks one value from a known list — an assignee, a country",
11427
+ "use": "Combobox",
11428
+ "why": "a combobox commits a choice and offers the options; a search field commits a query and offers nothing"
11429
+ },
11430
+ {
11431
+ "when": "the user is looking for a page or an action anywhere in the product",
11432
+ "use": "CommandPalette",
11433
+ "why": "that is global and keyboard-first; a SearchField searches the content in front of it"
11434
+ },
11435
+ {
11436
+ "when": "it is ordinary text entry — a name, an email — that happens to be used for lookup later",
11437
+ "use": "Input",
11438
+ "why": "announcing a searchbox promises search behaviour: Escape clears, Enter searches"
11439
+ }
11440
+ ],
11441
+ "composition": {
11442
+ "order": [
11443
+ "SearchField"
11444
+ ],
11445
+ "note": "Filter as the user types with `onChange`, or search on Enter with `onSubmit` — pick one per field and say which in the placeholder or label. In a table toolbar, give it `aria-label` naming what it searches.",
11446
+ "example": "<SearchField aria-label=\"Search agents\" placeholder=\"Search by name or purpose\" value={query} onChange={setQuery} />"
11447
+ },
11448
+ "variants": {
11449
+ "size": {
11450
+ "sm": {
11451
+ "use": "toolbars, table headers and Sidebar filters, beside small Buttons"
11452
+ },
11453
+ "md": {
11454
+ "use": "the default"
11455
+ },
11456
+ "lg": {
11457
+ "use": "a page whose main task is searching"
11458
+ }
11459
+ }
11460
+ },
11461
+ "a11y": {
11462
+ "role": "searchbox",
11463
+ "guarantees": [
11464
+ "the input is role=\"searchbox\" and type=\"search\", so it is announced as a search field",
11465
+ "Escape clears the query; a second Escape is left for whatever contains the field, so a search in a dialog clears before it closes",
11466
+ "Enter calls onSubmit with the query",
11467
+ "the clear button is named by React Aria in the user's language, appears only when there is something to clear, and returns focus to the field",
11468
+ "the clear button is out of the tab order on purpose: Escape is the keyboard's way to clear, and an extra tab stop in every search box costs every keyboard user a keystroke",
11469
+ "the browser's own cancel button is hidden, so there are never two"
11470
+ ],
11471
+ "requires": [
11472
+ "`label`, or an `aria-label` naming what is searched when no visible label is rendered — \"Search\" alone does not say what"
11473
+ ],
11474
+ "notes": [
11475
+ "When filtering as the user types, announce the result count with a polite live region near the results. The field cannot know how many results its query produced; the list does."
11476
+ ]
11477
+ },
11478
+ "antiPatterns": [
11479
+ {
11480
+ "dont": "<Input type=\"search\"> with a magnifier as leadingIcon",
11481
+ "why": "it looks the same and has none of the behaviour: no searchbox role, no Escape to clear, no labelled clear button"
11482
+ },
11483
+ {
11484
+ "dont": "placeholder as the only name",
11485
+ "why": "a placeholder disappears once the user types, and is not a reliable accessible name. Pass `aria-label` or `label`"
11486
+ },
11487
+ {
11488
+ "dont": "a separate Search button beside a field that already filters as you type",
11489
+ "why": "it teaches the user that nothing happens until they press it, and it does nothing when they do"
11490
+ }
11491
+ ],
11492
+ "stylesheet": "src/styles/search-field.css",
11493
+ "tokens": [
11494
+ "--border-focus",
11495
+ "--border-width-thick",
11496
+ "--icon-default",
11497
+ "--icon-secondary",
11498
+ "--ion-input-height",
11499
+ "--ion-input-icon-size",
11500
+ "--radius-sm",
11501
+ "--spacing-4",
11502
+ "--spacing-8",
11503
+ "--surface-default",
11504
+ "--surface-hover",
11505
+ "--surface-pressed"
11506
+ ],
11507
+ "props": {
11508
+ "size": {
11509
+ "type": "SearchFieldSize | undefined",
11510
+ "required": false,
11511
+ "origin": "own",
11512
+ "description": "Input's sizes: Small, Medium, Large.",
11513
+ "values": [
11514
+ "sm",
11515
+ "md",
11516
+ "lg"
11517
+ ],
11518
+ "default": "md"
11519
+ },
11520
+ "className": {
11521
+ "type": "string | undefined",
11522
+ "required": false,
11523
+ "origin": "own",
11524
+ "description": "Class names for the control box (`.ion-input`)."
11525
+ },
11526
+ "wrapperClassName": {
11527
+ "type": "string | undefined",
11528
+ "required": false,
11529
+ "origin": "own",
11530
+ "description": "Class names for the `.ion-field` wrapper when a label or helper is shown."
11531
+ },
11532
+ "enterKeyHint": {
11533
+ "type": "\"done\" | \"enter\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\" | undefined",
11534
+ "required": false,
11535
+ "origin": "aria",
11536
+ "description": "An enumerated attribute that defines what action label or icon to preset for the enter key on\nvirtual keyboards. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/enterkeyhint).",
11537
+ "values": [
11538
+ "done",
11539
+ "enter",
11540
+ "go",
11541
+ "next",
11542
+ "previous",
11543
+ "search",
11544
+ "send"
11545
+ ]
11546
+ },
11547
+ "type": {
11548
+ "type": "(string & {}) | \"search\" | \"text\" | \"url\" | \"tel\" | \"email\" | \"password\" | undefined",
11549
+ "required": false,
11550
+ "origin": "aria",
11551
+ "description": "The type of input to render. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdeftype).",
11552
+ "tags": {
11553
+ "default": "'search'"
11554
+ }
11555
+ },
11556
+ "onSubmit": {
11557
+ "type": "((value: string) => void) | undefined",
11558
+ "required": false,
11559
+ "origin": "other",
11560
+ "description": "Handler that is called when the SearchField is submitted."
11561
+ },
11562
+ "onClear": {
11563
+ "type": "(() => void) | undefined",
11564
+ "required": false,
11565
+ "origin": "other",
11566
+ "description": "Handler that is called when the clear button is pressed."
11567
+ },
11568
+ "isDisabled": {
11569
+ "type": "boolean | undefined",
11570
+ "required": false,
11571
+ "origin": "aria",
11572
+ "description": "Whether the input is disabled."
11573
+ },
11574
+ "isReadOnly": {
11575
+ "type": "boolean | undefined",
11576
+ "required": false,
11577
+ "origin": "aria",
11578
+ "description": "Whether the input can be selected but not changed by the user."
11579
+ },
11580
+ "isRequired": {
11581
+ "type": "boolean | undefined",
11582
+ "required": false,
11583
+ "origin": "aria",
11584
+ "description": "Whether user input is required on the input before form submission."
11585
+ },
11586
+ "isInvalid": {
11587
+ "type": "boolean | undefined",
11588
+ "required": false,
11589
+ "origin": "aria",
11590
+ "description": "Whether the input value is invalid."
11591
+ },
11592
+ "validationState": {
11593
+ "type": "ValidationState | undefined",
11594
+ "required": false,
11595
+ "origin": "aria",
11596
+ "values": [
11597
+ "valid",
11598
+ "invalid"
11599
+ ],
11600
+ "tags": {
11601
+ "deprecated": "Use `isInvalid` instead."
11602
+ }
11603
+ },
11604
+ "validationBehavior": {
11605
+ "type": "\"aria\" | \"native\" | undefined",
11606
+ "required": false,
11607
+ "origin": "aria",
11608
+ "description": "Whether to use native HTML form validation to prevent form submission\nwhen the value is missing or invalid, or mark the field as required\nor invalid via ARIA.",
11609
+ "values": [
11610
+ "aria",
11611
+ "native"
11612
+ ],
11613
+ "tags": {
11614
+ "default": "'aria'"
11615
+ }
11616
+ },
11617
+ "validate": {
11618
+ "type": "((value: string) => ValidationError | true | null | undefined) | undefined",
11619
+ "required": false,
11620
+ "origin": "aria",
11621
+ "description": "A function that returns an error message if a given value is invalid.\nValidation errors are displayed to the user when the form is submitted\nif `validationBehavior=\"native\"`. For realtime validation, use the `isInvalid`\nprop instead."
11622
+ },
11623
+ "description": {
11624
+ "type": "ReactNode",
11625
+ "required": false,
11626
+ "origin": "aria",
11627
+ "description": "A description for the field. Provides a hint such as specific requirements for what to choose."
11628
+ },
11629
+ "errorMessage": {
11630
+ "type": "ReactNode | ((v: ValidationResult) => ReactNode)",
11631
+ "required": false,
11632
+ "origin": "aria",
11633
+ "description": "An error message for the field."
11634
+ },
11635
+ "autoFocus": {
11636
+ "type": "boolean | undefined",
11637
+ "required": false,
11638
+ "origin": "aria",
11639
+ "description": "Whether the element should receive focus on render."
11640
+ },
11641
+ "onFocus": {
11642
+ "type": "((e: FocusEvent<HTMLInputElement, Element>) => void) | undefined",
11643
+ "required": false,
11644
+ "origin": "aria",
11645
+ "description": "Handler that is called when the element receives focus."
11646
+ },
11647
+ "onBlur": {
11648
+ "type": "((e: FocusEvent<HTMLInputElement, Element>) => void) | undefined",
11649
+ "required": false,
11650
+ "origin": "aria",
11651
+ "description": "Handler that is called when the element loses focus."
11652
+ },
11653
+ "onFocusChange": {
11654
+ "type": "((isFocused: boolean) => void) | undefined",
11655
+ "required": false,
11656
+ "origin": "aria",
11657
+ "description": "Handler that is called when the element's focus status changes."
11658
+ },
11659
+ "onKeyDown": {
11660
+ "type": "((e: KeyboardEvent) => void) | undefined",
11661
+ "required": false,
11662
+ "origin": "aria",
11663
+ "description": "Handler that is called when a key is pressed."
11664
+ },
11665
+ "onKeyUp": {
11666
+ "type": "((e: KeyboardEvent) => void) | undefined",
11667
+ "required": false,
11668
+ "origin": "aria",
11669
+ "description": "Handler that is called when a key is released."
11670
+ },
11671
+ "placeholder": {
11672
+ "type": "string | undefined",
11673
+ "required": false,
11674
+ "origin": "aria",
11675
+ "description": "Temporary text that occupies the text input when it is empty."
11676
+ },
11677
+ "value": {
11678
+ "type": "string | undefined",
11679
+ "required": false,
11680
+ "origin": "aria",
11681
+ "description": "The current value (controlled)."
11682
+ },
11683
+ "defaultValue": {
11684
+ "type": "string | undefined",
11685
+ "required": false,
11686
+ "origin": "aria",
11687
+ "description": "The default value (uncontrolled)."
11688
+ },
11689
+ "onChange": {
11690
+ "type": "((value: string) => void) | undefined",
11691
+ "required": false,
11692
+ "origin": "aria",
11693
+ "description": "Handler that is called when the value changes."
11694
+ },
11695
+ "label": {
11696
+ "type": "ReactNode",
11697
+ "required": false,
11698
+ "origin": "aria",
11699
+ "description": "The content to display as the label."
11700
+ },
11701
+ "id": {
11702
+ "type": "string | undefined",
11703
+ "required": false,
11704
+ "origin": "aria",
11705
+ "description": "The element's unique identifier. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id)."
11706
+ },
11707
+ "spellCheck": {
11708
+ "type": "string | undefined",
11709
+ "required": false,
11710
+ "origin": "aria",
11711
+ "description": "An enumerated attribute that defines whether the element may be checked for spelling errors.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/spellcheck)."
11712
+ },
11713
+ "autoCorrect": {
11714
+ "type": "string | undefined",
11715
+ "required": false,
11716
+ "origin": "aria",
11717
+ "description": "An attribute that takes as its value a space-separated string that describes what, if any, type\nof autocomplete functionality the input should provide. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#autocomplete)."
11718
+ },
11719
+ "inputMode": {
11720
+ "type": "\"none\" | \"search\" | \"text\" | \"url\" | \"tel\" | \"email\" | \"numeric\" | \"decimal\" | undefined",
11721
+ "required": false,
11722
+ "origin": "aria",
11723
+ "description": "Hints at the type of data that might be entered by the user while editing the element or its\ncontents. See\n[MDN](https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute).",
11724
+ "values": [
11725
+ "none",
11726
+ "search",
11727
+ "text",
11728
+ "url",
11729
+ "tel",
11730
+ "email",
11731
+ "numeric",
11732
+ "decimal"
11733
+ ]
11734
+ },
11735
+ "aria-activedescendant": {
11736
+ "type": "string | undefined",
11737
+ "required": false,
11738
+ "origin": "aria",
11739
+ "description": "Identifies the currently active element when DOM focus is on a composite widget, textbox,\ngroup, or application."
11740
+ },
11741
+ "aria-autocomplete": {
11742
+ "type": "\"inline\" | \"list\" | \"none\" | \"both\" | undefined",
11743
+ "required": false,
11744
+ "origin": "aria",
11745
+ "description": "Indicates whether inputting text could trigger display of one or more predictions of the user's\nintended value for an input and specifies how predictions would be presented if they are made.",
11746
+ "values": [
11747
+ "inline",
11748
+ "list",
11749
+ "none",
11750
+ "both"
11751
+ ]
11752
+ },
11753
+ "aria-controls": {
11754
+ "type": "string | undefined",
11755
+ "required": false,
11756
+ "origin": "aria",
11757
+ "description": "Identifies the element (or elements) whose contents or presence are controlled by the current\nelement."
11758
+ },
11759
+ "aria-describedby": {
11760
+ "type": "string | undefined",
11761
+ "required": false,
11762
+ "origin": "aria",
11763
+ "description": "Identifies the element (or elements) that describes the object."
11764
+ },
11765
+ "aria-details": {
11766
+ "type": "string | undefined",
11767
+ "required": false,
11768
+ "origin": "aria",
11769
+ "description": "Identifies the element (or elements) that provide a detailed, extended description for the\nobject."
11770
+ },
11771
+ "aria-errormessage": {
11772
+ "type": "string | undefined",
11773
+ "required": false,
11774
+ "origin": "aria",
11775
+ "description": "Identifies the element that provides an error message for the object."
11776
+ },
11777
+ "aria-haspopup": {
11778
+ "type": "boolean | \"true\" | \"false\" | \"menu\" | \"listbox\" | \"tree\" | \"grid\" | \"dialog\" | undefined",
11779
+ "required": false,
11780
+ "origin": "aria",
11781
+ "description": "Indicates the availability and type of interactive popup element, such as menu or dialog, that\ncan be triggered by an element."
11782
+ },
11783
+ "aria-label": {
11784
+ "type": "string | undefined",
11785
+ "required": false,
11786
+ "origin": "aria",
11787
+ "description": "Defines a string value that labels the current element."
11788
+ },
11789
+ "aria-labelledby": {
11790
+ "type": "string | undefined",
11791
+ "required": false,
11792
+ "origin": "aria",
11793
+ "description": "Identifies the element (or elements) that labels the current element."
11794
+ },
11795
+ "onCopy": {
11796
+ "type": "ClipboardEventHandler<HTMLInputElement> | undefined",
11797
+ "required": false,
11798
+ "origin": "aria",
11799
+ "description": "Handler that is called when the user copies text. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/oncopy)."
11800
+ },
11801
+ "onCut": {
11802
+ "type": "ClipboardEventHandler<HTMLInputElement> | undefined",
11803
+ "required": false,
11804
+ "origin": "aria",
11805
+ "description": "Handler that is called when the user cuts text. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/oncut)."
11806
+ },
11807
+ "onPaste": {
11808
+ "type": "ClipboardEventHandler<HTMLInputElement> | undefined",
11809
+ "required": false,
11810
+ "origin": "aria",
11811
+ "description": "Handler that is called when the user pastes text. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/onpaste)."
11812
+ },
11813
+ "onCompositionEnd": {
11814
+ "type": "CompositionEventHandler<HTMLInputElement> | undefined",
11815
+ "required": false,
11816
+ "origin": "aria",
11817
+ "description": "Handler that is called when a text composition system completes or cancels the current text\ncomposition session. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionend_event)."
10564
11818
  },
10565
- {
10566
- "dont": "deriving `progress` from a scroll listener without throttling",
10567
- "why": "this re-renders the rail on every scroll frame"
11819
+ "onCompositionStart": {
11820
+ "type": "CompositionEventHandler<HTMLInputElement> | undefined",
11821
+ "required": false,
11822
+ "origin": "aria",
11823
+ "description": "Handler that is called when a text composition system starts a new text composition session.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event)."
10568
11824
  },
10569
- {
10570
- "dont": "expecting every section to be visible at once",
10571
- "why": "the panel caps at 60vh and scrolls — a document-length index would otherwise grow off both edges of the viewport, taking the rows nearest the active one with it"
11825
+ "onCompositionUpdate": {
11826
+ "type": "CompositionEventHandler<HTMLInputElement> | undefined",
11827
+ "required": false,
11828
+ "origin": "aria",
11829
+ "description": "Handler that is called when a new character is received in the current text composition\nsession. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionupdate_event)."
10572
11830
  },
10573
- {
10574
- "dont": "leaving `placement` at `right` for a rail on the page's right edge",
10575
- "why": "the open list runs past the viewport; `left` opens it back over the content"
10576
- }
10577
- ],
10578
- "stylesheet": "src/styles/scroll-progress.css",
10579
- "tokens": [
10580
- "--border-default",
10581
- "--border-focus",
10582
- "--border-strong",
10583
- "--border-stronger",
10584
- "--border-width-default",
10585
- "--border-width-thick",
10586
- "--font-family-sans",
10587
- "--font-weight-regular",
10588
- "--ion-duration-base",
10589
- "--ion-ease-out",
10590
- "--ion-scroll-progress-slide",
10591
- "--ion-shadow-shadow-md",
10592
- "--radius-full",
10593
- "--radius-md",
10594
- "--radius-sm",
10595
- "--spacing-24",
10596
- "--spacing-32",
10597
- "--spacing-4",
10598
- "--spacing-6",
10599
- "--spacing-8",
10600
- "--surface-default",
10601
- "--surface-muted",
10602
- "--text-default",
10603
- "--text-secondary",
10604
- "--text-tertiary",
10605
- "--type-body-sm",
10606
- "--type-body-sm-line-height",
10607
- "--type-caption",
10608
- "--type-caption-line-height"
10609
- ],
10610
- "props": {
10611
- "progress": {
10612
- "type": "number",
10613
- "required": true,
10614
- "origin": "own",
10615
- "description": "0-100. The caller computes this from whatever it is tracking — a\nwindow scroll fraction, a custom scroll container, a reading-time\nestimate. This component has no opinion on the source."
11831
+ "onBeforeInput": {
11832
+ "type": "FormEventHandler<HTMLInputElement> | undefined",
11833
+ "required": false,
11834
+ "origin": "aria",
11835
+ "description": "Handler that is called when the input value is about to be modified. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event)."
10616
11836
  },
10617
- "sections": {
10618
- "type": "ScrollProgressSection[]",
10619
- "required": true,
10620
- "origin": "own"
11837
+ "onInput": {
11838
+ "type": "FormEventHandler<HTMLInputElement> | undefined",
11839
+ "required": false,
11840
+ "origin": "aria",
11841
+ "description": "Handler that is called when the input value is modified. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event)."
10621
11842
  },
10622
- "activeId": {
11843
+ "onSelect": {
11844
+ "type": "ReactEventHandler<HTMLInputElement> | undefined",
11845
+ "required": false,
11846
+ "origin": "aria",
11847
+ "description": "Handler that is called when text in the input is selected. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/select_event)."
11848
+ },
11849
+ "form": {
10623
11850
  "type": "string | undefined",
10624
11851
  "required": false,
10625
- "origin": "own",
10626
- "description": "The section the rail's heavier tick and the panel's selected row point\nto. Also the caller's to compute — see `progress`."
11852
+ "origin": "aria",
11853
+ "description": "The `<form>` element to associate the input with.\nThe value of this attribute must be the id of a `<form>` in the same document.\nSee [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form)."
10627
11854
  },
10628
- "onSelect": {
10629
- "type": "((id: string) => void) | undefined",
11855
+ "name": {
11856
+ "type": "string | undefined",
10630
11857
  "required": false,
10631
- "origin": "own",
10632
- "description": "Fired when a row is chosen. The component does not scroll anywhere\nitself; matching Menu, it reports the choice and lets the caller decide\nwhat \"select this section\" means for their page."
11858
+ "origin": "aria",
11859
+ "description": "The name of the input element, used when submitting an HTML form. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname)."
10633
11860
  },
10634
- "placement": {
10635
- "type": "ScrollProgressPlacement | undefined",
11861
+ "autoComplete": {
11862
+ "type": "string | undefined",
10636
11863
  "required": false,
10637
- "origin": "own",
10638
- "description": "Which way the section list opens. `right` suits a rail on the left of the\ncontent; a rail against the right edge of the page needs `left`, or the\nopen list runs off the screen.",
10639
- "values": [
10640
- "right",
10641
- "left"
10642
- ]
11864
+ "origin": "aria",
11865
+ "description": "Describes the type of autocomplete functionality the input should provide if any. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefautocomplete)."
11866
+ },
11867
+ "maxLength": {
11868
+ "type": "number | undefined",
11869
+ "required": false,
11870
+ "origin": "aria",
11871
+ "description": "The maximum number of characters supported by the input. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefmaxlength)."
11872
+ },
11873
+ "minLength": {
11874
+ "type": "number | undefined",
11875
+ "required": false,
11876
+ "origin": "aria",
11877
+ "description": "The minimum number of characters required by the input. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefminlength)."
11878
+ },
11879
+ "pattern": {
11880
+ "type": "string | undefined",
11881
+ "required": false,
11882
+ "origin": "aria",
11883
+ "description": "Regex pattern that the value of the input must match to be valid. See\n[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefpattern)."
11884
+ },
11885
+ "excludeFromTabOrder": {
11886
+ "type": "boolean | undefined",
11887
+ "required": false,
11888
+ "origin": "aria",
11889
+ "description": "Whether to exclude the element from the sequential tab order. If true,\nthe element will not be focusable via the keyboard by tabbing. This should\nbe avoided except in rare scenarios where an alternative means of accessing\nthe element or its functionality via the keyboard is available."
10643
11890
  }
10644
11891
  },
10645
11892
  "propCounts": {
10646
- "own": 5,
10647
- "aria": 0,
10648
- "dom": 277,
10649
- "other": 0
11893
+ "own": 3,
11894
+ "aria": 51,
11895
+ "dom": 264,
11896
+ "other": 2
10650
11897
  }
10651
11898
  },
10652
11899
  "SegmentedControl": {
@@ -11017,7 +12264,8 @@
11017
12264
  },
11018
12265
  {
11019
12266
  "when": "the user may pick more than one",
11020
- "use": "Checkbox"
12267
+ "use": "MultiSelect",
12268
+ "why": "a native <select multiple> needs Ctrl or Cmd for a second pick and cannot filter; for a handful of options use CheckboxGroup"
11021
12269
  }
11022
12270
  ],
11023
12271
  "variants": {
@@ -12339,8 +13587,8 @@
12339
13587
  "origin": "own",
12340
13588
  "description": "`horizontal` for a page-width header above the form, `vertical` for a side\nrail. Below 40rem a horizontal stepper hides every label but the current\none, so it does not need a second variant for mobile.",
12341
13589
  "values": [
12342
- "horizontal",
12343
- "vertical"
13590
+ "vertical",
13591
+ "horizontal"
12344
13592
  ]
12345
13593
  },
12346
13594
  "children": {
@@ -13467,13 +14715,13 @@
13467
14715
  "default": "md"
13468
14716
  },
13469
14717
  "orientation": {
13470
- "type": "\"horizontal\" | \"vertical\" | undefined",
14718
+ "type": "\"vertical\" | \"horizontal\" | undefined",
13471
14719
  "required": false,
13472
14720
  "origin": "own",
13473
14721
  "description": "Drives both the arrow-key axis and the track layout.\n\n`vertical` is keyboard- and ARIA-complete — up/down move between tabs, and\nthe track stacks — but its *decoration* is not: the underline rule and the\npill track's padding are still written for the horizontal axis only. Those\nare Figma's to specify, not this file's to invent. See tabs.css.",
13474
14722
  "values": [
13475
- "horizontal",
13476
- "vertical"
14723
+ "vertical",
14724
+ "horizontal"
13477
14725
  ],
13478
14726
  "tags": {
13479
14727
  "default": "'horizontal'"
@@ -15107,6 +16355,136 @@
15107
16355
  "other": 0
15108
16356
  }
15109
16357
  },
16358
+ "Toolbar": {
16359
+ "name": "Toolbar",
16360
+ "source": "src/components/Toolbar.tsx",
16361
+ "propsType": "ToolbarProps",
16362
+ "description": "Toolbar — a row of controls that act on one thing, reached as one stop.\n\nReact Aria's `useToolbar` gives it `role=\"toolbar\"` and `aria-orientation`,\narrow keys between its controls (mirrored in right-to-left), Tab that leaves\nthe whole toolbar rather than walking every button, and focus returned to the\ncontrol used last when Tab brings the user back. A toolbar inside a toolbar\nbecomes a `group`, so nesting one for grouping is safe.\n\nEvery control keeps its own tab stop in the DOM — React Aria moves focus to\nthe first or last control on Tab and lets the browser take it from there —\nso nothing here rewrites a child's `tabIndex`, and a control that mounts\nlater (the bulk actions appearing on selection) needs no registration.\n\nWHAT IT IS NOT FOR\n\nA filter bar. Tab leaves a toolbar in one press, so a search field inside one\nstrands the filters after it: Tab skips them and ← → belong to the caret.\nThat is WAI-ARIA's own caution about text fields in toolbars. A table's\nsearch and filters stay ordinary tab stops; the toolbar is the row of\nactions — the batch bar, a record's actions, an editor's formatting.",
16363
+ "import": "import { Toolbar } from 'ionbase-ui';",
16364
+ "status": "stable",
16365
+ "summary": "A row of controls that act on one thing, reached as one tab stop: arrow keys between the controls, Tab out in one press, and back to the control used last.",
16366
+ "useWhen": [
16367
+ "a set of actions on one selection or one record — the batch bar over a table, a run's Resume/Pause/Stop, an editor's formatting",
16368
+ "the row has enough controls that tabbing through each one costs a keyboard user more than it gives"
16369
+ ],
16370
+ "useInstead": [
16371
+ {
16372
+ "when": "the row holds a search field or filters",
16373
+ "use": "a plain flex row",
16374
+ "why": "Tab leaves a toolbar in one press and ← → belong to the caret, so a text field inside strands every filter after it — WAI-ARIA's own caution"
16375
+ },
16376
+ {
16377
+ "when": "two or three actions at the top of a page",
16378
+ "use": "PageHeader",
16379
+ "why": "its `actions` slot is a row of ordinary tab stops, which for a few buttons is what a keyboard user expects"
16380
+ },
16381
+ {
16382
+ "when": "the controls are one choice among options",
16383
+ "use": "SegmentedControl",
16384
+ "why": "that is a radio group with a selected value; a toolbar holds independent actions"
16385
+ }
16386
+ ],
16387
+ "composition": {
16388
+ "order": [
16389
+ "Toolbar",
16390
+ "Button",
16391
+ "Divider",
16392
+ "MenuTrigger"
16393
+ ],
16394
+ "note": "Buttons in the order they are read, a vertical Divider between groups, and a MenuTrigger with an icon-only Button last for the overflow. Name it with `aria-label` for what it acts on — \"Bulk actions\", not \"Toolbar\".",
16395
+ "example": "<Toolbar aria-label=\"Bulk actions\"><Button size=\"sm\" variant=\"secondary\">Pause</Button><Divider orientation=\"vertical\" /><Button size=\"sm\" variant=\"destructive\">Delete 3</Button></Toolbar>"
16396
+ },
16397
+ "variants": {
16398
+ "orientation": {
16399
+ "horizontal": {
16400
+ "use": "the default — ← → move between controls; wraps rather than overflowing"
16401
+ },
16402
+ "vertical": {
16403
+ "use": "a side rail of tools — ↑ ↓ move between controls"
16404
+ }
16405
+ }
16406
+ },
16407
+ "slots": {
16408
+ "children": {
16409
+ "accepts": "Button, MenuTrigger, SegmentedControl, Divider, Toolbar",
16410
+ "note": "a nested Toolbar becomes a named `group`, and the outer toolbar keeps the arrow keys"
16411
+ }
16412
+ },
16413
+ "a11y": {
16414
+ "role": "toolbar, with aria-orientation",
16415
+ "guarantees": [
16416
+ "React Aria's `useToolbar`: arrow keys move between controls, skipping disabled ones and separators, and are mirrored in right-to-left",
16417
+ "Tab leaves the whole toolbar in one press, and Shift+Tab or Tab back in returns to the control used last",
16418
+ "a text field, native select, combobox, slider or radio inside keeps its own arrow keys — the caret moves and the value changes instead of focus jumping",
16419
+ "↓ still reaches a MenuTrigger in a horizontal toolbar, so an overflow menu opens as it does anywhere else"
16420
+ ],
16421
+ "requires": [
16422
+ "`aria-label` or `aria-labelledby` naming what the toolbar acts on"
16423
+ ]
16424
+ },
16425
+ "antiPatterns": [
16426
+ {
16427
+ "dont": "a table's search field and filters inside a Toolbar",
16428
+ "why": "Tab from the search field leaves the toolbar, so the filters after it can only be reached by Shift+Tab and arrows"
16429
+ },
16430
+ {
16431
+ "dont": "a Toolbar with one button",
16432
+ "why": "a single stop gains nothing from arrow keys, and screen readers announce a toolbar the user has to learn"
16433
+ },
16434
+ {
16435
+ "dont": "an unnamed Toolbar",
16436
+ "why": "\"toolbar\" alone does not say what the buttons act on, and pages with two cannot be told apart"
16437
+ }
16438
+ ],
16439
+ "stylesheet": "src/styles/toolbar.css",
16440
+ "tokens": [
16441
+ "--spacing-4",
16442
+ "--spacing-8"
16443
+ ],
16444
+ "props": {
16445
+ "aria-label": {
16446
+ "type": "string | undefined",
16447
+ "required": false,
16448
+ "origin": "own",
16449
+ "description": "Names the toolbar — \"Bulk actions\", \"Formatting\". Required, or `aria-labelledby`.",
16450
+ "tags": {
16451
+ "see": "aria-labelledby."
16452
+ }
16453
+ },
16454
+ "aria-labelledby": {
16455
+ "type": "string | undefined",
16456
+ "required": false,
16457
+ "origin": "own",
16458
+ "description": "Identifies the element (or elements) that labels the current element.",
16459
+ "tags": {
16460
+ "see": "aria-describedby."
16461
+ }
16462
+ },
16463
+ "orientation": {
16464
+ "type": "ToolbarOrientation | undefined",
16465
+ "required": false,
16466
+ "origin": "own",
16467
+ "description": "Which arrow keys move between controls. Horizontal: ← →; vertical: ↑ ↓.",
16468
+ "values": [
16469
+ "vertical",
16470
+ "horizontal"
16471
+ ],
16472
+ "default": "horizontal"
16473
+ },
16474
+ "children": {
16475
+ "type": "React.ReactNode",
16476
+ "required": false,
16477
+ "origin": "own",
16478
+ "description": "Buttons, MenuTriggers, a SegmentedControl, and a vertical Divider between groups."
16479
+ }
16480
+ },
16481
+ "propCounts": {
16482
+ "own": 4,
16483
+ "aria": 0,
16484
+ "dom": 274,
16485
+ "other": 0
16486
+ }
16487
+ },
15110
16488
  "ToolCall": {
15111
16489
  "name": "ToolCall",
15112
16490
  "source": "src/components/ToolCall.tsx",