@discourser/design-system 0.4.0 → 0.5.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 (36) hide show
  1. package/README.md +12 -4
  2. package/dist/styles.css +5126 -0
  3. package/guidelines/Guidelines.md +67 -123
  4. package/guidelines/components/accordion.md +93 -0
  5. package/guidelines/components/avatar.md +70 -0
  6. package/guidelines/components/badge.md +61 -0
  7. package/guidelines/components/button.md +75 -40
  8. package/guidelines/components/card.md +84 -25
  9. package/guidelines/components/checkbox.md +88 -0
  10. package/guidelines/components/dialog.md +619 -31
  11. package/guidelines/components/drawer.md +655 -0
  12. package/guidelines/components/heading.md +71 -0
  13. package/guidelines/components/icon-button.md +92 -37
  14. package/guidelines/components/input-addon.md +685 -0
  15. package/guidelines/components/input-group.md +830 -0
  16. package/guidelines/components/input.md +92 -37
  17. package/guidelines/components/popover.md +71 -0
  18. package/guidelines/components/progress.md +63 -0
  19. package/guidelines/components/radio-group.md +95 -0
  20. package/guidelines/components/select.md +507 -0
  21. package/guidelines/components/skeleton.md +76 -0
  22. package/guidelines/components/slider.md +911 -0
  23. package/guidelines/components/spinner.md +783 -0
  24. package/guidelines/components/switch.md +105 -38
  25. package/guidelines/components/tabs.md +654 -0
  26. package/guidelines/components/textarea.md +70 -0
  27. package/guidelines/components/toast.md +77 -0
  28. package/guidelines/components/tooltip.md +80 -0
  29. package/guidelines/design-tokens/colors.md +309 -72
  30. package/guidelines/design-tokens/elevation.md +615 -45
  31. package/guidelines/design-tokens/spacing.md +654 -74
  32. package/guidelines/design-tokens/typography.md +432 -50
  33. package/guidelines/overview-components.md +9 -5
  34. package/guidelines/overview-imports.md +314 -0
  35. package/guidelines/overview-patterns.md +3852 -0
  36. package/package.json +4 -2
@@ -0,0 +1,830 @@
1
+ # InputGroup
2
+
3
+ **Purpose:** Layout wrapper component for composing inputs with decorative elements (icons, text, buttons) positioned at start or end following Material Design 3 patterns.
4
+
5
+ ## When to Use This Component
6
+
7
+ Use InputGroup when you need to **position elements inside or around an input field** (search icons, clear buttons, prefix/suffix text).
8
+
9
+ **Decision Tree:**
10
+
11
+ | Scenario | Use This | Why |
12
+ | -------------------------------------------------------- | --------------------- | ------------------------------------ |
13
+ | Input with prefix icon or text (search, user, currency) | InputGroup ✅ | Positions elements relative to input |
14
+ | Input with suffix icon or text (units, domains, actions) | InputGroup ✅ | Manages internal layout and spacing |
15
+ | Input with both prefix and suffix elements | InputGroup ✅ | Coordinates multiple elements |
16
+ | Standalone input without decoration | Input | No layout management needed |
17
+ | Input with external label above/below | Input with label prop | Built-in label positioning |
18
+ | Multiple inputs side by side | Flex/Grid layout | Different layout pattern |
19
+
20
+ **Component Comparison:**
21
+
22
+ ```typescript
23
+ // ✅ Use InputGroup for input with icon
24
+ <InputGroup.Root size="md">
25
+ <InputGroup.Element>
26
+ <SearchIcon />
27
+ </InputGroup.Element>
28
+ <Input placeholder="Search..." />
29
+ </InputGroup.Root>
30
+
31
+ // ✅ Use InputGroup for input with addon text
32
+ <InputGroup.Root size="md">
33
+ <InputAddon variant="outline">$</InputAddon>
34
+ <Input placeholder="0.00" type="number" />
35
+ </InputGroup.Root>
36
+
37
+ // ✅ Use InputGroup for input with action button
38
+ <InputGroup.Root size="md">
39
+ <Input placeholder="Password" type="password" />
40
+ <InputGroup.Element>
41
+ <IconButton variant="ghost" aria-label="Toggle visibility">
42
+ <EyeIcon />
43
+ </IconButton>
44
+ </InputGroup.Element>
45
+ </InputGroup.Root>
46
+
47
+ // ❌ Don't use InputGroup for standalone input
48
+ <InputGroup.Root size="md">
49
+ <Input placeholder="Email" /> // Wrong - no decorative elements
50
+ </InputGroup.Root>
51
+
52
+ <Input placeholder="Email" /> // Correct - no group needed
53
+
54
+ // ❌ Don't use InputGroup for external labels
55
+ <InputGroup.Root>
56
+ <label>Email Address</label> // Wrong - labels go outside
57
+ <Input />
58
+ </InputGroup.Root>
59
+
60
+ <div>
61
+ <label>Email Address</label> // Correct
62
+ <Input />
63
+ </div>
64
+
65
+ // ❌ Don't use InputGroup for multiple separate inputs
66
+ <InputGroup.Root>
67
+ <Input placeholder="First name" />
68
+ <Input placeholder="Last name" /> // Wrong - separate inputs
69
+ </InputGroup.Root>
70
+
71
+ <div className={css({ display: 'flex', gap: 'md' })}>
72
+ <Input placeholder="First name" />
73
+ <Input placeholder="Last name" /> // Correct
74
+ </div>
75
+ ```
76
+
77
+ ## Import
78
+
79
+ ```typescript
80
+ import { InputGroup, Input, InputAddon } from '@discourser/design-system';
81
+ ```
82
+
83
+ ## Component Structure
84
+
85
+ InputGroup uses a compound component pattern with these parts:
86
+
87
+ - `InputGroup.Root` - Container wrapper that manages layout and sizing
88
+ - `InputGroup.Element` - Positioned element container for icons or buttons (no border/background)
89
+ - `InputAddon` - Styled addon element for text or bordered content (separate component)
90
+
91
+ ```typescript
92
+ // Basic structure with Element (icon, button)
93
+ <InputGroup.Root size="md">
94
+ <InputGroup.Element><!-- icon or button --></InputGroup.Element>
95
+ <Input />
96
+ <InputGroup.Element><!-- icon or button --></InputGroup.Element>
97
+ </InputGroup.Root>
98
+
99
+ // Structure with InputAddon (text, bordered elements)
100
+ <InputGroup.Root size="md">
101
+ <InputAddon><!-- prefix text --></InputAddon>
102
+ <Input />
103
+ <InputAddon><!-- suffix text --></InputAddon>
104
+ </InputGroup.Root>
105
+ ```
106
+
107
+ ## Sizes
108
+
109
+ | Size | Input Height | Element Min Width | Icon Size | Usage |
110
+ | ---- | ------------ | ----------------- | --------- | ------------------------------- |
111
+ | `xs` | 32px | 32px | 16px | Extra compact forms, mobile |
112
+ | `sm` | 36px | 36px | 18px | Compact forms, dense layouts |
113
+ | `md` | 40px | 40px | 20px | Default, most use cases |
114
+ | `lg` | 44px | 44px | 20px | Touch targets, prominent inputs |
115
+ | `xl` | 48px | 44px | 22px | Large forms, hero sections |
116
+
117
+ **Recommendation:** Use `md` for most cases. Size automatically applies to child Input and addons.
118
+
119
+ ## Props
120
+
121
+ ### Root Props
122
+
123
+ | Prop | Type | Default | Description |
124
+ | ----------- | -------------------------------------- | -------- | ---------------------------- |
125
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Size applied to all children |
126
+ | `children` | `ReactNode` | Required | Input and addon elements |
127
+ | `className` | `string` | - | Additional CSS classes |
128
+
129
+ ### Element Props
130
+
131
+ | Prop | Type | Default | Description |
132
+ | ----------- | ----------- | -------- | ------------------------ |
133
+ | `children` | `ReactNode` | Required | Icon, button, or content |
134
+ | `className` | `string` | - | Additional CSS classes |
135
+
136
+ **Note:** InputGroup.Root and InputGroup.Element extend `HTMLAttributes<HTMLDivElement>`.
137
+
138
+ ## Examples
139
+
140
+ ### Basic Icon Elements
141
+
142
+ ```typescript
143
+ import { InputGroup, Input } from '@discourser/design-system';
144
+ import { SearchIcon, UserIcon, MailIcon, LockIcon } from 'your-icon-library';
145
+
146
+ // Search with icon prefix
147
+ <InputGroup.Root size="md">
148
+ <InputGroup.Element>
149
+ <SearchIcon />
150
+ </InputGroup.Element>
151
+ <Input placeholder="Search..." />
152
+ </InputGroup.Root>
153
+
154
+ // User icon prefix
155
+ <InputGroup.Root size="md">
156
+ <InputGroup.Element>
157
+ <UserIcon />
158
+ </InputGroup.Element>
159
+ <Input placeholder="Username" />
160
+ </InputGroup.Root>
161
+
162
+ // Email icon prefix
163
+ <InputGroup.Root size="md">
164
+ <InputGroup.Element>
165
+ <MailIcon />
166
+ </InputGroup.Element>
167
+ <Input type="email" placeholder="Email address" />
168
+ </InputGroup.Root>
169
+
170
+ // Lock icon for password
171
+ <InputGroup.Root size="md">
172
+ <InputGroup.Element>
173
+ <LockIcon />
174
+ </InputGroup.Element>
175
+ <Input type="password" placeholder="Password" />
176
+ </InputGroup.Root>
177
+ ```
178
+
179
+ ### Icon on Right
180
+
181
+ ```typescript
182
+ import { CalendarIcon, ChevronDownIcon } from 'your-icon-library';
183
+
184
+ // Calendar icon suffix
185
+ <InputGroup.Root size="md">
186
+ <Input type="date" />
187
+ <InputGroup.Element>
188
+ <CalendarIcon />
189
+ </InputGroup.Element>
190
+ </InputGroup.Root>
191
+
192
+ // Dropdown indicator
193
+ <InputGroup.Root size="md">
194
+ <Input placeholder="Select option..." readOnly />
195
+ <InputGroup.Element>
196
+ <ChevronDownIcon />
197
+ </InputGroup.Element>
198
+ </InputGroup.Root>
199
+ ```
200
+
201
+ ### Interactive Button Elements
202
+
203
+ ```typescript
204
+ import { IconButton } from '@discourser/design-system';
205
+ import { XIcon, EyeIcon, EyeOffIcon } from 'your-icon-library';
206
+
207
+ // Clear button
208
+ const [value, setValue] = useState('');
209
+
210
+ <InputGroup.Root size="md">
211
+ <Input
212
+ placeholder="Search..."
213
+ value={value}
214
+ onChange={(e) => setValue(e.target.value)}
215
+ />
216
+ {value && (
217
+ <InputGroup.Element>
218
+ <IconButton
219
+ variant="ghost"
220
+ size="sm"
221
+ aria-label="Clear"
222
+ onClick={() => setValue('')}
223
+ >
224
+ <XIcon />
225
+ </IconButton>
226
+ </InputGroup.Element>
227
+ )}
228
+ </InputGroup.Root>
229
+
230
+ // Password visibility toggle
231
+ const [showPassword, setShowPassword] = useState(false);
232
+
233
+ <InputGroup.Root size="md">
234
+ <Input
235
+ type={showPassword ? 'text' : 'password'}
236
+ placeholder="Password"
237
+ />
238
+ <InputGroup.Element>
239
+ <IconButton
240
+ variant="ghost"
241
+ size="sm"
242
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
243
+ onClick={() => setShowPassword(!showPassword)}
244
+ >
245
+ {showPassword ? <EyeOffIcon /> : <EyeIcon />}
246
+ </IconButton>
247
+ </InputGroup.Element>
248
+ </InputGroup.Root>
249
+ ```
250
+
251
+ ### With InputAddon (Text/Bordered Elements)
252
+
253
+ ```typescript
254
+ import { InputAddon } from '@discourser/design-system';
255
+
256
+ // Currency prefix
257
+ <InputGroup.Root size="md">
258
+ <InputAddon variant="outline">$</InputAddon>
259
+ <Input placeholder="0.00" type="number" />
260
+ </InputGroup.Root>
261
+
262
+ // Unit suffix
263
+ <InputGroup.Root size="md">
264
+ <Input placeholder="Enter weight" type="number" />
265
+ <InputAddon variant="outline">kg</InputAddon>
266
+ </InputGroup.Root>
267
+
268
+ // Domain suffix
269
+ <InputGroup.Root size="md">
270
+ <Input placeholder="username" />
271
+ <InputAddon variant="outline">@example.com</InputAddon>
272
+ </InputGroup.Root>
273
+
274
+ // URL prefix
275
+ <InputGroup.Root size="md">
276
+ <InputAddon variant="outline">https://</InputAddon>
277
+ <Input placeholder="example.com" />
278
+ </InputGroup.Root>
279
+ ```
280
+
281
+ ### Combining Element and InputAddon
282
+
283
+ ```typescript
284
+ // Icon with currency
285
+ <InputGroup.Root size="md">
286
+ <InputGroup.Element>
287
+ <DollarIcon />
288
+ </InputGroup.Element>
289
+ <Input placeholder="0.00" type="number" />
290
+ <InputAddon variant="outline">USD</InputAddon>
291
+ </InputGroup.Root>
292
+
293
+ // Search icon with clear button
294
+ <InputGroup.Root size="md">
295
+ <InputGroup.Element>
296
+ <SearchIcon />
297
+ </InputGroup.Element>
298
+ <Input
299
+ placeholder="Search..."
300
+ value={searchQuery}
301
+ onChange={(e) => setSearchQuery(e.target.value)}
302
+ />
303
+ {searchQuery && (
304
+ <InputGroup.Element>
305
+ <IconButton
306
+ variant="ghost"
307
+ size="sm"
308
+ aria-label="Clear"
309
+ onClick={() => setSearchQuery('')}
310
+ >
311
+ <XIcon />
312
+ </IconButton>
313
+ </InputGroup.Element>
314
+ )}
315
+ </InputGroup.Root>
316
+ ```
317
+
318
+ ### Different Sizes
319
+
320
+ ```typescript
321
+ // Extra small
322
+ <InputGroup.Root size="xs">
323
+ <InputGroup.Element>
324
+ <SearchIcon />
325
+ </InputGroup.Element>
326
+ <Input placeholder="Search..." />
327
+ </InputGroup.Root>
328
+
329
+ // Small
330
+ <InputGroup.Root size="sm">
331
+ <InputGroup.Element>
332
+ <SearchIcon />
333
+ </InputGroup.Element>
334
+ <Input placeholder="Search..." />
335
+ </InputGroup.Root>
336
+
337
+ // Medium (default)
338
+ <InputGroup.Root size="md">
339
+ <InputGroup.Element>
340
+ <SearchIcon />
341
+ </InputGroup.Element>
342
+ <Input placeholder="Search..." />
343
+ </InputGroup.Root>
344
+
345
+ // Large
346
+ <InputGroup.Root size="lg">
347
+ <InputGroup.Element>
348
+ <SearchIcon />
349
+ </InputGroup.Element>
350
+ <Input placeholder="Search..." />
351
+ </InputGroup.Root>
352
+
353
+ // Extra large
354
+ <InputGroup.Root size="xl">
355
+ <InputGroup.Element>
356
+ <SearchIcon />
357
+ </InputGroup.Element>
358
+ <Input placeholder="Search..." />
359
+ </InputGroup.Root>
360
+ ```
361
+
362
+ ### Multiple Elements
363
+
364
+ ```typescript
365
+ // Both prefix and suffix elements
366
+ <InputGroup.Root size="md">
367
+ <InputGroup.Element>
368
+ <UserIcon />
369
+ </InputGroup.Element>
370
+ <Input placeholder="Username" />
371
+ <InputGroup.Element>
372
+ <CheckIcon />
373
+ </InputGroup.Element>
374
+ </InputGroup.Root>
375
+
376
+ // Complex composition
377
+ <InputGroup.Root size="md">
378
+ <InputAddon variant="outline">https://</InputAddon>
379
+ <Input placeholder="mysite" />
380
+ <InputAddon variant="outline">.com</InputAddon>
381
+ <InputGroup.Element>
382
+ <IconButton variant="ghost" size="sm" aria-label="Copy">
383
+ <CopyIcon />
384
+ </IconButton>
385
+ </InputGroup.Element>
386
+ </InputGroup.Root>
387
+ ```
388
+
389
+ ### Loading State
390
+
391
+ ```typescript
392
+ import { Spinner } from '@discourser/design-system';
393
+
394
+ const [isSearching, setIsSearching] = useState(false);
395
+
396
+ <InputGroup.Root size="md">
397
+ <InputGroup.Element>
398
+ {isSearching ? (
399
+ <Spinner size="sm" />
400
+ ) : (
401
+ <SearchIcon />
402
+ )}
403
+ </InputGroup.Element>
404
+ <Input placeholder="Search..." />
405
+ </InputGroup.Root>
406
+ ```
407
+
408
+ ### With Form Label
409
+
410
+ ```typescript
411
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'xs' })}>
412
+ <label htmlFor="search" className={css({ fontWeight: 'medium', textStyle: 'sm' })}>
413
+ Search
414
+ </label>
415
+ <InputGroup.Root size="md">
416
+ <InputGroup.Element>
417
+ <SearchIcon />
418
+ </InputGroup.Element>
419
+ <Input id="search" placeholder="Search products..." />
420
+ </InputGroup.Root>
421
+ </div>
422
+ ```
423
+
424
+ ## Common Patterns
425
+
426
+ ### Search Input
427
+
428
+ ```typescript
429
+ const [searchQuery, setSearchQuery] = useState('');
430
+ const [isSearching, setIsSearching] = useState(false);
431
+
432
+ <InputGroup.Root size="md">
433
+ <InputGroup.Element>
434
+ {isSearching ? (
435
+ <Spinner size="sm" />
436
+ ) : (
437
+ <SearchIcon />
438
+ )}
439
+ </InputGroup.Element>
440
+ <Input
441
+ placeholder="Search products..."
442
+ value={searchQuery}
443
+ onChange={(e) => {
444
+ setSearchQuery(e.target.value);
445
+ setIsSearching(true);
446
+ // Debounce search logic
447
+ }}
448
+ />
449
+ {searchQuery && (
450
+ <InputGroup.Element>
451
+ <IconButton
452
+ variant="ghost"
453
+ size="sm"
454
+ aria-label="Clear search"
455
+ onClick={() => {
456
+ setSearchQuery('');
457
+ setIsSearching(false);
458
+ }}
459
+ >
460
+ <XIcon />
461
+ </IconButton>
462
+ </InputGroup.Element>
463
+ )}
464
+ </InputGroup.Root>
465
+ ```
466
+
467
+ ### Password Input with Toggle
468
+
469
+ ```typescript
470
+ const [password, setPassword] = useState('');
471
+ const [showPassword, setShowPassword] = useState(false);
472
+
473
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'xs' })}>
474
+ <label htmlFor="password" className={css({ fontWeight: 'medium' })}>
475
+ Password
476
+ </label>
477
+ <InputGroup.Root size="md">
478
+ <InputGroup.Element>
479
+ <LockIcon />
480
+ </InputGroup.Element>
481
+ <Input
482
+ id="password"
483
+ type={showPassword ? 'text' : 'password'}
484
+ placeholder="Enter password"
485
+ value={password}
486
+ onChange={(e) => setPassword(e.target.value)}
487
+ />
488
+ <InputGroup.Element>
489
+ <IconButton
490
+ variant="ghost"
491
+ size="sm"
492
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
493
+ onClick={() => setShowPassword(!showPassword)}
494
+ >
495
+ {showPassword ? <EyeOffIcon /> : <EyeIcon />}
496
+ </IconButton>
497
+ </InputGroup.Element>
498
+ </InputGroup.Root>
499
+ </div>
500
+ ```
501
+
502
+ ### Currency Input
503
+
504
+ ```typescript
505
+ const [amount, setAmount] = useState('');
506
+
507
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'xs' })}>
508
+ <label htmlFor="amount" className={css({ fontWeight: 'medium' })}>
509
+ Amount
510
+ </label>
511
+ <InputGroup.Root size="md">
512
+ <InputAddon variant="outline">$</InputAddon>
513
+ <Input
514
+ id="amount"
515
+ type="number"
516
+ placeholder="0.00"
517
+ value={amount}
518
+ onChange={(e) => setAmount(e.target.value)}
519
+ min="0"
520
+ step="0.01"
521
+ />
522
+ <InputAddon variant="outline">USD</InputAddon>
523
+ </InputGroup.Root>
524
+ </div>
525
+ ```
526
+
527
+ ### URL Input
528
+
529
+ ```typescript
530
+ const [url, setUrl] = useState('');
531
+
532
+ <InputGroup.Root size="md">
533
+ <InputAddon variant="outline">https://</InputAddon>
534
+ <Input
535
+ placeholder="example.com"
536
+ value={url}
537
+ onChange={(e) => setUrl(e.target.value)}
538
+ />
539
+ {url && (
540
+ <InputGroup.Element>
541
+ <IconButton
542
+ variant="ghost"
543
+ size="sm"
544
+ aria-label="Copy URL"
545
+ onClick={() => navigator.clipboard.writeText(`https://${url}`)}
546
+ >
547
+ <CopyIcon />
548
+ </IconButton>
549
+ </InputGroup.Element>
550
+ )}
551
+ </InputGroup.Root>
552
+ ```
553
+
554
+ ### Phone Number Input
555
+
556
+ ```typescript
557
+ const [phone, setPhone] = useState('');
558
+
559
+ <InputGroup.Root size="md">
560
+ <InputGroup.Element>
561
+ <PhoneIcon />
562
+ </InputGroup.Element>
563
+ <InputAddon variant="outline">+1</InputAddon>
564
+ <Input
565
+ placeholder="(555) 000-0000"
566
+ type="tel"
567
+ value={phone}
568
+ onChange={(e) => setPhone(e.target.value)}
569
+ />
570
+ </InputGroup.Root>
571
+ ```
572
+
573
+ ### Email Input with Validation
574
+
575
+ ```typescript
576
+ const [email, setEmail] = useState('');
577
+ const [isValid, setIsValid] = useState<boolean | null>(null);
578
+
579
+ const validateEmail = (value: string) => {
580
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
581
+ return regex.test(value);
582
+ };
583
+
584
+ <InputGroup.Root size="md">
585
+ <InputGroup.Element>
586
+ <MailIcon />
587
+ </InputGroup.Element>
588
+ <Input
589
+ type="email"
590
+ placeholder="you@example.com"
591
+ value={email}
592
+ onChange={(e) => {
593
+ setEmail(e.target.value);
594
+ setIsValid(e.target.value ? validateEmail(e.target.value) : null);
595
+ }}
596
+ />
597
+ <InputGroup.Element>
598
+ {isValid === true && <CheckIcon className={css({ color: 'success' })} />}
599
+ {isValid === false && <XIcon className={css({ color: 'error' })} />}
600
+ </InputGroup.Element>
601
+ </InputGroup.Root>
602
+ ```
603
+
604
+ ## DO NOT
605
+
606
+ ```typescript
607
+ // ❌ Don't use InputGroup without decorative elements
608
+ <InputGroup.Root>
609
+ <Input placeholder="Email" /> // Wrong - no point without addons/elements
610
+ </InputGroup.Root>
611
+
612
+ // ✅ Use Input directly if no decorations
613
+ <Input placeholder="Email" />
614
+
615
+ // ❌ Don't put labels inside InputGroup
616
+ <InputGroup.Root>
617
+ <label>Search</label> // Wrong - labels go outside
618
+ <Input />
619
+ </InputGroup.Root>
620
+
621
+ // ✅ Put labels outside
622
+ <div>
623
+ <label>Search</label>
624
+ <InputGroup.Root>
625
+ <InputGroup.Element><SearchIcon /></InputGroup.Element>
626
+ <Input />
627
+ </InputGroup.Root>
628
+ </div>
629
+
630
+ // ❌ Don't use multiple inputs in one group
631
+ <InputGroup.Root>
632
+ <Input placeholder="First name" />
633
+ <Input placeholder="Last name" /> // Wrong - separate inputs
634
+ </InputGroup.Root>
635
+
636
+ // ✅ Use separate groups or flex layout
637
+ <div className={css({ display: 'flex', gap: 'md' })}>
638
+ <Input placeholder="First name" />
639
+ <Input placeholder="Last name" />
640
+ </div>
641
+
642
+ // ❌ Don't nest InputGroups
643
+ <InputGroup.Root>
644
+ <InputGroup.Root> // Wrong - no nesting
645
+ <Input />
646
+ </InputGroup.Root>
647
+ </InputGroup.Root>
648
+
649
+ // ❌ Don't override positioning with inline styles
650
+ <InputGroup.Root>
651
+ <InputGroup.Element style={{ position: 'relative' }}>
652
+ <SearchIcon />
653
+ </InputGroup.Element>
654
+ <Input />
655
+ </InputGroup.Root> // Wrong - breaks internal positioning
656
+
657
+ // ✅ Use component as designed
658
+ <InputGroup.Root>
659
+ <InputGroup.Element>
660
+ <SearchIcon />
661
+ </InputGroup.Element>
662
+ <Input />
663
+ </InputGroup.Root>
664
+ ```
665
+
666
+ ## Accessibility
667
+
668
+ The InputGroup component follows WCAG 2.1 Level AA standards:
669
+
670
+ - **Visual Association**: Elements are visually grouped with inputs
671
+ - **Focus Management**: Focus stays on input, not decorative elements
672
+ - **Interactive Elements**: Buttons have proper labels and keyboard access
673
+ - **Icon Semantics**: Decorative icons are hidden from screen readers
674
+ - **Touch Targets**: Minimum 44x44px for interactive elements (size md or larger)
675
+
676
+ ### Accessibility Best Practices
677
+
678
+ ```typescript
679
+ // ✅ Provide aria-label for icon-only buttons
680
+ <InputGroup.Root>
681
+ <Input />
682
+ <InputGroup.Element>
683
+ <IconButton
684
+ variant="ghost"
685
+ aria-label="Clear input"
686
+ onClick={handleClear}
687
+ >
688
+ <XIcon />
689
+ </IconButton>
690
+ </InputGroup.Element>
691
+ </InputGroup.Root>
692
+
693
+ // ✅ Use proper labels for inputs
694
+ <div>
695
+ <label htmlFor="search">Search products</label>
696
+ <InputGroup.Root>
697
+ <InputGroup.Element>
698
+ <SearchIcon aria-hidden="true" />
699
+ </InputGroup.Element>
700
+ <Input id="search" placeholder="Search..." />
701
+ </InputGroup.Root>
702
+ </div>
703
+
704
+ // ✅ Hide decorative icons from screen readers
705
+ <InputGroup.Root>
706
+ <InputGroup.Element>
707
+ <SearchIcon aria-hidden="true" />
708
+ </InputGroup.Element>
709
+ <Input aria-label="Search" placeholder="Search..." />
710
+ </InputGroup.Root>
711
+
712
+ // ✅ Provide context in input labels
713
+ <InputGroup.Root>
714
+ <InputAddon variant="outline">$</InputAddon>
715
+ <Input
716
+ aria-label="Price in US dollars"
717
+ placeholder="0.00"
718
+ type="number"
719
+ />
720
+ </InputGroup.Root>
721
+
722
+ // ✅ Announce loading states
723
+ <InputGroup.Root>
724
+ <InputGroup.Element>
725
+ <Spinner size="sm" role="status" aria-label="Searching" />
726
+ </InputGroup.Element>
727
+ <Input placeholder="Search..." />
728
+ </InputGroup.Root>
729
+ ```
730
+
731
+ ## State Behaviors
732
+
733
+ | State | Visual Change | Behavior |
734
+ | ------------------ | -------------------------------------------- | --------------------------- |
735
+ | **Default** | Elements positioned, input ready | Static layout |
736
+ | **Input Focus** | Input receives focus ring | Elements remain in position |
737
+ | **Input Disabled** | All elements show disabled state | No interaction |
738
+ | **With Content** | Dynamic elements appear (e.g., clear button) | Conditional rendering |
739
+
740
+ ## Responsive Considerations
741
+
742
+ ```typescript
743
+ // Mobile-first: Use larger sizes for touch
744
+ <InputGroup.Root size={{ base: 'lg', md: 'md' }}>
745
+ <InputGroup.Element>
746
+ <SearchIcon />
747
+ </InputGroup.Element>
748
+ <Input placeholder="Search..." />
749
+ </InputGroup.Root>
750
+
751
+ // Adaptive sizing
752
+ <InputGroup.Root size={{ base: 'md', lg: 'sm' }}>
753
+ <InputAddon variant="outline">$</InputAddon>
754
+ <Input placeholder="0.00" />
755
+ </InputGroup.Root>
756
+
757
+ // Full width on mobile, constrained on desktop
758
+ <div className={css({ maxWidth: { base: 'full', md: '400px' } })}>
759
+ <InputGroup.Root size="md">
760
+ <InputGroup.Element>
761
+ <SearchIcon />
762
+ </InputGroup.Element>
763
+ <Input placeholder="Search..." />
764
+ </InputGroup.Root>
765
+ </div>
766
+ ```
767
+
768
+ ## Testing
769
+
770
+ ```typescript
771
+ import { render, screen } from '@testing-library/react';
772
+ import userEvent from '@testing-library/user-event';
773
+
774
+ test('input group renders icon element', () => {
775
+ render(
776
+ <InputGroup.Root>
777
+ <InputGroup.Element>
778
+ <SearchIcon data-testid="search-icon" />
779
+ </InputGroup.Element>
780
+ <Input placeholder="Search" />
781
+ </InputGroup.Root>
782
+ );
783
+
784
+ expect(screen.getByTestId('search-icon')).toBeInTheDocument();
785
+ expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
786
+ });
787
+
788
+ test('input group button triggers action', async () => {
789
+ const handleClear = vi.fn();
790
+
791
+ render(
792
+ <InputGroup.Root>
793
+ <Input value="test" />
794
+ <InputGroup.Element>
795
+ <IconButton aria-label="Clear" onClick={handleClear}>
796
+ <XIcon />
797
+ </IconButton>
798
+ </InputGroup.Element>
799
+ </InputGroup.Root>
800
+ );
801
+
802
+ const clearButton = screen.getByLabelText('Clear');
803
+ await userEvent.click(clearButton);
804
+
805
+ expect(handleClear).toHaveBeenCalledOnce();
806
+ });
807
+
808
+ test('input remains focusable with decorative elements', () => {
809
+ render(
810
+ <InputGroup.Root>
811
+ <InputGroup.Element>
812
+ <SearchIcon />
813
+ </InputGroup.Element>
814
+ <Input placeholder="Search" />
815
+ </InputGroup.Root>
816
+ );
817
+
818
+ const input = screen.getByPlaceholderText('Search');
819
+ input.focus();
820
+
821
+ expect(input).toHaveFocus();
822
+ });
823
+ ```
824
+
825
+ ## Related Components
826
+
827
+ - **Input** - Text input field that InputGroup wraps
828
+ - **InputAddon** - Styled addon elements for text/bordered content
829
+ - **IconButton** - For interactive buttons within elements
830
+ - **Spinner** - For loading states in elements