@choice-ui/radio 0.0.4

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.
package/README.md ADDED
@@ -0,0 +1,434 @@
1
+ # Radio
2
+
3
+ A form control component that allows users to select exactly one option from a set, with multiple visual variants and comprehensive group functionality.
4
+
5
+ ## Import
6
+
7
+ ```tsx
8
+ import { Radio, RadioGroup } from "@choice-ui/react"
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - Multiple visual variants (default, accent, outline)
14
+ - Support for disabled and focused states
15
+ - Two label approaches: simple string children or explicit `Radio.Label` for complex content
16
+ - Controlled usage for reliable state management
17
+ - Group functionality via `RadioGroup` component with two usage patterns
18
+ - Proper keyboard navigation (arrow keys within groups)
19
+ - Screen reader accessibility with proper ARIA attributes
20
+ - Individual and group-level disabled options
21
+
22
+ ## Usage
23
+
24
+ ### Basic Radio
25
+
26
+ ```tsx
27
+ import { useState } from "react"
28
+
29
+ const [selected, setSelected] = useState(false)
30
+
31
+ <Radio
32
+ value={selected}
33
+ onChange={setSelected}
34
+ >
35
+ Option Label
36
+ </Radio>
37
+ ```
38
+
39
+ ### Visual Variants
40
+
41
+ ```tsx
42
+ const [selections, setSelections] = useState({
43
+ default: false,
44
+ accent: false,
45
+ outline: false,
46
+ })
47
+
48
+ <>
49
+ <Radio
50
+ value={selections.default}
51
+ onChange={(value) => setSelections({ ...selections, default: value })}
52
+ >
53
+ Default
54
+ </Radio>
55
+ <Radio
56
+ value={selections.accent}
57
+ onChange={(value) => setSelections({ ...selections, accent: value })}
58
+ variant="accent"
59
+ >
60
+ Accent
61
+ </Radio>
62
+ <Radio
63
+ value={selections.outline}
64
+ onChange={(value) => setSelections({ ...selections, outline: value })}
65
+ variant="outline"
66
+ >
67
+ Outline
68
+ </Radio>
69
+ </>
70
+ ```
71
+
72
+ ### States
73
+
74
+ ```tsx
75
+ <>
76
+ <Radio
77
+ value={false}
78
+ onChange={() => {}}
79
+ >
80
+ Rest
81
+ </Radio>
82
+ <Radio
83
+ value={false}
84
+ onChange={() => {}}
85
+ focused
86
+ >
87
+ Focused
88
+ </Radio>
89
+ <Radio
90
+ value={false}
91
+ onChange={() => {}}
92
+ disabled
93
+ >
94
+ Disabled
95
+ </Radio>
96
+ </>
97
+ ```
98
+
99
+ ### Label Usage
100
+
101
+ ```tsx
102
+ const [simple, setSimple] = useState(false)
103
+ const [complex, setComplex] = useState(false)
104
+
105
+ <>
106
+ {/* Simple string label (auto-wrapped) */}
107
+ <Radio value={simple} onChange={setSimple}>
108
+ Simple text label
109
+ </Radio>
110
+
111
+ {/* Explicit Radio.Label for complex content */}
112
+ <Radio value={complex} onChange={setComplex}>
113
+ <Radio.Label>
114
+ <span className="text-accent-foreground">Complex</span> label with{" "}
115
+ <strong>formatting</strong>
116
+ </Radio.Label>
117
+ </Radio>
118
+ </>
119
+ ```
120
+
121
+ ### RadioGroup with Options
122
+
123
+ ```tsx
124
+ const [selected, setSelected] = useState("option1")
125
+
126
+ const options = [
127
+ { value: "option1", label: "First Option" },
128
+ { value: "option2", label: "Second Option" },
129
+ { value: "option3", label: "Third Option" },
130
+ ]
131
+
132
+ <RadioGroup
133
+ options={options}
134
+ value={selected}
135
+ onChange={setSelected}
136
+ />
137
+ ```
138
+
139
+ ### RadioGroup with Items
140
+
141
+ ```tsx
142
+ const [selected, setSelected] = useState("choice1")
143
+
144
+ <RadioGroup
145
+ value={selected}
146
+ onChange={setSelected}
147
+ >
148
+ <RadioGroup.Item value="choice1">
149
+ First Choice
150
+ </RadioGroup.Item>
151
+ <RadioGroup.Item value="choice2">
152
+ Second Choice
153
+ </RadioGroup.Item>
154
+ <RadioGroup.Item value="choice3">
155
+ Third Choice
156
+ </RadioGroup.Item>
157
+ </RadioGroup>
158
+ ```
159
+
160
+ ### Group with Variants
161
+
162
+ ```tsx
163
+ const [variant, setVariant] = useState("default")
164
+
165
+ <RadioGroup
166
+ variant={variant as "default" | "accent" | "outline"}
167
+ value={variant}
168
+ onChange={setVariant}
169
+ >
170
+ <RadioGroup.Item value="default">Default</RadioGroup.Item>
171
+ <RadioGroup.Item value="accent">Accent</RadioGroup.Item>
172
+ <RadioGroup.Item value="outline">Outline</RadioGroup.Item>
173
+ </RadioGroup>
174
+ ```
175
+
176
+ ### Group with Disabled Options
177
+
178
+ ```tsx
179
+ // Using options prop
180
+ const optionsWithDisabled = [
181
+ { value: "available1", label: "Available Option", disabled: false },
182
+ { value: "disabled1", label: "Disabled Option", disabled: true },
183
+ { value: "available2", label: "Another Available", disabled: false },
184
+ ]
185
+
186
+ const [selected1, setSelected1] = useState("available1")
187
+
188
+ <RadioGroup
189
+ options={optionsWithDisabled}
190
+ value={selected1}
191
+ onChange={setSelected1}
192
+ />
193
+
194
+ // Using RadioGroup.Item with individual disabled control
195
+ const [selected2, setSelected2] = useState("custom1")
196
+
197
+ <RadioGroup
198
+ value={selected2}
199
+ onChange={setSelected2}
200
+ >
201
+ <RadioGroup.Item value="custom1">Available Choice</RadioGroup.Item>
202
+ <RadioGroup.Item value="custom2" disabled>Disabled Choice</RadioGroup.Item>
203
+ <RadioGroup.Item value="custom3">Another Available</RadioGroup.Item>
204
+ </RadioGroup>
205
+ ```
206
+
207
+ ## Props
208
+
209
+ ### Radio Props
210
+
211
+ ```ts
212
+ interface RadioProps extends Omit<HTMLProps<HTMLInputElement>, "value" | "onChange"> {
213
+ /** Radio content - string is auto-wrapped with Radio.Label */
214
+ children?: ReactNode
215
+
216
+ /** Additional CSS class names */
217
+ className?: string
218
+
219
+ /** Whether the radio appears focused (for keyboard navigation) */
220
+ focused?: boolean
221
+
222
+ /** Callback fired when radio value changes */
223
+ onChange: (value: boolean) => void
224
+
225
+ /** Current radio value */
226
+ value: boolean
227
+
228
+ /** Visual style variant */
229
+ variant?: "default" | "accent" | "outline"
230
+ }
231
+ ```
232
+
233
+ ### RadioGroup Props
234
+
235
+ ```ts
236
+ interface RadioGroupProps {
237
+ /** Additional CSS class names */
238
+ className?: string
239
+
240
+ /** Radio group content (RadioGroup.Item components) */
241
+ children?: ReactNode
242
+
243
+ /** Array of option objects (alternative to children) */
244
+ options?: Array<{
245
+ value: string
246
+ label: string
247
+ disabled?: boolean
248
+ }>
249
+
250
+ /** Callback fired when selection changes */
251
+ onChange: (value: string) => void
252
+
253
+ /** Current selected value */
254
+ value: string
255
+
256
+ /** Visual variant applied to all radios in the group */
257
+ variant?: "default" | "accent" | "outline"
258
+ }
259
+
260
+ interface RadioGroupItemProps {
261
+ /** Radio content */
262
+ children: ReactNode
263
+
264
+ /** Whether this option is disabled */
265
+ disabled?: boolean
266
+
267
+ /** Option value */
268
+ value: string
269
+ }
270
+ ```
271
+
272
+ - Defaults:
273
+ - `variant`: "default"
274
+ - `focused`: `false`
275
+ - `disabled`: `false` (inherited from native prop)
276
+
277
+ - Accessibility:
278
+ - Uses proper `role="radio"` and `aria-checked` attributes
279
+ - Keyboard navigation with arrow keys in RadioGroups
280
+ - Proper focus management and visible focus states
281
+ - Label association for screen readers
282
+ - Group semantics with fieldset/legend pattern in RadioGroup
283
+
284
+ ## Styling
285
+
286
+ - Components use Tailwind CSS via `tailwind-variants` in shared checkbox/tv.ts.
287
+ - Customize using the `className` prop; classes are merged with internal classes.
288
+ - Radio uses the checkbox TV with `type: "radio"` for rounded appearance.
289
+ - Variants affect fill color, border, and focus states.
290
+
291
+ ## Best practices
292
+
293
+ - Use RadioGroup for related options that require exactly one selection
294
+ - Provide clear, concise labels for each option
295
+ - Always show all available options (unlike dropdowns)
296
+ - Use appropriate variants based on visual hierarchy and importance
297
+ - For simple labels, use string children; for complex content, use `Radio.Label`
298
+ - Consider disabled options when choices are conditionally unavailable
299
+ - Ensure at least one option remains selectable in groups
300
+ - Provide feedback about why options might be disabled
301
+
302
+ ## Examples
303
+
304
+ ### Settings form
305
+
306
+ ```tsx
307
+ const [theme, setTheme] = useState("light")
308
+ const [notifications, setNotifications] = useState("email")
309
+
310
+ <div className="space-y-6">
311
+ <fieldset>
312
+ <legend className="mb-3 font-strong">Theme Preference</legend>
313
+ <RadioGroup
314
+ variant="accent"
315
+ value={theme}
316
+ onChange={setTheme}
317
+ >
318
+ <RadioGroup.Item value="light">Light Theme</RadioGroup.Item>
319
+ <RadioGroup.Item value="dark">Dark Theme</RadioGroup.Item>
320
+ <RadioGroup.Item value="auto">Auto (System)</RadioGroup.Item>
321
+ </RadioGroup>
322
+ </fieldset>
323
+
324
+ <fieldset>
325
+ <legend className="mb-3 font-strong">Notifications</legend>
326
+ <RadioGroup
327
+ value={notifications}
328
+ onChange={setNotifications}
329
+ >
330
+ <RadioGroup.Item value="email">Email Only</RadioGroup.Item>
331
+ <RadioGroup.Item value="push">Push Notifications</RadioGroup.Item>
332
+ <RadioGroup.Item value="both">Email + Push</RadioGroup.Item>
333
+ <RadioGroup.Item value="none" disabled>
334
+ None (Requires Premium)
335
+ </RadioGroup.Item>
336
+ </RadioGroup>
337
+ </fieldset>
338
+ </div>
339
+ ```
340
+
341
+ ### Payment method selection
342
+
343
+ ```tsx
344
+ const [paymentMethod, setPaymentMethod] = useState("card")
345
+
346
+ <fieldset className="space-y-3">
347
+ <legend className="text-body-large-strong">Payment Method</legend>
348
+
349
+ <RadioGroup
350
+ variant="outline"
351
+ value={paymentMethod}
352
+ onChange={setPaymentMethod}
353
+ >
354
+ <RadioGroup.Item value="card">
355
+ <Radio.Label>
356
+ <div className="flex items-center gap-2">
357
+ <span>💳</span>
358
+ <div>
359
+ <div className="font-strong">Credit Card</div>
360
+ <div className="text-body-small text-secondary-foreground">
361
+ Visa, Mastercard, American Express
362
+ </div>
363
+ </div>
364
+ </div>
365
+ </Radio.Label>
366
+ </RadioGroup.Item>
367
+
368
+ <RadioGroup.Item value="paypal">
369
+ <Radio.Label>
370
+ <div className="flex items-center gap-2">
371
+ <span>🅿️</span>
372
+ <div>
373
+ <div className="font-strong">PayPal</div>
374
+ <div className="text-body-small text-secondary-foreground">
375
+ Pay with your PayPal account
376
+ </div>
377
+ </div>
378
+ </div>
379
+ </Radio.Label>
380
+ </RadioGroup.Item>
381
+
382
+ <RadioGroup.Item value="bank" disabled>
383
+ <Radio.Label>
384
+ <div className="flex items-center gap-2">
385
+ <span>🏦</span>
386
+ <div>
387
+ <div className="font-strong">Bank Transfer</div>
388
+ <div className="text-body-small text-secondary-foreground">
389
+ Currently unavailable
390
+ </div>
391
+ </div>
392
+ </div>
393
+ </Radio.Label>
394
+ </RadioGroup.Item>
395
+ </RadioGroup>
396
+ </fieldset>
397
+ ```
398
+
399
+ ### Quiz question
400
+
401
+ ```tsx
402
+ const [answer, setAnswer] = useState<string>("")
403
+
404
+ <div className="space-y-4">
405
+ <h3 className="font-strong">What is the capital of France?</h3>
406
+
407
+ <RadioGroup
408
+ value={answer}
409
+ onChange={setAnswer}
410
+ options={[
411
+ { value: "london", label: "London" },
412
+ { value: "berlin", label: "Berlin" },
413
+ { value: "paris", label: "Paris" },
414
+ { value: "madrid", label: "Madrid" },
415
+ ]}
416
+ />
417
+
418
+ {answer && (
419
+ <p className="text-body-small text-secondary-foreground">
420
+ Selected: {answer}
421
+ </p>
422
+ )}
423
+ </div>
424
+ ```
425
+
426
+ ## Notes
427
+
428
+ - Radio buttons enforce single selection within a group (use Checkbox for multiple selections)
429
+ - String children are automatically wrapped with `Radio.Label` for consistent styling
430
+ - `RadioGroup` manages state and provides proper keyboard navigation between options
431
+ - Use the `options` prop for simple cases, `RadioGroup.Item` children for complex layouts
432
+ - Disabled options maintain visual presence but prevent interaction
433
+ - The component follows native radio button behavior with enhanced styling and accessibility
434
+ - Focus management ensures proper keyboard navigation within groups