@discourser/design-system 0.3.1 → 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 +92 -41
  4. package/guidelines/components/accordion.md +732 -0
  5. package/guidelines/components/avatar.md +1015 -0
  6. package/guidelines/components/badge.md +728 -0
  7. package/guidelines/components/button.md +75 -40
  8. package/guidelines/components/card.md +84 -25
  9. package/guidelines/components/checkbox.md +671 -0
  10. package/guidelines/components/dialog.md +619 -31
  11. package/guidelines/components/drawer.md +1616 -0
  12. package/guidelines/components/heading.md +576 -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 +1271 -0
  18. package/guidelines/components/progress.md +836 -0
  19. package/guidelines/components/radio-group.md +852 -0
  20. package/guidelines/components/select.md +1662 -0
  21. package/guidelines/components/skeleton.md +802 -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 +1488 -0
  26. package/guidelines/components/textarea.md +495 -0
  27. package/guidelines/components/toast.md +784 -0
  28. package/guidelines/components/tooltip.md +912 -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 +60 -8
  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,495 @@
1
+ # Textarea
2
+
3
+ **Purpose:** Multi-line text input field for longer user input following Material Design 3 patterns.
4
+
5
+ ## When to Use This Component
6
+
7
+ Use Textarea when you need **multi-line text entry** where users may need to write paragraphs, enter line breaks, or provide longer content.
8
+
9
+ **Decision Tree:**
10
+
11
+ | Scenario | Use This | Why |
12
+ | ------------------------------------------------------- | ---------------------------- | ---------------------------------------- |
13
+ | Multi-line text (comments, descriptions, messages, bio) | Textarea ✅ | Allows line breaks, expandable content |
14
+ | Single-line text (name, email, username) | Input | More compact, better UX for short text |
15
+ | Select from predefined options | Select | Prevents typos, faster than typing |
16
+ | Rich text editing (bold, italic, links) | Rich Text Editor | Textarea is plain text only |
17
+ | Code input | Textarea with monospace font | Or use specialized code editor component |
18
+
19
+ **Component Comparison:**
20
+
21
+ ```typescript
22
+ // ✅ Use Textarea for multi-line content
23
+ <Textarea
24
+ label="Comment"
25
+ placeholder="Share your thoughts..."
26
+ rows={4}
27
+ />
28
+
29
+ <Textarea
30
+ label="Bio"
31
+ placeholder="Tell us about yourself"
32
+ rows={6}
33
+ />
34
+
35
+ // ❌ Don't use Textarea for single-line text - use Input
36
+ <Textarea
37
+ label="Username"
38
+ rows={1}
39
+ /> // Wrong - single-line input should use Input
40
+
41
+ <Input
42
+ label="Username"
43
+ /> // Correct
44
+
45
+ // ❌ Don't use Textarea when options are predefined - use Select
46
+ <Textarea
47
+ label="Country"
48
+ placeholder="Enter your country"
49
+ /> // Wrong - prone to typos and inconsistent data
50
+
51
+ <Select.Root collection={countries}>
52
+ <Select.Label>Country</Select.Label>
53
+ <Select.Control>
54
+ <Select.Trigger>
55
+ <Select.ValueText placeholder="Select your country" />
56
+ </Select.Trigger>
57
+ </Select.Control>
58
+ <Select.Content>
59
+ {/* country options */}
60
+ </Select.Content>
61
+ </Select.Root> // Correct
62
+
63
+ // ✅ Use Textarea for longer content that needs line breaks
64
+ <form onSubmit={handleSubmit}>
65
+ <Input label="Title" />
66
+ <Textarea
67
+ label="Description"
68
+ rows={5}
69
+ placeholder="Provide a detailed description..."
70
+ />
71
+ <Button type="submit">Submit</Button>
72
+ </form>
73
+ ```
74
+
75
+ ## Import
76
+
77
+ ```typescript
78
+ import { Textarea } from '@discourser/design-system';
79
+ ```
80
+
81
+ ## Variants
82
+
83
+ The Textarea component supports 4 visual variants:
84
+
85
+ | Variant | Visual Style | Usage | When to Use |
86
+ | --------- | ---------------------------------- | -------------------- | ------------------------------- |
87
+ | `surface` | Subtle background with border | Default text areas | Forms, comments, descriptions |
88
+ | `outline` | Transparent background with border | Prominent text areas | Key inputs that need emphasis |
89
+ | `subtle` | Light background, minimal border | Low-emphasis inputs | Secondary content, notes |
90
+ | `flushed` | Bottom border only, no background | Inline text areas | Minimal designs, embedded forms |
91
+
92
+ ### Visual Characteristics
93
+
94
+ - **surface**: Light surface background, thin border, focus ring inside
95
+ - **outline**: No background, thin outline border, focus ring inside
96
+ - **subtle**: Subtle gray background, transparent border until focus
97
+ - **flushed**: No background, bottom border only, minimal appearance
98
+
99
+ ## Sizes
100
+
101
+ | Size | Text Style | Padding | Usage |
102
+ | ---- | ---------- | ----------------------------- | --------------------------- |
103
+ | `xs` | sm | 8px vertical, 8px horizontal | Compact UI, inline comments |
104
+ | `sm` | sm | 8px vertical, 10px horizontal | Dense forms, small dialogs |
105
+ | `md` | md | 8px vertical, 12px horizontal | Default, most use cases |
106
+ | `lg` | md | 8px vertical, 14px horizontal | Emphasized inputs, mobile |
107
+ | `xl` | lg | 8px vertical, 16px horizontal | Large content areas, essays |
108
+
109
+ **Recommendation:** Use `md` for most cases. Use `lg` or `xl` for longer content like descriptions or essays.
110
+
111
+ ## Props
112
+
113
+ | Prop | Type | Default | Description |
114
+ | -------------- | ------------------------------------------------- | ----------- | --------------------------------- |
115
+ | `variant` | `'surface' \| 'outline' \| 'subtle' \| 'flushed'` | `'surface'` | Visual style variant |
116
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Textarea size |
117
+ | `rows` | `number` | `3` | Number of visible text lines |
118
+ | `value` | `string` | - | Controlled value |
119
+ | `defaultValue` | `string` | - | Uncontrolled default value |
120
+ | `placeholder` | `string` | - | Placeholder text (use with label) |
121
+ | `disabled` | `boolean` | `false` | Disable interaction |
122
+ | `readOnly` | `boolean` | `false` | Make read-only |
123
+ | `required` | `boolean` | `false` | Mark as required field |
124
+ | `onChange` | `(event: ChangeEvent) => void` | - | Change handler |
125
+ | `onBlur` | `(event: FocusEvent) => void` | - | Blur handler |
126
+ | `className` | `string` | - | Additional CSS classes |
127
+
128
+ **Note:** Textarea extends `TextareaHTMLAttributes<HTMLTextAreaElement>`, so all standard HTML textarea attributes are supported.
129
+
130
+ ## Examples
131
+
132
+ ### Basic Usage
133
+
134
+ ```typescript
135
+ // Default textarea
136
+ <Textarea placeholder="Enter your message..." />
137
+
138
+ // Outlined variant
139
+ <Textarea variant="outline" placeholder="Description" />
140
+
141
+ // Subtle variant (low emphasis)
142
+ <Textarea variant="subtle" placeholder="Additional notes" />
143
+
144
+ // Flushed variant (minimal)
145
+ <Textarea variant="flushed" placeholder="Comment" />
146
+ ```
147
+
148
+ ### With Field Label
149
+
150
+ Textarea should always be used with a label for accessibility:
151
+
152
+ ```typescript
153
+ import { Field } from '@discourser/design-system';
154
+
155
+ <Field.Root>
156
+ <Field.Label>Message</Field.Label>
157
+ <Textarea placeholder="Enter your message..." />
158
+ <Field.HelperText>Maximum 500 characters</Field.HelperText>
159
+ </Field.Root>
160
+ ```
161
+
162
+ ### Controlled Textarea
163
+
164
+ ```typescript
165
+ const [message, setMessage] = useState('');
166
+
167
+ <Field.Root>
168
+ <Field.Label>Feedback</Field.Label>
169
+ <Textarea
170
+ value={message}
171
+ onChange={(e) => setMessage(e.target.value)}
172
+ rows={5}
173
+ />
174
+ <Field.HelperText>{message.length} / 500</Field.HelperText>
175
+ </Field.Root>
176
+ ```
177
+
178
+ ### Different Sizes
179
+
180
+ ```typescript
181
+ // Extra small (compact)
182
+ <Textarea size="xs" rows={2} placeholder="Quick note" />
183
+
184
+ // Small
185
+ <Textarea size="sm" rows={3} placeholder="Short comment" />
186
+
187
+ // Medium (default)
188
+ <Textarea size="md" rows={4} placeholder="Standard message" />
189
+
190
+ // Large
191
+ <Textarea size="lg" rows={5} placeholder="Detailed description" />
192
+
193
+ // Extra large
194
+ <Textarea size="xl" rows={6} placeholder="Long-form content" />
195
+ ```
196
+
197
+ ### Custom Row Height
198
+
199
+ ```typescript
200
+ // 3 rows (default)
201
+ <Textarea placeholder="Short message" />
202
+
203
+ // 10 rows for longer content
204
+ <Textarea rows={10} placeholder="Essay or long-form content" />
205
+
206
+ // Auto-resize with CSS
207
+ <Textarea
208
+ style={{ minHeight: '100px', resize: 'vertical' }}
209
+ placeholder="Resizable textarea"
210
+ />
211
+ ```
212
+
213
+ ### With Validation
214
+
215
+ ```typescript
216
+ import { Field } from '@discourser/design-system';
217
+
218
+ const [bio, setBio] = useState('');
219
+ const [error, setError] = useState('');
220
+
221
+ const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
222
+ const value = e.target.value;
223
+ setBio(value);
224
+
225
+ if (value.length > 500) {
226
+ setError('Bio must be 500 characters or less');
227
+ } else {
228
+ setError('');
229
+ }
230
+ };
231
+
232
+ <Field.Root invalid={!!error}>
233
+ <Field.Label>Bio</Field.Label>
234
+ <Textarea
235
+ value={bio}
236
+ onChange={handleChange}
237
+ rows={5}
238
+ />
239
+ {error ? (
240
+ <Field.ErrorText>{error}</Field.ErrorText>
241
+ ) : (
242
+ <Field.HelperText>{bio.length} / 500 characters</Field.HelperText>
243
+ )}
244
+ </Field.Root>
245
+ ```
246
+
247
+ ### Disabled and Read-only States
248
+
249
+ ```typescript
250
+ // Disabled textarea
251
+ <Textarea disabled placeholder="Cannot edit" value="Disabled content" />
252
+
253
+ // Read-only textarea (can select/copy text)
254
+ <Textarea readOnly value="Read-only content that can be selected" rows={3} />
255
+ ```
256
+
257
+ ### Required Field
258
+
259
+ ```typescript
260
+ <Field.Root>
261
+ <Field.Label>
262
+ Comments <span style={{ color: 'red' }}>*</span>
263
+ </Field.Label>
264
+ <Textarea required placeholder="Required field..." />
265
+ <Field.HelperText>This field is required</Field.HelperText>
266
+ </Field.Root>
267
+ ```
268
+
269
+ ## Common Patterns
270
+
271
+ ### Comment Input
272
+
273
+ ```typescript
274
+ const [comment, setComment] = useState('');
275
+ const [isSubmitting, setIsSubmitting] = useState(false);
276
+
277
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
278
+ <Field.Root>
279
+ <Field.Label>Add Comment</Field.Label>
280
+ <Textarea
281
+ value={comment}
282
+ onChange={(e) => setComment(e.target.value)}
283
+ placeholder="Share your thoughts..."
284
+ rows={4}
285
+ />
286
+ </Field.Root>
287
+
288
+ <div className={css({ display: 'flex', gap: 'sm', justifyContent: 'flex-end' })}>
289
+ <Button variant="text" onClick={() => setComment('')}>Cancel</Button>
290
+ <Button
291
+ variant="filled"
292
+ disabled={!comment.trim() || isSubmitting}
293
+ onClick={handleSubmit}
294
+ >
295
+ Post Comment
296
+ </Button>
297
+ </div>
298
+ </div>
299
+ ```
300
+
301
+ ### Feedback Form
302
+
303
+ ```typescript
304
+ const [feedback, setFeedback] = useState('');
305
+ const charLimit = 1000;
306
+ const remaining = charLimit - feedback.length;
307
+
308
+ <Field.Root invalid={remaining < 0}>
309
+ <Field.Label>Feedback</Field.Label>
310
+ <Textarea
311
+ value={feedback}
312
+ onChange={(e) => setFeedback(e.target.value)}
313
+ rows={8}
314
+ size="lg"
315
+ placeholder="Tell us what you think..."
316
+ />
317
+ <Field.HelperText>
318
+ {remaining >= 0
319
+ ? `${remaining} characters remaining`
320
+ : `${Math.abs(remaining)} characters over limit`
321
+ }
322
+ </Field.HelperText>
323
+ </Field.Root>
324
+ ```
325
+
326
+ ### Description Field
327
+
328
+ ```typescript
329
+ <Field.Root>
330
+ <Field.Label>Product Description</Field.Label>
331
+ <Textarea
332
+ variant="outline"
333
+ size="lg"
334
+ rows={6}
335
+ placeholder="Describe your product in detail..."
336
+ />
337
+ <Field.HelperText>
338
+ Include key features, benefits, and specifications
339
+ </Field.HelperText>
340
+ </Field.Root>
341
+ ```
342
+
343
+ ## DO NOT
344
+
345
+ ```typescript
346
+ // ❌ Don't use native textarea element
347
+ <textarea className="..." placeholder="Message" /> // Use <Textarea> instead
348
+
349
+ // ❌ Don't use textarea without a label (accessibility issue)
350
+ <Textarea placeholder="Enter feedback" /> // Always use with Field.Label
351
+
352
+ // ❌ Don't use placeholder as the only label
353
+ <Textarea placeholder="Email Address" /> // Placeholder is not a label
354
+
355
+ // ❌ Don't make textarea too small for expected content
356
+ <Textarea rows={1} /> // For short input, use Input component instead
357
+
358
+ // ❌ Don't use inline styles for colors
359
+ <Textarea style={{ backgroundColor: 'blue', color: 'white' }} /> // Use variants
360
+
361
+ // ✅ Use Textarea with proper Field structure
362
+ <Field.Root>
363
+ <Field.Label>Feedback</Field.Label>
364
+ <Textarea placeholder="Share your thoughts..." rows={5} />
365
+ </Field.Root>
366
+ ```
367
+
368
+ ## Accessibility
369
+
370
+ The Textarea component follows WCAG 2.1 Level AA standards:
371
+
372
+ - **Keyboard Navigation**: Full keyboard support (Tab, arrow keys, text selection)
373
+ - **Focus Indicator**: Clear focus ring on focus-visible
374
+ - **Disabled State**: Uses `disabled` attribute, grayed out appearance
375
+ - **Screen Readers**: Works with Field.Label for proper labeling
376
+ - **Touch Targets**: Minimum 44x44px with size md or larger
377
+ - **Color Contrast**: All variants meet 4.5:1 contrast ratio
378
+
379
+ ### Accessibility Best Practices
380
+
381
+ ```typescript
382
+ // ✅ Always provide a label
383
+ <Field.Root>
384
+ <Field.Label>Message</Field.Label>
385
+ <Textarea />
386
+ </Field.Root>
387
+
388
+ // ✅ Use helper text for guidance
389
+ <Field.Root>
390
+ <Field.Label>Bio</Field.Label>
391
+ <Textarea />
392
+ <Field.HelperText>Tell us about yourself (max 500 characters)</Field.HelperText>
393
+ </Field.Root>
394
+
395
+ // ✅ Show validation errors clearly
396
+ <Field.Root invalid={hasError}>
397
+ <Field.Label>Feedback</Field.Label>
398
+ <Textarea />
399
+ <Field.ErrorText>Feedback is required</Field.ErrorText>
400
+ </Field.Root>
401
+
402
+ // ✅ Mark required fields
403
+ <Field.Root>
404
+ <Field.Label>
405
+ Comments <span aria-label="required">*</span>
406
+ </Field.Label>
407
+ <Textarea required />
408
+ </Field.Root>
409
+
410
+ // ✅ Provide character count for limits
411
+ <Field.Root>
412
+ <Field.Label>Tweet</Field.Label>
413
+ <Textarea />
414
+ <Field.HelperText aria-live="polite">
415
+ {280 - text.length} characters remaining
416
+ </Field.HelperText>
417
+ </Field.Root>
418
+ ```
419
+
420
+ ## Variant Selection Guide
421
+
422
+ | Scenario | Recommended Variant | Reasoning |
423
+ | ------------------------- | ------------------------ | ----------------------------------------------- |
424
+ | Form comments | `surface` | Default, good contrast with page background |
425
+ | Primary description field | `outline` | Emphasized, clear boundaries |
426
+ | Additional notes/metadata | `subtle` | Low emphasis, doesn't compete with main content |
427
+ | Inline editing | `flushed` | Minimal appearance, feels embedded in content |
428
+ | Feedback forms | `surface` or `outline` | Clear, prominent for important user input |
429
+ | Profile bio | `outline` with `lg` size | Prominent, allows longer content |
430
+ | Quick notes | `subtle` with `sm` size | Compact, unobtrusive |
431
+
432
+ ## State Behaviors
433
+
434
+ | State | Visual Change | Behavior |
435
+ | ------------- | -------------------------------- | -------------------------------------- |
436
+ | **Hover** | Border color changes | Subtle indication of interactivity |
437
+ | **Focus** | Focus ring appears | Clear focus indication (inside border) |
438
+ | **Invalid** | Red border | Error state, shows validation issue |
439
+ | **Disabled** | Grayed out, reduced opacity | Cannot be edited or focused |
440
+ | **Read-only** | Normal appearance, no focus ring | Can select/copy text, but not edit |
441
+
442
+ ## Responsive Considerations
443
+
444
+ ```typescript
445
+ // Mobile-first: Larger textarea for touch
446
+ <Textarea size="lg" rows={5} />
447
+
448
+ // Desktop: Can use smaller sizes
449
+ <Textarea size={{ base: 'lg', md: 'md' }} rows={4} />
450
+
451
+ // Responsive rows
452
+ <Textarea rows={{ base: 6, md: 4 }} />
453
+ ```
454
+
455
+ ## Testing
456
+
457
+ When testing Textarea components:
458
+
459
+ ```typescript
460
+ import { render, screen } from '@testing-library/react';
461
+ import userEvent from '@testing-library/user-event';
462
+
463
+ test('textarea accepts user input', async () => {
464
+ const handleChange = vi.fn();
465
+ render(
466
+ <Field.Root>
467
+ <Field.Label>Message</Field.Label>
468
+ <Textarea onChange={handleChange} />
469
+ </Field.Root>
470
+ );
471
+
472
+ const textarea = screen.getByLabelText('Message');
473
+ await userEvent.type(textarea, 'Hello world');
474
+
475
+ expect(textarea).toHaveValue('Hello world');
476
+ expect(handleChange).toHaveBeenCalled();
477
+ });
478
+
479
+ test('disabled textarea cannot be edited', async () => {
480
+ render(<Textarea disabled value="Cannot edit" />);
481
+
482
+ const textarea = screen.getByDisplayValue('Cannot edit');
483
+ await userEvent.type(textarea, 'Try to type');
484
+
485
+ expect(textarea).toHaveValue('Cannot edit');
486
+ expect(textarea).toBeDisabled();
487
+ });
488
+ ```
489
+
490
+ ## Related Components
491
+
492
+ - **Input** - For single-line text input
493
+ - **Field** - For adding labels, helper text, and error messages
494
+ - **Select** - For choosing from predefined options
495
+ - **Button** - For form submission actions