@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,685 @@
1
+ # InputAddon
2
+
3
+ **Purpose:** Decorative element for enhancing input fields with icons, text labels, or buttons following Material Design 3 patterns.
4
+
5
+ ## When to Use This Component
6
+
7
+ Use InputAddon when you need to **add visual context or actions** to input fields (currency symbols, units, search icons, clear buttons).
8
+
9
+ **Decision Tree:**
10
+
11
+ | Scenario | Use This | Why |
12
+ | ------------------------------------------------------- | ---------------------- | ------------------------------------- |
13
+ | Add prefix/suffix text (currency, units, domains) | InputAddon ✅ | Visual context for input values |
14
+ | Add icons to inputs (search, user, email icons) | InputAddon ✅ | Visual affordance for input type |
15
+ | Add action buttons to inputs (clear, visibility toggle) | InputAddon with Button | Interactive enhancements |
16
+ | Standalone input without decoration | Input | No additional context needed |
17
+ | Input with floating label inside | Input | Built-in label feature |
18
+ | Complex input composition with multiple elements | InputGroup | Layout management for multiple addons |
19
+
20
+ **Component Comparison:**
21
+
22
+ ```typescript
23
+ // ✅ Use InputAddon for currency prefix
24
+ <InputGroup.Root size="md">
25
+ <InputAddon variant="outline">$</InputAddon>
26
+ <Input placeholder="0.00" type="number" />
27
+ </InputGroup.Root>
28
+
29
+ // ✅ Use InputAddon for unit suffix
30
+ <InputGroup.Root size="md">
31
+ <Input placeholder="Enter weight" type="number" />
32
+ <InputAddon variant="outline">kg</InputAddon>
33
+ </InputGroup.Root>
34
+
35
+ // ✅ Use InputAddon for icons
36
+ <InputGroup.Root size="md">
37
+ <InputAddon variant="subtle">
38
+ <SearchIcon />
39
+ </InputAddon>
40
+ <Input placeholder="Search..." />
41
+ </InputGroup.Root>
42
+
43
+ // ❌ Don't use InputAddon alone - must be within InputGroup
44
+ <InputAddon>$</InputAddon> // Wrong - needs InputGroup wrapper
45
+ <Input placeholder="Price" />
46
+
47
+ // ❌ Don't use InputAddon for labels - use Input label prop
48
+ <InputGroup.Root>
49
+ <InputAddon>Email</InputAddon> // Wrong - this is a label
50
+ <Input />
51
+ </InputGroup.Root>
52
+
53
+ <Input label="Email" /> // Correct
54
+
55
+ // ❌ Don't use InputAddon for validation messages
56
+ <InputGroup.Root>
57
+ <Input />
58
+ <InputAddon variant="outline">Invalid email</InputAddon> // Wrong
59
+ </InputGroup.Root>
60
+
61
+ <Input errorText="Invalid email" /> // Correct
62
+ ```
63
+
64
+ ## Import
65
+
66
+ ```typescript
67
+ import { InputAddon, InputGroup, Input } from '@discourser/design-system';
68
+ ```
69
+
70
+ ## Component Structure
71
+
72
+ InputAddon must be used within InputGroup for proper positioning:
73
+
74
+ ```typescript
75
+ <InputGroup.Root>
76
+ <InputAddon><!-- prefix content --></InputAddon>
77
+ <Input />
78
+ <InputAddon><!-- suffix content --></InputAddon>
79
+ </InputGroup.Root>
80
+ ```
81
+
82
+ ## Variants
83
+
84
+ The InputAddon component supports 3 Material Design 3 variants:
85
+
86
+ | Variant | Visual Style | Usage | When to Use |
87
+ | --------- | ---------------------------------- | --------------- | -------------------------------- |
88
+ | `outline` | Border with transparent background | Standard addons | Default, matches outlined inputs |
89
+ | `surface` | Surface background with border | Elevated addons | Cards, elevated contexts |
90
+ | `subtle` | Subtle background, no border | Minimal addons | Search bars, minimal UI |
91
+
92
+ ### Visual Characteristics
93
+
94
+ - **outline**: 1px border, transparent background, matches input border color
95
+ - **surface**: Surface background color with subtle border
96
+ - **subtle**: Light gray background, seamless integration with filled inputs
97
+
98
+ ## Sizes
99
+
100
+ | Size | Height | Padding | Icon Size | Usage |
101
+ | ---- | ------ | ------- | --------- | ------------------------------- |
102
+ | `xs` | 32px | 8px | 16px | Extra compact forms, mobile |
103
+ | `sm` | 36px | 10px | 18px | Compact forms, dense layouts |
104
+ | `md` | 40px | 12px | 20px | Default, most use cases |
105
+ | `lg` | 44px | 14px | 20px | Touch targets, prominent inputs |
106
+ | `xl` | 48px | 16px | 22px | Large forms, hero sections |
107
+
108
+ **Recommendation:** Use `md` for most cases. Match the size with your Input component size.
109
+
110
+ ## Props
111
+
112
+ | Prop | Type | Default | Description |
113
+ | ----------- | -------------------------------------- | ----------- | ----------------------------- |
114
+ | `variant` | `'outline' \| 'surface' \| 'subtle'` | `'outline'` | Visual style variant |
115
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Addon size (match with Input) |
116
+ | `children` | `ReactNode` | Required | Content (text, icon, button) |
117
+ | `className` | `string` | - | Additional CSS classes |
118
+
119
+ **Note:** InputAddon extends `HTMLAttributes<HTMLDivElement>`, so all standard HTML div attributes are supported.
120
+
121
+ ## Examples
122
+
123
+ ### Basic Text Addons
124
+
125
+ ```typescript
126
+ import { InputAddon, InputGroup, Input } from '@discourser/design-system';
127
+
128
+ // Currency prefix
129
+ <InputGroup.Root size="md">
130
+ <InputAddon variant="outline">$</InputAddon>
131
+ <Input placeholder="0.00" type="number" />
132
+ </InputGroup.Root>
133
+
134
+ // Domain suffix
135
+ <InputGroup.Root size="md">
136
+ <Input placeholder="username" />
137
+ <InputAddon variant="outline">@example.com</InputAddon>
138
+ </InputGroup.Root>
139
+
140
+ // Unit suffix
141
+ <InputGroup.Root size="md">
142
+ <Input placeholder="Enter distance" type="number" />
143
+ <InputAddon variant="outline">miles</InputAddon>
144
+ </InputGroup.Root>
145
+
146
+ // Both prefix and suffix
147
+ <InputGroup.Root size="md">
148
+ <InputAddon variant="outline">https://</InputAddon>
149
+ <Input placeholder="mywebsite" />
150
+ <InputAddon variant="outline">.com</InputAddon>
151
+ </InputGroup.Root>
152
+ ```
153
+
154
+ ### Icon Addons
155
+
156
+ ```typescript
157
+ import { SearchIcon, UserIcon, LockIcon, CalendarIcon } from 'your-icon-library';
158
+
159
+ // Search icon prefix
160
+ <InputGroup.Root size="md">
161
+ <InputAddon variant="subtle">
162
+ <SearchIcon />
163
+ </InputAddon>
164
+ <Input placeholder="Search products..." />
165
+ </InputGroup.Root>
166
+
167
+ // User icon prefix
168
+ <InputGroup.Root size="md">
169
+ <InputAddon variant="outline">
170
+ <UserIcon />
171
+ </InputAddon>
172
+ <Input placeholder="Username" />
173
+ </InputGroup.Root>
174
+
175
+ // Lock icon for password
176
+ <InputGroup.Root size="md">
177
+ <InputAddon variant="outline">
178
+ <LockIcon />
179
+ </InputAddon>
180
+ <Input type="password" placeholder="Password" />
181
+ </InputGroup.Root>
182
+
183
+ // Calendar icon suffix
184
+ <InputGroup.Root size="md">
185
+ <Input type="date" />
186
+ <InputAddon variant="outline">
187
+ <CalendarIcon />
188
+ </InputAddon>
189
+ </InputGroup.Root>
190
+ ```
191
+
192
+ ### Button Addons
193
+
194
+ ```typescript
195
+ import { IconButton } from '@discourser/design-system';
196
+ import { XIcon, EyeIcon, EyeOffIcon } from 'your-icon-library';
197
+
198
+ // Clear button
199
+ const [value, setValue] = useState('');
200
+
201
+ <InputGroup.Root size="md">
202
+ <Input
203
+ placeholder="Search..."
204
+ value={value}
205
+ onChange={(e) => setValue(e.target.value)}
206
+ />
207
+ {value && (
208
+ <InputAddon variant="subtle">
209
+ <IconButton
210
+ variant="ghost"
211
+ size="sm"
212
+ aria-label="Clear"
213
+ onClick={() => setValue('')}
214
+ >
215
+ <XIcon />
216
+ </IconButton>
217
+ </InputAddon>
218
+ )}
219
+ </InputGroup.Root>
220
+
221
+ // Password visibility toggle
222
+ const [showPassword, setShowPassword] = useState(false);
223
+
224
+ <InputGroup.Root size="md">
225
+ <InputAddon variant="outline">
226
+ <LockIcon />
227
+ </InputAddon>
228
+ <Input
229
+ type={showPassword ? 'text' : 'password'}
230
+ placeholder="Password"
231
+ />
232
+ <InputAddon variant="subtle">
233
+ <IconButton
234
+ variant="ghost"
235
+ size="sm"
236
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
237
+ onClick={() => setShowPassword(!showPassword)}
238
+ >
239
+ {showPassword ? <EyeOffIcon /> : <EyeIcon />}
240
+ </IconButton>
241
+ </InputAddon>
242
+ </InputGroup.Root>
243
+ ```
244
+
245
+ ### Different Variants
246
+
247
+ ```typescript
248
+ // Outline variant (default)
249
+ <InputGroup.Root size="md">
250
+ <InputAddon variant="outline">$</InputAddon>
251
+ <Input placeholder="0.00" />
252
+ </InputGroup.Root>
253
+
254
+ // Surface variant
255
+ <InputGroup.Root size="md">
256
+ <InputAddon variant="surface">
257
+ <SearchIcon />
258
+ </InputAddon>
259
+ <Input placeholder="Search..." />
260
+ </InputGroup.Root>
261
+
262
+ // Subtle variant (seamless)
263
+ <InputGroup.Root size="md">
264
+ <InputAddon variant="subtle">
265
+ <UserIcon />
266
+ </InputAddon>
267
+ <Input placeholder="Username" />
268
+ </InputGroup.Root>
269
+ ```
270
+
271
+ ### Different Sizes
272
+
273
+ ```typescript
274
+ // Extra small
275
+ <InputGroup.Root size="xs">
276
+ <InputAddon size="xs">$</InputAddon>
277
+ <Input size="xs" placeholder="0.00" />
278
+ </InputGroup.Root>
279
+
280
+ // Small
281
+ <InputGroup.Root size="sm">
282
+ <InputAddon size="sm">$</InputAddon>
283
+ <Input size="sm" placeholder="0.00" />
284
+ </InputGroup.Root>
285
+
286
+ // Medium (default)
287
+ <InputGroup.Root size="md">
288
+ <InputAddon size="md">$</InputAddon>
289
+ <Input size="md" placeholder="0.00" />
290
+ </InputGroup.Root>
291
+
292
+ // Large
293
+ <InputGroup.Root size="lg">
294
+ <InputAddon size="lg">$</InputAddon>
295
+ <Input size="lg" placeholder="0.00" />
296
+ </InputGroup.Root>
297
+
298
+ // Extra large
299
+ <InputGroup.Root size="xl">
300
+ <InputAddon size="xl">$</InputAddon>
301
+ <Input size="xl" placeholder="0.00" />
302
+ </InputGroup.Root>
303
+ ```
304
+
305
+ ### Multiple Addons
306
+
307
+ ```typescript
308
+ // Multiple icons
309
+ <InputGroup.Root size="md">
310
+ <InputAddon variant="outline">
311
+ <UserIcon />
312
+ </InputAddon>
313
+ <Input placeholder="Search users..." />
314
+ <InputAddon variant="subtle">
315
+ <SearchIcon />
316
+ </InputAddon>
317
+ </InputGroup.Root>
318
+
319
+ // Mixed content types
320
+ <InputGroup.Root size="md">
321
+ <InputAddon variant="outline">From:</InputAddon>
322
+ <Input type="date" />
323
+ <InputAddon variant="outline">To:</InputAddon>
324
+ <Input type="date" />
325
+ </InputGroup.Root>
326
+ ```
327
+
328
+ ### With Form Labels
329
+
330
+ ```typescript
331
+ // Proper form structure
332
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'xs' })}>
333
+ <label htmlFor="price-input" className={css({ fontWeight: 'medium', textStyle: 'sm' })}>
334
+ Price
335
+ </label>
336
+ <InputGroup.Root size="md">
337
+ <InputAddon variant="outline">$</InputAddon>
338
+ <Input id="price-input" placeholder="0.00" type="number" />
339
+ <InputAddon variant="outline">USD</InputAddon>
340
+ </InputGroup.Root>
341
+ <span className={css({ color: 'fg.muted', textStyle: 'xs' })}>
342
+ Enter the product price in US dollars
343
+ </span>
344
+ </div>
345
+ ```
346
+
347
+ ### Loading State
348
+
349
+ ```typescript
350
+ import { Spinner } from '@discourser/design-system';
351
+
352
+ const [isSearching, setIsSearching] = useState(false);
353
+
354
+ <InputGroup.Root size="md">
355
+ <InputAddon variant="subtle">
356
+ {isSearching ? (
357
+ <Spinner size="sm" />
358
+ ) : (
359
+ <SearchIcon />
360
+ )}
361
+ </InputAddon>
362
+ <Input placeholder="Search..." />
363
+ </InputGroup.Root>
364
+ ```
365
+
366
+ ## Common Patterns
367
+
368
+ ### Currency Input
369
+
370
+ ```typescript
371
+ const [amount, setAmount] = useState('');
372
+
373
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'xs' })}>
374
+ <label htmlFor="amount" className={css({ fontWeight: 'medium' })}>
375
+ Amount
376
+ </label>
377
+ <InputGroup.Root size="md">
378
+ <InputAddon variant="outline">$</InputAddon>
379
+ <Input
380
+ id="amount"
381
+ type="number"
382
+ placeholder="0.00"
383
+ value={amount}
384
+ onChange={(e) => setAmount(e.target.value)}
385
+ min="0"
386
+ step="0.01"
387
+ />
388
+ <InputAddon variant="outline">USD</InputAddon>
389
+ </InputGroup.Root>
390
+ </div>
391
+ ```
392
+
393
+ ### Search Bar
394
+
395
+ ```typescript
396
+ const [searchQuery, setSearchQuery] = useState('');
397
+
398
+ <InputGroup.Root size="md">
399
+ <InputAddon variant="subtle">
400
+ <SearchIcon />
401
+ </InputAddon>
402
+ <Input
403
+ placeholder="Search products, categories, brands..."
404
+ value={searchQuery}
405
+ onChange={(e) => setSearchQuery(e.target.value)}
406
+ />
407
+ {searchQuery && (
408
+ <InputAddon variant="subtle">
409
+ <IconButton
410
+ variant="ghost"
411
+ size="sm"
412
+ aria-label="Clear search"
413
+ onClick={() => setSearchQuery('')}
414
+ >
415
+ <XIcon />
416
+ </IconButton>
417
+ </InputAddon>
418
+ )}
419
+ </InputGroup.Root>
420
+ ```
421
+
422
+ ### Website URL Input
423
+
424
+ ```typescript
425
+ const [url, setUrl] = useState('');
426
+
427
+ <InputGroup.Root size="md">
428
+ <InputAddon variant="outline">https://</InputAddon>
429
+ <Input
430
+ placeholder="example.com"
431
+ value={url}
432
+ onChange={(e) => setUrl(e.target.value)}
433
+ />
434
+ </InputGroup.Root>
435
+ ```
436
+
437
+ ### Email Input with Domain
438
+
439
+ ```typescript
440
+ const [username, setUsername] = useState('');
441
+
442
+ <InputGroup.Root size="md">
443
+ <InputAddon variant="outline">
444
+ <MailIcon />
445
+ </InputAddon>
446
+ <Input
447
+ placeholder="username"
448
+ value={username}
449
+ onChange={(e) => setUsername(e.target.value)}
450
+ />
451
+ <InputAddon variant="outline">@company.com</InputAddon>
452
+ </InputGroup.Root>
453
+ ```
454
+
455
+ ### Phone Number Input
456
+
457
+ ```typescript
458
+ const [phone, setPhone] = useState('');
459
+
460
+ <InputGroup.Root size="md">
461
+ <InputAddon variant="outline">
462
+ <PhoneIcon />
463
+ </InputAddon>
464
+ <InputAddon variant="outline">+1</InputAddon>
465
+ <Input
466
+ placeholder="(555) 000-0000"
467
+ type="tel"
468
+ value={phone}
469
+ onChange={(e) => setPhone(e.target.value)}
470
+ />
471
+ </InputGroup.Root>
472
+ ```
473
+
474
+ ## DO NOT
475
+
476
+ ```typescript
477
+ // ❌ Don't use InputAddon without InputGroup
478
+ <InputAddon>$</InputAddon>
479
+ <Input placeholder="Price" /> // Wrong - InputAddon must be inside InputGroup
480
+
481
+ // ✅ Wrap both in InputGroup
482
+ <InputGroup.Root>
483
+ <InputAddon>$</InputAddon>
484
+ <Input placeholder="Price" />
485
+ </InputGroup.Root>
486
+
487
+ // ❌ Don't mismatch sizes
488
+ <InputGroup.Root size="md">
489
+ <InputAddon size="lg">$</InputAddon> // Wrong - size mismatch
490
+ <Input size="md" placeholder="0.00" />
491
+ </InputGroup.Root>
492
+
493
+ // ✅ Match sizes
494
+ <InputGroup.Root size="md">
495
+ <InputAddon size="md">$</InputAddon>
496
+ <Input size="md" placeholder="0.00" />
497
+ </InputGroup.Root>
498
+
499
+ // ❌ Don't use InputAddon for labels
500
+ <InputGroup.Root>
501
+ <InputAddon>Email Address</InputAddon> // Wrong - this is a label
502
+ <Input />
503
+ </InputGroup.Root>
504
+
505
+ // ✅ Use proper label element
506
+ <div>
507
+ <label>Email Address</label>
508
+ <InputGroup.Root>
509
+ <InputAddon><MailIcon /></InputAddon>
510
+ <Input />
511
+ </InputGroup.Root>
512
+ </div>
513
+
514
+ // ❌ Don't override colors with inline styles
515
+ <InputAddon style={{ backgroundColor: 'red' }}>$</InputAddon>
516
+
517
+ // ✅ Use variants
518
+ <InputAddon variant="surface">$</InputAddon>
519
+
520
+ // ❌ Don't put too much content in addon
521
+ <InputAddon variant="outline">
522
+ This is way too much text for an input addon
523
+ </InputAddon> // Wrong - keeps addons concise
524
+
525
+ // ✅ Keep content short
526
+ <InputAddon variant="outline">USD</InputAddon>
527
+ ```
528
+
529
+ ## Accessibility
530
+
531
+ The InputAddon component follows WCAG 2.1 Level AA standards:
532
+
533
+ - **Color Contrast**: All variants meet 4.5:1 contrast ratio for text
534
+ - **Icon Accessibility**: Icons are decorative when paired with input context
535
+ - **Interactive Elements**: Buttons within addons have proper labels
536
+ - **Visual Association**: Addons are visually grouped with inputs
537
+ - **Touch Targets**: Minimum 44x44px for interactive addons (size md or larger)
538
+
539
+ ### Accessibility Best Practices
540
+
541
+ ```typescript
542
+ // ✅ Provide aria-label for icon-only buttons in addons
543
+ <InputGroup.Root>
544
+ <Input placeholder="Search..." />
545
+ <InputAddon variant="subtle">
546
+ <IconButton
547
+ variant="ghost"
548
+ size="sm"
549
+ aria-label="Clear search"
550
+ onClick={handleClear}
551
+ >
552
+ <XIcon />
553
+ </IconButton>
554
+ </InputAddon>
555
+ </InputGroup.Root>
556
+
557
+ // ✅ Use descriptive labels for inputs with addons
558
+ <div>
559
+ <label htmlFor="price">Price in USD</label>
560
+ <InputGroup.Root>
561
+ <InputAddon>$</InputAddon>
562
+ <Input id="price" type="number" />
563
+ </InputGroup.Root>
564
+ </div>
565
+
566
+ // ✅ Provide context for screen readers
567
+ <InputGroup.Root>
568
+ <InputAddon aria-label="Currency: US Dollar">$</InputAddon>
569
+ <Input
570
+ placeholder="0.00"
571
+ aria-label="Enter amount in US dollars"
572
+ />
573
+ </InputGroup.Root>
574
+
575
+ // ✅ Ensure interactive addons are keyboard accessible
576
+ <InputGroup.Root>
577
+ <Input type="password" />
578
+ <InputAddon>
579
+ <IconButton
580
+ variant="ghost"
581
+ aria-label="Toggle password visibility"
582
+ onClick={toggleVisibility}
583
+ tabIndex={0} // Keyboard accessible
584
+ >
585
+ <EyeIcon />
586
+ </IconButton>
587
+ </InputAddon>
588
+ </InputGroup.Root>
589
+ ```
590
+
591
+ ## Variant Selection Guide
592
+
593
+ | Scenario | Recommended Variant | Reasoning |
594
+ | ----------------------- | ------------------- | ----------------------------- |
595
+ | Standard forms | `outline` | Matches outlined input style |
596
+ | Search bars | `subtle` | Minimal, seamless integration |
597
+ | Currency/unit labels | `outline` | Clear visual separation |
598
+ | Icon indicators | `subtle` | Less emphasis, decorative |
599
+ | Elevated surfaces/cards | `surface` | Matches surface context |
600
+ | Interactive buttons | `subtle` | Reduces visual weight |
601
+
602
+ ## State Behaviors
603
+
604
+ | State | Visual Change | Behavior |
605
+ | ------------------------ | ----------------------- | ---------------------------- |
606
+ | **Default** | Matches variant styling | Static decoration |
607
+ | **With Input Focus** | No change | Addon remains static |
608
+ | **Interactive (Button)** | Hover/focus states | Button states apply |
609
+ | **Disabled Input** | Reduced opacity | Matches input disabled state |
610
+
611
+ ## Responsive Considerations
612
+
613
+ ```typescript
614
+ // Mobile-first: Use larger sizes for touch
615
+ <InputGroup.Root size={{ base: 'lg', md: 'md' }}>
616
+ <InputAddon size={{ base: 'lg', md: 'md' }}>$</InputAddon>
617
+ <Input size={{ base: 'lg', md: 'md' }} />
618
+ </InputGroup.Root>
619
+
620
+ // Compact on desktop, comfortable on mobile
621
+ <InputGroup.Root size={{ base: 'md', lg: 'sm' }}>
622
+ <InputAddon size={{ base: 'md', lg: 'sm' }}>
623
+ <SearchIcon />
624
+ </InputAddon>
625
+ <Input size={{ base: 'md', lg: 'sm' }} placeholder="Search..." />
626
+ </InputGroup.Root>
627
+ ```
628
+
629
+ ## Testing
630
+
631
+ ```typescript
632
+ import { render, screen } from '@testing-library/react';
633
+ import userEvent from '@testing-library/user-event';
634
+
635
+ test('addon displays text content', () => {
636
+ render(
637
+ <InputGroup.Root>
638
+ <InputAddon>$</InputAddon>
639
+ <Input placeholder="Amount" />
640
+ </InputGroup.Root>
641
+ );
642
+
643
+ expect(screen.getByText('$')).toBeInTheDocument();
644
+ });
645
+
646
+ test('addon button triggers action', async () => {
647
+ const handleClear = vi.fn();
648
+
649
+ render(
650
+ <InputGroup.Root>
651
+ <Input value="test" />
652
+ <InputAddon>
653
+ <IconButton aria-label="Clear" onClick={handleClear}>
654
+ <XIcon />
655
+ </IconButton>
656
+ </InputAddon>
657
+ </InputGroup.Root>
658
+ );
659
+
660
+ const clearButton = screen.getByLabelText('Clear');
661
+ await userEvent.click(clearButton);
662
+
663
+ expect(handleClear).toHaveBeenCalledOnce();
664
+ });
665
+
666
+ test('addon icon is visible', () => {
667
+ render(
668
+ <InputGroup.Root>
669
+ <InputAddon>
670
+ <SearchIcon data-testid="search-icon" />
671
+ </InputAddon>
672
+ <Input />
673
+ </InputGroup.Root>
674
+ );
675
+
676
+ expect(screen.getByTestId('search-icon')).toBeInTheDocument();
677
+ });
678
+ ```
679
+
680
+ ## Related Components
681
+
682
+ - **InputGroup** - Required wrapper for positioning addons
683
+ - **Input** - Text input field that addons enhance
684
+ - **IconButton** - For interactive buttons within addons
685
+ - **Spinner** - For loading states in addons