@pithyjs/pidl-catalog 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/dist/specs/atoms/actions/button.pidl.json +289 -0
  3. package/dist/specs/atoms/actions/icon-group.pidl.json +141 -0
  4. package/dist/specs/atoms/actions/segmented-control.pidl.json +137 -0
  5. package/dist/specs/atoms/content/action-tile.pidl.json +135 -0
  6. package/dist/specs/atoms/content/badge.pidl.json +189 -0
  7. package/dist/specs/atoms/content/callout.pidl.json +170 -0
  8. package/dist/specs/atoms/content/code-block.pidl.json +170 -0
  9. package/dist/specs/atoms/content/heading.pidl.json +175 -0
  10. package/dist/specs/atoms/content/icon.pidl.json +127 -0
  11. package/dist/specs/atoms/content/image.pidl.json +227 -0
  12. package/dist/specs/atoms/content/prose.pidl.json +183 -0
  13. package/dist/specs/atoms/content/stat.pidl.json +158 -0
  14. package/dist/specs/atoms/content/tabs.pidl.json +207 -0
  15. package/dist/specs/atoms/content/text.pidl.json +212 -0
  16. package/dist/specs/atoms/content/tile.pidl.json +156 -0
  17. package/dist/specs/atoms/data/donut.pidl.json +352 -0
  18. package/dist/specs/atoms/data/gauge.pidl.json +274 -0
  19. package/dist/specs/atoms/forms/checkbox-group.pidl.json +170 -0
  20. package/dist/specs/atoms/forms/checkbox.pidl.json +323 -0
  21. package/dist/specs/atoms/forms/file-picker.pidl.json +161 -0
  22. package/dist/specs/atoms/forms/form-field.pidl.json +106 -0
  23. package/dist/specs/atoms/forms/input.pidl.json +466 -0
  24. package/dist/specs/atoms/forms/radio-group.pidl.json +174 -0
  25. package/dist/specs/atoms/forms/radio.pidl.json +312 -0
  26. package/dist/specs/atoms/forms/search-input.pidl.json +265 -0
  27. package/dist/specs/atoms/forms/select.pidl.json +420 -0
  28. package/dist/specs/atoms/forms/textarea.pidl.json +422 -0
  29. package/dist/specs/atoms/forms/toggle.pidl.json +314 -0
  30. package/dist/specs/layout/grid-item.pidl.json +120 -0
  31. package/dist/specs/layout/grid.pidl.json +157 -0
  32. package/dist/specs/layout/page-section.pidl.json +105 -0
  33. package/dist/specs/layout/smart-container.pidl.json +148 -0
  34. package/dist/specs/layout/stack.pidl.json +144 -0
  35. package/dist/specs/organisms/content/accordion.pidl.json +196 -0
  36. package/dist/specs/organisms/content/card.pidl.json +283 -0
  37. package/dist/specs/organisms/feedback/alert-dialog.pidl.json +158 -0
  38. package/dist/specs/organisms/feedback/modal.pidl.json +133 -0
  39. package/dist/specs/organisms/feedback/toast.pidl.json +150 -0
  40. package/dist/specs/organisms/forms/dynamic-form.pidl.json +314 -0
  41. package/dist/specs/organisms/forms/form.pidl.json +135 -0
  42. package/dist/specs/organisms/forms/subscribe-form.pidl.json +188 -0
  43. package/dist/specs/organisms/navigation/command-palette.pidl.json +263 -0
  44. package/dist/specs/organisms/navigation/docs-sidebar.pidl.json +264 -0
  45. package/dist/specs/organisms/navigation/navbar.pidl.json +356 -0
  46. package/dist/specs/organisms/navigation/table-of-contents.pidl.json +197 -0
  47. package/dist/specs/sections/contact-section.pidl.json +183 -0
  48. package/dist/specs/sections/cta-section.pidl.json +168 -0
  49. package/dist/specs/sections/faq-section.pidl.json +202 -0
  50. package/dist/specs/sections/features-section.pidl.json +214 -0
  51. package/dist/specs/sections/footer-section.pidl.json +187 -0
  52. package/dist/specs/sections/hero-section.pidl.json +202 -0
  53. package/dist/src/borders-psychology.d.ts +17 -0
  54. package/dist/src/borders-psychology.d.ts.map +1 -0
  55. package/dist/src/borders-psychology.js +42 -0
  56. package/dist/src/borders-psychology.js.map +1 -0
  57. package/dist/src/color-psychology.d.ts +20 -0
  58. package/dist/src/color-psychology.d.ts.map +1 -0
  59. package/dist/src/color-psychology.js +57 -0
  60. package/dist/src/color-psychology.js.map +1 -0
  61. package/dist/src/design-intelligence.d.ts +13 -0
  62. package/dist/src/design-intelligence.d.ts.map +1 -0
  63. package/dist/src/design-intelligence.js +36 -0
  64. package/dist/src/design-intelligence.js.map +1 -0
  65. package/dist/src/index.d.ts +6238 -0
  66. package/dist/src/index.d.ts.map +1 -0
  67. package/dist/src/index.js +105 -0
  68. package/dist/src/index.js.map +1 -0
  69. package/dist/src/motion-psychology.d.ts +19 -0
  70. package/dist/src/motion-psychology.d.ts.map +1 -0
  71. package/dist/src/motion-psychology.js +47 -0
  72. package/dist/src/motion-psychology.js.map +1 -0
  73. package/dist/src/mutation-vocabulary.d.ts +13 -0
  74. package/dist/src/mutation-vocabulary.d.ts.map +1 -0
  75. package/dist/src/mutation-vocabulary.js +67 -0
  76. package/dist/src/mutation-vocabulary.js.map +1 -0
  77. package/dist/src/radius-psychology.d.ts +17 -0
  78. package/dist/src/radius-psychology.d.ts.map +1 -0
  79. package/dist/src/radius-psychology.js +42 -0
  80. package/dist/src/radius-psychology.js.map +1 -0
  81. package/dist/src/shadows-psychology.d.ts +17 -0
  82. package/dist/src/shadows-psychology.d.ts.map +1 -0
  83. package/dist/src/shadows-psychology.js +44 -0
  84. package/dist/src/shadows-psychology.js.map +1 -0
  85. package/dist/src/spacing-psychology.d.ts +22 -0
  86. package/dist/src/spacing-psychology.d.ts.map +1 -0
  87. package/dist/src/spacing-psychology.js +59 -0
  88. package/dist/src/spacing-psychology.js.map +1 -0
  89. package/dist/src/typography-psychology.d.ts +22 -0
  90. package/dist/src/typography-psychology.d.ts.map +1 -0
  91. package/dist/src/typography-psychology.js +68 -0
  92. package/dist/src/typography-psychology.js.map +1 -0
  93. package/package.json +64 -0
@@ -0,0 +1,174 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.radio-group",
4
+ "name": "RadioGroup",
5
+ "displayName": "Radio Group",
6
+ "version": "0.1.0",
7
+ "level": "atom",
8
+ "domain": "forms",
9
+ "bidi": true,
10
+ "description": "Accessible group wrapper for related <Radio> controls. Renders <fieldset><legend> with optional helper/error text and aria-describedby wiring. Native fieldset[disabled] cascades to all child radios. Children are projected through the full PithyJS pipeline. Does NOT auto-inject `name` onto children — each <Radio> must declare its own.",
11
+ "props": [
12
+ {
13
+ "name": "id",
14
+ "label": "ID",
15
+ "editor": { "tab": "settings" },
16
+ "type": "string",
17
+ "description": "Explicit fieldset ID — overrides auto-generated ID."
18
+ },
19
+ {
20
+ "name": "label",
21
+ "label": "Label",
22
+ "type": "string",
23
+ "required": true,
24
+ "placeholder": "Pick one",
25
+ "description": "Required group label rendered inside <legend>. Provides the accessible name for the group."
26
+ },
27
+ {
28
+ "name": "hideLabel",
29
+ "label": "Hide label",
30
+ "type": "boolean",
31
+ "default": false,
32
+ "description": "Visually hide the legend (still announced by screen readers via .u-visually-hidden)."
33
+ },
34
+ {
35
+ "name": "helperText",
36
+ "label": "Helper text",
37
+ "type": "string",
38
+ "description": "Persistent helper text below the group. Wired via aria-describedby when no error is shown."
39
+ },
40
+ {
41
+ "name": "errorMessage",
42
+ "label": "Error message",
43
+ "type": "string | Signal<string>",
44
+ "description": "Error message rendered in a span with role='alert'. Linked via aria-describedby when error is truthy. Accepts a signal accessor for reactive updates."
45
+ },
46
+ {
47
+ "name": "error",
48
+ "label": "Error",
49
+ "type": "boolean | Signal<boolean>",
50
+ "default": false,
51
+ "description": "Visual error state — sets aria-invalid='true' and data-error on the fieldset. Error message only displays when both error and errorMessage are set. Accepts a signal accessor for reactive updates."
52
+ },
53
+ {
54
+ "name": "disabled",
55
+ "label": "Disabled",
56
+ "type": "boolean | Signal<boolean>",
57
+ "default": false,
58
+ "description": "Disables the entire group via native fieldset[disabled], cascading to every child radio without extra plumbing. Accepts a signal accessor for reactive updates."
59
+ }
60
+ ],
61
+ "events": [],
62
+ "slots": [
63
+ {
64
+ "name": "default",
65
+ "displayName": "Radios",
66
+ "description": "Children projected through renderChildren() — typically <Radio> elements but any markup is supported. The fieldset is marked as a site-builder drop zone.",
67
+ "accepts": ["ui.radio"]
68
+ }
69
+ ],
70
+ "tokens": {
71
+ "legend.color": "--color-text",
72
+ "helper.color": "--color-text-muted",
73
+ "error.color": "--color-danger",
74
+ "group.gap": "--space-sm"
75
+ },
76
+ "spacing": {
77
+ "internal": {
78
+ "legend-to-children": { "relationship": "close_friends", "token": "sm" },
79
+ "children-to-helper": { "relationship": "lovers", "token": "xs" },
80
+ "children-to-error": { "relationship": "lovers", "token": "xs" }
81
+ }
82
+ },
83
+ "constraints": [
84
+ "Use semantic <fieldset role='radiogroup'>/<legend> — never a div with role='group'",
85
+ "label prop is required — every group must have an accessible name",
86
+ "Native fieldset[disabled] (not aria-disabled) — cascades automatically to children",
87
+ "Error message only shown when both error=true and errorMessage are set",
88
+ "Children must be Radio controls (or compose them) — group semantics assume radio descendants",
89
+ "data-slot='children' is required on the fieldset for site-builder drop targeting",
90
+ "RadioGroup does NOT auto-inject `name` onto children — each <Radio> must declare its own `name` explicitly",
91
+ "Siblings sharing the same `name` are mutually exclusive and share arrow-key navigation"
92
+ ],
93
+ "a11y": [
94
+ "Native <fieldset role='radiogroup'><legend> — explicit radiogroup role for assistive technology",
95
+ "label always rendered inside <legend> (visually hidden via hideLabel when needed)",
96
+ "aria-invalid='true' when error prop is truthy",
97
+ "aria-describedby points to helper id (no error) or error id (when shown)",
98
+ "Error message has role='alert' for live region announcement",
99
+ "fieldset[disabled] cascades native disabled state to all child controls",
100
+ "Auto-generated id enables external aria-describedby/labelling"
101
+ ],
102
+ "ai": {
103
+ "synonyms": [
104
+ "radio set",
105
+ "radio list",
106
+ "single-select group",
107
+ "fieldset",
108
+ "option group",
109
+ "radio button group"
110
+ ],
111
+ "avoid": [
112
+ "Wrapping a single Radio in RadioGroup — use Radio directly",
113
+ "Using FormField around individual Radios — Radio already provides its own label; use RadioGroup for grouped controls",
114
+ "div with role='group' or role='radiogroup' — use the native <fieldset> via <RadioGroup>",
115
+ "Omitting the label prop — the group must have an accessible name",
116
+ "Adding aria-disabled — use the native disabled prop instead",
117
+ "Expecting RadioGroup to auto-inject name — it doesn't, set name on each Radio"
118
+ ],
119
+ "patterns": [
120
+ "Basic: <RadioGroup label=\"'Plan'\"><Radio label=\"'Free'\" name=\"'plan'\" value=\"'free'\" /><Radio label=\"'Pro'\" name=\"'plan'\" value=\"'pro'\" /></RadioGroup>",
121
+ "With helper: <RadioGroup label=\"'Plan'\" helperText=\"'Choose your subscription'\">...</RadioGroup>",
122
+ "With error: <RadioGroup label=\"'Plan'\" error=\"hasError\" errorMessage=\"'Please select a plan'\">...</RadioGroup>",
123
+ "Disabled (cascades): <RadioGroup label=\"'Locked options'\" disabled=\"true\">...</RadioGroup>",
124
+ "Visually hidden legend: <RadioGroup label=\"'Sort order'\" hideLabel=\"true\">...</RadioGroup>"
125
+ ]
126
+ },
127
+ "examples": [
128
+ {
129
+ "id": "basic",
130
+ "title": "Basic radio group",
131
+ "pithy": "<RadioGroup label=\"'Plan'\"><Radio label=\"'Free'\" name=\"'plan'\" value=\"'free'\" /><Radio label=\"'Pro'\" name=\"'plan'\" value=\"'pro'\" /></RadioGroup>",
132
+ "assertions": [
133
+ "fieldset > legend with label text",
134
+ "data-slot='children' on fieldset",
135
+ "no axe violations"
136
+ ]
137
+ },
138
+ {
139
+ "id": "helper",
140
+ "title": "Group with helper text",
141
+ "pithy": "<RadioGroup label=\"'Plan'\" helperText=\"'Choose your subscription'\"><Radio label=\"'Free'\" name=\"'plan'\" value=\"'free'\" /></RadioGroup>",
142
+ "assertions": ["helper rendered", "aria-describedby points to helper id"]
143
+ },
144
+ {
145
+ "id": "error",
146
+ "title": "Group with error",
147
+ "pithy": "<RadioGroup label=\"'Plan'\" error=\"true\" errorMessage=\"'Please select a plan'\"><Radio label=\"'Free'\" name=\"'plan'\" value=\"'free'\" /></RadioGroup>",
148
+ "assertions": [
149
+ "aria-invalid='true' on fieldset",
150
+ "error span with role='alert' visible",
151
+ "aria-describedby points to error id"
152
+ ]
153
+ },
154
+ {
155
+ "id": "disabled-cascade",
156
+ "title": "Disabled group cascades to children",
157
+ "pithy": "<RadioGroup label=\"'Locked'\" disabled=\"true\"><Radio label=\"'Free'\" name=\"'plan'\" value=\"'free'\" /></RadioGroup>",
158
+ "assertions": ["fieldset[disabled]", "child input matches :disabled"]
159
+ },
160
+ {
161
+ "id": "hide-label",
162
+ "title": "Visually hidden legend",
163
+ "pithy": "<RadioGroup label=\"'Sort order'\" hideLabel=\"true\"><Radio label=\"'Newest'\" name=\"'sort'\" value=\"'new'\" /></RadioGroup>",
164
+ "assertions": ["legend present", "legend has u-visually-hidden class"]
165
+ }
166
+ ],
167
+ "designGuidance": {
168
+ "surface": "No background by default — inherits the page surface. The group is a semantic container, not a visual card.",
169
+ "depth": "Flat — no shadow or elevation. Children carry their own focus state.",
170
+ "borders": "No border on the fieldset itself (UA defaults reset). Visual grouping comes from spacing, not chrome.",
171
+ "interactivity": "Stateless wrapper — interactivity lives on individual Radio children. fieldset[disabled] cascades native disabled to descendants without per-child wiring. Arrow keys navigate between siblings sharing the same `name`.",
172
+ "bestPractices": "Always pair related radios inside a RadioGroup. Use a clear, concise legend label. Prefer native disabled over per-child disabling. Use hideLabel sparingly — only when surrounding context already provides the accessible name. Ensure all child Radios share the same `name` attribute for mutual exclusion and keyboard navigation."
173
+ }
174
+ }
@@ -0,0 +1,312 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.radio",
4
+ "name": "Radio",
5
+ "displayName": "Radio",
6
+ "version": "0.1.0",
7
+ "level": "atom",
8
+ "domain": "forms",
9
+ "bidi": true,
10
+ "description": "Accessible radio button atom with a custom-rendered circle and inline SVG inner dot (animated via transform/opacity), error/success states with shake animation, helper/error text, loading spinner, arrow-key navigation across siblings sharing a `name`, and full a11y wiring. No validation logic — states driven externally.",
11
+ "props": [
12
+ {
13
+ "name": "id",
14
+ "label": "ID",
15
+ "editor": { "tab": "settings" },
16
+ "type": "string",
17
+ "description": "Explicit input ID — overrides auto-generated ID."
18
+ },
19
+ {
20
+ "name": "name",
21
+ "label": "Name",
22
+ "editor": { "tab": "settings" },
23
+ "type": "string",
24
+ "required": true,
25
+ "description": "Input name attribute. Required — radios sharing the same name form a mutually exclusive group and share arrow-key navigation."
26
+ },
27
+ {
28
+ "name": "value",
29
+ "label": "Value",
30
+ "editor": { "tab": "settings" },
31
+ "type": "string",
32
+ "required": true,
33
+ "description": "Submitted value when this radio is selected."
34
+ },
35
+ {
36
+ "name": "checked",
37
+ "label": "Checked",
38
+ "editor": { "tab": "settings" },
39
+ "type": "boolean | Signal<boolean>",
40
+ "default": false,
41
+ "description": "Checked state — static or signal accessor for controlled radio."
42
+ },
43
+ {
44
+ "name": "disabled",
45
+ "label": "Disabled",
46
+ "editor": { "tab": "settings" },
47
+ "type": "boolean | Signal<boolean>",
48
+ "default": false,
49
+ "description": "Disabled state — uses native disabled attribute."
50
+ },
51
+ {
52
+ "name": "readonly",
53
+ "label": "Read-only",
54
+ "editor": { "tab": "settings" },
55
+ "type": "boolean | Signal<boolean>",
56
+ "default": false,
57
+ "description": "Readonly state — focusable but not toggleable. Native input ignores readonly, so toggling is intercepted via click preventDefault, Space keydown preventDefault, and a change-event rollback to the controlled value."
58
+ },
59
+ {
60
+ "name": "required",
61
+ "label": "Required",
62
+ "type": "boolean",
63
+ "default": false,
64
+ "description": "Sets aria-required and adds a visual asterisk to the label."
65
+ },
66
+ {
67
+ "name": "error",
68
+ "label": "Error",
69
+ "editor": { "tab": "settings" },
70
+ "type": "boolean | Signal<boolean>",
71
+ "default": false,
72
+ "description": "Visual error state — sets aria-invalid, red ring, shake on transition."
73
+ },
74
+ {
75
+ "name": "errorMessage",
76
+ "label": "Error message",
77
+ "editor": { "tab": "settings" },
78
+ "type": "string | Signal<string>",
79
+ "description": "Error message shown below the radio when error is truthy. Linked via aria-describedby with role='alert'."
80
+ },
81
+ {
82
+ "name": "helperText",
83
+ "label": "Helper text",
84
+ "type": "string",
85
+ "description": "Persistent hint text below the radio. Hidden when error is visible."
86
+ },
87
+ {
88
+ "name": "success",
89
+ "label": "Success",
90
+ "editor": { "tab": "settings" },
91
+ "type": "boolean | Signal<boolean>",
92
+ "default": false,
93
+ "description": "Visual success state — green border / fill. Error takes precedence."
94
+ },
95
+ {
96
+ "name": "size",
97
+ "label": "Size",
98
+ "type": "enum",
99
+ "enum": ["sm", "md", "lg"],
100
+ "default": "md",
101
+ "description": "Control size — drives circle dimensions and label font-size."
102
+ },
103
+ {
104
+ "name": "label",
105
+ "label": "Label",
106
+ "type": "string",
107
+ "required": true,
108
+ "placeholder": "Option label",
109
+ "description": "Visible label text rendered next to the circle. Always required."
110
+ },
111
+ {
112
+ "name": "loading",
113
+ "label": "Loading",
114
+ "editor": { "tab": "settings" },
115
+ "type": "boolean | Signal<boolean>",
116
+ "default": false,
117
+ "description": "Loading state — overlays a spinner inside the circle and sets aria-busy."
118
+ },
119
+ {
120
+ "name": "onChange",
121
+ "type": "(checked: boolean, e: Event) => void",
122
+ "description": "Fires whenever the checked state changes to true."
123
+ },
124
+ {
125
+ "name": "onBlur",
126
+ "type": "(e: FocusEvent) => void",
127
+ "description": "Fires when the radio loses focus."
128
+ },
129
+ {
130
+ "name": "onFocus",
131
+ "type": "(e: FocusEvent) => void",
132
+ "description": "Fires when the radio gains focus."
133
+ }
134
+ ],
135
+ "events": ["change", "blur", "focus"],
136
+ "slots": [],
137
+ "tokens": {
138
+ "circle.size.sm": "calc(var(--font-size-xs) * 1.25)",
139
+ "circle.size.md": "calc(var(--font-size-sm) * 1.25)",
140
+ "circle.size.lg": "calc(var(--font-size-body) * 1.25)",
141
+ "circle.bg.default": "--color-surface",
142
+ "circle.border.default": "border(control)",
143
+ "circle.border.checked": "--color-primary",
144
+ "circle.border.hover": "--color-primary",
145
+ "circle.border.error": "--color-danger",
146
+ "circle.border.success": "--color-success",
147
+ "circle.radius": "50%",
148
+ "dot.color": "--color-primary",
149
+ "label.color": "--color-text",
150
+ "label.color.error": "--color-danger",
151
+ "focus.ring": "ring() with --ring-offset: 2px",
152
+ "disabled.opacity": "--state-disabled-opacity",
153
+ "required.indicator": "--color-danger asterisk",
154
+ "active.scale": "--motion-scale-big",
155
+ "shake.animation": "ui-form-shake",
156
+ "touch-target": "a11y.touch-target-pseudo (--control-min-touch)",
157
+ "spinner.color": "--_cmp-spinner-color → --color-primary (default), --color-danger (error), --color-success (success, scoped with :not([data-error]) so error wins)"
158
+ },
159
+ "spacing": {
160
+ "internal": {
161
+ "circle-to-label": { "relationship": "lovers", "token": "xs" }
162
+ },
163
+ "external": {
164
+ "wrapper-to-helper": { "relationship": "lovers", "token": "xs" },
165
+ "wrapper-to-error": { "relationship": "lovers", "token": "xs" },
166
+ "radio-to-radio": { "relationship": "close_friends", "token": "sm" }
167
+ }
168
+ },
169
+ "constraints": [
170
+ "Never use raw hex colors — use design system tokens",
171
+ "Use logical CSS properties only",
172
+ "All transitions wrapped in @include a11y.motion-safe",
173
+ "Must handle forced-colors mode — Highlight for checked, GrayText for disabled",
174
+ "No validation logic — error/success props are visual only",
175
+ "Label prop is required",
176
+ "`name` prop is required — radios without a name cannot form a group",
177
+ "Radios cannot be un-checked by the user — selecting another radio in the same `name` group un-checks the previous one",
178
+ "Arrow Up/Left moves focus+check to the previous sibling radio with the same `name`; Arrow Down/Right moves to the next. Disabled siblings are skipped. Wraps at the ends",
179
+ "Native disabled (not aria-disabled)",
180
+ "Error takes precedence over success visually",
181
+ "Error shake skips initial render (firstRun guard)",
182
+ "Readonly is custom — native radios ignore the readonly attribute. Toggling is blocked via click preventDefault, Space keydown preventDefault, and a change-event rollback that restores the controlled value if a stray event slips through",
183
+ "Inner dot rendered as an inline SVG <circle>, animated via transform scale(0)→scale(1) + opacity",
184
+ "Native input is visually hidden but stays focusable; visual focus ring is on .ui-radio-circle via :has(:focus-visible)",
185
+ "State icon is kept detached (never in the DOM) — the custom circle conveys all state; applyFormState mutates the detached element without injecting unnecessary DOM nodes",
186
+ "Fieldset[disabled] cascade — when disabled via a parent <fieldset disabled>, hover/active/pressed styles are suppressed via :not(:has(.ui-radio-input:disabled)) guards on hover and active selectors; desaturation + cursor applied via a dedicated cascade rule",
187
+ "RadioProps extends FormControlBaseProps",
188
+ "`RadioGroup` does NOT auto-inject `name` onto children — each `<Radio>` must declare its own"
189
+ ],
190
+ "a11y": [
191
+ "Native <input type='radio'> — full keyboard support (Space selects)",
192
+ "Arrow keys navigate between siblings sharing `name` (custom handler — sr-only input defeats the UA default)",
193
+ "Wrapper is a <label> — clicking anywhere selects the input",
194
+ "aria-invalid='true' when error prop is truthy",
195
+ "aria-describedby dynamically updated for helper/error",
196
+ "Error message has role='alert'",
197
+ "aria-required='true' when required",
198
+ "Readonly signaled via data-readonly + aria-description='Read-only' — aria-readonly is disallowed on role=radio per ARIA spec; toggling is blocked in JS",
199
+ "aria-busy='true' when loading",
200
+ "Auto-generated id enables FormField association",
201
+ "Focus ring on the visible circle via :has(:focus-visible)",
202
+ "Forced-colors: Highlight for checked, GrayText for disabled, Highlight outline on focus",
203
+ "Custom circle has aria-hidden='true' (decorative — input is the real control)",
204
+ "Error shake respects prefers-reduced-motion"
205
+ ],
206
+ "ai": {
207
+ "synonyms": [
208
+ "radio button",
209
+ "option button",
210
+ "single-select option",
211
+ "choice button",
212
+ "mutually exclusive choice"
213
+ ],
214
+ "avoid": [
215
+ "div with role='radio' — use the native input via <Radio>",
216
+ "raw color values for circle / dot — use design tokens",
217
+ "validation inside the component — belongs in Form/DynamicForm",
218
+ "Radio without a label prop",
219
+ "Radio without a name prop — radios need a name to group",
220
+ "multiple Radios in the same group with different `name` values — they won't be mutually exclusive",
221
+ "RadioGroup trying to auto-inject name — it doesn't, set it per Radio"
222
+ ],
223
+ "patterns": [
224
+ "Basic: <Radio label=\"'Cheese'\" name=\"'topping'\" value=\"'cheese'\" />",
225
+ "Required: <Radio label=\"'I agree'\" name=\"'consent'\" value=\"'yes'\" required=\"true\" />",
226
+ "Controlled: <Radio label=\"'Pro'\" name=\"'plan'\" value=\"'pro'\" checked=\"isPro\" onChange=\"selectPro\" />",
227
+ "Helper text: <Radio label=\"'Email'\" name=\"'contact'\" value=\"'email'\" helperText=\"'We will only email important updates'\" />",
228
+ "Error: <Radio label=\"'Accept'\" name=\"'tos'\" value=\"'yes'\" error=\"hasError\" errorMessage=\"'Required'\" />",
229
+ "Group: <RadioGroup label=\"'Plan'\"><Radio label=\"'Free'\" name=\"'plan'\" value=\"'free'\" /><Radio label=\"'Pro'\" name=\"'plan'\" value=\"'pro'\" /></RadioGroup>"
230
+ ]
231
+ },
232
+ "examples": [
233
+ {
234
+ "id": "basic",
235
+ "title": "Basic radio",
236
+ "pithy": "<Radio label=\"'Cheese'\" name=\"'topping'\" value=\"'cheese'\" />",
237
+ "assertions": [
238
+ "renders <input type='radio'>",
239
+ "label is visible",
240
+ "no axe violations"
241
+ ]
242
+ },
243
+ {
244
+ "id": "checked",
245
+ "title": "Pre-checked radio",
246
+ "pithy": "<Radio label=\"'Pro'\" name=\"'plan'\" value=\"'pro'\" checked=\"true\" />",
247
+ "assertions": ["input.checked === true", "data-checked on wrapper"]
248
+ },
249
+ {
250
+ "id": "disabled",
251
+ "title": "Disabled radio",
252
+ "pithy": "<Radio label=\"'Locked'\" name=\"'x'\" value=\"'x'\" disabled=\"true\" />",
253
+ "assertions": ["input.disabled === true", "data-disabled on wrapper"]
254
+ },
255
+ {
256
+ "id": "readonly",
257
+ "title": "Readonly radio",
258
+ "pithy": "<Radio label=\"'Auto-set'\" name=\"'x'\" value=\"'x'\" readonly=\"true\" checked=\"true\" />",
259
+ "assertions": [
260
+ "click does not toggle",
261
+ "Space does not toggle",
262
+ "remains focusable"
263
+ ]
264
+ },
265
+ {
266
+ "id": "required",
267
+ "title": "Required radio",
268
+ "pithy": "<Radio label=\"'I agree'\" name=\"'x'\" value=\"'y'\" required=\"true\" />",
269
+ "assertions": ["aria-required on input", "asterisk visible on label"]
270
+ },
271
+ {
272
+ "id": "error",
273
+ "title": "Error with shake",
274
+ "pithy": "<Radio label=\"'Accept'\" name=\"'x'\" value=\"'y'\" error=\"hasError\" errorMessage=\"'Required'\" />",
275
+ "assertions": [
276
+ "aria-invalid on input",
277
+ "shake animation on transition",
278
+ "error message shown"
279
+ ]
280
+ },
281
+ {
282
+ "id": "success",
283
+ "title": "Success state",
284
+ "pithy": "<Radio label=\"'Verified'\" name=\"'x'\" value=\"'y'\" success=\"true\" checked=\"true\" />",
285
+ "assertions": ["data-success on wrapper", "green ring"]
286
+ },
287
+ {
288
+ "id": "loading",
289
+ "title": "Loading state",
290
+ "pithy": "<Radio label=\"'Saving'\" name=\"'x'\" value=\"'y'\" loading=\"isSaving\" />",
291
+ "assertions": ["spinner visible", "aria-busy='true'"]
292
+ },
293
+ {
294
+ "id": "group",
295
+ "title": "Radio group with fieldset",
296
+ "pithy": "<RadioGroup label=\"'Plan'\"><Radio label=\"'Free'\" name=\"'plan'\" value=\"'free'\" /><Radio label=\"'Pro'\" name=\"'plan'\" value=\"'pro'\" /></RadioGroup>",
297
+ "assertions": [
298
+ "fieldset contains legend",
299
+ "data-slot='children' on fieldset",
300
+ "fieldset[disabled] cascades",
301
+ "arrow keys move between radios"
302
+ ]
303
+ }
304
+ ],
305
+ "designGuidance": {
306
+ "surface": "Circle uses --color-surface by default. On checked, the ring adopts --color-primary and the inner dot scales in. Hover deepens the ring to --color-primary-hover.",
307
+ "depth": "No shadow by default. Colored micro-shadow under the ring when checked. Focus ring via box-shadow ring() with 2px offset on the visible circle.",
308
+ "borders": "Stable border width — only color changes across states. Circle is always a perfect circle (border-radius: 50%).",
309
+ "interactivity": "Hover tints border. Active applies pressed state-background and a subtle compression. Focus shows ring on the circle. Error shakes on transition. Arrow keys navigate siblings sharing the same `name`. Loading overlays spinner inside the circle.",
310
+ "bestPractices": "Always provide a label, a name, and a value. Use <RadioGroup> to wrap related radios with a legend and helper/error text. Use disabled (not readonly) when the option is permanently unavailable; use readonly when the chosen value is meaningful but locked. For exactly two mutually exclusive options, prefer <Toggle> unless both options deserve labels."
311
+ }
312
+ }