@arbor-education/design-system.components 0.12.0 → 0.13.1

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 (177) hide show
  1. package/.agent-memory/blanche-designspert/MEMORY.md +189 -0
  2. package/.agent-memory/dorothy-fact-checker/MEMORY.md +228 -0
  3. package/.agent-memory/dorothy-fact-checker/numberinput_component.md +53 -0
  4. package/.agent-memory/dorothy-fact-checker/progress_component.md +36 -0
  5. package/.agent-memory/rose-storybookspert/MEMORY.md +105 -0
  6. package/.agent-memory/sophia-componentspert/MEMORY.md +34 -0
  7. package/{.claude/agent-memory → .agent-memory}/sophia-componentspert/components.md +170 -17
  8. package/{.claude → .gather}/agents/blanche-designspert.md +7 -2
  9. package/{.claude → .gather}/agents/dorothy-fact-checker.md +7 -2
  10. package/{.claude → .gather}/agents/rose-storybookspert.md +80 -11
  11. package/{.claude → .gather}/agents/sophia-componentspert.md +9 -4
  12. package/.gather/gather.yaml +9 -0
  13. package/{CLAUDE.md → .gather/instructions/project-overview.md} +42 -9
  14. package/{.claude → .gather}/skills/analyze-design/README.md +5 -0
  15. package/{.claude → .gather}/skills/analyze-design/SKILL.md +1 -1
  16. package/.gather/skills/analyze-design/meta.md +4 -0
  17. package/{.claude → .gather}/skills/create-page/README.md +5 -0
  18. package/{.claude → .gather}/skills/create-page/design-analysis-template.md +5 -0
  19. package/.gather/skills/create-page/meta.md +4 -0
  20. package/{.claude → .gather}/skills/create-page/page-template.scss +5 -0
  21. package/{.claude → .gather}/skills/create-page/page-template.tsx +5 -0
  22. package/{.claude → .gather}/skills/map-legacy/README.md +5 -0
  23. package/.gather/skills/map-legacy/meta.md +4 -0
  24. package/{.claude → .gather}/skills/migrate-page/README.md +5 -0
  25. package/.gather/skills/migrate-page/meta.md +4 -0
  26. package/.gather/skills/write-stories/README.md +157 -0
  27. package/.gather/skills/write-stories/SKILL.md +841 -0
  28. package/.gather/skills/write-stories/meta.md +4 -0
  29. package/.ralph/storybook-upgrade/knowledge.md +308 -0
  30. package/.ralph/storybook-upgrade/prd.json +777 -0
  31. package/.ralph/storybook-upgrade/progress.md +342 -0
  32. package/.storybook/DocsTemplate.tsx +122 -0
  33. package/.storybook/preview.ts +40 -0
  34. package/.stylelintignore +2 -0
  35. package/CHANGELOG.md +12 -0
  36. package/{.claude/component-library.md → component-library.md} +27 -10
  37. package/dist/components/badge/Badge.stories.d.ts +85 -6
  38. package/dist/components/badge/Badge.stories.d.ts.map +1 -1
  39. package/dist/components/badge/Badge.stories.js +626 -27
  40. package/dist/components/badge/Badge.stories.js.map +1 -1
  41. package/dist/components/banner/Banner.stories.d.ts +129 -63
  42. package/dist/components/banner/Banner.stories.d.ts.map +1 -1
  43. package/dist/components/banner/Banner.stories.js +855 -39
  44. package/dist/components/banner/Banner.stories.js.map +1 -1
  45. package/dist/components/button/Button.stories.d.ts +148 -8
  46. package/dist/components/button/Button.stories.d.ts.map +1 -1
  47. package/dist/components/button/Button.stories.js +1089 -80
  48. package/dist/components/button/Button.stories.js.map +1 -1
  49. package/dist/components/datePicker/DatePicker.d.ts +1 -0
  50. package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
  51. package/dist/components/datePicker/DatePicker.js +2 -2
  52. package/dist/components/datePicker/DatePicker.js.map +1 -1
  53. package/dist/components/datePicker/DatePicker.stories.d.ts +1 -0
  54. package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
  55. package/dist/components/dot/Dot.stories.d.ts +46 -11
  56. package/dist/components/dot/Dot.stories.d.ts.map +1 -1
  57. package/dist/components/dot/Dot.stories.js +504 -15
  58. package/dist/components/dot/Dot.stories.js.map +1 -1
  59. package/dist/components/dropdown/Dropdown.stories.d.ts +89 -14
  60. package/dist/components/dropdown/Dropdown.stories.d.ts.map +1 -1
  61. package/dist/components/dropdown/Dropdown.stories.js +769 -17
  62. package/dist/components/dropdown/Dropdown.stories.js.map +1 -1
  63. package/dist/components/formField/FormField.stories.d.ts +95 -35
  64. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  65. package/dist/components/formField/FormField.stories.js +1174 -69
  66. package/dist/components/formField/FormField.stories.js.map +1 -1
  67. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +96 -9
  68. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
  69. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +717 -10
  70. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
  71. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts +149 -11
  72. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts.map +1 -1
  73. package/dist/components/formField/inputs/number/NumberInput.stories.js +624 -10
  74. package/dist/components/formField/inputs/number/NumberInput.stories.js.map +1 -1
  75. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts +74 -1
  76. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
  77. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +673 -44
  78. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
  79. package/dist/components/formField/inputs/text/TextInput.stories.d.ts +119 -1
  80. package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
  81. package/dist/components/formField/inputs/text/TextInput.stories.js +549 -10
  82. package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -1
  83. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +129 -4
  84. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
  85. package/dist/components/formField/inputs/textArea/TextArea.stories.js +577 -3
  86. package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -1
  87. package/dist/components/heading/Heading.stories.d.ts +449 -50
  88. package/dist/components/heading/Heading.stories.d.ts.map +1 -1
  89. package/dist/components/heading/Heading.stories.js +536 -60
  90. package/dist/components/heading/Heading.stories.js.map +1 -1
  91. package/dist/components/icon/Icon.stories.d.ts +81 -10
  92. package/dist/components/icon/Icon.stories.d.ts.map +1 -1
  93. package/dist/components/icon/Icon.stories.js +979 -8
  94. package/dist/components/icon/Icon.stories.js.map +1 -1
  95. package/dist/components/pill/Pill.stories.d.ts +71 -19
  96. package/dist/components/pill/Pill.stories.d.ts.map +1 -1
  97. package/dist/components/pill/Pill.stories.js +573 -14
  98. package/dist/components/pill/Pill.stories.js.map +1 -1
  99. package/dist/components/progress/Progress.stories.d.ts +75 -298
  100. package/dist/components/progress/Progress.stories.d.ts.map +1 -1
  101. package/dist/components/progress/Progress.stories.js +449 -52
  102. package/dist/components/progress/Progress.stories.js.map +1 -1
  103. package/dist/components/separator/Separator.stories.d.ts +58 -5
  104. package/dist/components/separator/Separator.stories.d.ts.map +1 -1
  105. package/dist/components/separator/Separator.stories.js +443 -4
  106. package/dist/components/separator/Separator.stories.js.map +1 -1
  107. package/dist/components/table/Table.d.ts +7 -0
  108. package/dist/components/table/Table.d.ts.map +1 -1
  109. package/dist/components/table/Table.js +9 -0
  110. package/dist/components/table/Table.js.map +1 -1
  111. package/dist/components/table/Table.stories.d.ts +1 -0
  112. package/dist/components/table/Table.stories.d.ts.map +1 -1
  113. package/dist/components/table/Table.stories.js +87 -0
  114. package/dist/components/table/Table.stories.js.map +1 -1
  115. package/dist/components/table/Table.test.js +49 -1
  116. package/dist/components/table/Table.test.js.map +1 -1
  117. package/dist/components/table/cellEditors/DateCellEditor.d.ts +3 -0
  118. package/dist/components/table/cellEditors/DateCellEditor.d.ts.map +1 -0
  119. package/dist/components/table/cellEditors/DateCellEditor.js +13 -0
  120. package/dist/components/table/cellEditors/DateCellEditor.js.map +1 -0
  121. package/dist/components/table/cellEditors/DateCellEditor.test.d.ts +2 -0
  122. package/dist/components/table/cellEditors/DateCellEditor.test.d.ts.map +1 -0
  123. package/dist/components/table/cellEditors/DateCellEditor.test.js +81 -0
  124. package/dist/components/table/cellEditors/DateCellEditor.test.js.map +1 -0
  125. package/dist/components/tag/Tag.stories.d.ts +116 -5
  126. package/dist/components/tag/Tag.stories.d.ts.map +1 -1
  127. package/dist/components/tag/Tag.stories.js +581 -28
  128. package/dist/components/tag/Tag.stories.js.map +1 -1
  129. package/dist/index.css +8 -1
  130. package/dist/index.css.map +1 -1
  131. package/dist/index.d.ts +0 -5
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +0 -5
  134. package/dist/index.js.map +1 -1
  135. package/eslint.config.mts +5 -1
  136. package/package.json +3 -3
  137. package/src/components/badge/Badge.stories.tsx +869 -42
  138. package/src/components/banner/Banner.stories.tsx +1081 -63
  139. package/src/components/button/Button.stories.tsx +1394 -99
  140. package/src/components/datePicker/DatePicker.tsx +3 -0
  141. package/src/components/dot/Dot.stories.tsx +723 -32
  142. package/src/components/dropdown/Dropdown.stories.tsx +1174 -35
  143. package/src/components/formField/FormField.stories.tsx +1522 -105
  144. package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +1020 -15
  145. package/src/components/formField/inputs/number/NumberInput.stories.tsx +908 -15
  146. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +932 -51
  147. package/src/components/formField/inputs/text/TextInput.stories.tsx +773 -13
  148. package/src/components/formField/inputs/textArea/TextArea.stories.tsx +756 -8
  149. package/src/components/heading/Heading.stories.tsx +752 -120
  150. package/src/components/icon/Icon.stories.tsx +1446 -12
  151. package/src/components/pill/Pill.stories.tsx +867 -21
  152. package/src/components/progress/Progress.stories.tsx +625 -58
  153. package/src/components/separator/Separator.stories.tsx +730 -8
  154. package/src/components/separator/separator.scss +12 -3
  155. package/src/components/table/Table.stories.tsx +102 -0
  156. package/src/components/table/Table.test.tsx +82 -3
  157. package/src/components/table/Table.tsx +9 -0
  158. package/src/components/table/cellEditors/DateCellEditor.test.tsx +109 -0
  159. package/src/components/table/cellEditors/DateCellEditor.tsx +27 -0
  160. package/src/components/tag/Tag.stories.tsx +755 -53
  161. package/src/index.ts +0 -5
  162. package/.claude/agent-memory/blanche-designspert/MEMORY.md +0 -64
  163. package/.claude/agent-memory/dorothy-fact-checker/MEMORY.md +0 -129
  164. package/.claude/agent-memory/rose-storybookspert/MEMORY.md +0 -29
  165. package/.claude/agent-memory/sophia-componentspert/MEMORY.md +0 -14
  166. package/.claude/design-assessment-daily-attendance-2026-04-10.md +0 -566
  167. package/.claude/figma-assessment-7154-58899.md +0 -404
  168. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-11086-97537.md +0 -392
  169. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-41974.md +0 -474
  170. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-43094.md +0 -462
  171. package/.claude/figma-assessment-fcFK4CGzkz2fVyY3koX8ZE-7154-59061.md +0 -440
  172. package/.claude/migration-report-custom-report-writer-2026-02-19.md +0 -591
  173. /package/{.claude/agent-memory → .agent-memory}/blanche-designspert/token-review-patterns.md +0 -0
  174. /package/{.claude/agent-memory → .agent-memory}/rose-storybookspert/patterns.md +0 -0
  175. /package/{.claude → .gather}/skills/create-page/SKILL.md +0 -0
  176. /package/{.claude → .gather}/skills/map-legacy/SKILL.md +0 -0
  177. /package/{.claude → .gather}/skills/migrate-page/SKILL.md +0 -0
@@ -1,13 +1,191 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Controls, Heading as DocHeading, Markdown, Primary as DocPrimary, Stories, Subtitle, Title, } from '@storybook/addon-docs/blocks';
3
+ import { useState } from 'react';
2
4
  import { fn } from 'storybook/test';
3
5
  import { RadioButtonInput } from './RadioButtonInput';
4
6
  import { RadioButtonGroup } from './RadioButtonGroup';
5
- import { useState } from 'react';
7
+ // ---------------------------------------------------------------------------
8
+ // Component description — built as joined arrays to avoid no-useless-escape
9
+ // on backtick code spans inside template literals.
10
+ // ---------------------------------------------------------------------------
11
+ const DESCRIPTION_INTRO = [
12
+ 'The **RadioButtonInput** is the Arbor design system radio control — a labelled, accessible single-select',
13
+ 'input for choosing exactly one option from a mutually exclusive set.',
14
+ 'Always use at least two `RadioButtonInput` elements grouped by a shared `name` attribute,',
15
+ 'or compose them with `RadioButtonGroup` which handles the group semantics automatically.',
16
+ ].join(' ');
17
+ const USAGE_GUIDANCE = [
18
+ '### When to use',
19
+ '',
20
+ '- **Single-select from a fixed set** — when exactly one option must be chosen and no "none" state',
21
+ ' is meaningful (e.g. "Which grouping should the attendance report use?")',
22
+ '- **Mutually exclusive choices** — when selecting one option implicitly deselects all others',
23
+ ' (e.g. "Notification method: Email / SMS / Post")',
24
+ '- **Short lists of 2–6 options** — radio buttons work best when all options are visible at once.',
25
+ ' For longer lists, consider `SelectDropdown` instead.',
26
+ '- **A default must always exist** — unlike checkboxes, radio buttons communicate that one option',
27
+ ' is always required. Pre-select the most common or safest option.',
28
+ '',
29
+ '---',
30
+ '',
31
+ '### When NOT to use',
32
+ '',
33
+ '| Instead of RadioButtonInput... | Use... | Why |',
34
+ '|---|---|---|',
35
+ '| Multiple options can be selected | [`CheckboxInput`](?path=/docs/components-formfield-inputs-checkbox--docs) | Checkboxes communicate multi-select; radios communicate single-select |',
36
+ '| Long lists (7+ options) | [`SelectDropdown`](?path=/docs/components-formfield-inputs-selectdropdown--docs) | Dropdowns save vertical space and are easier to scan |',
37
+ '| A single on/off toggle (instant apply) | [`Toggle`](?path=/docs/components-toggle--docs) | Toggle communicates immediate effect; radio needs a Submit/Save action |',
38
+ '| A lone single radio button | (avoid) | A lone radio button is always confusing — radios only make sense in groups of 2+ |',
39
+ '',
40
+ '---',
41
+ '',
42
+ '### Design guidance',
43
+ '',
44
+ '- **Always use in groups of 2+** — a single radio button has no meaning. The browser only enforces',
45
+ ' mutual exclusivity when there are multiple radios sharing the same `name` attribute.',
46
+ '- **Click target is the entire row** — the `<label>` element wraps both the indicator circle and the',
47
+ ' label text, so clicking anywhere on the row selects that option.',
48
+ '- **Vertical layout is the default** — stack radio buttons vertically. Horizontal layouts are only',
49
+ ' appropriate for very short sets with brief labels (e.g. "Yes / No").',
50
+ '- **Pre-select a sensible default** — radio buttons imply that a choice is required. Always start with',
51
+ ' one option selected rather than leaving all options unselected.',
52
+ '- **Error validation is at the field level** — do not show per-option errors.',
53
+ ' Render one error message below the entire group (see the `WithError` and `RadioGroupWithError` stories).',
54
+ '- **Keyboard interaction** — Tab moves focus to the radio group; arrow keys move between options',
55
+ ' within the group; Space selects the focused option. This is native browser behaviour.',
56
+ ].join('\n');
57
+ const DEVELOPER_NOTES = [
58
+ '### Critical gotchas',
59
+ '',
60
+ '#### 1. `hasError` is ARIA-only — it makes zero visual difference',
61
+ '`hasError={true}` sets `aria-invalid="true"` on the native `<input>`. There are NO CSS rules',
62
+ 'that change the appearance of the indicator circle based on `aria-invalid`.',
63
+ 'To show a visible error state, YOU must render an error message element below the group.',
64
+ 'See the `WithError` and `RadioGroupWithError` stories for the correct pattern.',
65
+ '',
66
+ '```tsx',
67
+ '// WRONG — hasError alone shows no visible change',
68
+ '<RadioButtonInput hasError name="period" value="term" label="Current term" checked onChange={() => {}} />',
69
+ '',
70
+ '// CORRECT — pair hasError with a visible error message rendered by the consumer',
71
+ '<div>',
72
+ ' <RadioButtonInput hasError name="period" value="term" label="Current term" checked onChange={() => {}} />',
73
+ ' <p role="alert" style={{ color: "var(--form-field-text-error-color-error-text)" }}>',
74
+ ' Please select a valid report period.',
75
+ ' </p>',
76
+ '</div>',
77
+ '```',
78
+ '',
79
+ '#### 2. `name` is not TypeScript-required but is functionally mandatory',
80
+ 'TypeScript will not warn you if you omit `name`, but without it the browser cannot enforce',
81
+ 'mutual exclusivity — all radios in the "group" can be selected simultaneously.',
82
+ 'Always provide a `name` prop that is shared across all options in a group.',
83
+ '',
84
+ '```tsx',
85
+ '// BAD — no name; browser cannot enforce mutual exclusivity',
86
+ '<RadioButtonInput value="email" label="Email" checked onChange={() => {}} />',
87
+ '<RadioButtonInput value="sms" label="SMS" checked={false} onChange={() => {}} />',
88
+ '',
89
+ '// GOOD — shared name groups them correctly',
90
+ '<RadioButtonInput name="notification-method" value="email" label="Email" checked onChange={() => {}} />',
91
+ '<RadioButtonInput name="notification-method" value="sms" label="SMS" checked={false} onChange={() => {}} />',
92
+ '```',
93
+ '',
94
+ '#### 3. `checked` defaults to `false` — this component is always controlled',
95
+ 'The component destructures `checked = false`, so React treats it as a controlled input from',
96
+ 'the moment it mounts. You must always pair `checked` with an `onChange` handler.',
97
+ 'Without `onChange`, the radio will appear frozen — clicking will not select it.',
98
+ '',
99
+ '#### 4. `option.id` is the React key in `RadioButtonGroup` — omitting it causes key warnings',
100
+ '`RadioButtonGroup` uses `option.id` as the React `key` when mapping over options.',
101
+ 'Every option object must include an `id` field, or React will warn about missing keys.',
102
+ '',
103
+ '#### 5. `RadioButtonGroup` API differs from `CheckboxGroup`',
104
+ '`RadioButtonGroup` uses a single `checkedValue: string` + `onChange` at the group level.',
105
+ '`CheckboxGroup` uses per-item `checked` + `onChange` in each option object.',
106
+ 'Do not mix up the two APIs.',
107
+ '',
108
+ '```tsx',
109
+ '// RadioButtonGroup — single checkedValue at group level',
110
+ '<RadioButtonGroup',
111
+ ' name="grouping"',
112
+ ' checkedValue={selectedValue}',
113
+ ' onChange={(e) => setSelectedValue(e.target.value)}',
114
+ ' options={[',
115
+ ' { id: "opt-year", value: "year-group", label: "Year group" },',
116
+ ' { id: "opt-reg", value: "reg-form", label: "Registration form" },',
117
+ ' ]}',
118
+ '/>',
119
+ '',
120
+ '// CheckboxGroup — per-item checked + onChange',
121
+ '<CheckboxGroup',
122
+ ' options={[',
123
+ ' { id: "opt-a", label: "Option A", checked: true, onChange: (e) => {} },',
124
+ ' { id: "opt-b", label: "Option B", checked: false, onChange: (e) => {} },',
125
+ ' ]}',
126
+ '/>',
127
+ '```',
128
+ '',
129
+ '---',
130
+ '',
131
+ '### Accessibility',
132
+ '',
133
+ '- **Keyboard interaction** — Tab moves focus to the radio group; arrow keys (Up/Down or Left/Right)',
134
+ ' cycle through options within the group; Space selects the focused option. This is native browser',
135
+ ' behaviour requiring no custom JavaScript.',
136
+ '- **`aria-invalid`** — set by `hasError={true}` on the real `<input>`. Screen readers will announce',
137
+ ' the input as invalid, but consumers must also render a visible error message so sighted users see it.',
138
+ '- **Group labelling** — always wrap `RadioButtonInput` elements in `RadioButtonGroup` (which uses',
139
+ ' `Fieldset` + `<legend>` internally) or in your own `<fieldset>` + `<legend>`. The legend is',
140
+ ' announced by screen readers as the group name before each individual option label.',
141
+ '- **Never use a lone radio button** — a single radio button is inaccessible because there is no way',
142
+ ' to deselect it. Use a checkbox instead, or add at least one more option.',
143
+ '- **`id` on each option** — required both as the React key in `RadioButtonGroup` and for potential',
144
+ ' `aria-describedby` associations (e.g. linking to an inline error message).',
145
+ '',
146
+ '---',
147
+ '',
148
+ '### TypeScript types',
149
+ '',
150
+ '```ts',
151
+ "import { RadioButtonInput, RadioButtonGroup } from '@arbor-education/design-system.components';",
152
+ '',
153
+ '// Prop type shorthand',
154
+ 'function MyField(props: RadioButtonInput.Props) { ... }',
155
+ 'function MyGroup(props: RadioButtonGroup.Props) { ... }',
156
+ '```',
157
+ '',
158
+ '| Type | Description |',
159
+ '|---|---|',
160
+ '| `RadioButtonInput.Props` | `label`, `hasError`, plus all `InputHTMLAttributes<HTMLInputElement>` except `type` |',
161
+ '| `RadioButtonGroup.Props` | `name`, `options: RadioButtonInputProps[]`, `checkedValue`, `onChange`, plus `Fieldset.Props` (`legend`, `HTMLFieldSetElement` attrs) |',
162
+ ].join('\n');
163
+ const RELATED_COMPONENTS = [
164
+ '## Related components',
165
+ '',
166
+ '[CheckboxInput](?path=/docs/components-formfield-inputs-checkbox--docs)',
167
+ '· [Toggle](?path=/docs/components-toggle--docs)',
168
+ '· [FormField](?path=/docs/components-formfield--docs)',
169
+ '· [Fieldset](?path=/docs/components-formfield-fieldset--docs)',
170
+ ].join('\n');
171
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
172
+ // ---------------------------------------------------------------------------
173
+ // Docs page
174
+ // ---------------------------------------------------------------------------
175
+ function RadioButtonInputDocsPage() {
176
+ return (_jsxs(_Fragment, { children: [_jsx(Title, {}), _jsx(Subtitle, {}), _jsx(Markdown, { children: DESCRIPTION_INTRO }), _jsx(DocHeading, { children: "Interactive example" }), _jsx(Markdown, { children: PROPS_INTRO }), _jsx(DocPrimary, {}), _jsx(Controls, {}), _jsx(DocHeading, { children: "Usage guidance" }), _jsx(Markdown, { children: USAGE_GUIDANCE }), _jsx(DocHeading, { children: "Developer notes" }), _jsx(Markdown, { children: DEVELOPER_NOTES }), _jsx(DocHeading, { children: "Examples" }), _jsx(Stories, { title: "" }), _jsx(Markdown, { children: RELATED_COMPONENTS })] }));
177
+ }
178
+ // ---------------------------------------------------------------------------
179
+ // Meta
180
+ // ---------------------------------------------------------------------------
6
181
  const meta = {
7
182
  title: 'Components/FormField/Inputs/RadioButton',
8
183
  component: RadioButtonInput,
9
184
  parameters: {
10
185
  layout: 'centered',
186
+ docs: {
187
+ page: RadioButtonInputDocsPage,
188
+ },
11
189
  },
12
190
  tags: ['autodocs'],
13
191
  args: {
@@ -16,74 +194,525 @@ const meta = {
16
194
  argTypes: {
17
195
  checked: {
18
196
  control: 'boolean',
19
- description: 'Whether the radio button is checked',
197
+ description: [
198
+ 'Whether the radio button is selected. The component is always controlled — `checked` defaults to `false`',
199
+ 'in the component destructuring, so React treats it as a controlled input immediately.',
200
+ 'You must always pair this prop with an `onChange` handler, otherwise the radio will appear frozen.',
201
+ ].join(' '),
202
+ table: {
203
+ type: { summary: 'boolean' },
204
+ defaultValue: { summary: 'false' },
205
+ },
20
206
  },
21
207
  disabled: {
22
208
  control: 'boolean',
23
- description: 'Disable the radio button',
209
+ description: [
210
+ 'Disables the radio button. Applies muted styling and removes it from the tab order.',
211
+ 'The value is NOT submitted in a form when disabled.',
212
+ 'Always accompany a disabled radio with a tooltip or helper text explaining why it is disabled.',
213
+ ].join(' '),
214
+ table: {
215
+ type: { summary: 'boolean' },
216
+ defaultValue: { summary: 'false' },
217
+ },
24
218
  },
25
219
  hasError: {
26
220
  control: 'boolean',
27
- description: 'Show error state',
221
+ description: [
222
+ '**ARIA-only** — sets `aria-invalid="true"` on the native `<input>` element.',
223
+ 'This makes ZERO visual difference to the indicator circle — there are no CSS rules for the error state.',
224
+ 'Consumers must separately render a visible error message below the group.',
225
+ 'See the `WithError` and `RadioGroupWithError` stories for the correct pattern.',
226
+ ].join(' '),
227
+ table: {
228
+ type: { summary: 'boolean' },
229
+ },
28
230
  },
29
231
  label: {
30
232
  control: 'text',
31
- description: 'Label text for the radio button',
233
+ description: [
234
+ 'Visible label text rendered next to the indicator circle.',
235
+ 'When present, it is wrapped in a `.ds-radio-button-input__text` span and the click target',
236
+ 'expands to the entire row.',
237
+ 'When absent, only the indicator circle is rendered — always provide `aria-label` in that case.',
238
+ ].join(' '),
239
+ table: {
240
+ type: { summary: 'string' },
241
+ },
32
242
  },
33
243
  name: {
34
244
  control: 'text',
35
- description: 'Name attribute for the radio button group',
245
+ description: [
246
+ 'HTML `name` attribute — functionally mandatory even though TypeScript does not require it.',
247
+ 'All radio buttons in the same group must share the same `name` value so the browser can enforce',
248
+ 'mutual exclusivity (selecting one deselects all others with the same name).',
249
+ 'Omitting `name` means multiple radios can be "selected" simultaneously — a broken experience.',
250
+ ].join(' '),
251
+ table: {
252
+ type: { summary: 'string' },
253
+ },
36
254
  },
37
255
  value: {
38
256
  control: 'text',
39
- description: 'Value attribute for the radio button',
257
+ description: [
258
+ 'The value submitted with the form when this radio button is selected.',
259
+ 'Also the value used in `RadioButtonGroup` — the `checkedValue` prop is compared against each',
260
+ 'option\'s `value` to determine which radio is selected.',
261
+ ].join(' '),
262
+ table: {
263
+ type: { summary: 'string' },
264
+ },
265
+ },
266
+ onChange: {
267
+ description: [
268
+ 'Change event handler — required for controlled usage. Fires when the user selects this option.',
269
+ 'Access the selected value via `e.target.value`.',
270
+ 'Without this prop, a controlled radio will appear frozen when clicked.',
271
+ ].join(' '),
272
+ control: false,
273
+ table: {
274
+ type: { summary: 'ChangeEventHandler<HTMLInputElement>' },
275
+ },
276
+ },
277
+ id: {
278
+ control: 'text',
279
+ description: [
280
+ 'HTML `id` attribute — spread onto the native `<input>` element.',
281
+ 'Required by `RadioButtonGroup` (passed via `options`) to key list items.',
282
+ 'Also useful for associating external `aria-describedby` targets (e.g. an error message).',
283
+ ].join(' '),
284
+ table: {
285
+ type: { summary: 'string' },
286
+ },
287
+ },
288
+ className: {
289
+ control: 'text',
290
+ description: [
291
+ 'Additional CSS class names applied to the outer `<label>` container element',
292
+ '(the `.ds-radio-button-input__container` wrapper), not to the indicator circle itself.',
293
+ ].join(' '),
294
+ table: {
295
+ type: { summary: 'string' },
296
+ },
40
297
  },
41
298
  },
42
299
  };
43
300
  export default meta;
44
- // Default radio button
301
+ // ---------------------------------------------------------------------------
302
+ // Helper: attach a per-story description to docs
303
+ // ---------------------------------------------------------------------------
304
+ const withDescription = (story, description) => ({
305
+ ...story,
306
+ parameters: {
307
+ ...story.parameters,
308
+ docs: {
309
+ ...story.parameters?.docs,
310
+ description: {
311
+ story: Array.isArray(description) ? description.join(' ') : description,
312
+ },
313
+ },
314
+ },
315
+ });
316
+ // ---------------------------------------------------------------------------
317
+ // Stateful template components
318
+ // Named components avoid hooks-in-callbacks lint issues (react-hooks plugin
319
+ // is NOT configured in this project — do not add eslint-disable comments).
320
+ // ---------------------------------------------------------------------------
321
+ const RadioGroupTemplate = () => {
322
+ const [grouping, setGrouping] = useState('year-group');
323
+ return (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }, children: [_jsx(RadioButtonGroup, { legend: "Attendance grouping", name: "attendance-grouping", checkedValue: grouping, onChange: e => setGrouping(e.target.value), options: [
324
+ { id: 'opt-custom', value: 'custom-group', label: 'Custom group' },
325
+ { id: 'opt-year', value: 'year-group', label: 'Year group' },
326
+ { id: 'opt-reg', value: 'reg-form', label: 'Registration form' },
327
+ ] }), _jsxs("p", { style: { margin: 'var(--spacing-medium) 0 0', color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: ["Selected:", ' ', grouping] })] }));
328
+ };
329
+ const RadioGroupWithErrorTemplate = () => {
330
+ const [method, setMethod] = useState('');
331
+ return (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }, children: [_jsx(RadioButtonGroup, { legend: "Notification method", name: "notification-method", checkedValue: method, onChange: e => setMethod(e.target.value), options: [
332
+ { id: 'opt-email', value: 'email', label: 'Email', hasError: true },
333
+ { id: 'opt-sms', value: 'sms', label: 'SMS', hasError: true },
334
+ { id: 'opt-post', value: 'post', label: 'Post', hasError: true },
335
+ ] }), _jsx("p", { role: "alert", style: {
336
+ margin: 'var(--spacing-small) 0 0',
337
+ color: 'var(--form-field-text-error-color-error-text)',
338
+ fontSize: 'var(--font-size-2-13)',
339
+ }, children: "Please select a notification method to continue." })] }));
340
+ };
341
+ // ---------------------------------------------------------------------------
342
+ // Stories
343
+ // ---------------------------------------------------------------------------
45
344
  export const Default = {
46
345
  args: {
47
- name: 'example',
48
- value: 'option1',
49
- label: 'Option 1',
346
+ name: 'report-period',
347
+ value: 'current-term',
348
+ label: 'Current term',
50
349
  checked: false,
350
+ disabled: false,
351
+ hasError: false,
51
352
  },
353
+ render: args => (_jsx("div", { style: { padding: 'var(--spacing-xlarge)' }, children: _jsx(RadioButtonInput, { ...args }) })),
52
354
  };
53
- // Checked radio button
54
- export const Checked = {
55
- args: {
56
- name: 'example',
57
- value: 'option1',
58
- label: 'Option 1',
59
- checked: true,
355
+ export const Checked = withDescription({
356
+ render: () => (_jsx("div", { style: { padding: 'var(--spacing-xlarge)' }, children: _jsx(RadioButtonInput, { id: "checked-example", name: "report-period", value: "current-term", checked: true, onChange: fn(), label: "Current term" }) })),
357
+ parameters: {
358
+ docs: {
359
+ source: {
360
+ language: 'tsx',
361
+ code: `
362
+ import { RadioButtonInput } from '@arbor-education/design-system.components';
363
+
364
+ function CheckedExample() {
365
+ return (
366
+ <RadioButtonInput
367
+ id="report-current-term"
368
+ name="report-period"
369
+ value="current-term"
370
+ checked
371
+ onChange={(e) => console.log(e.target.value)}
372
+ label="Current term"
373
+ />
374
+ );
375
+ }
376
+
377
+ export default CheckedExample;
378
+ `.trim(),
379
+ },
380
+ },
60
381
  },
61
- };
62
- // Disabled radio button
63
- export const Disabled = {
64
- args: {
65
- name: 'example',
66
- value: 'option1',
67
- label: 'Option 1',
68
- disabled: true,
382
+ }, 'The selected (checked) state — the indicator circle fills with a solid inner dot. `checked` is always controlled; pair it with an `onChange` handler so the user can change the selection.');
383
+ export const Disabled = withDescription({
384
+ render: () => (_jsx("div", { style: { padding: 'var(--spacing-xlarge)' }, children: _jsx(RadioButtonInput, { id: "disabled-example", name: "report-period", value: "last-term", checked: false, disabled: true, onChange: fn(), label: "Last term" }) })),
385
+ parameters: {
386
+ docs: {
387
+ source: {
388
+ language: 'tsx',
389
+ code: `
390
+ import { RadioButtonInput } from '@arbor-education/design-system.components';
391
+
392
+ function DisabledExample() {
393
+ // Disabled — pair with a Tooltip or helper text explaining why.
394
+ return (
395
+ <RadioButtonInput
396
+ id="report-last-term"
397
+ name="report-period"
398
+ value="last-term"
399
+ checked={false}
400
+ disabled
401
+ onChange={() => {}}
402
+ label="Last term"
403
+ />
404
+ );
405
+ }
406
+
407
+ export default DisabledExample;
408
+ `.trim(),
409
+ },
410
+ },
69
411
  },
70
- };
71
- // Disabled and checked
72
- export const DisabledChecked = {
73
- args: {
74
- name: 'example',
75
- value: 'option1',
76
- label: 'Option 1',
77
- disabled: true,
78
- checked: true,
412
+ }, [
413
+ 'A disabled, unselected radio button. The control renders with muted styling and the cursor changes to `not-allowed`.',
414
+ 'The field is removed from the tab order and its value is not submitted in a form.',
415
+ 'Always accompany a disabled radio with a tooltip or visible helper text that explains why it is',
416
+ 'disabled — for example: "Last term data is not yet available."',
417
+ ].join(' '));
418
+ export const DisabledChecked = withDescription({
419
+ render: () => (_jsx("div", { style: { padding: 'var(--spacing-xlarge)' }, children: _jsx(RadioButtonInput, { id: "disabled-checked-example", name: "report-period", value: "current-term", checked: true, disabled: true, onChange: fn(), label: "Current term" }) })),
420
+ parameters: {
421
+ docs: {
422
+ source: {
423
+ language: 'tsx',
424
+ code: `
425
+ import { RadioButtonInput } from '@arbor-education/design-system.components';
426
+
427
+ function DisabledCheckedExample() {
428
+ // Disabled and already selected — read-only enforced selection.
429
+ return (
430
+ <RadioButtonInput
431
+ id="report-current-term"
432
+ name="report-period"
433
+ value="current-term"
434
+ checked
435
+ disabled
436
+ onChange={() => {}}
437
+ label="Current term"
438
+ />
439
+ );
440
+ }
441
+
442
+ export default DisabledCheckedExample;
443
+ `.trim(),
444
+ },
445
+ },
79
446
  },
80
- };
81
- // Radio button group example
82
- export const RadioGroup = {
83
- render: () => {
84
- const options = [{ label: 'Option 1', value: 'option1' }, { label: 'Option 2', value: 'option2' }, { label: 'Option 3', value: 'option3' }];
85
- const [checkedValue, setCheckedValue] = useState(options[0]?.value ?? '');
86
- return (_jsx(RadioButtonGroup, { legend: "Radio group", name: "group", options: options, checkedValue: checkedValue, onChange: e => setCheckedValue(e.target.value) }));
447
+ }, 'A disabled radio button that is already selected. This is appropriate for read-only settings where the current selection is system-enforced and cannot be changed. Always provide a tooltip or helper text explaining the constraint.');
448
+ export const WithError = withDescription({
449
+ render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-medium)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Report period" }), _jsx(RadioButtonInput, { id: "error-current-term", name: "error-report-period", value: "current-term", checked: false, hasError: true, onChange: fn(), label: "Current term" }), _jsx(RadioButtonInput, { id: "error-last-term", name: "error-report-period", value: "last-term", checked: false, hasError: true, onChange: fn(), label: "Last term" }), _jsx(RadioButtonInput, { id: "error-full-year", name: "error-report-period", value: "full-year", checked: false, hasError: true, onChange: fn(), label: "Full academic year" }), _jsx("p", { role: "alert", style: {
450
+ margin: 0,
451
+ color: 'var(--form-field-text-error-color-error-text)',
452
+ fontSize: 'var(--font-size-2-13)',
453
+ }, children: "Please select a report period to continue." })] })),
454
+ parameters: {
455
+ docs: {
456
+ source: {
457
+ language: 'tsx',
458
+ code: `
459
+ import { RadioButtonInput } from '@arbor-education/design-system.components';
460
+
461
+ function WithErrorExample() {
462
+ // IMPORTANT: hasError sets aria-invalid="true" on the <input> but makes
463
+ // NO visual change to the indicator circle — there are zero CSS rules for
464
+ // the error state. You MUST render the error message yourself.
465
+ return (
466
+ <div>
467
+ <RadioButtonInput
468
+ id="period-current"
469
+ name="report-period"
470
+ value="current-term"
471
+ checked={false}
472
+ hasError
473
+ onChange={() => {}}
474
+ label="Current term"
475
+ />
476
+ <RadioButtonInput
477
+ id="period-last"
478
+ name="report-period"
479
+ value="last-term"
480
+ checked={false}
481
+ hasError
482
+ onChange={() => {}}
483
+ label="Last term"
484
+ />
485
+ <RadioButtonInput
486
+ id="period-full"
487
+ name="report-period"
488
+ value="full-year"
489
+ checked={false}
490
+ hasError
491
+ onChange={() => {}}
492
+ label="Full academic year"
493
+ />
494
+ {/* Consumer-rendered error message — hasError alone shows nothing visually */}
495
+ <p role="alert" style={{ color: 'var(--form-field-text-error-color-error-text)' }}>
496
+ Please select a report period to continue.
497
+ </p>
498
+ </div>
499
+ );
500
+ }
501
+
502
+ export default WithErrorExample;
503
+ `.trim(),
504
+ },
505
+ },
87
506
  },
88
- };
507
+ }, [
508
+ '**Important:** `hasError={true}` sets `aria-invalid="true"` on the native `<input>` — this is',
509
+ 'ARIA-only and makes zero visual difference to the indicator circle.',
510
+ 'There are no CSS rules in the design system that change the indicator\'s appearance based on `aria-invalid`.',
511
+ 'To communicate the error visually, you must render your own error message below the group (shown here',
512
+ 'with `role="alert"` so screen readers announce it automatically).',
513
+ 'Error validation should always be at the field level, not per-option.',
514
+ ].join(' '));
515
+ export const WithoutLabel = withDescription({
516
+ render: () => (_jsxs("div", { style: { padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-medium)', alignItems: 'flex-start' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Unselected (no label \u2014 indicator only):" }), _jsx(RadioButtonInput, { id: "no-label-unselected", name: "no-label-group", value: "option-a", checked: false, onChange: fn(), "aria-label": "Option A" }), _jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Selected (no label):" }), _jsx(RadioButtonInput, { id: "no-label-selected", name: "no-label-group", value: "option-b", checked: true, onChange: fn(), "aria-label": "Option B" })] })),
517
+ parameters: {
518
+ docs: {
519
+ source: {
520
+ language: 'tsx',
521
+ code: `
522
+ import { RadioButtonInput } from '@arbor-education/design-system.components';
523
+
524
+ function WithoutLabelExample() {
525
+ // When there is no visible label, always provide aria-label for screen readers.
526
+ return (
527
+ <RadioButtonInput
528
+ id="option-a"
529
+ name="my-group"
530
+ value="option-a"
531
+ checked={isSelected}
532
+ onChange={(e) => setSelected(e.target.value)}
533
+ aria-label="Option A"
534
+ />
535
+ );
536
+ }
537
+
538
+ export default WithoutLabelExample;
539
+ `.trim(),
540
+ },
541
+ },
542
+ },
543
+ }, [
544
+ 'RadioButtonInput without a `label` prop — renders just the indicator circle with no adjacent text.',
545
+ 'When omitting `label`, always pass `aria-label` or `aria-labelledby` so screen reader users',
546
+ 'understand what each option represents.',
547
+ ].join(' '));
548
+ export const AllStates = withDescription({
549
+ render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-medium)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Enabled states:" }), _jsx(RadioButtonInput, { id: "all-unselected", name: "all-states-group", value: "unselected", checked: false, onChange: fn(), label: "Unselected \u2014 default state" }), _jsx(RadioButtonInput, { id: "all-selected", name: "all-states-group", value: "selected", checked: true, onChange: fn(), label: "Selected \u2014 inner dot filled" }), _jsx("p", { style: { margin: 'var(--spacing-small) 0 0', color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Disabled states:" }), _jsx(RadioButtonInput, { id: "all-disabled-unselected", name: "all-disabled-group", value: "disabled-unselected", checked: false, disabled: true, onChange: fn(), label: "Disabled unselected" }), _jsx(RadioButtonInput, { id: "all-disabled-selected", name: "all-disabled-group", value: "disabled-selected", checked: true, disabled: true, onChange: fn(), label: "Disabled selected" })] })),
550
+ parameters: {
551
+ docs: {
552
+ source: {
553
+ language: 'tsx',
554
+ code: `
555
+ import { RadioButtonInput } from '@arbor-education/design-system.components';
556
+
557
+ function AllStatesExample() {
558
+ // Static display of all four visual states.
559
+ // In a real application every RadioButtonInput needs an onChange handler.
560
+ return (
561
+ <>
562
+ {/* Enabled */}
563
+ <RadioButtonInput name="g1" value="a" checked={false} onChange={() => {}} label="Unselected" />
564
+ <RadioButtonInput name="g1" value="b" checked onChange={() => {}} label="Selected" />
565
+ {/* Disabled */}
566
+ <RadioButtonInput name="g2" value="c" checked={false} disabled onChange={() => {}} label="Disabled unselected" />
567
+ <RadioButtonInput name="g2" value="d" checked disabled onChange={() => {}} label="Disabled selected" />
568
+ </>
569
+ );
570
+ }
571
+
572
+ export default AllStatesExample;
573
+ `.trim(),
574
+ },
575
+ },
576
+ },
577
+ }, 'A side-by-side reference showing all four visual states: unselected, selected, disabled unselected, and disabled selected. There is no visual error state on the indicator circle — `hasError` is ARIA-only. See the `WithError` story for the correct error pattern.');
578
+ export const RadioGroup = withDescription({
579
+ render: () => _jsx(RadioGroupTemplate, {}),
580
+ parameters: {
581
+ docs: {
582
+ source: {
583
+ language: 'tsx',
584
+ code: `
585
+ import { useState } from 'react';
586
+ import { RadioButtonGroup } from '@arbor-education/design-system.components';
587
+
588
+ function AttendanceGroupingExample() {
589
+ const [grouping, setGrouping] = useState('year-group');
590
+
591
+ return (
592
+ <RadioButtonGroup
593
+ legend="Attendance grouping"
594
+ name="attendance-grouping"
595
+ checkedValue={grouping}
596
+ onChange={(e) => setGrouping(e.target.value)}
597
+ options={[
598
+ { id: 'opt-custom', value: 'custom-group', label: 'Custom group' },
599
+ { id: 'opt-year', value: 'year-group', label: 'Year group' },
600
+ { id: 'opt-reg', value: 'reg-form', label: 'Registration form' },
601
+ ]}
602
+ />
603
+ );
604
+ }
605
+
606
+ export default AttendanceGroupingExample;
607
+ `.trim(),
608
+ },
609
+ },
610
+ },
611
+ }, [
612
+ '`RadioButtonGroup` is the canonical way to render a set of radio buttons.',
613
+ 'It composes `Fieldset` (which renders a `<fieldset>` + `<legend>`) and maps `options` to individual `RadioButtonInput` elements.',
614
+ 'The `legend` prop is announced by screen readers as the group label before each option.',
615
+ 'A single `checkedValue` + `onChange` at the group level manages the selection state — the group',
616
+ 'compares `checkedValue` against each option\'s `value` to determine which radio is selected.',
617
+ 'Try selecting different options — the state display below updates in real time.',
618
+ ].join(' '));
619
+ export const RadioGroupDisabled = withDescription({
620
+ name: 'RadioGroup — Fieldset-level disabled',
621
+ render: () => (_jsx("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)' }, children: _jsx(RadioButtonGroup, { legend: "Report period (locked \u2014 academic year not yet published)", name: "disabled-report-period", checkedValue: "current-term", onChange: fn(), disabled: true, options: [
622
+ { id: 'dis-opt-current', value: 'current-term', label: 'Current term' },
623
+ { id: 'dis-opt-last', value: 'last-term', label: 'Last term' },
624
+ { id: 'dis-opt-full', value: 'full-year', label: 'Full academic year' },
625
+ ] }) })),
626
+ parameters: {
627
+ docs: {
628
+ source: {
629
+ language: 'tsx',
630
+ code: `
631
+ import { RadioButtonGroup } from '@arbor-education/design-system.components';
632
+
633
+ function DisabledGroupExample() {
634
+ // Passing disabled to RadioButtonGroup flows through Fieldset's HTMLFieldSetElement props,
635
+ // disabling ALL child radio buttons at once — native HTML behaviour, zero extra JavaScript.
636
+ return (
637
+ <RadioButtonGroup
638
+ legend="Report period (locked — academic year not yet published)"
639
+ name="report-period"
640
+ checkedValue="current-term"
641
+ onChange={() => {}}
642
+ disabled
643
+ options={[
644
+ { id: 'opt-current', value: 'current-term', label: 'Current term' },
645
+ { id: 'opt-last', value: 'last-term', label: 'Last term' },
646
+ { id: 'opt-full', value: 'full-year', label: 'Full academic year' },
647
+ ]}
648
+ />
649
+ );
650
+ }
651
+
652
+ export default DisabledGroupExample;
653
+ `.trim(),
654
+ },
655
+ },
656
+ },
657
+ }, [
658
+ 'Passing `disabled` to `RadioButtonGroup` flows through the underlying `<fieldset>` element',
659
+ '(via `Fieldset`\'s `HTMLFieldSetElement` props), which disables ALL child `<input>` elements at once —',
660
+ 'native HTML behaviour, zero extra JavaScript.',
661
+ 'This is the correct pattern for locking an entire radio group (e.g. a settings panel that is',
662
+ 'read-only until the academic year is published).',
663
+ ].join(' '));
664
+ export const RadioGroupWithError = withDescription({
665
+ name: 'RadioGroup — field-level error',
666
+ render: () => _jsx(RadioGroupWithErrorTemplate, {}),
667
+ parameters: {
668
+ docs: {
669
+ source: {
670
+ language: 'tsx',
671
+ code: `
672
+ import { useState } from 'react';
673
+ import { RadioButtonGroup } from '@arbor-education/design-system.components';
674
+
675
+ function NotificationMethodWithErrorExample() {
676
+ const [method, setMethod] = useState('');
677
+
678
+ // hasError in each option sets aria-invalid="true" on each <input>.
679
+ // This makes ZERO visual difference to the indicator circles — there are
680
+ // no CSS rules for the aria-invalid error state. The visible error message
681
+ // below is the only thing that communicates the error to sighted users.
682
+ return (
683
+ <div>
684
+ <RadioButtonGroup
685
+ legend="Notification method"
686
+ name="notification-method"
687
+ checkedValue={method}
688
+ onChange={(e) => setMethod(e.target.value)}
689
+ options={[
690
+ { id: 'opt-email', value: 'email', label: 'Email', hasError: true },
691
+ { id: 'opt-sms', value: 'sms', label: 'SMS', hasError: true },
692
+ { id: 'opt-post', value: 'post', label: 'Post', hasError: true },
693
+ ]}
694
+ />
695
+ {/* Consumer-rendered error message — hasError alone shows nothing visually */}
696
+ <p role="alert" style={{ color: 'var(--form-field-text-error-color-error-text)' }}>
697
+ Please select a notification method to continue.
698
+ </p>
699
+ </div>
700
+ );
701
+ }
702
+
703
+ export default NotificationMethodWithErrorExample;
704
+ `.trim(),
705
+ },
706
+ },
707
+ },
708
+ }, [
709
+ '**Field-level validation** — error state is shown at the group level, not per option.',
710
+ '`hasError` is passed to each option in the `options` array, which sets `aria-invalid="true"`',
711
+ 'on each native `<input>`. Screen readers will announce each option as invalid.',
712
+ '**However, `hasError` makes ZERO visual change to the indicator circles** — there are no CSS rules',
713
+ 'for the error state. The visible error message rendered below the group (with `role="alert"`) is',
714
+ 'the only thing that communicates the error to sighted users.',
715
+ 'Start with no option selected and note how the error message is present — select any option to',
716
+ 'resolve the validation in a real implementation.',
717
+ ].join(' '));
89
718
  //# sourceMappingURL=RadioButtonInput.stories.js.map