@pnx-mixtape/mxds 0.1.0-rc.0 → 0.1.0-rc.2

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 (204) hide show
  1. package/.storybook/decorators.ts +15 -0
  2. package/.storybook/preview.ts +4 -1
  3. package/.storybook/themes/campus-vibrant.css +181 -0
  4. package/.storybook/themes/civic-formal.css +246 -0
  5. package/.storybook/{theme-demo.css → themes/demo.css} +14 -7
  6. package/.storybook/themes/health-services.css +255 -0
  7. package/.storybook/themes/research-faculty.css +169 -0
  8. package/dist/build/accordion.css +1 -1
  9. package/dist/build/base.css +1 -1
  10. package/dist/build/button.css +1 -1
  11. package/dist/build/chunks/DropMenu-1baQc8Yc.js +1 -0
  12. package/dist/build/chunks/InPageNavigation-CVieEl9u.js +1 -0
  13. package/dist/build/chunks/Popover-Cxg-s5Jn.js +1 -0
  14. package/dist/build/chunks/Sticky-cWUVSOXN.js +1 -0
  15. package/dist/build/chunks/{Tabs-Dm8g301e.js → Tabs-DA_Ez8pv.js} +2 -2
  16. package/dist/build/chunks/polyfills-C7pRt54M.js +1 -0
  17. package/dist/build/constants.css +1 -1
  18. package/dist/build/drop-menu.css +1 -1
  19. package/dist/build/drop-menu.entry.js +1 -1
  20. package/dist/build/elements.js +1 -1
  21. package/dist/build/form.css +1 -1
  22. package/dist/build/grid.css +1 -1
  23. package/dist/build/header.css +1 -1
  24. package/dist/build/icon.css +1 -1
  25. package/dist/build/in-page-alert.css +1 -1
  26. package/dist/build/in-page-navigation.entry.js +1 -1
  27. package/dist/build/navigation.css +1 -1
  28. package/dist/build/pagination.css +1 -1
  29. package/dist/build/popover.entry.js +1 -1
  30. package/dist/build/print.css +1 -1
  31. package/dist/build/progress-indicator.css +1 -0
  32. package/dist/build/react.js +1 -1
  33. package/dist/build/side-navigation.css +1 -1
  34. package/dist/build/sidebar.css +1 -1
  35. package/dist/build/steps.css +1 -1
  36. package/dist/build/sticky.entry.js +1 -1
  37. package/dist/build/tabs.entry.js +1 -1
  38. package/dist/build/tag.css +1 -1
  39. package/dist/build/utilities.css +1 -1
  40. package/dist/types/Atom/DefinitionList/DefinitionList.d.ts +3 -2
  41. package/dist/types/Atom/DefinitionList/DefinitionList.d.ts.map +1 -1
  42. package/dist/types/Atom/Heading/Heading.d.ts.map +1 -1
  43. package/dist/types/Atom/List/List.d.ts +10 -0
  44. package/dist/types/Atom/List/List.d.ts.map +1 -0
  45. package/dist/types/Component/DropMenu/Elements/DropMenu.d.ts.map +1 -1
  46. package/dist/types/Component/InPageNavigation/Elements/InPageNavigation.d.ts +2 -2
  47. package/dist/types/Component/InPageNavigation/Elements/InPageNavigation.d.ts.map +1 -1
  48. package/dist/types/Component/InPageNavigation/Hooks/useInPageNavigation.d.ts.map +1 -1
  49. package/dist/types/Component/ProgressIndicator/ProgressIndicator.d.ts +9 -0
  50. package/dist/types/Component/ProgressIndicator/ProgressIndicator.d.ts.map +1 -0
  51. package/dist/types/Component/Tabs/Elements/Tabs.d.ts.map +1 -1
  52. package/dist/types/Component/Tag/Tag.d.ts +4 -2
  53. package/dist/types/Component/Tag/Tag.d.ts.map +1 -1
  54. package/dist/types/Form/Autocomplete/Autocomplete.d.ts.map +1 -1
  55. package/dist/types/Form/Checkbox/FormCheckbox.d.ts.map +1 -1
  56. package/dist/types/Form/CheckboxCard/CheckboxCard.d.ts +23 -0
  57. package/dist/types/Form/CheckboxCard/CheckboxCard.d.ts.map +1 -0
  58. package/dist/types/Form/Radio/FormRadio.d.ts.map +1 -1
  59. package/dist/types/Form/RadioCard/RadioCard.d.ts +16 -0
  60. package/dist/types/Form/RadioCard/RadioCard.d.ts.map +1 -0
  61. package/dist/types/Form/Select/FormSelect.d.ts.map +1 -1
  62. package/dist/types/Form/TextInput/FormText.d.ts.map +1 -1
  63. package/dist/types/Form/Textarea/FormTextarea.d.ts.map +1 -1
  64. package/dist/types/enums.d.ts +15 -0
  65. package/dist/types/enums.d.ts.map +1 -1
  66. package/dist/types/react.d.ts +4 -0
  67. package/dist/types/react.d.ts.map +1 -1
  68. package/package.json +7 -8
  69. package/src/Atom/Background/_background.css +2 -2
  70. package/src/Atom/Blockquote/_blockquote.css +9 -2
  71. package/src/Atom/Button/README.md +1 -0
  72. package/src/Atom/Button/_buttons-styles.css +6 -6
  73. package/src/Atom/Button/_buttons.css +6 -3
  74. package/src/Atom/DefinitionList/DefinitionList.stories.ts +39 -2
  75. package/src/Atom/DefinitionList/DefinitionList.stories.tsx +36 -0
  76. package/src/Atom/DefinitionList/DefinitionList.tsx +4 -1
  77. package/src/Atom/DefinitionList/DefinitionListItem.stories.ts +1 -1
  78. package/src/Atom/DefinitionList/README.md +43 -0
  79. package/src/Atom/DefinitionList/__snapshots__/DefinitionList.stories.ts.snap +37 -3
  80. package/src/Atom/DefinitionList/__snapshots__/DefinitionList.stories.tsx.snap +2 -0
  81. package/src/Atom/DefinitionList/__snapshots__/DefinitionListItem.stories.ts.snap +3 -3
  82. package/src/Atom/DefinitionList/definition-list-item.twig +1 -0
  83. package/src/Atom/DefinitionList/definition-list.twig +6 -1
  84. package/src/Atom/Heading/Heading.tsx +6 -0
  85. package/src/Atom/Heading/_headings.css +2 -1
  86. package/src/Atom/Icon/_extended-set.css +30 -30
  87. package/src/Atom/Icon/_icon.css +37 -36
  88. package/src/Atom/Image/README.md +2 -2
  89. package/src/Atom/Image/_image.css +12 -6
  90. package/src/Atom/List/List.stories.ts +102 -0
  91. package/src/Atom/List/List.stories.tsx +103 -0
  92. package/src/Atom/List/List.tsx +22 -0
  93. package/src/Atom/List/README.md +150 -0
  94. package/src/Atom/List/__snapshots__/List.stories.ts.snap +43 -0
  95. package/src/Atom/List/__snapshots__/List.stories.tsx.snap +13 -0
  96. package/src/Atom/List/_list.css +123 -0
  97. package/src/Atom/List/list.twig +5 -0
  98. package/src/Atom/Media/Media.stories.ts +1 -1
  99. package/src/Atom/Media/__snapshots__/Media.stories.ts.snap +2 -2
  100. package/src/Atom/Spacing/_spacing.css +24 -24
  101. package/src/Atom/Table/__snapshots__/Table.stories.ts.snap +24 -4
  102. package/src/Atom/Table/_table.css +15 -3
  103. package/src/Atom/Table/table.twig +6 -1
  104. package/src/Atom/Text/__snapshots__/Text.stories.ts.snap +3 -2
  105. package/src/Atom/Text/_text-styles.css +21 -1
  106. package/src/Atom/Text/twig/text-styles-example.twig +3 -2
  107. package/src/Atom/_base.css +6 -0
  108. package/src/Atom/_flow.css +1 -1
  109. package/src/Atom/_generic.css +18 -11
  110. package/src/Component/Accordion/accordion.css +2 -0
  111. package/src/Component/Card/__snapshots__/Card.stories.ts.snap +2 -2
  112. package/src/Component/Card/card.twig +1 -1
  113. package/src/Component/Carousel/Carousel.stories.ts +46 -23
  114. package/src/Component/DropMenu/Elements/DropMenu.ts +10 -1
  115. package/src/Component/InPageAlert/in-page-alert.css +2 -1
  116. package/src/Component/InPageNavigation/Elements/InPageNavigation.ts +10 -5
  117. package/src/Component/InPageNavigation/Hooks/useInPageNavigation.ts +10 -5
  118. package/src/Component/InPageNavigation/InPageNavigation.stories.ts +33 -0
  119. package/src/Component/InPageNavigation/InPageNavigation.stories.tsx +52 -0
  120. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +26 -0
  121. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.tsx.snap +2 -0
  122. package/src/Component/InPageNavigation/twig/content-example-duplicate-headings.twig +20 -0
  123. package/src/Component/InPageNavigation/twig/in-page-navigation-story-duplicate-headings.twig +2 -0
  124. package/src/Component/ListItem/__snapshots__/ListItem.stories.ts.snap +4 -4
  125. package/src/Component/ListItem/list-item.twig +2 -2
  126. package/src/Component/Navigation/_navigation.css +1 -1
  127. package/src/Component/Pagination/Pagination.stories.ts +8 -4
  128. package/src/Component/Pagination/pagination.css +3 -3
  129. package/src/Component/ProgressIndicator/ProgressIndicator.stories.ts +36 -0
  130. package/src/Component/ProgressIndicator/ProgressIndicator.stories.tsx +30 -0
  131. package/src/Component/ProgressIndicator/ProgressIndicator.tsx +60 -0
  132. package/src/Component/ProgressIndicator/README.md +111 -0
  133. package/src/Component/ProgressIndicator/__snapshots__/ProgressIndicator.stories.ts.snap +67 -0
  134. package/src/Component/ProgressIndicator/__snapshots__/ProgressIndicator.stories.tsx.snap +7 -0
  135. package/src/Component/ProgressIndicator/progress-indicator.css +59 -0
  136. package/src/Component/ProgressIndicator/progress-indicator.twig +22 -0
  137. package/src/Component/Sticky/Elements/Sticky.ts +1 -1
  138. package/src/Component/Sticky/sticky.css +3 -3
  139. package/src/Component/Tabs/Elements/Tabs.ts +3 -1
  140. package/src/Component/Tabs/Tabs.stories.ts +55 -21
  141. package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +2 -2
  142. package/src/Component/Tag/README.md +52 -26
  143. package/src/Component/Tag/Tag.stories.tsx +24 -0
  144. package/src/Component/Tag/Tag.tsx +12 -2
  145. package/src/Component/Tag/TagItem.stories.ts +25 -0
  146. package/src/Component/Tag/__snapshots__/Tag.stories.ts.snap +2 -2
  147. package/src/Component/Tag/__snapshots__/Tag.stories.tsx.snap +2 -0
  148. package/src/Component/Tag/tag-item.twig +2 -2
  149. package/src/Component/Tag/tag.css +76 -2
  150. package/src/Form/Autocomplete/Autocomplete.stories.ts +8 -3
  151. package/src/Form/Autocomplete/Autocomplete.tsx +1 -4
  152. package/src/Form/Autocomplete/README.md +26 -26
  153. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.ts.snap +1 -1
  154. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.tsx.snap +1 -1
  155. package/src/Form/Autocomplete/autocomplete.twig +0 -2
  156. package/src/Form/Checkbox/FormCheckbox.tsx +2 -8
  157. package/src/Form/Checkbox/README.md +23 -18
  158. package/src/Form/Checkbox/input-checkbox.twig +0 -2
  159. package/src/Form/CheckboxCard/CheckboxCard.stories.ts +90 -0
  160. package/src/Form/CheckboxCard/CheckboxCard.stories.tsx +43 -0
  161. package/src/Form/CheckboxCard/CheckboxCard.tsx +102 -0
  162. package/src/Form/CheckboxCard/README.md +163 -0
  163. package/src/Form/CheckboxCard/__snapshots__/CheckboxCard.stories.ts.snap +63 -0
  164. package/src/Form/CheckboxCard/__snapshots__/CheckboxCard.stories.tsx.snap +7 -0
  165. package/src/Form/CheckboxCard/checkbox-card.twig +29 -0
  166. package/src/Form/FormItem/__snapshots__/FormItem.stories.ts.snap +2 -2
  167. package/src/Form/README.md +65 -13
  168. package/src/Form/Radio/FormRadio.tsx +1 -4
  169. package/src/Form/Radio/input-radio.twig +0 -2
  170. package/src/Form/RadioCard/README.md +146 -0
  171. package/src/Form/RadioCard/RadioCard.stories.ts +95 -0
  172. package/src/Form/RadioCard/RadioCard.stories.tsx +26 -0
  173. package/src/Form/RadioCard/RadioCard.tsx +59 -0
  174. package/src/Form/RadioCard/__snapshots__/RadioCard.stories.ts.snap +69 -0
  175. package/src/Form/RadioCard/__snapshots__/RadioCard.stories.tsx.snap +3 -0
  176. package/src/Form/RadioCard/radio-card.twig +20 -0
  177. package/src/Form/Select/FormSelect.tsx +1 -4
  178. package/src/Form/Select/input-select.twig +0 -2
  179. package/src/Form/TextInput/FormText.stories.tsx +14 -0
  180. package/src/Form/TextInput/FormText.tsx +1 -4
  181. package/src/Form/TextInput/__snapshots__/FormText.stories.tsx.snap +2 -0
  182. package/src/Form/TextInput/input-text.twig +0 -2
  183. package/src/Form/Textarea/FormTextarea.tsx +1 -4
  184. package/src/Form/Textarea/input-textarea.twig +0 -2
  185. package/src/Form/Validation.stories.ts +153 -0
  186. package/src/Form/__snapshots__/Validation.stories.ts.snap +130 -0
  187. package/src/Form/form.css +155 -28
  188. package/src/Layout/Grid/grid.css +4 -4
  189. package/src/Layout/Sidebar/__snapshots__/Sidebar.stories.ts.snap +5 -5
  190. package/src/Layout/Sidebar/sidebar.css +1 -0
  191. package/src/Layout/Sidebar/sidebar.twig +1 -1
  192. package/src/Utility/_layout-utils.css +8 -0
  193. package/src/Utility/print.css +50 -28
  194. package/src/Utility/utilities.css +2 -2
  195. package/src/constants.css +12 -2
  196. package/src/enums.ts +17 -0
  197. package/src/react.ts +4 -0
  198. package/tools/lib/extract-classnames.mjs +0 -0
  199. package/tools/lint-classnames.mjs +1 -1
  200. package/dist/build/chunks/DropMenu-Co9jZZ4S.js +0 -1
  201. package/dist/build/chunks/InPageNavigation-CzR_KZaz.js +0 -1
  202. package/dist/build/chunks/Popover-De_CQqcd.js +0 -1
  203. package/dist/build/chunks/Sticky-Kyjykb36.js +0 -1
  204. package/dist/build/chunks/polyfills-Dokbkxxk.js +0 -1
@@ -0,0 +1,163 @@
1
+ # CheckboxCard
2
+
3
+ A card-style checkbox — a full padded, bordered, selectable card instead of a small inline control. Use for a small set of options that benefit from a larger tap target and optional secondary hint text (e.g. a high-stakes form question), rather than a dense list of ordinary checkboxes.
4
+
5
+ > **HTML/Twig users:** use native `<input type="checkbox">` with both the `.mx-checkbox` and `.mx-choice-card` wrapper classes. The react-aria wiring is React-only.
6
+
7
+ `.mx-choice-card` layers on top of `.mx-checkbox` rather than replacing it — the base class still supplies input reset/sizing, disabled state, and the spacing between stacked cards in a group; `.mx-choice-card`'s higher cascade layer (`design-system.components` vs. `.mx-checkbox`'s `design-system.atoms`) overrides only what the card treatment needs to change (position, padding, border).
8
+
9
+ `.mx-choice-card` is shared with `RadioCard` — it lives in `src/Form/form.css` alongside the base `.mx-checkbox`/`.mx-radio` rules, not in a per-component CSS file, since the two components' card treatments were identical enough to not warrant separate copies.
10
+
11
+ ## CSS Import
12
+
13
+ ```css
14
+ @import "@pnx-mixtape/mxds/src/Form/form.css";
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Twig
20
+
21
+ ```twig
22
+ {# Single card #}
23
+ {% include '@mixtape/Form/CheckboxCard/checkbox-card.twig' with {
24
+ id: 'pay-carer',
25
+ name: 'payments',
26
+ value: 'Carer Payment or Allowance',
27
+ label: 'Carer Payment or Carer Allowance',
28
+ } %}
29
+
30
+ {# With a secondary hint line under the label #}
31
+ {% include '@mixtape/Form/CheckboxCard/checkbox-card.twig' with {
32
+ id: 'pay-other',
33
+ name: 'payments',
34
+ value: 'Another income support payment',
35
+ label: 'Another income support payment',
36
+ hint: 'You will be asked which one before you submit',
37
+ } %}
38
+ ```
39
+
40
+ **checkbox-card.twig props:**
41
+
42
+ | Prop | Type | Required | Description |
43
+ | ---------------- | ----------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
44
+ | `id` | `string` | Yes | Input id — also used to derive the `aria-describedby` ids (`help-{id}`, `status-{id}`) |
45
+ | `name` | `string` | Yes | `name` attribute on the input |
46
+ | `value` | `string` | Yes | `value` attribute on the input |
47
+ | `label` | `string` | Yes | Label text rendered inside `<label for="{id}">` |
48
+ | `hint` | `string` | No | Secondary line rendered under the label, inside the card |
49
+ | `checked` | `boolean` | No | Adds `checked="checked"` to the input |
50
+ | `required` | `boolean` | No | Adds the `required` attribute |
51
+ | `status` | `FormStatusTypes` | No | `invalid` adds the `error` class and `aria-invalid="true"`; `valid` adds the `success` class. Defaults to `default` |
52
+ | `hasDescription` | `boolean` | No | When `true`, `aria-describedby` is wired up to `help-{id}` (and `status-{id}` when `status` is set) |
53
+ | `attributes` | `Attribute` | No | Optional Drupal-style attributes object — `mx-checkbox mx-choice-card` (and the status class) is always merged on top |
54
+
55
+ For a group of cards, compose several `checkbox-card.twig` includes inside a plain `<fieldset>`, same as `Checkbox` — see HTML below.
56
+
57
+ ---
58
+
59
+ ## HTML
60
+
61
+ ```html
62
+ <fieldset class="mx-form-item mx-fieldset mx-fieldset--plain">
63
+ <legend class="mx-legend">Which payments do you currently receive?</legend>
64
+
65
+ <div class="mx-checkbox mx-choice-card">
66
+ <input type="checkbox" id="pay-age" name="payments" value="Age Pension" />
67
+ <label for="pay-age">Age Pension</label>
68
+ </div>
69
+
70
+ <div class="mx-checkbox mx-choice-card">
71
+ <input type="checkbox" id="pay-other" name="payments" value="Another income support payment" />
72
+ <label for="pay-other">
73
+ Another income support payment
74
+ <span class="mx-choice-card__hint">You will be asked which one before you submit</span>
75
+ </label>
76
+ </div>
77
+ </fieldset>
78
+ ```
79
+
80
+ ### "Or" divider between two option groups
81
+
82
+ `.mx-choice-divider` renders a horizontal rule with centred text — useful for splitting a set of card choices into "pick one of these — or — pick this other option" (see the source mockup's card question). It's plain markup, not tied to `CheckboxCard`/`RadioCard` specifically:
83
+
84
+ ```html
85
+ <div class="mx-choice-divider">or</div>
86
+ ```
87
+
88
+ ---
89
+
90
+ ## React
91
+
92
+ ### Standalone card
93
+
94
+ ```tsx
95
+ import { SingleCheckboxCard } from "@pnx-mixtape/mxds/react"
96
+
97
+ ;<SingleCheckboxCard value="carer">Carer Payment or Carer Allowance</SingleCheckboxCard>
98
+ ```
99
+
100
+ ### Card group
101
+
102
+ Use `FormCheckboxCard` as the group wrapper and `CheckboxCard` for each option — same pattern as `FormCheckbox`/`Checkbox`.
103
+
104
+ ```tsx
105
+ import { FormCheckboxCard, CheckboxCard } from "@pnx-mixtape/mxds/react"
106
+
107
+ ;<FormCheckboxCard label="Which payments do you currently receive?" onChange={setSelected}>
108
+ <CheckboxCard value="age">Age Pension</CheckboxCard>
109
+ <CheckboxCard value="dsp">Disability Support Pension</CheckboxCard>
110
+ <CheckboxCard value="other" hint="You will be asked which one before you submit">
111
+ Another income support payment
112
+ </CheckboxCard>
113
+ </FormCheckboxCard>
114
+ ```
115
+
116
+ **`FormCheckboxCard` props:** all `CheckboxGroupProps` from react-stately + `label: string`
117
+
118
+ **`CheckboxCard` props:** all `AriaCheckboxGroupItemProps` + `hint?: string`, `status?: FormStatus`, `className?: string`
119
+
120
+ **`SingleCheckboxCard` props:** all `AriaCheckboxProps` + `hint?: string`, `status?: FormStatus`, `className?: string`
121
+
122
+ ---
123
+
124
+ ### React ⇄ Twig parity
125
+
126
+ The prop shapes intentionally diverge, same as `Checkbox`/`Radio`:
127
+
128
+ - Twig's `label` becomes React `children` — react-aria composes the accessible name from rendered content, not a string prop.
129
+ - Twig's `id`/`name`/`checked`/`required`/`value` are handled by the react-aria hooks (`useCheckboxGroupItem`/`useCheckbox`) via prop spread — they aren't declared as explicit props because react-aria owns generating/wiring them (e.g. `id` comes from `useId()`).
130
+ - Twig's `hasDescription` manually wires `aria-describedby` to a `help-{id}` node; React doesn't need it because react-aria's hooks generate and wire that association themselves when a description is passed through their own props.
131
+
132
+ ---
133
+
134
+ ## Modifiers
135
+
136
+ No BEM modifier classes — `error`/`success` are applied as plain (unprefixed) status classes, matching `Checkbox`'s convention, driven by the `status` prop/Twig var.
137
+
138
+ ---
139
+
140
+ ## Accessibility
141
+
142
+ - The input stays a real, focusable `<input type="checkbox">` — only its default appearance is replaced (`appearance: none` plus a custom checked-state image), so it keeps native checkbox semantics, keyboard support (<kbd>Space</kbd> to toggle), and form submission behaviour.
143
+ - The whole card is clickable because the `<label>` fills it — the input itself only needs to be as large as `--control-size` dictates, not the full card.
144
+ - The focus ring (`:focus-visible`) renders on the input itself, inside the card, rather than around the whole card — keeps the indicator anchored to the actual interactive control.
145
+ - `hint` text is inside the same `<label>` as the primary text, so both are announced together when the control receives focus — don't rely on `hint` to convey anything the primary `label` text doesn't already establish.
146
+
147
+ ---
148
+
149
+ ## Customisation
150
+
151
+ | Custom property | Applies to | Default |
152
+ | ------------------------- | ----------------------------------------------------------------- | --------------------------------------------------- |
153
+ | `--choice-padding-block` | `.mx-choice-card` | `var(--spacing-s)` |
154
+ | `--choice-padding-inline` | `.mx-choice-card` | `var(--spacing-m)` |
155
+ | `--choice-colour-active` | `.mx-choice-card` | `var(--colour-primary)` |
156
+ | `--control-size` | `.mx-choice-card input`, and `.mx-checkbox`'s own input styling | `var(--spacing-m)` (shared with `Checkbox`/`Radio`) |
157
+ | `--line-width` | `.mx-choice-card`'s border, and `.mx-checkbox`'s own input border | `2px` |
158
+
159
+ ```css
160
+ .theme--health-services .mx-choice-card {
161
+ --choice-colour-active: var(--colour-primary);
162
+ }
163
+ ```
@@ -0,0 +1,63 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Checkbox Card 1`] = `
4
+ "
5
+ <div class="mx-checkbox mx-choice-card">
6
+ <input id="pay-carer" type="checkbox" name="payments" value="Carer Payment or Allowance">
7
+ <label for="pay-carer">
8
+ Carer Payment or Carer Allowance
9
+ </label>
10
+ </div>
11
+ "
12
+ `;
13
+
14
+ exports[`Checked 1`] = `
15
+ "
16
+ <div class="mx-checkbox mx-choice-card">
17
+ <input id="pay-carer" type="checkbox" name="payments" value="Carer Payment or Allowance" checked="checked">
18
+ <label for="pay-carer">
19
+ Carer Payment or Carer Allowance
20
+ </label>
21
+ </div>
22
+ "
23
+ `;
24
+
25
+ exports[`Divider 1`] = `
26
+ "
27
+ <div class="mx-checkbox mx-choice-card">
28
+ <input id="card-yes" type="checkbox" name="card" value="Yes, I hold a card">
29
+ <label for="card-yes">
30
+ Yes, I hold a card
31
+ <span class="mx-choice-card__hint">Pensioner Concession, Health Care or DVA card</span> </label>
32
+ </div>
33
+ <div class="mx-choice-divider">or</div>
34
+ <div class="mx-checkbox mx-choice-card">
35
+ <input id="card-unsure" type="checkbox" name="card" value="Not sure">
36
+ <label for="card-unsure">
37
+ I am not sure
38
+ <span class="mx-choice-card__hint">We will ask a few more questions to work it out</span> </label>
39
+ </div>
40
+ "
41
+ `;
42
+
43
+ exports[`Invalid 1`] = `
44
+ "
45
+ <div class="mx-checkbox mx-choice-card">
46
+ <input id="pay-carer" type="checkbox" name="unique-0" value="Carer Payment or Allowance" aria-describedby="unique-0" aria-invalid="true">
47
+ <label for="pay-carer">
48
+ Carer Payment or Carer Allowance
49
+ </label>
50
+ </div>
51
+ "
52
+ `;
53
+
54
+ exports[`With Hint 1`] = `
55
+ "
56
+ <div class="mx-checkbox mx-choice-card">
57
+ <input id="pay-other" type="checkbox" name="payments" value="Another income support payment">
58
+ <label for="pay-other">
59
+ Another income support payment
60
+ <span class="mx-choice-card__hint">You will be asked which one before you submit</span> </label>
61
+ </div>
62
+ "
63
+ `;
@@ -0,0 +1,7 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Group 1`] = `"<fieldset class="mx-form-item mx-fieldset" role="group" id="react-aria-_r_6_"><legend class="mx-legend">Which payments do you currently receive?</legend><div class="mx-checkbox mx-choice-card"><input id="_r_b_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="age"><label for="_r_b_">Age Pension</label></div><div class="mx-checkbox mx-choice-card"><input id="_r_e_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="dsp"><label for="_r_e_">Disability Support Pension</label></div><div class="mx-checkbox mx-choice-card"><input id="_r_h_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="carer"><label for="_r_h_">Carer Payment or Carer Allowance</label></div><div class="mx-checkbox mx-choice-card"><input id="_r_k_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="other"><label for="_r_k_">Another income support payment<span class="mx-choice-card__hint">You will be asked which one before you submit</span></label></div></fieldset>"`;
4
+
5
+ exports[`Single 1`] = `"<div class="mx-checkbox mx-choice-card"><input id="_r_0_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="carer"><label for="_r_0_">Carer Payment or Carer Allowance</label></div>"`;
6
+
7
+ exports[`With Hint 1`] = `"<div class="mx-checkbox mx-choice-card"><input id="_r_3_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="other"><label for="_r_3_">Another income support payment<span class="mx-choice-card__hint">You will be asked which one before you submit</span></label></div>"`;
@@ -0,0 +1,29 @@
1
+ {% set classes = [
2
+ 'mx-checkbox',
3
+ 'mx-choice-card',
4
+ ] %}
5
+ {% set attributes = (attributes ?? create_attribute()).addClass(classes) %}
6
+ {% set status = status|default("default") %}
7
+ {% if hasDescription or status != "default" %}
8
+ {% set helpers = [
9
+ hasDescription ? "help-"~ id : null,
10
+ status != "default" ? "status-"~ id : null,
11
+ ]|join(" ")|trim() %}
12
+ {% endif %}
13
+
14
+ <div{{ attributes }}>
15
+ <input
16
+ id="{{ id }}"
17
+ type="checkbox"
18
+ name="{{ name }}"
19
+ value="{{ value }}"
20
+ {% if checked %} checked="checked"{% endif %}
21
+ {% if helpers is not empty %} aria-describedby="{{ helpers }}"{% endif %}
22
+ {% if status == "invalid" %} aria-invalid="true"{% endif %}
23
+ {% if required %} required{% endif %}
24
+ >
25
+ <label for="{{ id }}">
26
+ {{ label }}
27
+ {% if hint %}<span class="mx-choice-card__hint">{{ hint }}</span>{% endif %}
28
+ </label>
29
+ </div>
@@ -31,7 +31,7 @@ exports[`Error 1`] = `
31
31
 
32
32
 
33
33
 
34
- <input class="mx-input__text error" id="form-item" name="unique-0" type="text" value="Musicals" placeholder="eg. Funk, Soul, Blues.." aria-describedby="unique-0" aria-invalid="true">
34
+ <input class="mx-input__text" id="form-item" name="unique-0" type="text" value="Musicals" placeholder="eg. Funk, Soul, Blues.." aria-describedby="unique-0" aria-invalid="true">
35
35
 
36
36
  <div class="mx-description" id="help-form-item">
37
37
  Help text that provides additional information about the field.
@@ -118,7 +118,7 @@ exports[`Success 1`] = `
118
118
 
119
119
 
120
120
 
121
- <input class="mx-input__text success" id="form-item" name="unique-0" type="text" value="Jazz" placeholder="eg. Funk, Soul, Blues.." aria-describedby="unique-0">
121
+ <input class="mx-input__text" id="form-item" name="unique-0" type="text" value="Jazz" placeholder="eg. Funk, Soul, Blues.." aria-describedby="unique-0">
122
122
 
123
123
  <div class="mx-description" id="help-form-item">
124
124
  Help text that provides additional information about the field.
@@ -32,7 +32,7 @@ The input's `aria-describedby` is automatically wired to `help-{id}` — `FormDe
32
32
  <div class="mx-form-item">
33
33
  <label class="mx-label" for="email">Email address</label>
34
34
  <input
35
- class="mx-input__text error"
35
+ class="mx-input__text"
36
36
  id="email"
37
37
  type="email"
38
38
  name="email"
@@ -46,12 +46,13 @@ The input's `aria-describedby` is automatically wired to `help-{id}` — `FormDe
46
46
 
47
47
  ## Input modifier classes
48
48
 
49
- | Class | Effect |
50
- | ------------------------ | ------------------------------------------------------------ |
51
- | `mx-input--wide` | `inline-size: 100%` — fills its container |
52
- | `mx-input--small` | Reduced border (1px), smaller chevron offset |
53
- | `mx-input--icon` | Adds leading icon via `::before` mask |
54
- | `mx-form--inline-button` | Input + button fused into one bordered unit (used in Search) |
49
+ | Class | Effect |
50
+ | ------------------------ | ------------------------------------------------------------- |
51
+ | `mx-input--wide` | `inline-size: 100%` — fills its container |
52
+ | `mx-input--small` | Reduced border (1px), smaller chevron offset |
53
+ | `mx-input--icon` | Adds leading icon via `::before` mask |
54
+ | `mx-form--inline-button` | Input + button fused into one bordered unit (used in Search) |
55
+ | `mx-validate` | Opt in to live `:user-valid` / `:user-invalid` border colours |
55
56
 
56
57
  ## Form actions
57
58
 
@@ -186,13 +187,64 @@ import { FormStatus } from "../../enums"
186
187
 
187
188
  ---
188
189
 
189
- ## Status states (`FormStatus` enum)
190
+ ## Validation state
190
191
 
191
- | Value | Class suffix | Description |
192
- | -------------------- | ------------ | ------------------------------------------------------------ |
193
- | `FormStatus.DEFAULT` | — | Normal state |
194
- | `FormStatus.VALID` | `success` | Green border, success icon on description |
195
- | `FormStatus.INVALID` | `error` | Red border, error icon on description, `aria-invalid="true"` |
192
+ There is no `error` or `success` class to apply. Two hooks drive input borders:
193
+
194
+ ### 1. Live native validation — opt in with `mx-validate`
195
+
196
+ Add `mx-validate` to a control to let the browser colour it as the user types.
197
+ Without the class, a control is **never** coloured automatically — so filling in
198
+ an unmarked field doesn't turn it green.
199
+
200
+ ```html
201
+ <input class="mx-input__text mx-validate" type="email" required />
202
+ ```
203
+
204
+ For checkbox and radio, put it on the wrapper — that's where `className` and
205
+ Drupal `attributes` land:
206
+
207
+ ```html
208
+ <div class="mx-checkbox mx-validate"><input type="checkbox" required /><label>…</label></div>
209
+ ```
210
+
211
+ | Selector | Effect | When it matches |
212
+ | --------------- | ------------ | -------------------------------------------------------------------- |
213
+ | `:user-valid` | Green border | Field passes validation **and** the user has interacted or submitted |
214
+ | `:user-invalid` | Red border | Field fails validation **and** the user has interacted or submitted |
215
+
216
+ `:user-*` is used rather than `:valid`/`:invalid` so a blank `required` field
217
+ isn't red on page load.
218
+
219
+ ### 2. Explicit errors — `aria-invalid`, no opt-in
220
+
221
+ `[aria-invalid="true"]` always paints a red border, with or without
222
+ `mx-validate`. This is for failures the browser can't see: server-rendered
223
+ (Drupal) or async. The consumer has already decided the field is wrong, so
224
+ there's nothing to opt into.
225
+
226
+ Live success styling excludes `[aria-invalid="true"]`, so an explicit error
227
+ stays red even when the field passes native validation.
228
+
229
+ ### `status` prop (`FormStatus` enum)
230
+
231
+ `status` no longer sets border classes. It controls the ARIA state and which
232
+ message block renders:
233
+
234
+ | Value | Effect |
235
+ | -------------------- | -------------------------------------------------------------- |
236
+ | `FormStatus.DEFAULT` | `aria-invalid="false"`, no message block |
237
+ | `FormStatus.VALID` | `mx-success-message` block, green icon — **no green border** |
238
+ | `FormStatus.INVALID` | `aria-invalid="true"` (⇒ red border), `mx-error-message` block |
239
+
240
+ For a server-side error, set `status={FormStatus.INVALID}` — the border follows
241
+ from `aria-invalid`. For client-side rules, use native attributes (`required`,
242
+ `pattern`, `type="email"`, `min`/`max`), add `mx-validate`, and let the browser
243
+ do it.
244
+
245
+ Green borders are live-only: there is no ARIA counterpart to `aria-invalid`, so
246
+ an explicitly-set `VALID` status renders the success message without recolouring
247
+ the border.
196
248
 
197
249
  ---
198
250
 
@@ -40,10 +40,7 @@ const Radio = ({
40
40
  const state = use(RadioContext)
41
41
  const { inputProps } = useRadio(props, state, ref)
42
42
 
43
- const classes = classNames("mx-radio", className, {
44
- error: status === FormStatus.INVALID,
45
- success: status === FormStatus.VALID,
46
- })
43
+ const classes = classNames("mx-radio", className)
47
44
 
48
45
  return (
49
46
  <div className={classes}>
@@ -1,7 +1,5 @@
1
1
  {% set classes = [
2
2
  'mx-radio',
3
- status == "invalid" ? "error" : null,
4
- status == "valid" ? "success" : null,
5
3
  ] %}
6
4
  {% set attributes = (attributes ?? create_attribute()).addClass(classes) %}
7
5
  <div{{ attributes }}>
@@ -0,0 +1,146 @@
1
+ # RadioCard
2
+
3
+ A card-style radio — a full padded, bordered, selectable card instead of a small inline control. Use for a small set of mutually-exclusive options that benefit from a larger tap target and optional secondary hint text (e.g. a high-stakes form question), rather than a dense list of ordinary radios.
4
+
5
+ > **HTML/Twig users:** use native `<input type="radio">` with both the `.mx-radio` and `.mx-choice-card` wrapper classes. The react-aria wiring is React-only.
6
+
7
+ `.mx-choice-card` layers on top of `.mx-radio` rather than replacing it — the base class still supplies input reset/sizing, disabled state, and the spacing between stacked cards in a group; `.mx-choice-card`'s higher cascade layer (`design-system.components` vs. `.mx-radio`'s `design-system.atoms`) overrides only what the card treatment needs to change (position, padding, border).
8
+
9
+ `.mx-choice-card` is shared with `CheckboxCard` — it lives in `src/Form/form.css` alongside the base `.mx-checkbox`/`.mx-radio` rules, not in a per-component CSS file, since the two components' card treatments were identical enough to not warrant separate copies.
10
+
11
+ ## CSS Import
12
+
13
+ ```css
14
+ @import "@pnx-mixtape/mxds/src/Form/form.css";
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Twig
20
+
21
+ ```twig
22
+ {% include '@mixtape/Form/RadioCard/radio-card.twig' with {
23
+ id: 'card-yes',
24
+ name: 'card',
25
+ value: 'yes',
26
+ label: 'Yes, I hold a card',
27
+ hint: 'Pensioner Concession, Health Care or DVA card',
28
+ } %}
29
+ ```
30
+
31
+ **radio-card.twig props:**
32
+
33
+ | Prop | Type | Required | Description |
34
+ | ------------ | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
35
+ | `id` | `string` | Yes | Input id |
36
+ | `name` | `string` | Yes | `name` attribute on the input |
37
+ | `value` | `string` | Yes | `value` attribute on the input |
38
+ | `label` | `string` | Yes | Label text rendered inside `<label for="{id}">` |
39
+ | `hint` | `string` | No | Secondary line rendered under the label, inside the card |
40
+ | `checked` | `boolean` | No | Adds `checked="checked"` to the input |
41
+ | `required` | `boolean` | No | Adds the `required` attribute |
42
+ | `status` | `FormStatusTypes` | No | `invalid` adds the `error` class and `aria-invalid="true"`; `valid` adds the `success` class. Defaults to `default` |
43
+ | `attributes` | `Attribute` | No | Optional Drupal-style attributes object — `mx-radio mx-choice-card` (and the status class) is always merged on top |
44
+
45
+ For a group of cards, compose several `radio-card.twig` includes inside a `<fieldset>` — see HTML below.
46
+
47
+ ---
48
+
49
+ ## HTML
50
+
51
+ ```html
52
+ <fieldset class="mx-form-item mx-fieldset mx-fieldset--plain">
53
+ <legend class="mx-legend">Do you hold a current concession card?</legend>
54
+
55
+ <div class="mx-radio mx-choice-card">
56
+ <input type="radio" id="card-yes" name="card" value="yes" />
57
+ <label for="card-yes">
58
+ Yes, I hold a card
59
+ <span class="mx-choice-card__hint">Pensioner Concession, Health Care or DVA card</span>
60
+ </label>
61
+ </div>
62
+
63
+ <div class="mx-radio mx-choice-card">
64
+ <input type="radio" id="card-no" name="card" value="no" />
65
+ <label for="card-no">
66
+ No
67
+ <span class="mx-choice-card__hint">You may still be eligible based on your income</span>
68
+ </label>
69
+ </div>
70
+
71
+ <div class="mx-choice-divider">or</div>
72
+
73
+ <div class="mx-radio mx-choice-card">
74
+ <input type="radio" id="card-unsure" name="card" value="unsure" />
75
+ <label for="card-unsure">I am not sure</label>
76
+ </div>
77
+ </fieldset>
78
+ ```
79
+
80
+ `.mx-choice-divider` is also defined in `src/Form/form.css` — plain markup used to split either card type into two groups, already available once you import `form.css`. See [`CheckboxCard`'s README](../CheckboxCard/README.md#or-divider-between-two-option-groups) for more.
81
+
82
+ ---
83
+
84
+ ## React
85
+
86
+ Use `FormRadioCard` as the group wrapper and `RadioCard` for each option — same pattern as `FormRadio`/`Radio`. There's no standalone/single variant (a lone radio isn't a meaningful control on its own, same as `Radio`).
87
+
88
+ ```tsx
89
+ import { FormRadioCard, RadioCard } from "@pnx-mixtape/mxds/react"
90
+
91
+ ;<FormRadioCard label="Do you hold a current concession card?" onChange={setSelected}>
92
+ <RadioCard value="yes" hint="Pensioner Concession, Health Care or DVA card">
93
+ Yes, I hold a card
94
+ </RadioCard>
95
+ <RadioCard value="no" hint="You may still be eligible based on your income">
96
+ No
97
+ </RadioCard>
98
+ </FormRadioCard>
99
+ ```
100
+
101
+ **`FormRadioCard` props:** all `AriaRadioGroupProps` from react-aria + `label: string`
102
+
103
+ **`RadioCard` props:** all `AriaRadioProps` + `hint?: string`, `status?: FormStatus`, `className?: string`
104
+
105
+ ---
106
+
107
+ ### React ⇄ Twig parity
108
+
109
+ Same intentional divergence as `Radio`/`Checkbox`/`CheckboxCard`:
110
+
111
+ - Twig's `label` becomes React `children` — react-aria composes the accessible name from rendered content, not a string prop.
112
+ - Twig's `id`/`name`/`checked`/`required`/`value` are handled by the react-aria hooks (`useRadio`/`useRadioGroup`) via prop spread rather than declared explicitly.
113
+
114
+ ---
115
+
116
+ ## Modifiers
117
+
118
+ No BEM modifier classes — `error`/`success` are applied as plain (unprefixed) status classes, matching `Radio`'s convention, driven by the `status` prop/Twig var.
119
+
120
+ ---
121
+
122
+ ## Accessibility
123
+
124
+ - The input stays a real, focusable `<input type="radio">` — only its default appearance is replaced (`appearance: none` plus a custom checked-state dot), so it keeps native radio semantics, keyboard support (arrow keys to move within the group, per the browser's native radio-group behaviour), and form submission behaviour.
125
+ - The whole card is clickable because the `<label>` fills it — the input itself only needs to be as large as `--control-size` dictates, not the full card.
126
+ - The focus ring (`:focus-visible`) renders on the input itself, inside the card, rather than around the whole card.
127
+ - `hint` text is inside the same `<label>` as the primary text, so both are announced together when the control receives focus — don't rely on `hint` to convey anything the primary `label` text doesn't already establish.
128
+
129
+ ---
130
+
131
+ ## Customisation
132
+
133
+ | Custom property | Applies to | Default |
134
+ | ------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------- |
135
+ | `--choice-padding-block` | `.mx-choice-card` | `var(--spacing-s)` |
136
+ | `--choice-padding-inline` | `.mx-choice-card` | `var(--spacing-m)` |
137
+ | `--choice-colour-active` | `.mx-choice-card`'s border/background tint when checked | `var(--colour-primary)` |
138
+ | `--radio-colour` | The checked dot itself — inherited from `.mx-radio`, not `.mx-choice-card` | `var(--colour-primary)` |
139
+ | `--control-size` | `.mx-choice-card input`, and `.mx-radio`'s own input styling | `var(--spacing-m)` (shared with `Checkbox`/`Radio`) |
140
+ | `--line-width` | `.mx-choice-card`'s border, and `.mx-radio`'s own input border | `2px` |
141
+
142
+ ```css
143
+ .theme--health-services .mx-choice-card {
144
+ --choice-colour-active: var(--colour-primary);
145
+ }
146
+ ```
@@ -0,0 +1,95 @@
1
+ import { Meta, StoryObj } from "@storybook/html-vite"
2
+ import Component from "../FormItem/form-item.twig"
3
+ import InputRadioCard from "./radio-card.twig"
4
+ import Label from "../Label/label.twig"
5
+ import "../form.css"
6
+ import { FormItem as FormItemType } from "@pnx-mixtape/ids-shape"
7
+
8
+ type MxFormItemType = Omit<FormItemType, "fields"> & {
9
+ fields?: string
10
+ }
11
+
12
+ const meta: Meta<MxFormItemType> = {
13
+ tags: ["autodocs"],
14
+ component: Component,
15
+ args: {
16
+ id: "card-set",
17
+ as: "fieldset",
18
+ label: Label({
19
+ title: "Do you hold a current concession card?",
20
+ as: "legend",
21
+ }),
22
+ fields: [
23
+ InputRadioCard({
24
+ id: "card-yes",
25
+ name: "card",
26
+ label: "Yes, I hold a card",
27
+ hint: "Pensioner Concession, Health Care or DVA card",
28
+ value: "yes",
29
+ }),
30
+ InputRadioCard({
31
+ id: "card-applied",
32
+ name: "card",
33
+ label: "I have applied but not received it",
34
+ hint: "You can continue, but we may ask for evidence later",
35
+ value: "applied",
36
+ }),
37
+ InputRadioCard({
38
+ id: "card-no",
39
+ name: "card",
40
+ label: "No",
41
+ hint: "You may still be eligible based on your income",
42
+ value: "no",
43
+ }),
44
+ '<div class="mx-choice-divider">or</div>',
45
+ InputRadioCard({
46
+ id: "card-unsure",
47
+ name: "card",
48
+ label: "I am not sure",
49
+ hint: "We will ask a few more questions to work it out",
50
+ value: "unsure",
51
+ }),
52
+ ].join(""),
53
+ },
54
+ parameters: {
55
+ deepControls: { enabled: true },
56
+ a11y: {
57
+ config: {
58
+ rules: [
59
+ {
60
+ // ID's are not unique due to test-runner.
61
+ id: "label",
62
+ enabled: false,
63
+ },
64
+ ],
65
+ },
66
+ },
67
+ },
68
+ }
69
+
70
+ export default meta
71
+ type Story = StoryObj<MxFormItemType>
72
+
73
+ export const RadioCard: Story = {}
74
+
75
+ export const Checked: Story = {
76
+ args: {
77
+ fields: [
78
+ InputRadioCard({
79
+ id: "card-yes",
80
+ name: "card",
81
+ label: "Yes, I hold a card",
82
+ hint: "Pensioner Concession, Health Care or DVA card",
83
+ value: "yes",
84
+ checked: true,
85
+ }),
86
+ InputRadioCard({
87
+ id: "card-no",
88
+ name: "card",
89
+ label: "No",
90
+ hint: "You may still be eligible based on your income",
91
+ value: "no",
92
+ }),
93
+ ].join(""),
94
+ },
95
+ }