@discourser/design-system 0.3.0 → 0.4.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.
@@ -0,0 +1,583 @@
1
+ # Checkbox
2
+
3
+ **Purpose:** Binary selection control for toggling options on/off, supporting single checkboxes and checkbox groups.
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import * as Checkbox from '@discourser/design-system';
9
+ ```
10
+
11
+ ## Component Structure
12
+
13
+ Checkbox uses a compound component pattern with these parts:
14
+
15
+ - `Checkbox.Root` - Container for the checkbox
16
+ - `Checkbox.Control` - The visual checkbox element
17
+ - `Checkbox.Label` - Text label for the checkbox
18
+ - `Checkbox.Indicator` - Checkmark icon (built-in)
19
+ - `Checkbox.HiddenInput` - Hidden native input
20
+ - `Checkbox.Group` - Container for multiple checkboxes
21
+
22
+ ## Variants
23
+
24
+ | Variant | Visual Style | Usage | When to Use |
25
+ | --------- | ------------------------------------- | -------------------- | ------------------------ |
26
+ | `solid` | Filled background when checked | Primary checkboxes | Default, most use cases |
27
+ | `outline` | Border only, highlighted when checked | Secondary checkboxes | Forms with less emphasis |
28
+ | `surface` | Surface background | Alternative style | Cards, elevated surfaces |
29
+ | `subtle` | Subtle background | Low-emphasis options | Settings, preferences |
30
+ | `plain` | Minimal styling | Text-like checkboxes | Inline selections |
31
+
32
+ ### Visual Characteristics
33
+
34
+ - **solid**: Filled with primary color when checked, white checkmark
35
+ - **outline**: Border changes to primary color when checked, checkmark inside
36
+ - **surface**: Surface background with border
37
+ - **subtle**: Subtle gray background
38
+ - **plain**: Minimal styling, blends with text
39
+
40
+ ## Sizes
41
+
42
+ | Size | Box Size | Label Text | Usage |
43
+ | ---- | -------- | ------------- | ---------------------------- |
44
+ | `sm` | 18px | Small (14px) | Compact forms, dense layouts |
45
+ | `md` | 20px | Medium (16px) | Default, most use cases |
46
+ | `lg` | 22px | Large (18px) | Touch targets, mobile-first |
47
+
48
+ **Recommendation:** Use `md` for most cases. Use `lg` for mobile or touch-focused interfaces.
49
+
50
+ ## Props
51
+
52
+ ### Root Props
53
+
54
+ | Prop | Type | Default | Description |
55
+ | ----------------- | ---------------------------- | ------- | ----------------------------------- |
56
+ | `checked` | `boolean \| 'indeterminate'` | - | Controlled checked state |
57
+ | `defaultChecked` | `boolean` | `false` | Uncontrolled default checked state |
58
+ | `onCheckedChange` | `(details) => void` | - | Callback when checked state changes |
59
+ | `disabled` | `boolean` | `false` | Disable interaction |
60
+ | `invalid` | `boolean` | `false` | Mark as invalid |
61
+ | `required` | `boolean` | `false` | Mark as required |
62
+ | `name` | `string` | - | Form field name |
63
+ | `value` | `string` | - | Form field value |
64
+
65
+ ### Style Props
66
+
67
+ | Prop | Type | Default | Description |
68
+ | --------- | ---------------------------------------------------------- | --------- | -------------------- |
69
+ | `variant` | `'solid' \| 'outline' \| 'surface' \| 'subtle' \| 'plain'` | `'solid'` | Visual style variant |
70
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Checkbox size |
71
+
72
+ ## Examples
73
+
74
+ ### Basic Usage
75
+
76
+ ```typescript
77
+ import * as Checkbox from '@discourser/design-system';
78
+
79
+ // Single checkbox
80
+ <Checkbox.Root>
81
+ <Checkbox.HiddenInput />
82
+ <Checkbox.Control>
83
+ <Checkbox.Indicator />
84
+ </Checkbox.Control>
85
+ <Checkbox.Label>Accept terms and conditions</Checkbox.Label>
86
+ </Checkbox.Root>
87
+ ```
88
+
89
+ ### Controlled Checkbox
90
+
91
+ ```typescript
92
+ const [accepted, setAccepted] = useState(false);
93
+
94
+ <Checkbox.Root
95
+ checked={accepted}
96
+ onCheckedChange={(details) => setAccepted(details.checked)}
97
+ >
98
+ <Checkbox.HiddenInput />
99
+ <Checkbox.Control>
100
+ <Checkbox.Indicator />
101
+ </Checkbox.Control>
102
+ <Checkbox.Label>I agree to the terms</Checkbox.Label>
103
+ </Checkbox.Root>
104
+ ```
105
+
106
+ ### Different Variants
107
+
108
+ ```typescript
109
+ // Solid (default)
110
+ <Checkbox.Root variant="solid">
111
+ <Checkbox.HiddenInput />
112
+ <Checkbox.Control>
113
+ <Checkbox.Indicator />
114
+ </Checkbox.Control>
115
+ <Checkbox.Label>Solid checkbox</Checkbox.Label>
116
+ </Checkbox.Root>
117
+
118
+ // Outline
119
+ <Checkbox.Root variant="outline">
120
+ <Checkbox.HiddenInput />
121
+ <Checkbox.Control>
122
+ <Checkbox.Indicator />
123
+ </Checkbox.Control>
124
+ <Checkbox.Label>Outline checkbox</Checkbox.Label>
125
+ </Checkbox.Root>
126
+
127
+ // Subtle
128
+ <Checkbox.Root variant="subtle">
129
+ <Checkbox.HiddenInput />
130
+ <Checkbox.Control>
131
+ <Checkbox.Indicator />
132
+ </Checkbox.Control>
133
+ <Checkbox.Label>Subtle checkbox</Checkbox.Label>
134
+ </Checkbox.Root>
135
+ ```
136
+
137
+ ### Different Sizes
138
+
139
+ ```typescript
140
+ // Small
141
+ <Checkbox.Root size="sm">
142
+ <Checkbox.HiddenInput />
143
+ <Checkbox.Control>
144
+ <Checkbox.Indicator />
145
+ </Checkbox.Control>
146
+ <Checkbox.Label>Small checkbox</Checkbox.Label>
147
+ </Checkbox.Root>
148
+
149
+ // Medium (default)
150
+ <Checkbox.Root size="md">
151
+ <Checkbox.HiddenInput />
152
+ <Checkbox.Control>
153
+ <Checkbox.Indicator />
154
+ </Checkbox.Control>
155
+ <Checkbox.Label>Medium checkbox</Checkbox.Label>
156
+ </Checkbox.Root>
157
+
158
+ // Large
159
+ <Checkbox.Root size="lg">
160
+ <Checkbox.HiddenInput />
161
+ <Checkbox.Control>
162
+ <Checkbox.Indicator />
163
+ </Checkbox.Control>
164
+ <Checkbox.Label>Large checkbox</Checkbox.Label>
165
+ </Checkbox.Root>
166
+ ```
167
+
168
+ ### Indeterminate State
169
+
170
+ ```typescript
171
+ // "Select all" checkbox that shows indeterminate when some items selected
172
+ const [selectedItems, setSelectedItems] = useState<string[]>([]);
173
+ const allItems = ['item1', 'item2', 'item3'];
174
+ const allSelected = selectedItems.length === allItems.length;
175
+ const someSelected = selectedItems.length > 0 && !allSelected;
176
+
177
+ <Checkbox.Root
178
+ checked={someSelected ? 'indeterminate' : allSelected}
179
+ onCheckedChange={(details) => {
180
+ if (details.checked) {
181
+ setSelectedItems(allItems);
182
+ } else {
183
+ setSelectedItems([]);
184
+ }
185
+ }}
186
+ >
187
+ <Checkbox.HiddenInput />
188
+ <Checkbox.Control>
189
+ <Checkbox.Indicator />
190
+ </Checkbox.Control>
191
+ <Checkbox.Label>Select all</Checkbox.Label>
192
+ </Checkbox.Root>
193
+ ```
194
+
195
+ ### Checkbox Group
196
+
197
+ ```typescript
198
+ import * as Checkbox from '@discourser/design-system';
199
+
200
+ const [selectedFeatures, setSelectedFeatures] = useState<string[]>(['email']);
201
+
202
+ <Checkbox.Group
203
+ value={selectedFeatures}
204
+ onValueChange={(details) => setSelectedFeatures(details.value)}
205
+ >
206
+ <label>
207
+ <span>Notification Preferences</span>
208
+ </label>
209
+
210
+ <Checkbox.Root value="email">
211
+ <Checkbox.HiddenInput />
212
+ <Checkbox.Control>
213
+ <Checkbox.Indicator />
214
+ </Checkbox.Control>
215
+ <Checkbox.Label>Email notifications</Checkbox.Label>
216
+ </Checkbox.Root>
217
+
218
+ <Checkbox.Root value="push">
219
+ <Checkbox.HiddenInput />
220
+ <Checkbox.Control>
221
+ <Checkbox.Indicator />
222
+ </Checkbox.Control>
223
+ <Checkbox.Label>Push notifications</Checkbox.Label>
224
+ </Checkbox.Root>
225
+
226
+ <Checkbox.Root value="sms">
227
+ <Checkbox.HiddenInput />
228
+ <Checkbox.Control>
229
+ <Checkbox.Indicator />
230
+ </Checkbox.Control>
231
+ <Checkbox.Label>SMS notifications</Checkbox.Label>
232
+ </Checkbox.Root>
233
+ </Checkbox.Group>
234
+ ```
235
+
236
+ ### Disabled State
237
+
238
+ ```typescript
239
+ // Disabled unchecked
240
+ <Checkbox.Root disabled>
241
+ <Checkbox.HiddenInput />
242
+ <Checkbox.Control>
243
+ <Checkbox.Indicator />
244
+ </Checkbox.Control>
245
+ <Checkbox.Label>Disabled option</Checkbox.Label>
246
+ </Checkbox.Root>
247
+
248
+ // Disabled checked
249
+ <Checkbox.Root disabled checked>
250
+ <Checkbox.HiddenInput />
251
+ <Checkbox.Control>
252
+ <Checkbox.Indicator />
253
+ </Checkbox.Control>
254
+ <Checkbox.Label>Disabled checked option</Checkbox.Label>
255
+ </Checkbox.Root>
256
+ ```
257
+
258
+ ### Invalid State
259
+
260
+ ```typescript
261
+ const [accepted, setAccepted] = useState(false);
262
+ const [submitted, setSubmitted] = useState(false);
263
+ const invalid = submitted && !accepted;
264
+
265
+ <div>
266
+ <Checkbox.Root
267
+ checked={accepted}
268
+ invalid={invalid}
269
+ onCheckedChange={(details) => setAccepted(details.checked)}
270
+ >
271
+ <Checkbox.HiddenInput />
272
+ <Checkbox.Control>
273
+ <Checkbox.Indicator />
274
+ </Checkbox.Control>
275
+ <Checkbox.Label>I accept the terms and conditions *</Checkbox.Label>
276
+ </Checkbox.Root>
277
+
278
+ {invalid && (
279
+ <div className={css({ color: 'error', textStyle: 'sm', mt: '1' })}>
280
+ You must accept the terms to continue
281
+ </div>
282
+ )}
283
+
284
+ <Button onClick={() => setSubmitted(true)}>Submit</Button>
285
+ </div>
286
+ ```
287
+
288
+ ## Common Patterns
289
+
290
+ ### Terms and Conditions
291
+
292
+ ```typescript
293
+ const [agreed, setAgreed] = useState(false);
294
+
295
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
296
+ <Checkbox.Root
297
+ checked={agreed}
298
+ onCheckedChange={(details) => setAgreed(details.checked)}
299
+ >
300
+ <Checkbox.HiddenInput />
301
+ <Checkbox.Control>
302
+ <Checkbox.Indicator />
303
+ </Checkbox.Control>
304
+ <Checkbox.Label>
305
+ I agree to the{' '}
306
+ <a href="/terms" className={css({ color: 'primary', textDecoration: 'underline' })}>
307
+ Terms of Service
308
+ </a>{' '}
309
+ and{' '}
310
+ <a href="/privacy" className={css({ color: 'primary', textDecoration: 'underline' })}>
311
+ Privacy Policy
312
+ </a>
313
+ </Checkbox.Label>
314
+ </Checkbox.Root>
315
+
316
+ <Button variant="filled" disabled={!agreed}>
317
+ Continue
318
+ </Button>
319
+ </div>
320
+ ```
321
+
322
+ ### Settings List
323
+
324
+ ```typescript
325
+ const [settings, setSettings] = useState({
326
+ emailNotifications: true,
327
+ pushNotifications: false,
328
+ darkMode: true,
329
+ autoSave: true,
330
+ });
331
+
332
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'lg' })}>
333
+ <Heading size="md">Settings</Heading>
334
+
335
+ <Checkbox.Root
336
+ checked={settings.emailNotifications}
337
+ onCheckedChange={(details) =>
338
+ setSettings({ ...settings, emailNotifications: details.checked })
339
+ }
340
+ >
341
+ <Checkbox.HiddenInput />
342
+ <Checkbox.Control>
343
+ <Checkbox.Indicator />
344
+ </Checkbox.Control>
345
+ <Checkbox.Label>Email notifications</Checkbox.Label>
346
+ </Checkbox.Root>
347
+
348
+ <Checkbox.Root
349
+ checked={settings.pushNotifications}
350
+ onCheckedChange={(details) =>
351
+ setSettings({ ...settings, pushNotifications: details.checked })
352
+ }
353
+ >
354
+ <Checkbox.HiddenInput />
355
+ <Checkbox.Control>
356
+ <Checkbox.Indicator />
357
+ </Checkbox.Control>
358
+ <Checkbox.Label>Push notifications</Checkbox.Label>
359
+ </Checkbox.Root>
360
+
361
+ <Checkbox.Root
362
+ checked={settings.darkMode}
363
+ onCheckedChange={(details) =>
364
+ setSettings({ ...settings, darkMode: details.checked })
365
+ }
366
+ >
367
+ <Checkbox.HiddenInput />
368
+ <Checkbox.Control>
369
+ <Checkbox.Indicator />
370
+ </Checkbox.Control>
371
+ <Checkbox.Label>Dark mode</Checkbox.Label>
372
+ </Checkbox.Root>
373
+ </div>
374
+ ```
375
+
376
+ ### Filter List
377
+
378
+ ```typescript
379
+ const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
380
+
381
+ <div>
382
+ <Heading size="sm">Filter Results</Heading>
383
+
384
+ <Checkbox.Group
385
+ value={selectedFilters}
386
+ onValueChange={(details) => setSelectedFilters(details.value)}
387
+ >
388
+ <Checkbox.Root value="inStock" variant="outline">
389
+ <Checkbox.HiddenInput />
390
+ <Checkbox.Control>
391
+ <Checkbox.Indicator />
392
+ </Checkbox.Control>
393
+ <Checkbox.Label>In stock</Checkbox.Label>
394
+ </Checkbox.Root>
395
+
396
+ <Checkbox.Root value="onSale" variant="outline">
397
+ <Checkbox.HiddenInput />
398
+ <Checkbox.Control>
399
+ <Checkbox.Indicator />
400
+ </Checkbox.Control>
401
+ <Checkbox.Label>On sale</Checkbox.Label>
402
+ </Checkbox.Root>
403
+
404
+ <Checkbox.Root value="freeShipping" variant="outline">
405
+ <Checkbox.HiddenInput />
406
+ <Checkbox.Control>
407
+ <Checkbox.Indicator />
408
+ </Checkbox.Control>
409
+ <Checkbox.Label>Free shipping</Checkbox.Label>
410
+ </Checkbox.Root>
411
+ </Checkbox.Group>
412
+ </div>
413
+ ```
414
+
415
+ ## DO NOT
416
+
417
+ ```typescript
418
+ // ❌ Don't use native checkbox input
419
+ <input type="checkbox" /> // Use Checkbox component instead
420
+
421
+ // ❌ Don't forget HiddenInput (needed for forms)
422
+ <Checkbox.Root>
423
+ <Checkbox.Control>
424
+ <Checkbox.Indicator />
425
+ </Checkbox.Control>
426
+ <Checkbox.Label>Option</Checkbox.Label>
427
+ </Checkbox.Root> // Missing <Checkbox.HiddenInput />
428
+
429
+ // ❌ Don't forget Label (accessibility issue)
430
+ <Checkbox.Root>
431
+ <Checkbox.HiddenInput />
432
+ <Checkbox.Control>
433
+ <Checkbox.Indicator />
434
+ </Checkbox.Control>
435
+ </Checkbox.Root> // Missing label
436
+
437
+ // ❌ Don't use checkbox for single exclusive choice
438
+ <Checkbox.Root>...</Checkbox.Root> // Use RadioGroup for exclusive selections
439
+
440
+ // ❌ Don't override checkbox colors with inline styles
441
+ <Checkbox.Control style={{ backgroundColor: 'red' }}>
442
+ <Checkbox.Indicator />
443
+ </Checkbox.Control> // Use variants instead
444
+
445
+ // ✅ Use complete Checkbox structure
446
+ <Checkbox.Root>
447
+ <Checkbox.HiddenInput />
448
+ <Checkbox.Control>
449
+ <Checkbox.Indicator />
450
+ </Checkbox.Control>
451
+ <Checkbox.Label>Accept terms</Checkbox.Label>
452
+ </Checkbox.Root>
453
+ ```
454
+
455
+ ## Accessibility
456
+
457
+ The Checkbox component follows WCAG 2.1 Level AA standards:
458
+
459
+ - **Keyboard Navigation**: Space to toggle, Tab to navigate
460
+ - **Focus Indicator**: Clear focus ring on keyboard navigation
461
+ - **ARIA Attributes**: Proper `role="checkbox"` and `aria-checked`
462
+ - **Labels**: Associated labels for screen readers
463
+ - **Disabled State**: Proper `aria-disabled` attribute
464
+ - **Touch Targets**: Minimum 44x44px with size md or larger
465
+
466
+ ### Accessibility Best Practices
467
+
468
+ ```typescript
469
+ // ✅ Always provide a label
470
+ <Checkbox.Root>
471
+ <Checkbox.HiddenInput />
472
+ <Checkbox.Control>
473
+ <Checkbox.Indicator />
474
+ </Checkbox.Control>
475
+ <Checkbox.Label>Subscribe to newsletter</Checkbox.Label>
476
+ </Checkbox.Root>
477
+
478
+ // ✅ Use required attribute for required fields
479
+ <Checkbox.Root required>
480
+ <Checkbox.HiddenInput />
481
+ <Checkbox.Control>
482
+ <Checkbox.Indicator />
483
+ </Checkbox.Control>
484
+ <Checkbox.Label>I accept the terms *</Checkbox.Label>
485
+ </Checkbox.Root>
486
+
487
+ // ✅ Provide error messages for invalid state
488
+ <div>
489
+ <Checkbox.Root invalid={!accepted && submitted}>
490
+ <Checkbox.HiddenInput />
491
+ <Checkbox.Control>
492
+ <Checkbox.Indicator />
493
+ </Checkbox.Control>
494
+ <Checkbox.Label>Accept terms</Checkbox.Label>
495
+ </Checkbox.Root>
496
+ {!accepted && submitted && (
497
+ <span role="alert" className={css({ color: 'error' })}>
498
+ Required field
499
+ </span>
500
+ )}
501
+ </div>
502
+
503
+ // ✅ Group related checkboxes
504
+ <Checkbox.Group aria-label="Notification preferences">
505
+ {/* Checkboxes here */}
506
+ </Checkbox.Group>
507
+ ```
508
+
509
+ ## Variant Selection Guide
510
+
511
+ | Scenario | Recommended Variant | Reasoning |
512
+ | ----------------- | ------------------- | -------------------------------------- |
513
+ | Terms acceptance | `solid` | Clear, prominent for important consent |
514
+ | Settings toggles | `solid` or `subtle` | Clear state indication |
515
+ | Filter options | `outline` | Less prominent, multiple selections |
516
+ | Feature flags | `solid` | Default, clear on/off state |
517
+ | Inline selections | `plain` | Minimal, blends with content |
518
+ | Form checkboxes | `solid` | Standard, clear visual feedback |
519
+
520
+ ## State Behaviors
521
+
522
+ | State | Visual Change | Behavior |
523
+ | ----------------- | ----------------------------- | -------------------------------------- |
524
+ | **Unchecked** | Empty box with border | Default state |
525
+ | **Checked** | Filled box with checkmark | Selected state |
526
+ | **Indeterminate** | Filled box with dash | Partial selection (e.g., "select all") |
527
+ | **Hover** | Background color change | Interactive feedback |
528
+ | **Focus** | Focus ring appears | Keyboard navigation indicator |
529
+ | **Disabled** | Grayed out, reduced opacity | Cannot be toggled |
530
+ | **Invalid** | Error color border/background | Validation error |
531
+
532
+ ## Testing
533
+
534
+ ```typescript
535
+ import { render, screen } from '@testing-library/react';
536
+ import userEvent from '@testing-library/user-event';
537
+
538
+ test('checkbox can be checked and unchecked', async () => {
539
+ const handleChange = vi.fn();
540
+
541
+ render(
542
+ <Checkbox.Root onCheckedChange={handleChange}>
543
+ <Checkbox.HiddenInput />
544
+ <Checkbox.Control>
545
+ <Checkbox.Indicator />
546
+ </Checkbox.Control>
547
+ <Checkbox.Label>Accept terms</Checkbox.Label>
548
+ </Checkbox.Root>
549
+ );
550
+
551
+ const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' });
552
+
553
+ await userEvent.click(checkbox);
554
+ expect(handleChange).toHaveBeenCalledWith(expect.objectContaining({ checked: true }));
555
+
556
+ await userEvent.click(checkbox);
557
+ expect(handleChange).toHaveBeenCalledWith(expect.objectContaining({ checked: false }));
558
+ });
559
+
560
+ test('disabled checkbox cannot be toggled', async () => {
561
+ render(
562
+ <Checkbox.Root disabled>
563
+ <Checkbox.HiddenInput />
564
+ <Checkbox.Control>
565
+ <Checkbox.Indicator />
566
+ </Checkbox.Control>
567
+ <Checkbox.Label>Disabled</Checkbox.Label>
568
+ </Checkbox.Root>
569
+ );
570
+
571
+ const checkbox = screen.getByRole('checkbox');
572
+ await userEvent.click(checkbox);
573
+
574
+ expect(checkbox).not.toBeChecked();
575
+ });
576
+ ```
577
+
578
+ ## Related Components
579
+
580
+ - **RadioGroup** - For exclusive single selections
581
+ - **Switch** - For on/off toggles (different visual metaphor)
582
+ - **Button** - For action triggers
583
+ - **Select** - For choosing from many options