@grampro/kit 2.6.3 → 2.7.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 (93) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +5 -4
  2. package/.gbs/skills/gbs-components/references/install.md +17 -14
  3. package/CHANGELOG.md +164 -0
  4. package/README.md +19 -6
  5. package/index.cjs +59 -59
  6. package/package.json +2 -2
  7. package/schema/passport-overlay-v1.schema.json +67 -0
  8. package/source/components/accordion/passport.json +45 -8
  9. package/source/components/accordion/passport.manual.json +46 -0
  10. package/source/components/alert/passport.json +41 -8
  11. package/source/components/alert/passport.manual.json +42 -0
  12. package/source/components/avatar/passport.json +40 -7
  13. package/source/components/avatar/passport.manual.json +43 -0
  14. package/source/components/badge/passport.json +43 -7
  15. package/source/components/badge/passport.manual.json +46 -0
  16. package/source/components/breadcrumb/passport.json +36 -7
  17. package/source/components/breadcrumb/passport.manual.json +39 -0
  18. package/source/components/button/passport.json +1 -1
  19. package/source/components/button/passport.manual.json +1 -1
  20. package/source/components/card/passport.json +45 -8
  21. package/source/components/card/passport.manual.json +47 -0
  22. package/source/components/checkbox/core/agent.ts +33 -0
  23. package/source/components/checkbox/core/index.ts +2 -0
  24. package/source/components/checkbox/passport.json +83 -10
  25. package/source/components/checkbox/passport.manual.json +82 -0
  26. package/source/components/combobox/core/agent.ts +38 -0
  27. package/source/components/combobox/core/index.ts +2 -0
  28. package/source/components/combobox/passport.json +103 -11
  29. package/source/components/combobox/passport.manual.json +101 -0
  30. package/source/components/combobox/react/props.ts +14 -0
  31. package/source/components/combobox/react/useCombobox.ts +7 -1
  32. package/source/components/data-grid/export/xlsx.ts +9 -2
  33. package/source/components/data-grid/passport.json +2 -2
  34. package/source/components/data-grid/passport.manual.json +1 -1
  35. package/source/components/date-picker/core/agent.ts +42 -0
  36. package/source/components/date-picker/core/index.ts +2 -0
  37. package/source/components/date-picker/passport.json +102 -11
  38. package/source/components/date-picker/passport.manual.json +100 -0
  39. package/source/components/date-picker/react/DatePicker.tsx +6 -0
  40. package/source/components/date-picker/react/DateRangePicker.tsx +4 -0
  41. package/source/components/date-picker/react/props.ts +17 -0
  42. package/source/components/dialog/passport.json +54 -9
  43. package/source/components/dialog/passport.manual.json +54 -0
  44. package/source/components/file-uploader/passport.json +65 -10
  45. package/source/components/file-uploader/passport.manual.json +64 -0
  46. package/source/components/input/core/agent.ts +36 -0
  47. package/source/components/input/core/index.ts +2 -0
  48. package/source/components/input/passport.json +89 -12
  49. package/source/components/input/passport.manual.json +86 -0
  50. package/source/components/menu/passport.json +64 -9
  51. package/source/components/menu/passport.manual.json +64 -0
  52. package/source/components/modal/passport.json +52 -9
  53. package/source/components/modal/passport.manual.json +52 -0
  54. package/source/components/number-input/core/agent.ts +38 -0
  55. package/source/components/number-input/core/index.ts +2 -0
  56. package/source/components/number-input/passport.json +92 -11
  57. package/source/components/number-input/passport.manual.json +90 -0
  58. package/source/components/popover/passport.json +47 -8
  59. package/source/components/popover/passport.manual.json +48 -0
  60. package/source/components/progress/passport.json +37 -8
  61. package/source/components/progress/passport.manual.json +39 -0
  62. package/source/components/radio-group/core/agent.ts +34 -0
  63. package/source/components/radio-group/core/index.ts +2 -0
  64. package/source/components/radio-group/passport.json +77 -10
  65. package/source/components/radio-group/passport.manual.json +76 -0
  66. package/source/components/shared/core/agent/field-agent.ts +101 -0
  67. package/source/components/shared/core/agent/field-executors.ts +137 -0
  68. package/source/components/shared/core/agent/field.ts +649 -0
  69. package/source/components/shared/core/agent/index.ts +30 -0
  70. package/source/components/shared/core/agent/write.ts +58 -0
  71. package/source/components/shared/react/useFieldAgent.ts +88 -0
  72. package/source/components/skeleton/passport.json +37 -6
  73. package/source/components/skeleton/passport.manual.json +39 -0
  74. package/source/components/spinner/passport.json +34 -7
  75. package/source/components/spinner/passport.manual.json +37 -0
  76. package/source/components/switch/core/agent.ts +31 -0
  77. package/source/components/switch/core/index.ts +2 -0
  78. package/source/components/switch/passport.json +68 -10
  79. package/source/components/switch/passport.manual.json +67 -0
  80. package/source/components/tabs/passport.json +50 -9
  81. package/source/components/tabs/passport.manual.json +50 -0
  82. package/source/components/textarea/core/agent.ts +34 -0
  83. package/source/components/textarea/core/index.ts +2 -0
  84. package/source/components/textarea/passport.json +83 -10
  85. package/source/components/textarea/passport.manual.json +82 -0
  86. package/source/components/toaster/passport.json +41 -8
  87. package/source/components/toaster/passport.manual.json +42 -0
  88. package/source/components/tooltip/passport.json +39 -6
  89. package/source/components/tooltip/passport.manual.json +41 -0
  90. package/tools/mcp/design.cjs +6 -1
  91. package/tools/mcp/figma.cjs +49 -9
  92. package/tools/mcp/install.cjs +6 -1
  93. package/tools/mcp/server.cjs +14 -2
@@ -8,8 +8,9 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.3",
11
+ "libraryVersion": "2.7.0",
12
12
  "files": [
13
+ "core/agent.ts",
13
14
  "core/format.ts",
14
15
  "core/index.ts",
15
16
  "core/parse.ts",
@@ -21,8 +22,9 @@
21
22
  "react/locale.ts",
22
23
  "react/props.ts"
23
24
  ],
24
- "sourceHash": "sha256:0cb6347602f25bd7c77b8738d9d1ac5216ee1a6ef50b5bf54f34deeb2d914f7b"
25
+ "sourceHash": "sha256:b62e20ffb71a95c6e30e7c55f895750de1015364734a3cceacea972e3dc89e6a"
25
26
  },
27
+ "purpose": "A numeric field that reads and writes numbers the way the locale does, with a stepper, clamping and currency formatting.",
26
28
  "props": [
27
29
  {
28
30
  "name": "aria-describedby",
@@ -79,7 +81,8 @@
79
81
  "default": "blur",
80
82
  "origin": "derived",
81
83
  "description": "When a value outside the limits is pulled back. Default `blur`.",
82
- "descriptionOrigin": "jsdoc"
84
+ "descriptionOrigin": "jsdoc",
85
+ "note": "Decides whether an out-of-range entry is corrected as it is typed or left for validation. Changing it changes what the user is allowed to leave in the field."
83
86
  },
84
87
  {
85
88
  "name": "className",
@@ -456,7 +459,8 @@
456
459
  "default": false,
457
460
  "origin": "derived",
458
461
  "description": "Let the wheel change the value while the field has focus. Default **false**:\r\na wheel that edits a focused field turns an ordinary page scroll into a\r\nsilent change to someone's data.",
459
- "descriptionOrigin": "jsdoc"
462
+ "descriptionOrigin": "jsdoc",
463
+ "note": "Scroll-to-change is off by default because it fires while a page is being scrolled past."
460
464
  }
461
465
  ],
462
466
  "inherits": [
@@ -534,20 +538,97 @@
534
538
  }
535
539
  ],
536
540
  "accessibility": {
537
- "supplied": [],
538
- "required": []
541
+ "supplied": [
542
+ "role=spinbutton semantics with aria-valuenow, aria-valuemin and aria-valuemax",
543
+ "ArrowUp and ArrowDown step; PageUp and PageDown use `largeStep`",
544
+ "the stepper buttons are aria-hidden, because the keyboard already does what they do",
545
+ "description and error linked by aria-describedby"
546
+ ],
547
+ "required": [
548
+ "a `label`, or `aria-label` when the field is visually unlabelled",
549
+ "`min` and `max` where a range exists, so assistive technology can announce it"
550
+ ]
551
+ },
552
+ "composition": {
553
+ "mustNotContain": [
554
+ "label",
555
+ "input"
556
+ ],
557
+ "notes": "Renders its own label and input."
558
+ },
559
+ "operations": {
560
+ "setValue": {
561
+ "summary": "Write a value into the field, as though a person had entered it.",
562
+ "effects": [
563
+ "replaces the field's current value",
564
+ "fires the host's change handler"
565
+ ],
566
+ "reversible": true
567
+ },
568
+ "clear": {
569
+ "summary": "Empty the field.",
570
+ "effects": [
571
+ "discards the field's current value",
572
+ "fires the host's change handler"
573
+ ],
574
+ "reversible": true
575
+ },
576
+ "focus": {
577
+ "summary": "Move keyboard focus to the field.",
578
+ "effects": [
579
+ "moves focus"
580
+ ],
581
+ "reversible": false
582
+ }
583
+ },
584
+ "intentDomains": [
585
+ "ActionIntent"
586
+ ],
587
+ "safeMutations": {
588
+ "allowed": [
589
+ "size",
590
+ "placeholder",
591
+ "description",
592
+ "stepper",
593
+ "leading",
594
+ "trailing",
595
+ "selectOnFocus",
596
+ "useGrouping",
597
+ "clearable"
598
+ ],
599
+ "forbidden": [
600
+ "value",
601
+ "defaultValue",
602
+ "name",
603
+ "min",
604
+ "max",
605
+ "step",
606
+ "largeStep",
607
+ "decimals",
608
+ "currency",
609
+ "locale",
610
+ "clampBehavior",
611
+ "snapToStep",
612
+ "required",
613
+ "disabled",
614
+ "readOnly",
615
+ "onValueChange",
616
+ "error"
617
+ ],
618
+ "rationale": "Everything that defines the number — its bounds, its precision, its currency, how it snaps — is domain truth, not styling. Changing `decimals` or `currency` silently reinterprets a stored value."
539
619
  },
540
- "composition": {},
541
- "operations": {},
542
- "intentDomains": [],
543
- "safeMutations": {},
544
620
  "examples": [],
545
621
  "coverage": {
546
622
  "propsTotal": 24,
547
623
  "propsDescribed": 23,
548
624
  "describedPct": 96,
549
625
  "authoredSections": [
550
- "accessibility"
626
+ "accessibility",
627
+ "composition",
628
+ "intentDomains",
629
+ "operations",
630
+ "purpose",
631
+ "safeMutations"
551
632
  ]
552
633
  }
553
634
  }
@@ -0,0 +1,90 @@
1
+ {
2
+ "$schema": "../../../schema/passport-overlay-v1.schema.json",
3
+ "component": "NumberInput",
4
+ "purpose": "A numeric field that reads and writes numbers the way the locale does, with a stepper, clamping and currency formatting.",
5
+ "intentDomains": [
6
+ "ActionIntent"
7
+ ],
8
+ "$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap.",
9
+ "operations": {
10
+ "setValue": {
11
+ "summary": "Write a value into the field, as though a person had entered it.",
12
+ "effects": [
13
+ "replaces the field's current value",
14
+ "fires the host's change handler"
15
+ ],
16
+ "reversible": true
17
+ },
18
+ "clear": {
19
+ "summary": "Empty the field.",
20
+ "effects": [
21
+ "discards the field's current value",
22
+ "fires the host's change handler"
23
+ ],
24
+ "reversible": true
25
+ },
26
+ "focus": {
27
+ "summary": "Move keyboard focus to the field.",
28
+ "effects": [
29
+ "moves focus"
30
+ ],
31
+ "reversible": false
32
+ }
33
+ },
34
+ "accessibility": {
35
+ "supplied": [
36
+ "role=spinbutton semantics with aria-valuenow, aria-valuemin and aria-valuemax",
37
+ "ArrowUp and ArrowDown step; PageUp and PageDown use `largeStep`",
38
+ "the stepper buttons are aria-hidden, because the keyboard already does what they do",
39
+ "description and error linked by aria-describedby"
40
+ ],
41
+ "required": [
42
+ "a `label`, or `aria-label` when the field is visually unlabelled",
43
+ "`min` and `max` where a range exists, so assistive technology can announce it"
44
+ ]
45
+ },
46
+ "composition": {
47
+ "mustNotContain": [
48
+ "label",
49
+ "input"
50
+ ],
51
+ "notes": "Renders its own label and input."
52
+ },
53
+ "safeMutations": {
54
+ "allowed": [
55
+ "size",
56
+ "placeholder",
57
+ "description",
58
+ "stepper",
59
+ "leading",
60
+ "trailing",
61
+ "selectOnFocus",
62
+ "useGrouping",
63
+ "clearable"
64
+ ],
65
+ "forbidden": [
66
+ "value",
67
+ "defaultValue",
68
+ "name",
69
+ "min",
70
+ "max",
71
+ "step",
72
+ "largeStep",
73
+ "decimals",
74
+ "currency",
75
+ "locale",
76
+ "clampBehavior",
77
+ "snapToStep",
78
+ "required",
79
+ "disabled",
80
+ "readOnly",
81
+ "onValueChange",
82
+ "error"
83
+ ],
84
+ "rationale": "Everything that defines the number — its bounds, its precision, its currency, how it snaps — is domain truth, not styling. Changing `decimals` or `currency` silently reinterprets a stored value."
85
+ },
86
+ "propNotes": {
87
+ "clampBehavior": "Decides whether an out-of-range entry is corrected as it is typed or left for validation. Changing it changes what the user is allowed to leave in the field.",
88
+ "wheel": "Scroll-to-change is off by default because it fires while a page is being scrolled past."
89
+ }
90
+ }
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.6.3",
11
+ "libraryVersion": "2.7.0",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/types.ts",
@@ -19,6 +19,7 @@
19
19
  ],
20
20
  "sourceHash": "sha256:71722134c43ba8dec7c9139a8c2c84ab4009247f7b8e1699308f6abbc634f278"
21
21
  },
22
+ "purpose": "A panel anchored to the control that opened it, in the top layer, with light dismiss, Escape, and flipping when there is no room.",
22
23
  "props": [
23
24
  {
24
25
  "name": "align",
@@ -166,7 +167,8 @@
166
167
  "default": "bottom",
167
168
  "origin": "derived",
168
169
  "description": "Side of the trigger to open on, before flipping for space. Default `bottom`.",
169
- "descriptionOrigin": "jsdoc"
170
+ "descriptionOrigin": "jsdoc",
171
+ "note": "A preference, not a guarantee: it flips when there is no room, which is the behaviour you want."
170
172
  },
171
173
  {
172
174
  "name": "style",
@@ -227,20 +229,57 @@
227
229
  }
228
230
  ],
229
231
  "accessibility": {
230
- "supplied": [],
231
- "required": []
232
+ "supplied": [
233
+ "the trigger carries aria-expanded and aria-controls",
234
+ "Escape closes and focus returns to the trigger",
235
+ "a click outside dismisses without trapping focus, which is what makes it a popover and not a modal",
236
+ "the panel renders in the top layer, so no ancestor's overflow clips it"
237
+ ],
238
+ "required": [
239
+ "an accessible name on the trigger when it is an icon",
240
+ "do not put a form that must be completed in here — focus is not trapped, so a user can wander out of it. That is a Modal."
241
+ ]
242
+ },
243
+ "composition": {
244
+ "mustNotContain": [
245
+ "Modal",
246
+ "Dialog"
247
+ ],
248
+ "notes": "A popover does not trap focus. Anything that must be finished before the user moves on belongs in a Modal instead."
232
249
  },
233
- "composition": {},
234
250
  "operations": {},
235
- "intentDomains": [],
236
- "safeMutations": {},
251
+ "intentDomains": [
252
+ "ActionIntent"
253
+ ],
254
+ "safeMutations": {
255
+ "allowed": [
256
+ "side",
257
+ "align",
258
+ "gap",
259
+ "scrollable",
260
+ "description"
261
+ ],
262
+ "forbidden": [
263
+ "open",
264
+ "defaultOpen",
265
+ "onOpenChange",
266
+ "trigger",
267
+ "autoFocus",
268
+ "children"
269
+ ],
270
+ "rationale": "Placement is presentation and the component re-flips it anyway when space runs out. `open` is the application's state."
271
+ },
237
272
  "examples": [],
238
273
  "coverage": {
239
274
  "propsTotal": 14,
240
275
  "propsDescribed": 12,
241
276
  "describedPct": 86,
242
277
  "authoredSections": [
243
- "accessibility"
278
+ "accessibility",
279
+ "composition",
280
+ "intentDomains",
281
+ "purpose",
282
+ "safeMutations"
244
283
  ]
245
284
  }
246
285
  }
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "../../../schema/passport-overlay-v1.schema.json",
3
+ "component": "Popover",
4
+ "purpose": "A panel anchored to the control that opened it, in the top layer, with light dismiss, Escape, and flipping when there is no room.",
5
+ "intentDomains": [
6
+ "ActionIntent"
7
+ ],
8
+ "accessibility": {
9
+ "supplied": [
10
+ "the trigger carries aria-expanded and aria-controls",
11
+ "Escape closes and focus returns to the trigger",
12
+ "a click outside dismisses without trapping focus, which is what makes it a popover and not a modal",
13
+ "the panel renders in the top layer, so no ancestor's overflow clips it"
14
+ ],
15
+ "required": [
16
+ "an accessible name on the trigger when it is an icon",
17
+ "do not put a form that must be completed in here — focus is not trapped, so a user can wander out of it. That is a Modal."
18
+ ]
19
+ },
20
+ "composition": {
21
+ "mustNotContain": [
22
+ "Modal",
23
+ "Dialog"
24
+ ],
25
+ "notes": "A popover does not trap focus. Anything that must be finished before the user moves on belongs in a Modal instead."
26
+ },
27
+ "safeMutations": {
28
+ "allowed": [
29
+ "side",
30
+ "align",
31
+ "gap",
32
+ "scrollable",
33
+ "description"
34
+ ],
35
+ "forbidden": [
36
+ "open",
37
+ "defaultOpen",
38
+ "onOpenChange",
39
+ "trigger",
40
+ "autoFocus",
41
+ "children"
42
+ ],
43
+ "rationale": "Placement is presentation and the component re-flips it anyway when space runs out. `open` is the application's state."
44
+ },
45
+ "propNotes": {
46
+ "side": "A preference, not a guarantee: it flips when there is no room, which is the behaviour you want."
47
+ }
48
+ }
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.6.3",
12
+ "libraryVersion": "2.7.0",
13
13
  "files": [
14
14
  "core/index.ts",
15
15
  "core/progress.ts",
@@ -22,6 +22,7 @@
22
22
  ],
23
23
  "sourceHash": "sha256:8c989dc440492a833963b37e6c28fd90a99e54435c545a4f053b77c83d745eaf"
24
24
  },
25
+ "purpose": "A bar or a ring for work with a known end, where an unknown length is a different thing from zero per cent.",
25
26
  "props": [
26
27
  {
27
28
  "name": "aria-describedby",
@@ -176,7 +177,8 @@
176
177
  "default": null,
177
178
  "origin": "derived",
178
179
  "description": "How far along. `null` means the length is not known yet.",
179
- "descriptionOrigin": "jsdoc"
180
+ "descriptionOrigin": "jsdoc",
181
+ "note": "Undefined means indeterminate, which is announced differently from 0."
180
182
  },
181
183
  {
182
184
  "name": "valueText",
@@ -185,7 +187,8 @@
185
187
  "required": false,
186
188
  "origin": "derived",
187
189
  "description": "Replace what is shown and read, e.g. \"3.2 MB of 8 MB\".",
188
- "descriptionOrigin": "jsdoc"
190
+ "descriptionOrigin": "jsdoc",
191
+ "note": "Overrides the announced string, for units that are not percentages."
189
192
  },
190
193
  {
191
194
  "name": "variant",
@@ -251,20 +254,46 @@
251
254
  }
252
255
  ],
253
256
  "accessibility": {
254
- "supplied": [],
255
- "required": []
257
+ "supplied": [
258
+ "role=progressbar with aria-valuenow, aria-valuemin and aria-valuemax",
259
+ "an indeterminate progress omits aria-valuenow rather than reporting zero, which is what tells a screen reader the length is unknown",
260
+ "`valueText` for when a percentage is not the useful reading"
261
+ ],
262
+ "required": [
263
+ "a `label` saying what is in progress",
264
+ "leave `value` undefined for work of unknown length — zero is a claim that nothing has happened"
265
+ ]
266
+ },
267
+ "composition": {
268
+ "notes": "A bar and a ring are the same component with a different `variant`."
256
269
  },
257
- "composition": {},
258
270
  "operations": {},
259
271
  "intentDomains": [],
260
- "safeMutations": {},
272
+ "safeMutations": {
273
+ "allowed": [
274
+ "variant",
275
+ "size",
276
+ "thickness",
277
+ "showValue",
278
+ "label"
279
+ ],
280
+ "forbidden": [
281
+ "value",
282
+ "max",
283
+ "valueText"
284
+ ],
285
+ "rationale": "The shape is presentation; the value is the measurement."
286
+ },
261
287
  "examples": [],
262
288
  "coverage": {
263
289
  "propsTotal": 9,
264
290
  "propsDescribed": 8,
265
291
  "describedPct": 89,
266
292
  "authoredSections": [
267
- "accessibility"
293
+ "accessibility",
294
+ "composition",
295
+ "purpose",
296
+ "safeMutations"
268
297
  ]
269
298
  }
270
299
  }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "../../../schema/passport-overlay-v1.schema.json",
3
+ "component": "Progress",
4
+ "purpose": "A bar or a ring for work with a known end, where an unknown length is a different thing from zero per cent.",
5
+ "intentDomains": [],
6
+ "accessibility": {
7
+ "supplied": [
8
+ "role=progressbar with aria-valuenow, aria-valuemin and aria-valuemax",
9
+ "an indeterminate progress omits aria-valuenow rather than reporting zero, which is what tells a screen reader the length is unknown",
10
+ "`valueText` for when a percentage is not the useful reading"
11
+ ],
12
+ "required": [
13
+ "a `label` saying what is in progress",
14
+ "leave `value` undefined for work of unknown length — zero is a claim that nothing has happened"
15
+ ]
16
+ },
17
+ "composition": {
18
+ "notes": "A bar and a ring are the same component with a different `variant`."
19
+ },
20
+ "safeMutations": {
21
+ "allowed": [
22
+ "variant",
23
+ "size",
24
+ "thickness",
25
+ "showValue",
26
+ "label"
27
+ ],
28
+ "forbidden": [
29
+ "value",
30
+ "max",
31
+ "valueText"
32
+ ],
33
+ "rationale": "The shape is presentation; the value is the measurement."
34
+ },
35
+ "propNotes": {
36
+ "value": "Undefined means indeterminate, which is announced differently from 0.",
37
+ "valueText": "Overrides the announced string, for units that are not percentages."
38
+ }
39
+ }
@@ -0,0 +1,34 @@
1
+ /*
2
+ * RadioGroup's agent contract.
3
+ *
4
+ * A choice over the group's own options, so an unknown value is refused by
5
+ * the reference layer with the nearest real option suggested.
6
+ *
7
+ * Pure: no React, no DOM. The contract is what this instance will accept, and
8
+ * the validator in shared/core/agent decides everything from it. Build it from
9
+ * props on every render so that a field which has just become read-only stops
10
+ * accepting writes immediately.
11
+ */
12
+
13
+ import type { FieldContract } from "../../shared/core/agent";
14
+
15
+ export interface RadioGroupAgentProps {
16
+ label?: string;
17
+ disabled?: boolean;
18
+ readOnly?: boolean;
19
+ required?: boolean;
20
+ options?: readonly { value: string; label: string; disabled?: boolean }[];
21
+ }
22
+
23
+ export function radioGroupContract(props: RadioGroupAgentProps = {}): FieldContract {
24
+ return {
25
+ component: "RadioGroup",
26
+ kind: "choice",
27
+ operations: ["setValue", "clear", "focus"],
28
+ label: props.label,
29
+ disabled: props.disabled,
30
+ readOnly: props.readOnly,
31
+ required: props.required,
32
+ options: props.options,
33
+ };
34
+ }
@@ -1,2 +1,4 @@
1
1
  export { hasEnabledOption, normalizeOptions, resolveValue } from "./group";
2
2
  export type * from "./types";
3
+ // The agent contract: what a model may ask this field to do.
4
+ export { radioGroupContract, type RadioGroupAgentProps } from "./agent";
@@ -10,8 +10,9 @@
10
10
  ]
11
11
  },
12
12
  "source": {
13
- "libraryVersion": "2.6.3",
13
+ "libraryVersion": "2.7.0",
14
14
  "files": [
15
+ "core/agent.ts",
15
16
  "core/group.ts",
16
17
  "core/index.ts",
17
18
  "core/types.ts",
@@ -22,8 +23,9 @@
22
23
  "react/locale.ts",
23
24
  "react/props.ts"
24
25
  ],
25
- "sourceHash": "sha256:7e13b8cd8a2bab1ee7a89cf691c1dc388951723939ca4a87def57d6301d8aa33"
26
+ "sourceHash": "sha256:8630723accbd754b823c4f2d7f5c1c0da02252d3a51f87b8abeaad117bd75064"
26
27
  },
28
+ "purpose": "One choice out of a few, on native radios: a single tab stop, arrow keys between the choices, and a card variant for richer options.",
27
29
  "props": [
28
30
  {
29
31
  "name": "aria-describedby",
@@ -394,7 +396,8 @@
394
396
  "default": "default",
395
397
  "origin": "derived",
396
398
  "description": "Default `default`. `card` draws each choice as a tile.",
397
- "descriptionOrigin": "jsdoc"
399
+ "descriptionOrigin": "jsdoc",
400
+ "note": "`card` renders each option as a selectable surface; the semantics are identical."
398
401
  }
399
402
  ],
400
403
  "inherits": [
@@ -476,20 +479,84 @@
476
479
  }
477
480
  ],
478
481
  "accessibility": {
479
- "supplied": [],
480
- "required": []
482
+ "supplied": [
483
+ "native radio semantics: one tab stop for the group, arrow keys to move and select",
484
+ "a fieldset with the group label as its legend",
485
+ "description and error linked by aria-describedby"
486
+ ],
487
+ "required": [
488
+ "a `label` for the group — a set of radios with no question is meaningless",
489
+ "`name`, so the browser treats them as one group"
490
+ ]
491
+ },
492
+ "composition": {
493
+ "allowedChildren": [
494
+ "Radio",
495
+ "RadioCard"
496
+ ],
497
+ "notes": "Children or `options`, not both. A radio outside a RadioGroup loses the arrow-key behaviour."
498
+ },
499
+ "operations": {
500
+ "setValue": {
501
+ "summary": "Write a value into the field, as though a person had entered it.",
502
+ "effects": [
503
+ "replaces the field's current value",
504
+ "fires the host's change handler"
505
+ ],
506
+ "reversible": true
507
+ },
508
+ "clear": {
509
+ "summary": "Empty the field.",
510
+ "effects": [
511
+ "discards the field's current value",
512
+ "fires the host's change handler"
513
+ ],
514
+ "reversible": true
515
+ },
516
+ "focus": {
517
+ "summary": "Move keyboard focus to the field.",
518
+ "effects": [
519
+ "moves focus"
520
+ ],
521
+ "reversible": false
522
+ }
523
+ },
524
+ "intentDomains": [
525
+ "ActionIntent"
526
+ ],
527
+ "safeMutations": {
528
+ "allowed": [
529
+ "size",
530
+ "orientation",
531
+ "variant",
532
+ "description"
533
+ ],
534
+ "forbidden": [
535
+ "value",
536
+ "defaultValue",
537
+ "name",
538
+ "options",
539
+ "required",
540
+ "disabled",
541
+ "readOnly",
542
+ "onChange",
543
+ "onValueChange",
544
+ "error"
545
+ ],
546
+ "rationale": "The selected value is the user's answer and `options` is the question."
481
547
  },
482
- "composition": {},
483
- "operations": {},
484
- "intentDomains": [],
485
- "safeMutations": {},
486
548
  "examples": [],
487
549
  "coverage": {
488
550
  "propsTotal": 20,
489
551
  "propsDescribed": 16,
490
552
  "describedPct": 80,
491
553
  "authoredSections": [
492
- "accessibility"
554
+ "accessibility",
555
+ "composition",
556
+ "intentDomains",
557
+ "operations",
558
+ "purpose",
559
+ "safeMutations"
493
560
  ]
494
561
  }
495
562
  }