@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,3852 @@
1
+ # Common UI Patterns
2
+
3
+ This guide demonstrates common patterns for combining components from the Discourser Design System to create complete user interfaces. These patterns help developers and AI tools understand how to build real-world UIs using the design system.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Form Patterns](#form-patterns)
8
+ - [Vertical Form (Default)](#vertical-form-default)
9
+ - [Horizontal Form (Compact)](#horizontal-form-compact)
10
+ - [Multi-Step Form](#multi-step-form)
11
+ - [Form with Inline Validation](#form-with-inline-validation)
12
+ - [Form with Field Dependencies](#form-with-field-dependencies)
13
+ - [Navigation Patterns](#navigation-patterns)
14
+ - [Sidebar Navigation](#sidebar-navigation)
15
+ - [Top Navigation Bar](#top-navigation-bar)
16
+ - [Tabbed Interface](#tabbed-interface)
17
+ - [Breadcrumb Navigation](#breadcrumb-navigation)
18
+ - [Feedback Patterns](#feedback-patterns)
19
+ - [Success Flow](#success-flow)
20
+ - [Error Handling](#error-handling)
21
+ - [Confirmation Dialogs](#confirmation-dialogs)
22
+ - [Inline Notifications](#inline-notifications)
23
+ - [Loading States](#loading-states)
24
+ - [Page Load](#page-load)
25
+ - [Partial Load (Section)](#partial-load-section)
26
+ - [Button Loading State](#button-loading-state)
27
+ - [Infinite Scroll](#infinite-scroll)
28
+ - [Data Display Patterns](#data-display-patterns)
29
+ - [Card Grid](#card-grid)
30
+ - [List with Actions](#list-with-actions)
31
+ - [List with Avatar](#list-with-avatar)
32
+ - [Expandable/Collapsible List](#expandablecollapsible-list)
33
+ - [Search & Filter Patterns](#search--filter-patterns)
34
+ - [Search Bar (Simple)](#search-bar-simple)
35
+ - [Search with Filters](#search-with-filters)
36
+ - [Search with Results](#search-with-results)
37
+ - [Authentication Patterns](#authentication-patterns)
38
+ - [Login Form](#login-form)
39
+ - [Sign Up Form](#sign-up-form)
40
+ - [Password Reset Flow](#password-reset-flow)
41
+ - [Settings Patterns](#settings-patterns)
42
+ - [Settings Panel](#settings-panel)
43
+ - [Profile Settings](#profile-settings)
44
+ - [Empty States](#empty-states)
45
+ - [No Data](#no-data)
46
+ - [No Search Results](#no-search-results)
47
+
48
+ ---
49
+
50
+ ## Form Patterns
51
+
52
+ ### Vertical Form (Default)
53
+
54
+ **When to use:** Standard form layout for most use cases. Provides clear hierarchy and is mobile-friendly.
55
+
56
+ **Components used:** Input, Textarea, Select, Button, Toast
57
+
58
+ **Example:**
59
+
60
+ ```typescript
61
+ import { Input, Textarea, Button, toaster } from '@discourser/design-system';
62
+ import * as Select from '@discourser/design-system';
63
+ import { css } from '@discourser/design-system/styled-system/css';
64
+ import { useState, FormEvent } from 'react';
65
+
66
+ function ContactForm() {
67
+ const [loading, setLoading] = useState(false);
68
+ const [formData, setFormData] = useState({
69
+ name: '',
70
+ email: '',
71
+ subject: '',
72
+ message: ''
73
+ });
74
+
75
+ const handleSubmit = async (e: FormEvent) => {
76
+ e.preventDefault();
77
+ setLoading(true);
78
+
79
+ try {
80
+ // Submit form data
81
+ await submitForm(formData);
82
+
83
+ toaster.create({
84
+ title: 'Message sent!',
85
+ description: "We'll get back to you soon.",
86
+ type: 'success'
87
+ });
88
+
89
+ // Reset form
90
+ setFormData({ name: '', email: '', subject: '', message: '' });
91
+ } catch (error) {
92
+ toaster.create({
93
+ title: 'Failed to send',
94
+ description: 'Please try again later.',
95
+ type: 'error'
96
+ });
97
+ } finally {
98
+ setLoading(false);
99
+ }
100
+ };
101
+
102
+ return (
103
+ <form
104
+ onSubmit={handleSubmit}
105
+ className={css({
106
+ display: 'flex',
107
+ flexDirection: 'column',
108
+ gap: 'lg', // Spacing - 24px between fields
109
+ maxWidth: '500px',
110
+ mx: 'auto',
111
+ p: 'xl' // Spacing - 32px padding
112
+ })}
113
+ >
114
+ <Input
115
+ label="Name"
116
+ value={formData.name}
117
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
118
+ required
119
+ />
120
+
121
+ <Input
122
+ label="Email"
123
+ type="email"
124
+ value={formData.email}
125
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
126
+ required
127
+ />
128
+
129
+ <Input
130
+ label="Subject"
131
+ value={formData.subject}
132
+ onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
133
+ required
134
+ />
135
+
136
+ <Textarea
137
+ label="Message"
138
+ value={formData.message}
139
+ onChange={(e) => setFormData({ ...formData, message: e.target.value })}
140
+ rows={5}
141
+ required
142
+ />
143
+
144
+ <Button
145
+ type="submit"
146
+ variant="filled"
147
+ disabled={loading}
148
+ >
149
+ {loading ? 'Sending...' : 'Send Message'}
150
+ </Button>
151
+ </form>
152
+ );
153
+ }
154
+ ```
155
+
156
+ **Best practices:**
157
+
158
+ - Use `gap` for consistent spacing between form fields
159
+ - Always include labels for accessibility
160
+ - Provide loading states for async submissions
161
+ - Use Toast for success/error feedback
162
+ - Make forms responsive with `maxWidth`
163
+
164
+ **Accessibility:**
165
+
166
+ - All inputs have labels
167
+ - Submit button has clear text
168
+ - Loading state is communicated
169
+ - Form can be submitted with Enter key
170
+
171
+ ---
172
+
173
+ ### Horizontal Form (Compact)
174
+
175
+ **When to use:** Space-constrained layouts, filters, or inline editing where vertical space is limited.
176
+
177
+ **Components used:** Input, Button
178
+
179
+ **Example:**
180
+
181
+ ```typescript
182
+ import { Input, Button } from '@discourser/design-system';
183
+ import { css } from '@discourser/design-system/styled-system/css';
184
+ import { useState } from 'react';
185
+
186
+ function InlineSearchForm() {
187
+ const [query, setQuery] = useState('');
188
+
189
+ return (
190
+ <form
191
+ className={css({
192
+ display: 'flex',
193
+ gap: 'md', // Spacing - 16px between elements
194
+ alignItems: 'flex-end', // Align button with input
195
+ flexWrap: 'wrap' // Stack on mobile if needed
196
+ })}
197
+ onSubmit={(e) => {
198
+ e.preventDefault();
199
+ handleSearch(query);
200
+ }}
201
+ >
202
+ <div className={css({ flex: 1, minWidth: '200px' })}>
203
+ <Input
204
+ label="Search query"
205
+ value={query}
206
+ onChange={(e) => setQuery(e.target.value)}
207
+ placeholder="Enter keywords..."
208
+ size="sm"
209
+ />
210
+ </div>
211
+
212
+ <Button
213
+ type="submit"
214
+ variant="filled"
215
+ size="sm"
216
+ >
217
+ Search
218
+ </Button>
219
+ </form>
220
+ );
221
+ }
222
+ ```
223
+
224
+ **Best practices:**
225
+
226
+ - Use `alignItems: 'flex-end'` to align button with input baseline
227
+ - Add `flexWrap: 'wrap'` for mobile responsiveness
228
+ - Keep labels visible (don't rely only on placeholders)
229
+ - Use `size="sm"` for more compact layouts
230
+
231
+ **Accessibility:**
232
+
233
+ - Labels are present even in horizontal layout
234
+ - Tab order follows visual order
235
+ - Works with keyboard navigation
236
+
237
+ ---
238
+
239
+ ### Multi-Step Form
240
+
241
+ **When to use:** Complex forms with many fields that benefit from being broken into logical sections.
242
+
243
+ **Components used:** Tabs, Input, Button, Progress
244
+
245
+ **Example:**
246
+
247
+ ```typescript
248
+ import { Input, Textarea, Button, toaster } from '@discourser/design-system';
249
+ import * as Tabs from '@discourser/design-system';
250
+ import * as Progress from '@discourser/design-system';
251
+ import { css } from '@discourser/design-system/styled-system/css';
252
+ import { useState } from 'react';
253
+
254
+ function MultiStepForm() {
255
+ const [currentStep, setCurrentStep] = useState(0);
256
+ const [formData, setFormData] = useState({
257
+ // Step 1: Personal info
258
+ name: '',
259
+ email: '',
260
+ // Step 2: Company info
261
+ company: '',
262
+ role: '',
263
+ // Step 3: Preferences
264
+ interests: ''
265
+ });
266
+
267
+ const steps = ['Personal Info', 'Company Info', 'Preferences'];
268
+ const progress = ((currentStep + 1) / steps.length) * 100;
269
+
270
+ const handleNext = () => {
271
+ if (currentStep < steps.length - 1) {
272
+ setCurrentStep(currentStep + 1);
273
+ }
274
+ };
275
+
276
+ const handleBack = () => {
277
+ if (currentStep > 0) {
278
+ setCurrentStep(currentStep - 1);
279
+ }
280
+ };
281
+
282
+ const handleSubmit = async () => {
283
+ try {
284
+ await submitForm(formData);
285
+ toaster.create({
286
+ title: 'Registration complete!',
287
+ description: 'Your account has been created.',
288
+ type: 'success'
289
+ });
290
+ } catch (error) {
291
+ toaster.create({
292
+ title: 'Registration failed',
293
+ description: 'Please try again.',
294
+ type: 'error'
295
+ });
296
+ }
297
+ };
298
+
299
+ return (
300
+ <div className={css({ maxWidth: '600px', mx: 'auto', p: 'xl' })}>
301
+ {/* Progress indicator */}
302
+ <div className={css({ mb: 'xl' })}>
303
+ <Progress.Root value={progress}>
304
+ <Progress.Track>
305
+ <Progress.Range />
306
+ </Progress.Track>
307
+ </Progress.Root>
308
+ <div className={css({
309
+ textStyle: 'labelMedium',
310
+ color: 'onSurfaceVariant',
311
+ mt: 'xs'
312
+ })}>
313
+ Step {currentStep + 1} of {steps.length}: {steps[currentStep]}
314
+ </div>
315
+ </div>
316
+
317
+ {/* Form content */}
318
+ <div className={css({
319
+ display: 'flex',
320
+ flexDirection: 'column',
321
+ gap: 'lg',
322
+ mb: 'xl'
323
+ })}>
324
+ {currentStep === 0 && (
325
+ <>
326
+ <Input
327
+ label="Full Name"
328
+ value={formData.name}
329
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
330
+ required
331
+ />
332
+ <Input
333
+ label="Email"
334
+ type="email"
335
+ value={formData.email}
336
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
337
+ required
338
+ />
339
+ </>
340
+ )}
341
+
342
+ {currentStep === 1 && (
343
+ <>
344
+ <Input
345
+ label="Company Name"
346
+ value={formData.company}
347
+ onChange={(e) => setFormData({ ...formData, company: e.target.value })}
348
+ required
349
+ />
350
+ <Input
351
+ label="Your Role"
352
+ value={formData.role}
353
+ onChange={(e) => setFormData({ ...formData, role: e.target.value })}
354
+ required
355
+ />
356
+ </>
357
+ )}
358
+
359
+ {currentStep === 2 && (
360
+ <Textarea
361
+ label="What are you interested in?"
362
+ value={formData.interests}
363
+ onChange={(e) => setFormData({ ...formData, interests: e.target.value })}
364
+ rows={5}
365
+ />
366
+ )}
367
+ </div>
368
+
369
+ {/* Navigation buttons */}
370
+ <div className={css({
371
+ display: 'flex',
372
+ gap: 'sm',
373
+ justifyContent: 'space-between'
374
+ })}>
375
+ <Button
376
+ variant="outlined"
377
+ onClick={handleBack}
378
+ disabled={currentStep === 0}
379
+ >
380
+ Back
381
+ </Button>
382
+
383
+ {currentStep < steps.length - 1 ? (
384
+ <Button variant="filled" onClick={handleNext}>
385
+ Next
386
+ </Button>
387
+ ) : (
388
+ <Button variant="filled" onClick={handleSubmit}>
389
+ Submit
390
+ </Button>
391
+ )}
392
+ </div>
393
+ </div>
394
+ );
395
+ }
396
+ ```
397
+
398
+ **Best practices:**
399
+
400
+ - Show progress indicator to communicate position
401
+ - Validate each step before allowing next
402
+ - Allow users to go back to previous steps
403
+ - Save progress automatically if possible
404
+ - Use clear step labels
405
+
406
+ **Accessibility:**
407
+
408
+ - Progress indicator announces current step
409
+ - Navigation buttons have clear labels
410
+ - Keyboard navigation works between steps
411
+
412
+ ---
413
+
414
+ ### Form with Inline Validation
415
+
416
+ **When to use:** Forms where immediate feedback helps users correct errors while filling out the form.
417
+
418
+ **Components used:** Input, Button, Toast
419
+
420
+ **Example:**
421
+
422
+ ```typescript
423
+ import { Input, Button, toaster } from '@discourser/design-system';
424
+ import { css } from '@discourser/design-system/styled-system/css';
425
+ import { useState } from 'react';
426
+
427
+ function ValidatedForm() {
428
+ const [formData, setFormData] = useState({
429
+ username: '',
430
+ email: '',
431
+ password: '',
432
+ confirmPassword: ''
433
+ });
434
+
435
+ const [errors, setErrors] = useState<Record<string, string>>({});
436
+ const [touched, setTouched] = useState<Record<string, boolean>>({});
437
+
438
+ // Validation functions
439
+ const validateUsername = (value: string) => {
440
+ if (!value) return 'Username is required';
441
+ if (value.length < 3) return 'Username must be at least 3 characters';
442
+ if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Username can only contain letters, numbers, and underscores';
443
+ return '';
444
+ };
445
+
446
+ const validateEmail = (value: string) => {
447
+ if (!value) return 'Email is required';
448
+ if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
449
+ return 'Invalid email address';
450
+ }
451
+ return '';
452
+ };
453
+
454
+ const validatePassword = (value: string) => {
455
+ if (!value) return 'Password is required';
456
+ if (value.length < 8) return 'Password must be at least 8 characters';
457
+ if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
458
+ return 'Password must contain uppercase, lowercase, and number';
459
+ }
460
+ return '';
461
+ };
462
+
463
+ const validateConfirmPassword = (value: string) => {
464
+ if (!value) return 'Please confirm your password';
465
+ if (value !== formData.password) return 'Passwords do not match';
466
+ return '';
467
+ };
468
+
469
+ const handleBlur = (field: string) => {
470
+ setTouched({ ...touched, [field]: true });
471
+
472
+ let error = '';
473
+ switch (field) {
474
+ case 'username':
475
+ error = validateUsername(formData.username);
476
+ break;
477
+ case 'email':
478
+ error = validateEmail(formData.email);
479
+ break;
480
+ case 'password':
481
+ error = validatePassword(formData.password);
482
+ break;
483
+ case 'confirmPassword':
484
+ error = validateConfirmPassword(formData.confirmPassword);
485
+ break;
486
+ }
487
+
488
+ setErrors({ ...errors, [field]: error });
489
+ };
490
+
491
+ const handleSubmit = async (e: React.FormEvent) => {
492
+ e.preventDefault();
493
+
494
+ // Validate all fields
495
+ const newErrors = {
496
+ username: validateUsername(formData.username),
497
+ email: validateEmail(formData.email),
498
+ password: validatePassword(formData.password),
499
+ confirmPassword: validateConfirmPassword(formData.confirmPassword)
500
+ };
501
+
502
+ setErrors(newErrors);
503
+ setTouched({ username: true, email: true, password: true, confirmPassword: true });
504
+
505
+ const hasErrors = Object.values(newErrors).some(error => error !== '');
506
+
507
+ if (!hasErrors) {
508
+ try {
509
+ await registerUser(formData);
510
+ toaster.create({
511
+ title: 'Account created!',
512
+ description: 'Welcome to our platform.',
513
+ type: 'success'
514
+ });
515
+ } catch (error) {
516
+ toaster.create({
517
+ title: 'Registration failed',
518
+ description: 'Please try again.',
519
+ type: 'error'
520
+ });
521
+ }
522
+ }
523
+ };
524
+
525
+ return (
526
+ <form onSubmit={handleSubmit} className={css({
527
+ display: 'flex',
528
+ flexDirection: 'column',
529
+ gap: 'lg',
530
+ maxWidth: '400px',
531
+ mx: 'auto',
532
+ p: 'xl'
533
+ })}>
534
+ <Input
535
+ label="Username"
536
+ value={formData.username}
537
+ onChange={(e) => setFormData({ ...formData, username: e.target.value })}
538
+ onBlur={() => handleBlur('username')}
539
+ errorText={touched.username ? errors.username : ''}
540
+ required
541
+ />
542
+
543
+ <Input
544
+ label="Email"
545
+ type="email"
546
+ value={formData.email}
547
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
548
+ onBlur={() => handleBlur('email')}
549
+ errorText={touched.email ? errors.email : ''}
550
+ required
551
+ />
552
+
553
+ <Input
554
+ label="Password"
555
+ type="password"
556
+ value={formData.password}
557
+ onChange={(e) => setFormData({ ...formData, password: e.target.value })}
558
+ onBlur={() => handleBlur('password')}
559
+ errorText={touched.password ? errors.password : ''}
560
+ helperText="Must be 8+ characters with uppercase, lowercase, and number"
561
+ required
562
+ />
563
+
564
+ <Input
565
+ label="Confirm Password"
566
+ type="password"
567
+ value={formData.confirmPassword}
568
+ onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
569
+ onBlur={() => handleBlur('confirmPassword')}
570
+ errorText={touched.confirmPassword ? errors.confirmPassword : ''}
571
+ required
572
+ />
573
+
574
+ <Button type="submit" variant="filled">
575
+ Create Account
576
+ </Button>
577
+ </form>
578
+ );
579
+ }
580
+ ```
581
+
582
+ **Best practices:**
583
+
584
+ - Validate on blur, not on every keystroke
585
+ - Show errors only after user leaves field
586
+ - Provide helpful error messages
587
+ - Include helper text for complex requirements
588
+ - Validate all fields on submit
589
+
590
+ **Accessibility:**
591
+
592
+ - Error messages are announced to screen readers
593
+ - Errors are associated with inputs via ARIA
594
+ - Helper text provides guidance upfront
595
+
596
+ ---
597
+
598
+ ### Form with Field Dependencies
599
+
600
+ **When to use:** Forms where certain fields only appear or are required based on previous selections.
601
+
602
+ **Components used:** Input, Select, RadioGroup, Checkbox
603
+
604
+ **Example:**
605
+
606
+ ```typescript
607
+ import { Input, Button } from '@discourser/design-system';
608
+ import * as Select from '@discourser/design-system';
609
+ import * as RadioGroup from '@discourser/design-system';
610
+ import * as Checkbox from '@discourser/design-system';
611
+ import { css } from '@discourser/design-system/styled-system/css';
612
+ import { useState } from 'react';
613
+ import { createListCollection } from '@ark-ui/react';
614
+
615
+ function DependentFieldsForm() {
616
+ const [accountType, setAccountType] = useState('personal');
617
+ const [hasCompany, setHasCompany] = useState(false);
618
+ const [receiveNewsletter, setReceiveNewsletter] = useState(false);
619
+
620
+ const accountTypes = createListCollection({
621
+ items: [
622
+ { label: 'Personal', value: 'personal' },
623
+ { label: 'Business', value: 'business' }
624
+ ]
625
+ });
626
+
627
+ return (
628
+ <form className={css({
629
+ display: 'flex',
630
+ flexDirection: 'column',
631
+ gap: 'lg',
632
+ maxWidth: '500px',
633
+ mx: 'auto',
634
+ p: 'xl'
635
+ })}>
636
+ {/* Account type selection */}
637
+ <RadioGroup.Root
638
+ value={accountType}
639
+ onValueChange={(details) => setAccountType(details.value)}
640
+ >
641
+ <RadioGroup.Label>Account Type</RadioGroup.Label>
642
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'sm' })}>
643
+ <RadioGroup.Item value="personal">
644
+ <RadioGroup.ItemControl />
645
+ <RadioGroup.ItemText>Personal</RadioGroup.ItemText>
646
+ </RadioGroup.Item>
647
+ <RadioGroup.Item value="business">
648
+ <RadioGroup.ItemControl />
649
+ <RadioGroup.ItemText>Business</RadioGroup.ItemText>
650
+ </RadioGroup.Item>
651
+ </div>
652
+ </RadioGroup.Root>
653
+
654
+ {/* Show company field only for business accounts */}
655
+ {accountType === 'business' && (
656
+ <>
657
+ <Input
658
+ label="Company Name"
659
+ required
660
+ />
661
+ <Input
662
+ label="Tax ID"
663
+ required
664
+ />
665
+ </>
666
+ )}
667
+
668
+ {/* Standard fields */}
669
+ <Input
670
+ label="Full Name"
671
+ required
672
+ />
673
+
674
+ <Input
675
+ label="Email"
676
+ type="email"
677
+ required
678
+ />
679
+
680
+ {/* Newsletter subscription */}
681
+ <Checkbox.Root
682
+ checked={receiveNewsletter}
683
+ onCheckedChange={(details) => setReceiveNewsletter(details.checked === true)}
684
+ >
685
+ <Checkbox.Control>
686
+ <Checkbox.Indicator>✓</Checkbox.Indicator>
687
+ </Checkbox.Control>
688
+ <Checkbox.Label>Subscribe to newsletter</Checkbox.Label>
689
+ </Checkbox.Root>
690
+
691
+ {/* Show frequency only if subscribed */}
692
+ {receiveNewsletter && (
693
+ <Select.Root collection={createListCollection({
694
+ items: [
695
+ { label: 'Daily', value: 'daily' },
696
+ { label: 'Weekly', value: 'weekly' },
697
+ { label: 'Monthly', value: 'monthly' }
698
+ ]
699
+ })}>
700
+ <Select.Label>Newsletter Frequency</Select.Label>
701
+ <Select.Control>
702
+ <Select.Trigger>
703
+ <Select.ValueText placeholder="Select frequency" />
704
+ </Select.Trigger>
705
+ </Select.Control>
706
+ <Select.Positioner>
707
+ <Select.Content>
708
+ {accountTypes.items.map((item) => (
709
+ <Select.Item key={item.value} item={item}>
710
+ <Select.ItemText>{item.label}</Select.ItemText>
711
+ </Select.Item>
712
+ ))}
713
+ </Select.Content>
714
+ </Select.Positioner>
715
+ </Select.Root>
716
+ )}
717
+
718
+ <Button type="submit" variant="filled">
719
+ Create Account
720
+ </Button>
721
+ </form>
722
+ );
723
+ }
724
+ ```
725
+
726
+ **Best practices:**
727
+
728
+ - Clear dependent fields when parent changes
729
+ - Use smooth transitions when showing/hiding fields
730
+ - Validate dependent fields only when visible
731
+ - Provide clear context for conditional fields
732
+
733
+ **Accessibility:**
734
+
735
+ - ARIA live regions announce field changes
736
+ - Focus management when fields appear
737
+ - Clear relationship between parent and dependent fields
738
+
739
+ ---
740
+
741
+ ## Navigation Patterns
742
+
743
+ ### Sidebar Navigation
744
+
745
+ **When to use:** Applications with multiple primary sections that need persistent navigation.
746
+
747
+ **Components used:** Drawer, IconButton, Button
748
+
749
+ **Example:**
750
+
751
+ ```typescript
752
+ import { Button } from '@discourser/design-system';
753
+ import * as Drawer from '@discourser/design-system';
754
+ import * as IconButton from '@discourser/design-system';
755
+ import { css } from '@discourser/design-system/styled-system/css';
756
+ import { useState } from 'react';
757
+ import { HomeIcon, ProjectsIcon, SettingsIcon, MenuIcon } from 'your-icon-library';
758
+
759
+ function SidebarNavigation() {
760
+ const [open, setOpen] = useState(false);
761
+ const [activeSection, setActiveSection] = useState('home');
762
+
763
+ const navItems = [
764
+ { id: 'home', label: 'Home', icon: HomeIcon },
765
+ { id: 'projects', label: 'Projects', icon: ProjectsIcon },
766
+ { id: 'settings', label: 'Settings', icon: SettingsIcon }
767
+ ];
768
+
769
+ return (
770
+ <>
771
+ {/* Mobile menu button */}
772
+ <IconButton.Root
773
+ variant="standard"
774
+ onClick={() => setOpen(true)}
775
+ className={css({ position: 'fixed', top: 'md', left: 'md', zIndex: 10 })}
776
+ >
777
+ <MenuIcon />
778
+ </IconButton.Root>
779
+
780
+ {/* Drawer */}
781
+ <Drawer.Root open={open} onOpenChange={(e) => setOpen(e.open)} placement="start">
782
+ <Drawer.Backdrop />
783
+ <Drawer.Positioner>
784
+ <Drawer.Content className={css({ width: '280px' })}>
785
+ <Drawer.Header>
786
+ <Drawer.Title>Menu</Drawer.Title>
787
+ <Drawer.CloseTrigger asChild>
788
+ <IconButton.Root variant="standard" size="sm">
789
+
790
+ </IconButton.Root>
791
+ </Drawer.CloseTrigger>
792
+ </Drawer.Header>
793
+
794
+ <Drawer.Body className={css({
795
+ display: 'flex',
796
+ flexDirection: 'column',
797
+ gap: 'xs',
798
+ pt: 'md'
799
+ })}>
800
+ {navItems.map((item) => (
801
+ <Button
802
+ key={item.id}
803
+ variant={activeSection === item.id ? 'tonal' : 'text'}
804
+ onClick={() => {
805
+ setActiveSection(item.id);
806
+ setOpen(false);
807
+ }}
808
+ leftIcon={<item.icon />}
809
+ className={css({
810
+ justifyContent: 'flex-start',
811
+ width: '100%'
812
+ })}
813
+ >
814
+ {item.label}
815
+ </Button>
816
+ ))}
817
+ </Drawer.Body>
818
+
819
+ <Drawer.Footer className={css({
820
+ borderTopWidth: '1px',
821
+ borderTopColor: 'outlineVariant',
822
+ pt: 'md'
823
+ })}>
824
+ <Button variant="outlined" className={css({ width: '100%' })}>
825
+ Sign Out
826
+ </Button>
827
+ </Drawer.Footer>
828
+ </Drawer.Content>
829
+ </Drawer.Positioner>
830
+ </Drawer.Root>
831
+ </>
832
+ );
833
+ }
834
+ ```
835
+
836
+ **Best practices:**
837
+
838
+ - Use icons with labels for clarity
839
+ - Highlight active section
840
+ - Close drawer on mobile after selection
841
+ - Provide sign out option in footer
842
+ - Keep navigation items organized
843
+
844
+ **Accessibility:**
845
+
846
+ - Drawer traps focus when open
847
+ - Escape key closes drawer
848
+ - Active item is clearly indicated
849
+ - Screen readers announce drawer state
850
+
851
+ ---
852
+
853
+ ### Top Navigation Bar
854
+
855
+ **When to use:** Simple websites or apps with few primary sections that fit horizontally.
856
+
857
+ **Components used:** Button, IconButton, Avatar
858
+
859
+ **Example:**
860
+
861
+ ```typescript
862
+ import { Button } from '@discourser/design-system';
863
+ import * as IconButton from '@discourser/design-system';
864
+ import * as Avatar from '@discourser/design-system';
865
+ import { css } from '@discourser/design-system/styled-system/css';
866
+
867
+ function TopNavigation() {
868
+ const navItems = ['Home', 'Products', 'About', 'Contact'];
869
+
870
+ return (
871
+ <nav className={css({
872
+ display: 'flex',
873
+ alignItems: 'center',
874
+ justifyContent: 'space-between',
875
+ px: { base: 'md', lg: 'xl' },
876
+ py: 'md',
877
+ bg: 'surface',
878
+ borderBottomWidth: '1px',
879
+ borderBottomColor: 'outlineVariant',
880
+ position: 'sticky',
881
+ top: 0,
882
+ zIndex: 100
883
+ })}>
884
+ {/* Logo */}
885
+ <div className={css({
886
+ textStyle: 'titleLarge',
887
+ color: 'primary',
888
+ fontWeight: 'bold'
889
+ })}>
890
+ Brand
891
+ </div>
892
+
893
+ {/* Navigation links - hidden on mobile */}
894
+ <div className={css({
895
+ display: { base: 'none', md: 'flex' },
896
+ gap: 'sm',
897
+ alignItems: 'center'
898
+ })}>
899
+ {navItems.map((item) => (
900
+ <Button key={item} variant="text">
901
+ {item}
902
+ </Button>
903
+ ))}
904
+ </div>
905
+
906
+ {/* User actions */}
907
+ <div className={css({ display: 'flex', gap: 'sm', alignItems: 'center' })}>
908
+ <IconButton.Root variant="standard">
909
+ <SearchIcon />
910
+ </IconButton.Root>
911
+
912
+ <Avatar.Root size="sm">
913
+ <Avatar.Image src="/user-avatar.jpg" alt="User" />
914
+ <Avatar.Fallback>JD</Avatar.Fallback>
915
+ </Avatar.Root>
916
+ </div>
917
+ </nav>
918
+ );
919
+ }
920
+ ```
921
+
922
+ **Best practices:**
923
+
924
+ - Keep navigation items concise
925
+ - Use sticky positioning for accessibility
926
+ - Hide secondary items on mobile
927
+ - Include logo/brand on left
928
+ - Group user actions on right
929
+
930
+ **Accessibility:**
931
+
932
+ - Use semantic `nav` element
933
+ - Links have clear focus indicators
934
+ - Works with keyboard navigation
935
+
936
+ ---
937
+
938
+ ### Tabbed Interface
939
+
940
+ **When to use:** Content that can be organized into distinct categories or views.
941
+
942
+ **Components used:** Tabs, Card
943
+
944
+ **Example:**
945
+
946
+ ```typescript
947
+ import { Card } from '@discourser/design-system';
948
+ import * as Tabs from '@discourser/design-system';
949
+ import { css } from '@discourser/design-system/styled-system/css';
950
+
951
+ function TabbedInterface() {
952
+ return (
953
+ <Tabs.Root defaultValue="overview" className={css({ maxWidth: '800px', mx: 'auto' })}>
954
+ <Tabs.List className={css({ mb: 'lg' })}>
955
+ <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
956
+ <Tabs.Trigger value="activity">Activity</Tabs.Trigger>
957
+ <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
958
+ <Tabs.Indicator />
959
+ </Tabs.List>
960
+
961
+ <Tabs.Content value="overview">
962
+ <Card variant="elevated" className={css({ p: 'xl' })}>
963
+ <h2 className={css({ textStyle: 'headlineSmall', mb: 'md' })}>
964
+ Overview
965
+ </h2>
966
+ <p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
967
+ Welcome to your dashboard. Here you'll find a summary of your account activity.
968
+ </p>
969
+ </Card>
970
+ </Tabs.Content>
971
+
972
+ <Tabs.Content value="activity">
973
+ <Card variant="elevated" className={css({ p: 'xl' })}>
974
+ <h2 className={css({ textStyle: 'headlineSmall', mb: 'md' })}>
975
+ Recent Activity
976
+ </h2>
977
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
978
+ <div className={css({ textStyle: 'bodyMedium' })}>
979
+ Project updated - 2 hours ago
980
+ </div>
981
+ <div className={css({ textStyle: 'bodyMedium' })}>
982
+ Comment added - 5 hours ago
983
+ </div>
984
+ </div>
985
+ </Card>
986
+ </Tabs.Content>
987
+
988
+ <Tabs.Content value="settings">
989
+ <Card variant="elevated" className={css({ p: 'xl' })}>
990
+ <h2 className={css({ textStyle: 'headlineSmall', mb: 'md' })}>
991
+ Settings
992
+ </h2>
993
+ <p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
994
+ Configure your account preferences.
995
+ </p>
996
+ </Card>
997
+ </Tabs.Content>
998
+ </Tabs.Root>
999
+ );
1000
+ }
1001
+ ```
1002
+
1003
+ **Best practices:**
1004
+
1005
+ - Use for 3-6 related content sections
1006
+ - Keep tab labels short and clear
1007
+ - Load content lazily if expensive
1008
+ - Indicate active tab clearly
1009
+
1010
+ **Accessibility:**
1011
+
1012
+ - Tabs follow WAI-ARIA pattern
1013
+ - Arrow keys navigate between tabs
1014
+ - Active tab is indicated to screen readers
1015
+
1016
+ ---
1017
+
1018
+ ### Breadcrumb Navigation
1019
+
1020
+ **When to use:** Deep hierarchical navigation to show user's location and allow easy backtracking.
1021
+
1022
+ **Components used:** Button (text variant)
1023
+
1024
+ **Example:**
1025
+
1026
+ ```typescript
1027
+ import { Button } from '@discourser/design-system';
1028
+ import { css } from '@discourser/design-system/styled-system/css';
1029
+ import { ChevronRightIcon } from 'your-icon-library';
1030
+
1031
+ function BreadcrumbNavigation() {
1032
+ const breadcrumbs = [
1033
+ { label: 'Home', href: '/' },
1034
+ { label: 'Products', href: '/products' },
1035
+ { label: 'Electronics', href: '/products/electronics' },
1036
+ { label: 'Laptops', href: '/products/electronics/laptops' }
1037
+ ];
1038
+
1039
+ return (
1040
+ <nav
1041
+ aria-label="Breadcrumb"
1042
+ className={css({
1043
+ display: 'flex',
1044
+ alignItems: 'center',
1045
+ flexWrap: 'wrap',
1046
+ gap: 'xs',
1047
+ py: 'md',
1048
+ px: { base: 'md', lg: 'xl' }
1049
+ })}
1050
+ >
1051
+ {breadcrumbs.map((crumb, index) => (
1052
+ <div key={crumb.href} className={css({ display: 'flex', alignItems: 'center', gap: 'xs' })}>
1053
+ {index === breadcrumbs.length - 1 ? (
1054
+ // Current page - not clickable
1055
+ <span className={css({
1056
+ textStyle: 'labelMedium',
1057
+ color: 'onSurface',
1058
+ fontWeight: 'medium'
1059
+ })}>
1060
+ {crumb.label}
1061
+ </span>
1062
+ ) : (
1063
+ <>
1064
+ <Button
1065
+ variant="text"
1066
+ size="sm"
1067
+ onClick={() => window.location.href = crumb.href}
1068
+ className={css({ minWidth: 'auto' })}
1069
+ >
1070
+ {crumb.label}
1071
+ </Button>
1072
+ <ChevronRightIcon className={css({ color: 'onSurfaceVariant' })} />
1073
+ </>
1074
+ )}
1075
+ </div>
1076
+ ))}
1077
+ </nav>
1078
+ );
1079
+ }
1080
+ ```
1081
+
1082
+ **Best practices:**
1083
+
1084
+ - Show full path to current page
1085
+ - Make all items except current clickable
1086
+ - Use chevron or slash as separator
1087
+ - Truncate on mobile if too long
1088
+
1089
+ **Accessibility:**
1090
+
1091
+ - Use `nav` with `aria-label="Breadcrumb"`
1092
+ - Current page is not a link
1093
+ - Screen readers understand hierarchy
1094
+
1095
+ ---
1096
+
1097
+ ## Feedback Patterns
1098
+
1099
+ ### Success Flow
1100
+
1101
+ **When to use:** Confirming successful completion of user actions with visual feedback.
1102
+
1103
+ **Components used:** Toast, Progress, Button
1104
+
1105
+ **Example:**
1106
+
1107
+ ```typescript
1108
+ import { Button, toaster } from '@discourser/design-system';
1109
+ import * as Progress from '@discourser/design-system';
1110
+ import { css } from '@discourser/design-system/styled-system/css';
1111
+ import { useState } from 'react';
1112
+
1113
+ function SuccessFlow() {
1114
+ const [uploading, setUploading] = useState(false);
1115
+ const [progress, setProgress] = useState(0);
1116
+
1117
+ const handleUpload = async () => {
1118
+ setUploading(true);
1119
+ setProgress(0);
1120
+
1121
+ // Show loading toast
1122
+ const toastId = toaster.create({
1123
+ title: 'Uploading file...',
1124
+ description: 'Please wait while we process your file.',
1125
+ type: 'loading'
1126
+ });
1127
+
1128
+ // Simulate upload progress
1129
+ const interval = setInterval(() => {
1130
+ setProgress((prev) => {
1131
+ if (prev >= 100) {
1132
+ clearInterval(interval);
1133
+ return 100;
1134
+ }
1135
+ return prev + 10;
1136
+ });
1137
+ }, 300);
1138
+
1139
+ // Wait for completion
1140
+ await new Promise((resolve) => setTimeout(resolve, 3000));
1141
+
1142
+ // Update to success
1143
+ toaster.update(toastId, {
1144
+ title: 'Upload complete!',
1145
+ description: 'Your file has been processed successfully.',
1146
+ type: 'success',
1147
+ duration: 3000
1148
+ });
1149
+
1150
+ setUploading(false);
1151
+ setProgress(0);
1152
+ };
1153
+
1154
+ return (
1155
+ <div className={css({
1156
+ display: 'flex',
1157
+ flexDirection: 'column',
1158
+ gap: 'lg',
1159
+ maxWidth: '400px',
1160
+ mx: 'auto',
1161
+ p: 'xl'
1162
+ })}>
1163
+ <Button
1164
+ variant="filled"
1165
+ onClick={handleUpload}
1166
+ disabled={uploading}
1167
+ >
1168
+ {uploading ? 'Uploading...' : 'Upload File'}
1169
+ </Button>
1170
+
1171
+ {uploading && (
1172
+ <div>
1173
+ <Progress.Root value={progress}>
1174
+ <Progress.Track>
1175
+ <Progress.Range />
1176
+ </Progress.Track>
1177
+ </Progress.Root>
1178
+ <div className={css({
1179
+ textStyle: 'labelMedium',
1180
+ color: 'onSurfaceVariant',
1181
+ mt: 'xs'
1182
+ })}>
1183
+ {progress}% complete
1184
+ </div>
1185
+ </div>
1186
+ )}
1187
+ </div>
1188
+ );
1189
+ }
1190
+ ```
1191
+
1192
+ **Best practices:**
1193
+
1194
+ - Show loading state during async operations
1195
+ - Update with progress if available
1196
+ - Transition from loading to success smoothly
1197
+ - Provide clear success confirmation
1198
+ - Auto-dismiss success messages
1199
+
1200
+ **Accessibility:**
1201
+
1202
+ - Progress is announced to screen readers
1203
+ - Loading state is clearly communicated
1204
+ - Success message is announced
1205
+
1206
+ ---
1207
+
1208
+ ### Error Handling
1209
+
1210
+ **When to use:** Gracefully handling and communicating errors to users.
1211
+
1212
+ **Components used:** Input, Toast, Dialog
1213
+
1214
+ **Example:**
1215
+
1216
+ ```typescript
1217
+ import { Input, Button, toaster } from '@discourser/design-system';
1218
+ import * as Dialog from '@discourser/design-system';
1219
+ import { css } from '@discourser/design-system/styled-system/css';
1220
+ import { useState } from 'react';
1221
+
1222
+ function ErrorHandling() {
1223
+ const [email, setEmail] = useState('');
1224
+ const [error, setError] = useState('');
1225
+ const [criticalError, setCriticalError] = useState<string | null>(null);
1226
+
1227
+ const handleSubmit = async (e: React.FormEvent) => {
1228
+ e.preventDefault();
1229
+ setError('');
1230
+
1231
+ try {
1232
+ await submitEmail(email);
1233
+
1234
+ // Show success
1235
+ toaster.create({
1236
+ title: 'Email sent!',
1237
+ description: 'Check your inbox for the confirmation link.',
1238
+ type: 'success'
1239
+ });
1240
+ } catch (err) {
1241
+ if (err.code === 'NETWORK_ERROR') {
1242
+ // Critical error - show dialog
1243
+ setCriticalError('Unable to connect to the server. Please check your internet connection and try again.');
1244
+ } else if (err.code === 'INVALID_EMAIL') {
1245
+ // Field-level error
1246
+ setError('This email address is not valid.');
1247
+ } else {
1248
+ // General error - show toast
1249
+ toaster.create({
1250
+ title: 'Something went wrong',
1251
+ description: 'Please try again later.',
1252
+ type: 'error',
1253
+ duration: 5000
1254
+ });
1255
+ }
1256
+ }
1257
+ };
1258
+
1259
+ return (
1260
+ <>
1261
+ <form onSubmit={handleSubmit} className={css({
1262
+ display: 'flex',
1263
+ flexDirection: 'column',
1264
+ gap: 'lg',
1265
+ maxWidth: '400px',
1266
+ mx: 'auto',
1267
+ p: 'xl'
1268
+ })}>
1269
+ <Input
1270
+ label="Email Address"
1271
+ type="email"
1272
+ value={email}
1273
+ onChange={(e) => {
1274
+ setEmail(e.target.value);
1275
+ setError(''); // Clear error on change
1276
+ }}
1277
+ errorText={error}
1278
+ required
1279
+ />
1280
+
1281
+ <Button type="submit" variant="filled">
1282
+ Submit
1283
+ </Button>
1284
+ </form>
1285
+
1286
+ {/* Critical error dialog */}
1287
+ <Dialog.Root open={!!criticalError} onOpenChange={() => setCriticalError(null)}>
1288
+ <Dialog.Backdrop />
1289
+ <Dialog.Positioner>
1290
+ <Dialog.Content>
1291
+ <Dialog.Header>
1292
+ <Dialog.Title>Connection Error</Dialog.Title>
1293
+ </Dialog.Header>
1294
+ <Dialog.Body>
1295
+ <Dialog.Description>
1296
+ {criticalError}
1297
+ </Dialog.Description>
1298
+ </Dialog.Body>
1299
+ <Dialog.Footer className={css({ display: 'flex', gap: 'sm', justifyContent: 'flex-end' })}>
1300
+ <Button variant="filled" onClick={() => setCriticalError(null)}>
1301
+ OK
1302
+ </Button>
1303
+ </Dialog.Footer>
1304
+ <Dialog.CloseTrigger />
1305
+ </Dialog.Content>
1306
+ </Dialog.Positioner>
1307
+ </Dialog.Root>
1308
+ </>
1309
+ );
1310
+ }
1311
+ ```
1312
+
1313
+ **Best practices:**
1314
+
1315
+ - Use inline errors for field validation
1316
+ - Use toasts for non-critical errors
1317
+ - Use dialogs for critical errors requiring action
1318
+ - Provide clear, actionable error messages
1319
+ - Allow retry when appropriate
1320
+
1321
+ **Accessibility:**
1322
+
1323
+ - Errors are announced to screen readers
1324
+ - Focus moves to error when critical
1325
+ - Error messages are associated with inputs
1326
+
1327
+ ---
1328
+
1329
+ ### Confirmation Dialogs
1330
+
1331
+ **When to use:** Destructive actions or important decisions that require explicit confirmation.
1332
+
1333
+ **Components used:** Dialog, Button
1334
+
1335
+ **Example:**
1336
+
1337
+ ```typescript
1338
+ import { Button, toaster } from '@discourser/design-system';
1339
+ import * as Dialog from '@discourser/design-system';
1340
+ import { css } from '@discourser/design-system/styled-system/css';
1341
+ import { useState } from 'react';
1342
+
1343
+ function ConfirmationDialog() {
1344
+ const [open, setOpen] = useState(false);
1345
+ const [loading, setLoading] = useState(false);
1346
+
1347
+ const handleDelete = async () => {
1348
+ setLoading(true);
1349
+
1350
+ try {
1351
+ await deleteItem();
1352
+
1353
+ setOpen(false);
1354
+ toaster.create({
1355
+ title: 'Item deleted',
1356
+ description: 'The item has been permanently deleted.',
1357
+ type: 'success'
1358
+ });
1359
+ } catch (error) {
1360
+ toaster.create({
1361
+ title: 'Delete failed',
1362
+ description: 'Unable to delete the item. Please try again.',
1363
+ type: 'error'
1364
+ });
1365
+ } finally {
1366
+ setLoading(false);
1367
+ }
1368
+ };
1369
+
1370
+ return (
1371
+ <>
1372
+ <Button variant="filled" onClick={() => setOpen(true)}>
1373
+ Delete Item
1374
+ </Button>
1375
+
1376
+ <Dialog.Root open={open} onOpenChange={(e) => setOpen(e.open)}>
1377
+ <Dialog.Backdrop />
1378
+ <Dialog.Positioner>
1379
+ <Dialog.Content>
1380
+ <Dialog.Header>
1381
+ <Dialog.Title>Confirm Deletion</Dialog.Title>
1382
+ </Dialog.Header>
1383
+
1384
+ <Dialog.Body>
1385
+ <Dialog.Description>
1386
+ Are you sure you want to delete this item? This action cannot be undone.
1387
+ </Dialog.Description>
1388
+ </Dialog.Body>
1389
+
1390
+ <Dialog.Footer className={css({
1391
+ display: 'flex',
1392
+ gap: 'sm',
1393
+ justifyContent: 'flex-end'
1394
+ })}>
1395
+ <Button
1396
+ variant="text"
1397
+ onClick={() => setOpen(false)}
1398
+ disabled={loading}
1399
+ >
1400
+ Cancel
1401
+ </Button>
1402
+ <Button
1403
+ variant="filled"
1404
+ onClick={handleDelete}
1405
+ disabled={loading}
1406
+ className={css({ bg: 'error', color: 'onError' })}
1407
+ >
1408
+ {loading ? 'Deleting...' : 'Delete'}
1409
+ </Button>
1410
+ </Dialog.Footer>
1411
+
1412
+ <Dialog.CloseTrigger />
1413
+ </Dialog.Content>
1414
+ </Dialog.Positioner>
1415
+ </Dialog.Root>
1416
+ </>
1417
+ );
1418
+ }
1419
+ ```
1420
+
1421
+ **Best practices:**
1422
+
1423
+ - Use clear, specific titles
1424
+ - Explain consequences of action
1425
+ - Use warning colors for destructive actions
1426
+ - Provide both cancel and confirm options
1427
+ - Disable buttons during processing
1428
+
1429
+ **Accessibility:**
1430
+
1431
+ - Dialog traps focus
1432
+ - Escape key cancels
1433
+ - Focus returns to trigger after close
1434
+
1435
+ ---
1436
+
1437
+ ### Inline Notifications
1438
+
1439
+ **When to use:** Contextual feedback that needs to stay visible near related content.
1440
+
1441
+ **Components used:** Badge, Toast
1442
+
1443
+ **Example:**
1444
+
1445
+ ```typescript
1446
+ import { Badge, Button, toaster } from '@discourser/design-system';
1447
+ import { css } from '@discourser/design-system/styled-system/css';
1448
+ import { useState } from 'react';
1449
+
1450
+ function InlineNotifications() {
1451
+ const [notifications, setNotifications] = useState([
1452
+ { id: 1, message: 'New comment on your post', read: false },
1453
+ { id: 2, message: 'Your profile was viewed 10 times', read: false },
1454
+ { id: 3, message: 'New follower: John Doe', read: true }
1455
+ ]);
1456
+
1457
+ const unreadCount = notifications.filter(n => !n.read).length;
1458
+
1459
+ const markAsRead = (id: number) => {
1460
+ setNotifications(notifications.map(n =>
1461
+ n.id === id ? { ...n, read: true } : n
1462
+ ));
1463
+
1464
+ toaster.create({
1465
+ title: 'Marked as read',
1466
+ type: 'success',
1467
+ duration: 2000
1468
+ });
1469
+ };
1470
+
1471
+ return (
1472
+ <div className={css({ maxWidth: '500px', mx: 'auto', p: 'xl' })}>
1473
+ <div className={css({
1474
+ display: 'flex',
1475
+ alignItems: 'center',
1476
+ gap: 'sm',
1477
+ mb: 'lg'
1478
+ })}>
1479
+ <h2 className={css({ textStyle: 'headlineSmall' })}>
1480
+ Notifications
1481
+ </h2>
1482
+ {unreadCount > 0 && (
1483
+ <Badge variant="solid">
1484
+ {unreadCount} new
1485
+ </Badge>
1486
+ )}
1487
+ </div>
1488
+
1489
+ <div className={css({
1490
+ display: 'flex',
1491
+ flexDirection: 'column',
1492
+ gap: 'sm'
1493
+ })}>
1494
+ {notifications.map((notification) => (
1495
+ <div
1496
+ key={notification.id}
1497
+ className={css({
1498
+ p: 'md',
1499
+ bg: notification.read ? 'surface' : 'secondaryContainer',
1500
+ borderRadius: 'l2',
1501
+ display: 'flex',
1502
+ alignItems: 'center',
1503
+ justifyContent: 'space-between',
1504
+ gap: 'md'
1505
+ })}
1506
+ >
1507
+ <div className={css({
1508
+ flex: 1,
1509
+ textStyle: 'bodyMedium',
1510
+ color: 'onSurface'
1511
+ })}>
1512
+ {notification.message}
1513
+ </div>
1514
+
1515
+ {!notification.read && (
1516
+ <Button
1517
+ variant="text"
1518
+ size="sm"
1519
+ onClick={() => markAsRead(notification.id)}
1520
+ >
1521
+ Mark read
1522
+ </Button>
1523
+ )}
1524
+ </div>
1525
+ ))}
1526
+ </div>
1527
+ </div>
1528
+ );
1529
+ }
1530
+ ```
1531
+
1532
+ **Best practices:**
1533
+
1534
+ - Use badges to show count
1535
+ - Visually distinguish read/unread
1536
+ - Provide action buttons in context
1537
+ - Group related notifications
1538
+ - Keep messages concise
1539
+
1540
+ **Accessibility:**
1541
+
1542
+ - Unread count is announced
1543
+ - List structure is semantic
1544
+ - Actions have clear labels
1545
+
1546
+ ---
1547
+
1548
+ ## Loading States
1549
+
1550
+ ### Page Load
1551
+
1552
+ **When to use:** Initial page load when fetching primary content.
1553
+
1554
+ **Components used:** Skeleton
1555
+
1556
+ **Example:**
1557
+
1558
+ ```typescript
1559
+ import { Card } from '@discourser/design-system';
1560
+ import * as Skeleton from '@discourser/design-system';
1561
+ import { css } from '@discourser/design-system/styled-system/css';
1562
+ import { useState, useEffect } from 'react';
1563
+
1564
+ function PageLoad() {
1565
+ const [loading, setLoading] = useState(true);
1566
+ const [data, setData] = useState(null);
1567
+
1568
+ useEffect(() => {
1569
+ // Simulate data fetching
1570
+ setTimeout(() => {
1571
+ setData({ title: 'Article Title', content: 'Article content...' });
1572
+ setLoading(false);
1573
+ }, 2000);
1574
+ }, []);
1575
+
1576
+ if (loading) {
1577
+ return (
1578
+ <div className={css({ maxWidth: '800px', mx: 'auto', p: 'xl' })}>
1579
+ <Card variant="elevated" className={css({ p: 'xl' })}>
1580
+ <Skeleton.Root>
1581
+ {/* Title skeleton */}
1582
+ <Skeleton.Item height="32px" width="70%" className={css({ mb: 'md' })} />
1583
+
1584
+ {/* Content skeleton */}
1585
+ <Skeleton.Item height="16px" width="100%" className={css({ mb: 'sm' })} />
1586
+ <Skeleton.Item height="16px" width="95%" className={css({ mb: 'sm' })} />
1587
+ <Skeleton.Item height="16px" width="90%" className={css({ mb: 'sm' })} />
1588
+ <Skeleton.Item height="16px" width="85%" />
1589
+ </Skeleton.Root>
1590
+ </Card>
1591
+ </div>
1592
+ );
1593
+ }
1594
+
1595
+ return (
1596
+ <div className={css({ maxWidth: '800px', mx: 'auto', p: 'xl' })}>
1597
+ <Card variant="elevated" className={css({ p: 'xl' })}>
1598
+ <h1 className={css({ textStyle: 'headlineLarge', mb: 'md' })}>
1599
+ {data.title}
1600
+ </h1>
1601
+ <p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
1602
+ {data.content}
1603
+ </p>
1604
+ </Card>
1605
+ </div>
1606
+ );
1607
+ }
1608
+ ```
1609
+
1610
+ **Best practices:**
1611
+
1612
+ - Match skeleton to actual content structure
1613
+ - Use appropriate widths for text lines
1614
+ - Animate skeletons for better UX
1615
+ - Show skeleton for minimum 300ms
1616
+
1617
+ **Accessibility:**
1618
+
1619
+ - Loading state is announced
1620
+ - Content structure is predictable
1621
+
1622
+ ---
1623
+
1624
+ ### Partial Load (Section)
1625
+
1626
+ **When to use:** Loading specific sections while keeping rest of page interactive.
1627
+
1628
+ **Components used:** Spinner, Card
1629
+
1630
+ **Example:**
1631
+
1632
+ ```typescript
1633
+ import { Card, Spinner, Button } from '@discourser/design-system';
1634
+ import { css } from '@discourser/design-system/styled-system/css';
1635
+ import { useState } from 'react';
1636
+
1637
+ function PartialLoad() {
1638
+ const [loading, setLoading] = useState(false);
1639
+ const [data, setData] = useState<string[]>([]);
1640
+
1641
+ const loadMore = async () => {
1642
+ setLoading(true);
1643
+
1644
+ // Simulate API call
1645
+ await new Promise(resolve => setTimeout(resolve, 1000));
1646
+
1647
+ setData([...data, `Item ${data.length + 1}`, `Item ${data.length + 2}`]);
1648
+ setLoading(false);
1649
+ };
1650
+
1651
+ return (
1652
+ <div className={css({ maxWidth: '600px', mx: 'auto', p: 'xl' })}>
1653
+ <Card variant="elevated" className={css({ p: 'xl' })}>
1654
+ <h2 className={css({ textStyle: 'headlineSmall', mb: 'lg' })}>
1655
+ Content List
1656
+ </h2>
1657
+
1658
+ <div className={css({
1659
+ display: 'flex',
1660
+ flexDirection: 'column',
1661
+ gap: 'md',
1662
+ mb: 'lg'
1663
+ })}>
1664
+ {data.map((item, index) => (
1665
+ <div
1666
+ key={index}
1667
+ className={css({
1668
+ p: 'md',
1669
+ bg: 'surfaceContainerHighest',
1670
+ borderRadius: 'l2',
1671
+ textStyle: 'bodyMedium'
1672
+ })}
1673
+ >
1674
+ {item}
1675
+ </div>
1676
+ ))}
1677
+ </div>
1678
+
1679
+ {loading ? (
1680
+ <div className={css({
1681
+ display: 'flex',
1682
+ justifyContent: 'center',
1683
+ alignItems: 'center',
1684
+ gap: 'sm',
1685
+ py: 'lg'
1686
+ })}>
1687
+ <Spinner size="md" />
1688
+ <span className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
1689
+ Loading more...
1690
+ </span>
1691
+ </div>
1692
+ ) : (
1693
+ <Button variant="outlined" onClick={loadMore} className={css({ width: '100%' })}>
1694
+ Load More
1695
+ </Button>
1696
+ )}
1697
+ </Card>
1698
+ </div>
1699
+ );
1700
+ }
1701
+ ```
1702
+
1703
+ **Best practices:**
1704
+
1705
+ - Show spinner in loading area only
1706
+ - Keep rest of page interactive
1707
+ - Provide loading text with spinner
1708
+ - Disable trigger while loading
1709
+
1710
+ **Accessibility:**
1711
+
1712
+ - Loading state is announced
1713
+ - Focus remains on page
1714
+ - Loading area is marked with ARIA
1715
+
1716
+ ---
1717
+
1718
+ ### Button Loading State
1719
+
1720
+ **When to use:** Async actions triggered by buttons.
1721
+
1722
+ **Components used:** Button, Spinner
1723
+
1724
+ **Example:**
1725
+
1726
+ ```typescript
1727
+ import { Button, Spinner, toaster } from '@discourser/design-system';
1728
+ import { css } from '@discourser/design-system/styled-system/css';
1729
+ import { useState } from 'react';
1730
+
1731
+ function ButtonLoadingState() {
1732
+ const [loading, setLoading] = useState(false);
1733
+
1734
+ const handleSubmit = async () => {
1735
+ setLoading(true);
1736
+
1737
+ try {
1738
+ await new Promise(resolve => setTimeout(resolve, 2000));
1739
+
1740
+ toaster.create({
1741
+ title: 'Success!',
1742
+ description: 'Your changes have been saved.',
1743
+ type: 'success'
1744
+ });
1745
+ } catch (error) {
1746
+ toaster.create({
1747
+ title: 'Error',
1748
+ description: 'Failed to save changes.',
1749
+ type: 'error'
1750
+ });
1751
+ } finally {
1752
+ setLoading(false);
1753
+ }
1754
+ };
1755
+
1756
+ return (
1757
+ <div className={css({
1758
+ display: 'flex',
1759
+ flexDirection: 'column',
1760
+ gap: 'md',
1761
+ maxWidth: '400px',
1762
+ mx: 'auto',
1763
+ p: 'xl'
1764
+ })}>
1765
+ {/* Button with inline spinner */}
1766
+ <Button
1767
+ variant="filled"
1768
+ onClick={handleSubmit}
1769
+ disabled={loading}
1770
+ leftIcon={loading ? <Spinner size="sm" /> : undefined}
1771
+ >
1772
+ {loading ? 'Saving...' : 'Save Changes'}
1773
+ </Button>
1774
+
1775
+ {/* Alternative: Spinner replaces text */}
1776
+ <Button
1777
+ variant="outlined"
1778
+ onClick={handleSubmit}
1779
+ disabled={loading}
1780
+ >
1781
+ {loading ? (
1782
+ <div className={css({ display: 'flex', alignItems: 'center', gap: 'xs' })}>
1783
+ <Spinner size="sm" />
1784
+ <span>Processing</span>
1785
+ </div>
1786
+ ) : (
1787
+ 'Submit'
1788
+ )}
1789
+ </Button>
1790
+
1791
+ {/* Alternative: Just text change */}
1792
+ <Button
1793
+ variant="text"
1794
+ onClick={handleSubmit}
1795
+ disabled={loading}
1796
+ >
1797
+ {loading ? 'Loading...' : 'Click Me'}
1798
+ </Button>
1799
+ </div>
1800
+ );
1801
+ }
1802
+ ```
1803
+
1804
+ **Best practices:**
1805
+
1806
+ - Disable button during loading
1807
+ - Show spinner or loading text
1808
+ - Keep button width stable
1809
+ - Use aria-busy attribute
1810
+
1811
+ **Accessibility:**
1812
+
1813
+ - Loading state is announced
1814
+ - Button is disabled and marked busy
1815
+ - Screen readers know action is in progress
1816
+
1817
+ ---
1818
+
1819
+ ### Infinite Scroll
1820
+
1821
+ **When to use:** Long lists that load more content as user scrolls.
1822
+
1823
+ **Components used:** Spinner, Card
1824
+
1825
+ **Example:**
1826
+
1827
+ ```typescript
1828
+ import { Card, Spinner } from '@discourser/design-system';
1829
+ import { css } from '@discourser/design-system/styled-system/css';
1830
+ import { useState, useEffect, useRef } from 'react';
1831
+
1832
+ function InfiniteScroll() {
1833
+ const [items, setItems] = useState<number[]>(Array.from({ length: 10 }, (_, i) => i));
1834
+ const [loading, setLoading] = useState(false);
1835
+ const [hasMore, setHasMore] = useState(true);
1836
+ const observerRef = useRef<HTMLDivElement>(null);
1837
+
1838
+ useEffect(() => {
1839
+ const observer = new IntersectionObserver(
1840
+ (entries) => {
1841
+ if (entries[0].isIntersecting && !loading && hasMore) {
1842
+ loadMore();
1843
+ }
1844
+ },
1845
+ { threshold: 0.1 }
1846
+ );
1847
+
1848
+ if (observerRef.current) {
1849
+ observer.observe(observerRef.current);
1850
+ }
1851
+
1852
+ return () => observer.disconnect();
1853
+ }, [loading, hasMore]);
1854
+
1855
+ const loadMore = async () => {
1856
+ setLoading(true);
1857
+
1858
+ // Simulate API call
1859
+ await new Promise(resolve => setTimeout(resolve, 1000));
1860
+
1861
+ const newItems = Array.from({ length: 10 }, (_, i) => items.length + i);
1862
+ setItems([...items, ...newItems]);
1863
+
1864
+ // Stop after 50 items (demo)
1865
+ if (items.length >= 40) {
1866
+ setHasMore(false);
1867
+ }
1868
+
1869
+ setLoading(false);
1870
+ };
1871
+
1872
+ return (
1873
+ <div className={css({ maxWidth: '600px', mx: 'auto', p: 'xl' })}>
1874
+ <div className={css({
1875
+ display: 'flex',
1876
+ flexDirection: 'column',
1877
+ gap: 'md'
1878
+ })}>
1879
+ {items.map((item) => (
1880
+ <Card key={item} variant="elevated" className={css({ p: 'md' })}>
1881
+ <div className={css({ textStyle: 'bodyMedium' })}>
1882
+ Item #{item + 1}
1883
+ </div>
1884
+ </Card>
1885
+ ))}
1886
+ </div>
1887
+
1888
+ {/* Loading indicator */}
1889
+ {loading && (
1890
+ <div className={css({
1891
+ display: 'flex',
1892
+ justifyContent: 'center',
1893
+ alignItems: 'center',
1894
+ gap: 'sm',
1895
+ py: 'lg'
1896
+ })}>
1897
+ <Spinner size="md" />
1898
+ <span className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
1899
+ Loading more items...
1900
+ </span>
1901
+ </div>
1902
+ )}
1903
+
1904
+ {/* End message */}
1905
+ {!hasMore && (
1906
+ <div className={css({
1907
+ textAlign: 'center',
1908
+ py: 'lg',
1909
+ textStyle: 'bodyMedium',
1910
+ color: 'onSurfaceVariant'
1911
+ })}>
1912
+ No more items to load
1913
+ </div>
1914
+ )}
1915
+
1916
+ {/* Intersection observer target */}
1917
+ <div ref={observerRef} className={css({ height: '1px' })} />
1918
+ </div>
1919
+ );
1920
+ }
1921
+ ```
1922
+
1923
+ **Best practices:**
1924
+
1925
+ - Use IntersectionObserver for performance
1926
+ - Show loading indicator while fetching
1927
+ - Indicate when no more items
1928
+ - Handle errors gracefully
1929
+
1930
+ **Accessibility:**
1931
+
1932
+ - Loading is announced
1933
+ - New content is accessible
1934
+ - End of list is communicated
1935
+
1936
+ ---
1937
+
1938
+ ## Data Display Patterns
1939
+
1940
+ ### Card Grid
1941
+
1942
+ **When to use:** Displaying multiple items of equal importance in a grid layout.
1943
+
1944
+ **Components used:** Card, Badge, Button
1945
+
1946
+ **Example:**
1947
+
1948
+ ```typescript
1949
+ import { Card, Badge, Button } from '@discourser/design-system';
1950
+ import { css } from '@discourser/design-system/styled-system/css';
1951
+
1952
+ function CardGrid() {
1953
+ const products = [
1954
+ { id: 1, name: 'Product A', price: '$29.99', status: 'new', image: '/product-a.jpg' },
1955
+ { id: 2, name: 'Product B', price: '$39.99', status: 'sale', image: '/product-b.jpg' },
1956
+ { id: 3, name: 'Product C', price: '$49.99', status: null, image: '/product-c.jpg' }
1957
+ ];
1958
+
1959
+ return (
1960
+ <div className={css({
1961
+ display: 'grid',
1962
+ gridTemplateColumns: { base: '1fr', md: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' },
1963
+ gap: 'lg',
1964
+ p: { base: 'md', lg: 'xl' }
1965
+ })}>
1966
+ {products.map((product) => (
1967
+ <Card key={product.id} variant="elevated">
1968
+ <div className={css({
1969
+ p: 'lg',
1970
+ display: 'flex',
1971
+ flexDirection: 'column',
1972
+ gap: 'md'
1973
+ })}>
1974
+ {/* Image */}
1975
+ <div className={css({
1976
+ aspectRatio: '16/9',
1977
+ bg: 'surfaceContainerHighest',
1978
+ borderRadius: 'l2',
1979
+ overflow: 'hidden'
1980
+ })}>
1981
+ <img
1982
+ src={product.image}
1983
+ alt={product.name}
1984
+ className={css({ width: '100%', height: '100%', objectFit: 'cover' })}
1985
+ />
1986
+ </div>
1987
+
1988
+ {/* Content */}
1989
+ <div className={css({ flex: 1 })}>
1990
+ <div className={css({
1991
+ display: 'flex',
1992
+ alignItems: 'center',
1993
+ gap: 'xs',
1994
+ mb: 'xs'
1995
+ })}>
1996
+ <h3 className={css({ textStyle: 'titleMedium', color: 'onSurface' })}>
1997
+ {product.name}
1998
+ </h3>
1999
+ {product.status && (
2000
+ <Badge variant={product.status === 'new' ? 'solid' : 'subtle'}>
2001
+ {product.status}
2002
+ </Badge>
2003
+ )}
2004
+ </div>
2005
+
2006
+ <p className={css({ textStyle: 'titleLarge', color: 'primary', mb: 'md' })}>
2007
+ {product.price}
2008
+ </p>
2009
+ </div>
2010
+
2011
+ {/* Action */}
2012
+ <Button variant="filled" className={css({ width: '100%' })}>
2013
+ Add to Cart
2014
+ </Button>
2015
+ </div>
2016
+ </Card>
2017
+ ))}
2018
+ </div>
2019
+ );
2020
+ }
2021
+ ```
2022
+
2023
+ **Best practices:**
2024
+
2025
+ - Use responsive grid columns
2026
+ - Keep card heights consistent
2027
+ - Use appropriate gap spacing
2028
+ - Include clear call-to-action
2029
+ - Optimize images for performance
2030
+
2031
+ **Accessibility:**
2032
+
2033
+ - Images have alt text
2034
+ - Cards are keyboard navigable
2035
+ - Interactive elements are focusable
2036
+
2037
+ ---
2038
+
2039
+ ### List with Actions
2040
+
2041
+ **When to use:** Lists where each item has associated actions (edit, delete, etc.).
2042
+
2043
+ **Components used:** Card, IconButton, Badge
2044
+
2045
+ **Example:**
2046
+
2047
+ ```typescript
2048
+ import { Card, Badge } from '@discourser/design-system';
2049
+ import * as IconButton from '@discourser/design-system';
2050
+ import { css } from '@discourser/design-system/styled-system/css';
2051
+ import { EditIcon, DeleteIcon, MoreIcon } from 'your-icon-library';
2052
+
2053
+ function ListWithActions() {
2054
+ const items = [
2055
+ { id: 1, title: 'Project Alpha', status: 'active', updated: '2 hours ago' },
2056
+ { id: 2, title: 'Project Beta', status: 'pending', updated: '1 day ago' },
2057
+ { id: 3, title: 'Project Gamma', status: 'completed', updated: '3 days ago' }
2058
+ ];
2059
+
2060
+ const statusVariant = {
2061
+ active: 'solid',
2062
+ pending: 'subtle',
2063
+ completed: 'outline'
2064
+ };
2065
+
2066
+ return (
2067
+ <div className={css({ maxWidth: '800px', mx: 'auto', p: 'xl' })}>
2068
+ <div className={css({
2069
+ display: 'flex',
2070
+ flexDirection: 'column',
2071
+ gap: 'md'
2072
+ })}>
2073
+ {items.map((item) => (
2074
+ <Card key={item.id} variant="elevated">
2075
+ <div className={css({
2076
+ p: 'md',
2077
+ display: 'flex',
2078
+ alignItems: 'center',
2079
+ gap: 'md'
2080
+ })}>
2081
+ {/* Content */}
2082
+ <div className={css({ flex: 1 })}>
2083
+ <div className={css({
2084
+ display: 'flex',
2085
+ alignItems: 'center',
2086
+ gap: 'sm',
2087
+ mb: 'xs'
2088
+ })}>
2089
+ <h3 className={css({ textStyle: 'titleMedium', color: 'onSurface' })}>
2090
+ {item.title}
2091
+ </h3>
2092
+ <Badge variant={statusVariant[item.status]}>
2093
+ {item.status}
2094
+ </Badge>
2095
+ </div>
2096
+ <p className={css({ textStyle: 'bodySmall', color: 'onSurfaceVariant' })}>
2097
+ Updated {item.updated}
2098
+ </p>
2099
+ </div>
2100
+
2101
+ {/* Actions */}
2102
+ <div className={css({ display: 'flex', gap: 'xs' })}>
2103
+ <IconButton.Root variant="standard" size="sm">
2104
+ <IconButton.Icon>
2105
+ <EditIcon />
2106
+ </IconButton.Icon>
2107
+ </IconButton.Root>
2108
+
2109
+ <IconButton.Root variant="standard" size="sm">
2110
+ <IconButton.Icon>
2111
+ <DeleteIcon />
2112
+ </IconButton.Icon>
2113
+ </IconButton.Root>
2114
+
2115
+ <IconButton.Root variant="standard" size="sm">
2116
+ <IconButton.Icon>
2117
+ <MoreIcon />
2118
+ </IconButton.Icon>
2119
+ </IconButton.Root>
2120
+ </div>
2121
+ </div>
2122
+ </Card>
2123
+ ))}
2124
+ </div>
2125
+ </div>
2126
+ );
2127
+ }
2128
+ ```
2129
+
2130
+ **Best practices:**
2131
+
2132
+ - Place actions on right side
2133
+ - Use icon buttons for space efficiency
2134
+ - Show actions on hover (desktop)
2135
+ - Always show on mobile
2136
+ - Confirm destructive actions
2137
+
2138
+ **Accessibility:**
2139
+
2140
+ - Icon buttons have aria-labels
2141
+ - Actions are keyboard accessible
2142
+ - Focus order is logical
2143
+
2144
+ ---
2145
+
2146
+ ### List with Avatar
2147
+
2148
+ **When to use:** Lists representing people or entities with profile images.
2149
+
2150
+ **Components used:** Avatar, Badge, Card
2151
+
2152
+ **Example:**
2153
+
2154
+ ```typescript
2155
+ import { Card, Badge } from '@discourser/design-system';
2156
+ import * as Avatar from '@discourser/design-system';
2157
+ import { css } from '@discourser/design-system/styled-system/css';
2158
+
2159
+ function ListWithAvatar() {
2160
+ const users = [
2161
+ { id: 1, name: 'Jane Doe', email: 'jane@example.com', status: 'online', avatar: '/jane.jpg' },
2162
+ { id: 2, name: 'John Smith', email: 'john@example.com', status: 'offline', avatar: null },
2163
+ { id: 3, name: 'Alice Johnson', email: 'alice@example.com', status: 'away', avatar: '/alice.jpg' }
2164
+ ];
2165
+
2166
+ const statusColor = {
2167
+ online: 'success',
2168
+ offline: 'onSurfaceVariant',
2169
+ away: 'warning'
2170
+ };
2171
+
2172
+ return (
2173
+ <div className={css({ maxWidth: '600px', mx: 'auto', p: 'xl' })}>
2174
+ <div className={css({
2175
+ display: 'flex',
2176
+ flexDirection: 'column',
2177
+ gap: 'sm'
2178
+ })}>
2179
+ {users.map((user) => (
2180
+ <Card key={user.id} variant="elevated">
2181
+ <div className={css({
2182
+ p: 'md',
2183
+ display: 'flex',
2184
+ alignItems: 'center',
2185
+ gap: 'md'
2186
+ })}>
2187
+ {/* Avatar with status */}
2188
+ <div className={css({ position: 'relative' })}>
2189
+ <Avatar.Root size="md">
2190
+ {user.avatar ? (
2191
+ <Avatar.Image src={user.avatar} alt={user.name} />
2192
+ ) : (
2193
+ <Avatar.Fallback>
2194
+ {user.name.split(' ').map(n => n[0]).join('')}
2195
+ </Avatar.Fallback>
2196
+ )}
2197
+ </Avatar.Root>
2198
+
2199
+ {/* Status indicator */}
2200
+ <div className={css({
2201
+ position: 'absolute',
2202
+ bottom: 0,
2203
+ right: 0,
2204
+ width: '12px',
2205
+ height: '12px',
2206
+ borderRadius: 'full',
2207
+ bg: statusColor[user.status],
2208
+ border: '2px solid',
2209
+ borderColor: 'surface'
2210
+ })} />
2211
+ </div>
2212
+
2213
+ {/* User info */}
2214
+ <div className={css({ flex: 1 })}>
2215
+ <h3 className={css({ textStyle: 'titleMedium', color: 'onSurface', mb: 'xxs' })}>
2216
+ {user.name}
2217
+ </h3>
2218
+ <p className={css({ textStyle: 'bodySmall', color: 'onSurfaceVariant' })}>
2219
+ {user.email}
2220
+ </p>
2221
+ </div>
2222
+
2223
+ {/* Status badge */}
2224
+ <Badge variant="subtle">
2225
+ {user.status}
2226
+ </Badge>
2227
+ </div>
2228
+ </Card>
2229
+ ))}
2230
+ </div>
2231
+ </div>
2232
+ );
2233
+ }
2234
+ ```
2235
+
2236
+ **Best practices:**
2237
+
2238
+ - Use appropriate avatar size
2239
+ - Show status indicator when relevant
2240
+ - Include fallback for missing images
2241
+ - Keep information hierarchy clear
2242
+ - Add hover states for interactivity
2243
+
2244
+ **Accessibility:**
2245
+
2246
+ - Avatar images have alt text
2247
+ - Status is communicated via badge
2248
+ - Information is properly structured
2249
+
2250
+ ---
2251
+
2252
+ ### Expandable/Collapsible List
2253
+
2254
+ **When to use:** Long lists with detailed information that can be expanded on demand.
2255
+
2256
+ **Components used:** Accordion
2257
+
2258
+ **Example:**
2259
+
2260
+ ```typescript
2261
+ import * as Accordion from '@discourser/design-system';
2262
+ import { css } from '@discourser/design-system/styled-system/css';
2263
+
2264
+ function ExpandableList() {
2265
+ const faqs = [
2266
+ {
2267
+ id: '1',
2268
+ question: 'How do I reset my password?',
2269
+ answer: 'Click on the "Forgot Password" link on the login page. Enter your email address and we\'ll send you instructions to reset your password.'
2270
+ },
2271
+ {
2272
+ id: '2',
2273
+ question: 'What payment methods do you accept?',
2274
+ answer: 'We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and bank transfers for enterprise customers.'
2275
+ },
2276
+ {
2277
+ id: '3',
2278
+ question: 'How can I contact support?',
2279
+ answer: 'You can reach our support team via email at support@example.com, through our live chat feature, or by phone at 1-800-123-4567 during business hours.'
2280
+ }
2281
+ ];
2282
+
2283
+ return (
2284
+ <div className={css({ maxWidth: '800px', mx: 'auto', p: 'xl' })}>
2285
+ <h2 className={css({ textStyle: 'headlineMedium', mb: 'lg' })}>
2286
+ Frequently Asked Questions
2287
+ </h2>
2288
+
2289
+ <Accordion.Root multiple>
2290
+ {faqs.map((faq) => (
2291
+ <Accordion.Item key={faq.id} value={faq.id}>
2292
+ <Accordion.ItemTrigger className={css({
2293
+ py: 'md',
2294
+ px: 'lg',
2295
+ textStyle: 'titleMedium',
2296
+ color: 'onSurface',
2297
+ cursor: 'pointer',
2298
+ _hover: { bg: 'surfaceContainerHighest' },
2299
+ borderRadius: 'l2'
2300
+ })}>
2301
+ {faq.question}
2302
+ <Accordion.ItemIndicator>▼</Accordion.ItemIndicator>
2303
+ </Accordion.ItemTrigger>
2304
+
2305
+ <Accordion.ItemContent className={css({
2306
+ px: 'lg',
2307
+ pb: 'md'
2308
+ })}>
2309
+ <p className={css({
2310
+ textStyle: 'bodyMedium',
2311
+ color: 'onSurfaceVariant'
2312
+ })}>
2313
+ {faq.answer}
2314
+ </p>
2315
+ </Accordion.ItemContent>
2316
+ </Accordion.Item>
2317
+ ))}
2318
+ </Accordion.Root>
2319
+ </div>
2320
+ );
2321
+ }
2322
+ ```
2323
+
2324
+ **Best practices:**
2325
+
2326
+ - Use clear, descriptive titles
2327
+ - Support multiple open items for FAQs
2328
+ - Animate expand/collapse smoothly
2329
+ - Include visual indicator (arrow)
2330
+ - Keep content concise
2331
+
2332
+ **Accessibility:**
2333
+
2334
+ - Follows WAI-ARIA accordion pattern
2335
+ - Keyboard navigation (Tab, Enter, Arrow keys)
2336
+ - Screen readers announce expanded state
2337
+
2338
+ ---
2339
+
2340
+ ## Search & Filter Patterns
2341
+
2342
+ ### Search Bar (Simple)
2343
+
2344
+ **When to use:** Basic keyword search without complex filtering needs.
2345
+
2346
+ **Components used:** InputGroup, Input, Button
2347
+
2348
+ **Example:**
2349
+
2350
+ ```typescript
2351
+ import { Input, Button } from '@discourser/design-system';
2352
+ import { css } from '@discourser/design-system/styled-system/css';
2353
+ import { useState } from 'react';
2354
+ import { SearchIcon } from 'your-icon-library';
2355
+
2356
+ function SearchBar() {
2357
+ const [query, setQuery] = useState('');
2358
+
2359
+ const handleSearch = (e: React.FormEvent) => {
2360
+ e.preventDefault();
2361
+ console.log('Searching for:', query);
2362
+ // Perform search
2363
+ };
2364
+
2365
+ const handleClear = () => {
2366
+ setQuery('');
2367
+ };
2368
+
2369
+ return (
2370
+ <form onSubmit={handleSearch} className={css({ maxWidth: '600px', mx: 'auto', p: 'xl' })}>
2371
+ <div className={css({ display: 'flex', gap: 'sm' })}>
2372
+ <div className={css({ flex: 1, position: 'relative' })}>
2373
+ <Input
2374
+ label="Search"
2375
+ value={query}
2376
+ onChange={(e) => setQuery(e.target.value)}
2377
+ placeholder="Search products, articles, or docs..."
2378
+ className={css({ pr: query ? 'xxxl' : 'md' })}
2379
+ />
2380
+
2381
+ {/* Clear button */}
2382
+ {query && (
2383
+ <button
2384
+ type="button"
2385
+ onClick={handleClear}
2386
+ className={css({
2387
+ position: 'absolute',
2388
+ right: 'xs',
2389
+ top: '50%',
2390
+ transform: 'translateY(-50%)',
2391
+ p: 'xs',
2392
+ color: 'onSurfaceVariant',
2393
+ cursor: 'pointer',
2394
+ borderRadius: 'full',
2395
+ _hover: { bg: 'surfaceContainerHighest' }
2396
+ })}
2397
+ aria-label="Clear search"
2398
+ >
2399
+
2400
+ </button>
2401
+ )}
2402
+ </div>
2403
+
2404
+ <Button
2405
+ type="submit"
2406
+ variant="filled"
2407
+ leftIcon={<SearchIcon />}
2408
+ >
2409
+ Search
2410
+ </Button>
2411
+ </div>
2412
+ </form>
2413
+ );
2414
+ }
2415
+ ```
2416
+
2417
+ **Best practices:**
2418
+
2419
+ - Include clear button when text present
2420
+ - Use search icon for recognition
2421
+ - Submit on Enter key
2422
+ - Provide placeholder text
2423
+ - Show recent searches (optional)
2424
+
2425
+ **Accessibility:**
2426
+
2427
+ - Label is present
2428
+ - Clear button has aria-label
2429
+ - Keyboard shortcuts work
2430
+ - Search icon is decorative
2431
+
2432
+ ---
2433
+
2434
+ ### Search with Filters
2435
+
2436
+ **When to use:** Search that needs additional filtering criteria (category, price range, etc.).
2437
+
2438
+ **Components used:** Input, Select, Checkbox, Button
2439
+
2440
+ **Example:**
2441
+
2442
+ ```typescript
2443
+ import { Input, Button } from '@discourser/design-system';
2444
+ import * as Select from '@discourser/design-system';
2445
+ import * as Checkbox from '@discourser/design-system';
2446
+ import { css } from '@discourser/design-system/styled-system/css';
2447
+ import { useState } from 'react';
2448
+ import { createListCollection } from '@ark-ui/react';
2449
+
2450
+ function SearchWithFilters() {
2451
+ const [query, setQuery] = useState('');
2452
+ const [category, setCategory] = useState('all');
2453
+ const [inStock, setInStock] = useState(false);
2454
+ const [onSale, setOnSale] = useState(false);
2455
+
2456
+ const categories = createListCollection({
2457
+ items: [
2458
+ { label: 'All Categories', value: 'all' },
2459
+ { label: 'Electronics', value: 'electronics' },
2460
+ { label: 'Clothing', value: 'clothing' },
2461
+ { label: 'Books', value: 'books' }
2462
+ ]
2463
+ });
2464
+
2465
+ const handleSearch = (e: React.FormEvent) => {
2466
+ e.preventDefault();
2467
+ console.log({ query, category, inStock, onSale });
2468
+ // Perform filtered search
2469
+ };
2470
+
2471
+ return (
2472
+ <form onSubmit={handleSearch} className={css({
2473
+ maxWidth: '800px',
2474
+ mx: 'auto',
2475
+ p: 'xl'
2476
+ })}>
2477
+ <div className={css({
2478
+ display: 'flex',
2479
+ flexDirection: 'column',
2480
+ gap: 'md'
2481
+ })}>
2482
+ {/* Search input */}
2483
+ <Input
2484
+ label="Search"
2485
+ value={query}
2486
+ onChange={(e) => setQuery(e.target.value)}
2487
+ placeholder="What are you looking for?"
2488
+ />
2489
+
2490
+ {/* Filters */}
2491
+ <div className={css({
2492
+ display: 'grid',
2493
+ gridTemplateColumns: { base: '1fr', md: 'repeat(3, 1fr)' },
2494
+ gap: 'md'
2495
+ })}>
2496
+ {/* Category filter */}
2497
+ <Select.Root
2498
+ collection={categories}
2499
+ value={[category]}
2500
+ onValueChange={(details) => setCategory(details.value[0])}
2501
+ >
2502
+ <Select.Label>Category</Select.Label>
2503
+ <Select.Control>
2504
+ <Select.Trigger>
2505
+ <Select.ValueText placeholder="Select category" />
2506
+ </Select.Trigger>
2507
+ </Select.Control>
2508
+ <Select.Positioner>
2509
+ <Select.Content>
2510
+ {categories.items.map((item) => (
2511
+ <Select.Item key={item.value} item={item}>
2512
+ <Select.ItemText>{item.label}</Select.ItemText>
2513
+ </Select.Item>
2514
+ ))}
2515
+ </Select.Content>
2516
+ </Select.Positioner>
2517
+ </Select.Root>
2518
+
2519
+ {/* Checkbox filters */}
2520
+ <div className={css({
2521
+ display: 'flex',
2522
+ flexDirection: 'column',
2523
+ gap: 'sm',
2524
+ justifyContent: 'center'
2525
+ })}>
2526
+ <Checkbox.Root
2527
+ checked={inStock}
2528
+ onCheckedChange={(details) => setInStock(details.checked === true)}
2529
+ >
2530
+ <Checkbox.Control>
2531
+ <Checkbox.Indicator>✓</Checkbox.Indicator>
2532
+ </Checkbox.Control>
2533
+ <Checkbox.Label>In Stock Only</Checkbox.Label>
2534
+ </Checkbox.Root>
2535
+
2536
+ <Checkbox.Root
2537
+ checked={onSale}
2538
+ onCheckedChange={(details) => setOnSale(details.checked === true)}
2539
+ >
2540
+ <Checkbox.Control>
2541
+ <Checkbox.Indicator>✓</Checkbox.Indicator>
2542
+ </Checkbox.Control>
2543
+ <Checkbox.Label>On Sale</Checkbox.Label>
2544
+ </Checkbox.Root>
2545
+ </div>
2546
+
2547
+ {/* Search button */}
2548
+ <div className={css({ display: 'flex', alignItems: 'flex-end' })}>
2549
+ <Button type="submit" variant="filled" className={css({ width: '100%' })}>
2550
+ Search
2551
+ </Button>
2552
+ </div>
2553
+ </div>
2554
+ </div>
2555
+ </form>
2556
+ );
2557
+ }
2558
+ ```
2559
+
2560
+ **Best practices:**
2561
+
2562
+ - Group related filters together
2563
+ - Use appropriate filter types
2564
+ - Apply filters immediately or on submit
2565
+ - Show active filter count
2566
+ - Allow clearing all filters
2567
+
2568
+ **Accessibility:**
2569
+
2570
+ - All filters are labeled
2571
+ - Keyboard navigation works
2572
+ - Screen readers understand filter relationships
2573
+
2574
+ ---
2575
+
2576
+ ### Search with Results
2577
+
2578
+ **When to use:** Showing search results with loading and empty states.
2579
+
2580
+ **Components used:** Input, Card, Skeleton, Button
2581
+
2582
+ **Example:**
2583
+
2584
+ ```typescript
2585
+ import { Input, Card, Button } from '@discourser/design-system';
2586
+ import * as Skeleton from '@discourser/design-system';
2587
+ import { css } from '@discourser/design-system/styled-system/css';
2588
+ import { useState } from 'react';
2589
+
2590
+ function SearchWithResults() {
2591
+ const [query, setQuery] = useState('');
2592
+ const [loading, setLoading] = useState(false);
2593
+ const [results, setResults] = useState<any[]>([]);
2594
+ const [searched, setSearched] = useState(false);
2595
+
2596
+ const handleSearch = async (e: React.FormEvent) => {
2597
+ e.preventDefault();
2598
+
2599
+ if (!query.trim()) return;
2600
+
2601
+ setLoading(true);
2602
+ setSearched(true);
2603
+
2604
+ // Simulate API call
2605
+ await new Promise(resolve => setTimeout(resolve, 1000));
2606
+
2607
+ // Mock results
2608
+ const mockResults = query.length > 0
2609
+ ? [
2610
+ { id: 1, title: `Result for "${query}" #1`, description: 'This is a relevant result...' },
2611
+ { id: 2, title: `Result for "${query}" #2`, description: 'Another matching result...' }
2612
+ ]
2613
+ : [];
2614
+
2615
+ setResults(mockResults);
2616
+ setLoading(false);
2617
+ };
2618
+
2619
+ return (
2620
+ <div className={css({ maxWidth: '800px', mx: 'auto', p: 'xl' })}>
2621
+ {/* Search form */}
2622
+ <form onSubmit={handleSearch} className={css({ mb: 'xl' })}>
2623
+ <div className={css({ display: 'flex', gap: 'sm' })}>
2624
+ <div className={css({ flex: 1 })}>
2625
+ <Input
2626
+ label="Search"
2627
+ value={query}
2628
+ onChange={(e) => setQuery(e.target.value)}
2629
+ placeholder="Enter search term..."
2630
+ />
2631
+ </div>
2632
+ <Button type="submit" variant="filled" disabled={loading}>
2633
+ {loading ? 'Searching...' : 'Search'}
2634
+ </Button>
2635
+ </div>
2636
+ </form>
2637
+
2638
+ {/* Loading state */}
2639
+ {loading && (
2640
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
2641
+ {[1, 2, 3].map((i) => (
2642
+ <Card key={i} variant="elevated" className={css({ p: 'lg' })}>
2643
+ <Skeleton.Root>
2644
+ <Skeleton.Item height="24px" width="60%" className={css({ mb: 'sm' })} />
2645
+ <Skeleton.Item height="16px" width="100%" className={css({ mb: 'xs' })} />
2646
+ <Skeleton.Item height="16px" width="80%" />
2647
+ </Skeleton.Root>
2648
+ </Card>
2649
+ ))}
2650
+ </div>
2651
+ )}
2652
+
2653
+ {/* Results */}
2654
+ {!loading && searched && (
2655
+ <>
2656
+ {results.length > 0 ? (
2657
+ <>
2658
+ <div className={css({
2659
+ textStyle: 'bodyMedium',
2660
+ color: 'onSurfaceVariant',
2661
+ mb: 'md'
2662
+ })}>
2663
+ Found {results.length} results for "{query}"
2664
+ </div>
2665
+
2666
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
2667
+ {results.map((result) => (
2668
+ <Card key={result.id} variant="elevated" className={css({ p: 'lg' })}>
2669
+ <h3 className={css({ textStyle: 'titleMedium', color: 'primary', mb: 'xs' })}>
2670
+ {result.title}
2671
+ </h3>
2672
+ <p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
2673
+ {result.description}
2674
+ </p>
2675
+ </Card>
2676
+ ))}
2677
+ </div>
2678
+ </>
2679
+ ) : (
2680
+ // Empty state
2681
+ <div className={css({
2682
+ textAlign: 'center',
2683
+ py: 'xxxl'
2684
+ })}>
2685
+ <div className={css({
2686
+ textStyle: 'headlineSmall',
2687
+ color: 'onSurface',
2688
+ mb: 'md'
2689
+ })}>
2690
+ No results found
2691
+ </div>
2692
+ <p className={css({
2693
+ textStyle: 'bodyMedium',
2694
+ color: 'onSurfaceVariant',
2695
+ mb: 'lg'
2696
+ })}>
2697
+ Try different keywords or check your spelling
2698
+ </p>
2699
+ <Button variant="outlined" onClick={() => setQuery('')}>
2700
+ Clear Search
2701
+ </Button>
2702
+ </div>
2703
+ )}
2704
+ </>
2705
+ )}
2706
+ </div>
2707
+ );
2708
+ }
2709
+ ```
2710
+
2711
+ **Best practices:**
2712
+
2713
+ - Show loading skeleton while searching
2714
+ - Display result count
2715
+ - Provide empty state with guidance
2716
+ - Highlight search terms in results
2717
+ - Add pagination for many results
2718
+
2719
+ **Accessibility:**
2720
+
2721
+ - Results are announced to screen readers
2722
+ - Empty state provides clear guidance
2723
+ - Keyboard navigation works throughout
2724
+
2725
+ ---
2726
+
2727
+ ## Authentication Patterns
2728
+
2729
+ ### Login Form
2730
+
2731
+ **When to use:** User authentication for accessing protected areas.
2732
+
2733
+ **Components used:** Input, Button, Switch, Toast
2734
+
2735
+ **Example:**
2736
+
2737
+ ```typescript
2738
+ import { Input, Button, toaster } from '@discourser/design-system';
2739
+ import * as Switch from '@discourser/design-system';
2740
+ import { css } from '@discourser/design-system/styled-system/css';
2741
+ import { useState } from 'react';
2742
+
2743
+ function LoginForm() {
2744
+ const [formData, setFormData] = useState({
2745
+ email: '',
2746
+ password: '',
2747
+ rememberMe: false
2748
+ });
2749
+ const [loading, setLoading] = useState(false);
2750
+
2751
+ const handleSubmit = async (e: React.FormEvent) => {
2752
+ e.preventDefault();
2753
+ setLoading(true);
2754
+
2755
+ try {
2756
+ await loginUser(formData.email, formData.password, formData.rememberMe);
2757
+
2758
+ toaster.create({
2759
+ title: 'Welcome back!',
2760
+ description: 'You have successfully logged in.',
2761
+ type: 'success'
2762
+ });
2763
+
2764
+ // Redirect to dashboard
2765
+ } catch (error) {
2766
+ toaster.create({
2767
+ title: 'Login failed',
2768
+ description: 'Invalid email or password. Please try again.',
2769
+ type: 'error'
2770
+ });
2771
+ } finally {
2772
+ setLoading(false);
2773
+ }
2774
+ };
2775
+
2776
+ return (
2777
+ <div className={css({
2778
+ maxWidth: '400px',
2779
+ mx: 'auto',
2780
+ p: 'xl',
2781
+ mt: 'xxxl'
2782
+ })}>
2783
+ <div className={css({ textAlign: 'center', mb: 'xl' })}>
2784
+ <h1 className={css({ textStyle: 'headlineLarge', color: 'onSurface', mb: 'xs' })}>
2785
+ Welcome Back
2786
+ </h1>
2787
+ <p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
2788
+ Sign in to your account to continue
2789
+ </p>
2790
+ </div>
2791
+
2792
+ <form onSubmit={handleSubmit} className={css({
2793
+ display: 'flex',
2794
+ flexDirection: 'column',
2795
+ gap: 'lg'
2796
+ })}>
2797
+ <Input
2798
+ label="Email"
2799
+ type="email"
2800
+ value={formData.email}
2801
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
2802
+ placeholder="you@example.com"
2803
+ required
2804
+ />
2805
+
2806
+ <Input
2807
+ label="Password"
2808
+ type="password"
2809
+ value={formData.password}
2810
+ onChange={(e) => setFormData({ ...formData, password: e.target.value })}
2811
+ required
2812
+ />
2813
+
2814
+ <div className={css({
2815
+ display: 'flex',
2816
+ alignItems: 'center',
2817
+ justifyContent: 'space-between'
2818
+ })}>
2819
+ <Switch.Root
2820
+ checked={formData.rememberMe}
2821
+ onCheckedChange={(details) => setFormData({ ...formData, rememberMe: details.checked })}
2822
+ >
2823
+ <Switch.Label>Remember me</Switch.Label>
2824
+ <Switch.Control>
2825
+ <Switch.Thumb />
2826
+ </Switch.Control>
2827
+ </Switch.Root>
2828
+
2829
+ <Button variant="text" size="sm">
2830
+ Forgot password?
2831
+ </Button>
2832
+ </div>
2833
+
2834
+ <Button type="submit" variant="filled" disabled={loading}>
2835
+ {loading ? 'Signing in...' : 'Sign In'}
2836
+ </Button>
2837
+
2838
+ <div className={css({
2839
+ textAlign: 'center',
2840
+ textStyle: 'bodyMedium',
2841
+ color: 'onSurfaceVariant'
2842
+ })}>
2843
+ Don't have an account?{' '}
2844
+ <Button variant="text" size="sm">
2845
+ Sign up
2846
+ </Button>
2847
+ </div>
2848
+ </form>
2849
+ </div>
2850
+ );
2851
+ }
2852
+ ```
2853
+
2854
+ **Best practices:**
2855
+
2856
+ - Use email/username and password fields
2857
+ - Include "Remember me" option
2858
+ - Provide "Forgot password" link
2859
+ - Show loading state during authentication
2860
+ - Link to sign up for new users
2861
+ - Use password type for security
2862
+
2863
+ **Accessibility:**
2864
+
2865
+ - All inputs are labeled
2866
+ - Form can be submitted with Enter
2867
+ - Error messages are clear
2868
+ - Focus management is correct
2869
+
2870
+ ---
2871
+
2872
+ ### Sign Up Form
2873
+
2874
+ **When to use:** New user registration with account creation.
2875
+
2876
+ **Components used:** Input, Checkbox, Button, Toast
2877
+
2878
+ **Example:**
2879
+
2880
+ ```typescript
2881
+ import { Input, Button, toaster } from '@discourser/design-system';
2882
+ import * as Checkbox from '@discourser/design-system';
2883
+ import { css } from '@discourser/design-system/styled-system/css';
2884
+ import { useState } from 'react';
2885
+
2886
+ function SignUpForm() {
2887
+ const [formData, setFormData] = useState({
2888
+ name: '',
2889
+ email: '',
2890
+ password: '',
2891
+ confirmPassword: '',
2892
+ agreeToTerms: false
2893
+ });
2894
+ const [errors, setErrors] = useState<Record<string, string>>({});
2895
+ const [loading, setLoading] = useState(false);
2896
+
2897
+ const validateForm = () => {
2898
+ const newErrors: Record<string, string> = {};
2899
+
2900
+ if (!formData.name) newErrors.name = 'Name is required';
2901
+ if (!formData.email) newErrors.email = 'Email is required';
2902
+ if (!formData.password) newErrors.password = 'Password is required';
2903
+ else if (formData.password.length < 8) newErrors.password = 'Password must be at least 8 characters';
2904
+
2905
+ if (formData.password !== formData.confirmPassword) {
2906
+ newErrors.confirmPassword = 'Passwords do not match';
2907
+ }
2908
+
2909
+ if (!formData.agreeToTerms) {
2910
+ newErrors.terms = 'You must agree to the terms and conditions';
2911
+ }
2912
+
2913
+ setErrors(newErrors);
2914
+ return Object.keys(newErrors).length === 0;
2915
+ };
2916
+
2917
+ const handleSubmit = async (e: React.FormEvent) => {
2918
+ e.preventDefault();
2919
+
2920
+ if (!validateForm()) {
2921
+ toaster.create({
2922
+ title: 'Validation error',
2923
+ description: 'Please check the form for errors.',
2924
+ type: 'error'
2925
+ });
2926
+ return;
2927
+ }
2928
+
2929
+ setLoading(true);
2930
+
2931
+ try {
2932
+ await registerUser(formData);
2933
+
2934
+ toaster.create({
2935
+ title: 'Account created!',
2936
+ description: 'Welcome! Please check your email to verify your account.',
2937
+ type: 'success'
2938
+ });
2939
+
2940
+ // Redirect to email verification
2941
+ } catch (error) {
2942
+ toaster.create({
2943
+ title: 'Registration failed',
2944
+ description: 'Email already exists or server error.',
2945
+ type: 'error'
2946
+ });
2947
+ } finally {
2948
+ setLoading(false);
2949
+ }
2950
+ };
2951
+
2952
+ return (
2953
+ <div className={css({
2954
+ maxWidth: '450px',
2955
+ mx: 'auto',
2956
+ p: 'xl',
2957
+ mt: 'xxl'
2958
+ })}>
2959
+ <div className={css({ textAlign: 'center', mb: 'xl' })}>
2960
+ <h1 className={css({ textStyle: 'headlineLarge', color: 'onSurface', mb: 'xs' })}>
2961
+ Create Account
2962
+ </h1>
2963
+ <p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
2964
+ Join us today and get started
2965
+ </p>
2966
+ </div>
2967
+
2968
+ <form onSubmit={handleSubmit} className={css({
2969
+ display: 'flex',
2970
+ flexDirection: 'column',
2971
+ gap: 'lg'
2972
+ })}>
2973
+ <Input
2974
+ label="Full Name"
2975
+ value={formData.name}
2976
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
2977
+ errorText={errors.name}
2978
+ required
2979
+ />
2980
+
2981
+ <Input
2982
+ label="Email"
2983
+ type="email"
2984
+ value={formData.email}
2985
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
2986
+ errorText={errors.email}
2987
+ required
2988
+ />
2989
+
2990
+ <Input
2991
+ label="Password"
2992
+ type="password"
2993
+ value={formData.password}
2994
+ onChange={(e) => setFormData({ ...formData, password: e.target.value })}
2995
+ errorText={errors.password}
2996
+ helperText="Must be at least 8 characters"
2997
+ required
2998
+ />
2999
+
3000
+ <Input
3001
+ label="Confirm Password"
3002
+ type="password"
3003
+ value={formData.confirmPassword}
3004
+ onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
3005
+ errorText={errors.confirmPassword}
3006
+ required
3007
+ />
3008
+
3009
+ <div>
3010
+ <Checkbox.Root
3011
+ checked={formData.agreeToTerms}
3012
+ onCheckedChange={(details) => setFormData({ ...formData, agreeToTerms: details.checked === true })}
3013
+ >
3014
+ <Checkbox.Control>
3015
+ <Checkbox.Indicator>✓</Checkbox.Indicator>
3016
+ </Checkbox.Control>
3017
+ <Checkbox.Label className={css({ textStyle: 'bodySmall' })}>
3018
+ I agree to the{' '}
3019
+ <Button variant="text" size="sm" className={css({ display: 'inline', p: 0 })}>
3020
+ Terms of Service
3021
+ </Button>
3022
+ {' '}and{' '}
3023
+ <Button variant="text" size="sm" className={css({ display: 'inline', p: 0 })}>
3024
+ Privacy Policy
3025
+ </Button>
3026
+ </Checkbox.Label>
3027
+ </Checkbox.Root>
3028
+ {errors.terms && (
3029
+ <p className={css({ textStyle: 'bodySmall', color: 'error', mt: 'xs' })}>
3030
+ {errors.terms}
3031
+ </p>
3032
+ )}
3033
+ </div>
3034
+
3035
+ <Button type="submit" variant="filled" disabled={loading}>
3036
+ {loading ? 'Creating account...' : 'Create Account'}
3037
+ </Button>
3038
+
3039
+ <div className={css({
3040
+ textAlign: 'center',
3041
+ textStyle: 'bodyMedium',
3042
+ color: 'onSurfaceVariant'
3043
+ })}>
3044
+ Already have an account?{' '}
3045
+ <Button variant="text" size="sm">
3046
+ Sign in
3047
+ </Button>
3048
+ </div>
3049
+ </form>
3050
+ </div>
3051
+ );
3052
+ }
3053
+ ```
3054
+
3055
+ **Best practices:**
3056
+
3057
+ - Collect minimal required information
3058
+ - Validate password strength
3059
+ - Require password confirmation
3060
+ - Include terms acceptance checkbox
3061
+ - Show validation errors inline
3062
+ - Link to existing account login
3063
+
3064
+ **Accessibility:**
3065
+
3066
+ - All inputs labeled
3067
+ - Errors associated with inputs
3068
+ - Terms links are accessible
3069
+ - Form validates before submission
3070
+
3071
+ ---
3072
+
3073
+ ### Password Reset Flow
3074
+
3075
+ **When to use:** Allowing users to recover account access via email verification.
3076
+
3077
+ **Components used:** Input, Button, Toast
3078
+
3079
+ **Example:**
3080
+
3081
+ ```typescript
3082
+ import { Input, Button, toaster } from '@discourser/design-system';
3083
+ import { css } from '@discourser/design-system/styled-system/css';
3084
+ import { useState } from 'react';
3085
+
3086
+ function PasswordResetFlow() {
3087
+ const [step, setStep] = useState<'request' | 'sent' | 'reset'>('request');
3088
+ const [email, setEmail] = useState('');
3089
+ const [newPassword, setNewPassword] = useState('');
3090
+ const [confirmPassword, setConfirmPassword] = useState('');
3091
+ const [loading, setLoading] = useState(false);
3092
+
3093
+ const handleRequestReset = async (e: React.FormEvent) => {
3094
+ e.preventDefault();
3095
+ setLoading(true);
3096
+
3097
+ try {
3098
+ await requestPasswordReset(email);
3099
+
3100
+ toaster.create({
3101
+ title: 'Reset email sent',
3102
+ description: 'Check your inbox for password reset instructions.',
3103
+ type: 'success'
3104
+ });
3105
+
3106
+ setStep('sent');
3107
+ } catch (error) {
3108
+ toaster.create({
3109
+ title: 'Failed to send email',
3110
+ description: 'Please check your email address and try again.',
3111
+ type: 'error'
3112
+ });
3113
+ } finally {
3114
+ setLoading(false);
3115
+ }
3116
+ };
3117
+
3118
+ const handleResetPassword = async (e: React.FormEvent) => {
3119
+ e.preventDefault();
3120
+
3121
+ if (newPassword !== confirmPassword) {
3122
+ toaster.create({
3123
+ title: 'Passwords do not match',
3124
+ description: 'Please make sure both passwords are identical.',
3125
+ type: 'error'
3126
+ });
3127
+ return;
3128
+ }
3129
+
3130
+ setLoading(true);
3131
+
3132
+ try {
3133
+ await resetPassword(newPassword);
3134
+
3135
+ toaster.create({
3136
+ title: 'Password reset successful',
3137
+ description: 'You can now log in with your new password.',
3138
+ type: 'success'
3139
+ });
3140
+
3141
+ // Redirect to login
3142
+ } catch (error) {
3143
+ toaster.create({
3144
+ title: 'Reset failed',
3145
+ description: 'Your reset link may have expired.',
3146
+ type: 'error'
3147
+ });
3148
+ } finally {
3149
+ setLoading(false);
3150
+ }
3151
+ };
3152
+
3153
+ return (
3154
+ <div className={css({
3155
+ maxWidth: '400px',
3156
+ mx: 'auto',
3157
+ p: 'xl',
3158
+ mt: 'xxxl'
3159
+ })}>
3160
+ {step === 'request' && (
3161
+ <>
3162
+ <div className={css({ textAlign: 'center', mb: 'xl' })}>
3163
+ <h1 className={css({ textStyle: 'headlineLarge', color: 'onSurface', mb: 'xs' })}>
3164
+ Reset Password
3165
+ </h1>
3166
+ <p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
3167
+ Enter your email to receive reset instructions
3168
+ </p>
3169
+ </div>
3170
+
3171
+ <form onSubmit={handleRequestReset} className={css({
3172
+ display: 'flex',
3173
+ flexDirection: 'column',
3174
+ gap: 'lg'
3175
+ })}>
3176
+ <Input
3177
+ label="Email"
3178
+ type="email"
3179
+ value={email}
3180
+ onChange={(e) => setEmail(e.target.value)}
3181
+ placeholder="you@example.com"
3182
+ required
3183
+ />
3184
+
3185
+ <Button type="submit" variant="filled" disabled={loading}>
3186
+ {loading ? 'Sending...' : 'Send Reset Link'}
3187
+ </Button>
3188
+
3189
+ <Button variant="text" onClick={() => window.location.href = '/login'}>
3190
+ Back to Login
3191
+ </Button>
3192
+ </form>
3193
+ </>
3194
+ )}
3195
+
3196
+ {step === 'sent' && (
3197
+ <div className={css({ textAlign: 'center' })}>
3198
+ <div className={css({
3199
+ width: '64px',
3200
+ height: '64px',
3201
+ bg: 'primaryContainer',
3202
+ borderRadius: 'full',
3203
+ display: 'flex',
3204
+ alignItems: 'center',
3205
+ justifyContent: 'center',
3206
+ mx: 'auto',
3207
+ mb: 'lg',
3208
+ textStyle: 'headlineLarge',
3209
+ color: 'onPrimaryContainer'
3210
+ })}>
3211
+
3212
+ </div>
3213
+
3214
+ <h1 className={css({ textStyle: 'headlineMedium', color: 'onSurface', mb: 'md' })}>
3215
+ Check Your Email
3216
+ </h1>
3217
+
3218
+ <p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant', mb: 'lg' })}>
3219
+ We've sent password reset instructions to <strong>{email}</strong>
3220
+ </p>
3221
+
3222
+ <Button variant="outlined" onClick={() => setStep('request')}>
3223
+ Didn't receive email?
3224
+ </Button>
3225
+ </div>
3226
+ )}
3227
+
3228
+ {step === 'reset' && (
3229
+ <>
3230
+ <div className={css({ textAlign: 'center', mb: 'xl' })}>
3231
+ <h1 className={css({ textStyle: 'headlineLarge', color: 'onSurface', mb: 'xs' })}>
3232
+ Create New Password
3233
+ </h1>
3234
+ <p className={css({ textStyle: 'bodyMedium', color: 'onSurfaceVariant' })}>
3235
+ Enter your new password below
3236
+ </p>
3237
+ </div>
3238
+
3239
+ <form onSubmit={handleResetPassword} className={css({
3240
+ display: 'flex',
3241
+ flexDirection: 'column',
3242
+ gap: 'lg'
3243
+ })}>
3244
+ <Input
3245
+ label="New Password"
3246
+ type="password"
3247
+ value={newPassword}
3248
+ onChange={(e) => setNewPassword(e.target.value)}
3249
+ helperText="Must be at least 8 characters"
3250
+ required
3251
+ />
3252
+
3253
+ <Input
3254
+ label="Confirm Password"
3255
+ type="password"
3256
+ value={confirmPassword}
3257
+ onChange={(e) => setConfirmPassword(e.target.value)}
3258
+ required
3259
+ />
3260
+
3261
+ <Button type="submit" variant="filled" disabled={loading}>
3262
+ {loading ? 'Resetting...' : 'Reset Password'}
3263
+ </Button>
3264
+ </form>
3265
+ </>
3266
+ )}
3267
+ </div>
3268
+ );
3269
+ }
3270
+ ```
3271
+
3272
+ **Best practices:**
3273
+
3274
+ - Use multi-step flow
3275
+ - Send reset link to email
3276
+ - Show confirmation after email sent
3277
+ - Validate new password strength
3278
+ - Expire reset links after time
3279
+ - Confirm password match
3280
+
3281
+ **Accessibility:**
3282
+
3283
+ - Each step is clearly labeled
3284
+ - Status changes are announced
3285
+ - All inputs are accessible
3286
+ - Navigation is logical
3287
+
3288
+ ---
3289
+
3290
+ ## Settings Patterns
3291
+
3292
+ ### Settings Panel
3293
+
3294
+ **When to use:** User preferences and configuration options.
3295
+
3296
+ **Components used:** Card, Switch, Select, Button
3297
+
3298
+ **Example:**
3299
+
3300
+ ```typescript
3301
+ import { Card, Button, toaster } from '@discourser/design-system';
3302
+ import * as Switch from '@discourser/design-system';
3303
+ import * as Select from '@discourser/design-system';
3304
+ import { css } from '@discourser/design-system/styled-system/css';
3305
+ import { useState } from 'react';
3306
+ import { createListCollection } from '@ark-ui/react';
3307
+
3308
+ function SettingsPanel() {
3309
+ const [settings, setSettings] = useState({
3310
+ notifications: true,
3311
+ darkMode: false,
3312
+ language: 'en',
3313
+ autoSave: true
3314
+ });
3315
+
3316
+ const languages = createListCollection({
3317
+ items: [
3318
+ { label: 'English', value: 'en' },
3319
+ { label: 'Spanish', value: 'es' },
3320
+ { label: 'French', value: 'fr' }
3321
+ ]
3322
+ });
3323
+
3324
+ const handleSave = () => {
3325
+ toaster.create({
3326
+ title: 'Settings saved',
3327
+ description: 'Your preferences have been updated.',
3328
+ type: 'success'
3329
+ });
3330
+ };
3331
+
3332
+ return (
3333
+ <div className={css({ maxWidth: '700px', mx: 'auto', p: 'xl' })}>
3334
+ <h1 className={css({ textStyle: 'headlineMedium', mb: 'xl' })}>
3335
+ Settings
3336
+ </h1>
3337
+
3338
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'lg' })}>
3339
+ {/* Notifications section */}
3340
+ <Card variant="elevated" className={css({ p: 'lg' })}>
3341
+ <h2 className={css({ textStyle: 'titleLarge', mb: 'md' })}>
3342
+ Notifications
3343
+ </h2>
3344
+
3345
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
3346
+ <div className={css({
3347
+ display: 'flex',
3348
+ justifyContent: 'space-between',
3349
+ alignItems: 'center'
3350
+ })}>
3351
+ <div>
3352
+ <div className={css({ textStyle: 'titleMedium', mb: 'xs' })}>
3353
+ Push Notifications
3354
+ </div>
3355
+ <div className={css({ textStyle: 'bodySmall', color: 'onSurfaceVariant' })}>
3356
+ Receive notifications about activity
3357
+ </div>
3358
+ </div>
3359
+ <Switch.Root
3360
+ checked={settings.notifications}
3361
+ onCheckedChange={(details) => setSettings({ ...settings, notifications: details.checked })}
3362
+ >
3363
+ <Switch.Control>
3364
+ <Switch.Thumb />
3365
+ </Switch.Control>
3366
+ </Switch.Root>
3367
+ </div>
3368
+ </div>
3369
+ </Card>
3370
+
3371
+ {/* Appearance section */}
3372
+ <Card variant="elevated" className={css({ p: 'lg' })}>
3373
+ <h2 className={css({ textStyle: 'titleLarge', mb: 'md' })}>
3374
+ Appearance
3375
+ </h2>
3376
+
3377
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
3378
+ <div className={css({
3379
+ display: 'flex',
3380
+ justifyContent: 'space-between',
3381
+ alignItems: 'center'
3382
+ })}>
3383
+ <div>
3384
+ <div className={css({ textStyle: 'titleMedium', mb: 'xs' })}>
3385
+ Dark Mode
3386
+ </div>
3387
+ <div className={css({ textStyle: 'bodySmall', color: 'onSurfaceVariant' })}>
3388
+ Use dark theme throughout the app
3389
+ </div>
3390
+ </div>
3391
+ <Switch.Root
3392
+ checked={settings.darkMode}
3393
+ onCheckedChange={(details) => setSettings({ ...settings, darkMode: details.checked })}
3394
+ >
3395
+ <Switch.Control>
3396
+ <Switch.Thumb />
3397
+ </Switch.Control>
3398
+ </Switch.Root>
3399
+ </div>
3400
+
3401
+ <Select.Root
3402
+ collection={languages}
3403
+ value={[settings.language]}
3404
+ onValueChange={(details) => setSettings({ ...settings, language: details.value[0] })}
3405
+ >
3406
+ <Select.Label>Language</Select.Label>
3407
+ <Select.Control>
3408
+ <Select.Trigger>
3409
+ <Select.ValueText placeholder="Select language" />
3410
+ </Select.Trigger>
3411
+ </Select.Control>
3412
+ <Select.Positioner>
3413
+ <Select.Content>
3414
+ {languages.items.map((item) => (
3415
+ <Select.Item key={item.value} item={item}>
3416
+ <Select.ItemText>{item.label}</Select.ItemText>
3417
+ </Select.Item>
3418
+ ))}
3419
+ </Select.Content>
3420
+ </Select.Positioner>
3421
+ </Select.Root>
3422
+ </div>
3423
+ </Card>
3424
+
3425
+ {/* Editor section */}
3426
+ <Card variant="elevated" className={css({ p: 'lg' })}>
3427
+ <h2 className={css({ textStyle: 'titleLarge', mb: 'md' })}>
3428
+ Editor
3429
+ </h2>
3430
+
3431
+ <div className={css({
3432
+ display: 'flex',
3433
+ justifyContent: 'space-between',
3434
+ alignItems: 'center'
3435
+ })}>
3436
+ <div>
3437
+ <div className={css({ textStyle: 'titleMedium', mb: 'xs' })}>
3438
+ Auto-save
3439
+ </div>
3440
+ <div className={css({ textStyle: 'bodySmall', color: 'onSurfaceVariant' })}>
3441
+ Automatically save your work
3442
+ </div>
3443
+ </div>
3444
+ <Switch.Root
3445
+ checked={settings.autoSave}
3446
+ onCheckedChange={(details) => setSettings({ ...settings, autoSave: details.checked })}
3447
+ >
3448
+ <Switch.Control>
3449
+ <Switch.Thumb />
3450
+ </Switch.Control>
3451
+ </Switch.Root>
3452
+ </div>
3453
+ </Card>
3454
+
3455
+ {/* Save button */}
3456
+ <Button variant="filled" onClick={handleSave}>
3457
+ Save Settings
3458
+ </Button>
3459
+ </div>
3460
+ </div>
3461
+ );
3462
+ }
3463
+ ```
3464
+
3465
+ **Best practices:**
3466
+
3467
+ - Group related settings
3468
+ - Use cards for visual separation
3469
+ - Provide descriptions for clarity
3470
+ - Show save confirmation
3471
+ - Consider auto-save for better UX
3472
+
3473
+ **Accessibility:**
3474
+
3475
+ - Settings are clearly labeled
3476
+ - Toggle states are announced
3477
+ - Keyboard navigation works
3478
+ - Changes can be undone
3479
+
3480
+ ---
3481
+
3482
+ ### Profile Settings
3483
+
3484
+ **When to use:** User profile information editing.
3485
+
3486
+ **Components used:** Avatar, Input, Textarea, Button
3487
+
3488
+ **Example:**
3489
+
3490
+ ```typescript
3491
+ import { Input, Textarea, Button, toaster } from '@discourser/design-system';
3492
+ import * as Avatar from '@discourser/design-system';
3493
+ import { css } from '@discourser/design-system/styled-system/css';
3494
+ import { useState } from 'react';
3495
+
3496
+ function ProfileSettings() {
3497
+ const [profile, setProfile] = useState({
3498
+ name: 'Jane Doe',
3499
+ email: 'jane@example.com',
3500
+ bio: 'Product designer passionate about user experience',
3501
+ avatar: '/avatar.jpg'
3502
+ });
3503
+ const [loading, setLoading] = useState(false);
3504
+
3505
+ const handleSave = async () => {
3506
+ setLoading(true);
3507
+
3508
+ try {
3509
+ await updateProfile(profile);
3510
+
3511
+ toaster.create({
3512
+ title: 'Profile updated',
3513
+ description: 'Your changes have been saved.',
3514
+ type: 'success'
3515
+ });
3516
+ } catch (error) {
3517
+ toaster.create({
3518
+ title: 'Update failed',
3519
+ description: 'Please try again.',
3520
+ type: 'error'
3521
+ });
3522
+ } finally {
3523
+ setLoading(false);
3524
+ }
3525
+ };
3526
+
3527
+ return (
3528
+ <div className={css({ maxWidth: '600px', mx: 'auto', p: 'xl' })}>
3529
+ <h1 className={css({ textStyle: 'headlineMedium', mb: 'xl' })}>
3530
+ Profile Settings
3531
+ </h1>
3532
+
3533
+ <div className={css({
3534
+ display: 'flex',
3535
+ flexDirection: 'column',
3536
+ gap: 'xl'
3537
+ })}>
3538
+ {/* Avatar section */}
3539
+ <div className={css({
3540
+ display: 'flex',
3541
+ alignItems: 'center',
3542
+ gap: 'lg'
3543
+ })}>
3544
+ <Avatar.Root size="2xl">
3545
+ <Avatar.Image src={profile.avatar} alt={profile.name} />
3546
+ <Avatar.Fallback>
3547
+ {profile.name.split(' ').map(n => n[0]).join('')}
3548
+ </Avatar.Fallback>
3549
+ </Avatar.Root>
3550
+
3551
+ <div>
3552
+ <Button variant="outlined" size="sm">
3553
+ Change Photo
3554
+ </Button>
3555
+ <p className={css({
3556
+ textStyle: 'bodySmall',
3557
+ color: 'onSurfaceVariant',
3558
+ mt: 'xs'
3559
+ })}>
3560
+ JPG, PNG or GIF (max 2MB)
3561
+ </p>
3562
+ </div>
3563
+ </div>
3564
+
3565
+ {/* Form fields */}
3566
+ <div className={css({
3567
+ display: 'flex',
3568
+ flexDirection: 'column',
3569
+ gap: 'lg'
3570
+ })}>
3571
+ <Input
3572
+ label="Full Name"
3573
+ value={profile.name}
3574
+ onChange={(e) => setProfile({ ...profile, name: e.target.value })}
3575
+ required
3576
+ />
3577
+
3578
+ <Input
3579
+ label="Email"
3580
+ type="email"
3581
+ value={profile.email}
3582
+ onChange={(e) => setProfile({ ...profile, email: e.target.value })}
3583
+ helperText="We'll send updates to this email"
3584
+ required
3585
+ />
3586
+
3587
+ <Textarea
3588
+ label="Bio"
3589
+ value={profile.bio}
3590
+ onChange={(e) => setProfile({ ...profile, bio: e.target.value })}
3591
+ rows={4}
3592
+ helperText="Tell us about yourself in a few words"
3593
+ />
3594
+ </div>
3595
+
3596
+ {/* Actions */}
3597
+ <div className={css({
3598
+ display: 'flex',
3599
+ gap: 'sm',
3600
+ justifyContent: 'flex-end',
3601
+ pt: 'md',
3602
+ borderTopWidth: '1px',
3603
+ borderTopColor: 'outlineVariant'
3604
+ })}>
3605
+ <Button variant="outlined" disabled={loading}>
3606
+ Cancel
3607
+ </Button>
3608
+ <Button variant="filled" onClick={handleSave} disabled={loading}>
3609
+ {loading ? 'Saving...' : 'Save Changes'}
3610
+ </Button>
3611
+ </div>
3612
+ </div>
3613
+ </div>
3614
+ );
3615
+ }
3616
+ ```
3617
+
3618
+ **Best practices:**
3619
+
3620
+ - Show current profile info
3621
+ - Allow avatar upload
3622
+ - Validate email format
3623
+ - Provide cancel option
3624
+ - Show save confirmation
3625
+
3626
+ **Accessibility:**
3627
+
3628
+ - All inputs labeled
3629
+ - Avatar upload is accessible
3630
+ - Form submits properly
3631
+ - Changes are confirmed
3632
+
3633
+ ---
3634
+
3635
+ ## Empty States
3636
+
3637
+ ### No Data
3638
+
3639
+ **When to use:** When a section has no content yet but users can add items.
3640
+
3641
+ **Components used:** Button
3642
+
3643
+ **Example:**
3644
+
3645
+ ```typescript
3646
+ import { Button } from '@discourser/design-system';
3647
+ import { css } from '@discourser/design-system/styled-system/css';
3648
+
3649
+ function NoData() {
3650
+ return (
3651
+ <div className={css({
3652
+ textAlign: 'center',
3653
+ py: 'xxxl',
3654
+ px: 'xl'
3655
+ })}>
3656
+ {/* Illustration or icon */}
3657
+ <div className={css({
3658
+ width: '120px',
3659
+ height: '120px',
3660
+ bg: 'surfaceContainerHighest',
3661
+ borderRadius: 'full',
3662
+ display: 'flex',
3663
+ alignItems: 'center',
3664
+ justifyContent: 'center',
3665
+ mx: 'auto',
3666
+ mb: 'lg',
3667
+ textStyle: 'displaySmall',
3668
+ color: 'onSurfaceVariant'
3669
+ })}>
3670
+ 📂
3671
+ </div>
3672
+
3673
+ {/* Message */}
3674
+ <h2 className={css({
3675
+ textStyle: 'headlineSmall',
3676
+ color: 'onSurface',
3677
+ mb: 'md'
3678
+ })}>
3679
+ No projects yet
3680
+ </h2>
3681
+
3682
+ <p className={css({
3683
+ textStyle: 'bodyMedium',
3684
+ color: 'onSurfaceVariant',
3685
+ mb: 'lg',
3686
+ maxWidth: '400px',
3687
+ mx: 'auto'
3688
+ })}>
3689
+ Get started by creating your first project. Projects help you organize your work and collaborate with your team.
3690
+ </p>
3691
+
3692
+ {/* Action */}
3693
+ <Button variant="filled">
3694
+ Create Project
3695
+ </Button>
3696
+ </div>
3697
+ );
3698
+ }
3699
+ ```
3700
+
3701
+ **Best practices:**
3702
+
3703
+ - Use friendly illustration or icon
3704
+ - Explain why it's empty
3705
+ - Provide clear call-to-action
3706
+ - Keep message concise
3707
+ - Center content vertically
3708
+
3709
+ **Accessibility:**
3710
+
3711
+ - Message is clear and helpful
3712
+ - Action button is prominent
3713
+ - Works with screen readers
3714
+
3715
+ ---
3716
+
3717
+ ### No Search Results
3718
+
3719
+ **When to use:** Search returned no matches.
3720
+
3721
+ **Components used:** Button
3722
+
3723
+ **Example:**
3724
+
3725
+ ```typescript
3726
+ import { Button } from '@discourser/design-system';
3727
+ import { css } from '@discourser/design-system/styled-system/css';
3728
+
3729
+ function NoSearchResults({ query, onClear }: { query: string; onClear: () => void }) {
3730
+ return (
3731
+ <div className={css({
3732
+ textAlign: 'center',
3733
+ py: 'xxxl',
3734
+ px: 'xl'
3735
+ })}>
3736
+ {/* Icon */}
3737
+ <div className={css({
3738
+ width: '80px',
3739
+ height: '80px',
3740
+ bg: 'surfaceContainerHighest',
3741
+ borderRadius: 'full',
3742
+ display: 'flex',
3743
+ alignItems: 'center',
3744
+ justifyContent: 'center',
3745
+ mx: 'auto',
3746
+ mb: 'lg',
3747
+ textStyle: 'headlineLarge',
3748
+ color: 'onSurfaceVariant'
3749
+ })}>
3750
+ 🔍
3751
+ </div>
3752
+
3753
+ {/* Message */}
3754
+ <h2 className={css({
3755
+ textStyle: 'headlineSmall',
3756
+ color: 'onSurface',
3757
+ mb: 'md'
3758
+ })}>
3759
+ No results for "{query}"
3760
+ </h2>
3761
+
3762
+ <p className={css({
3763
+ textStyle: 'bodyMedium',
3764
+ color: 'onSurfaceVariant',
3765
+ mb: 'lg'
3766
+ })}>
3767
+ Try different keywords or check your spelling
3768
+ </p>
3769
+
3770
+ {/* Suggestions */}
3771
+ <div className={css({ mb: 'lg' })}>
3772
+ <p className={css({
3773
+ textStyle: 'labelMedium',
3774
+ color: 'onSurfaceVariant',
3775
+ mb: 'sm'
3776
+ })}>
3777
+ Suggestions:
3778
+ </p>
3779
+ <ul className={css({
3780
+ textStyle: 'bodySmall',
3781
+ color: 'onSurfaceVariant',
3782
+ listStyle: 'none',
3783
+ p: 0
3784
+ })}>
3785
+ <li>Check spelling and try again</li>
3786
+ <li>Try more general keywords</li>
3787
+ <li>Try different keywords</li>
3788
+ </ul>
3789
+ </div>
3790
+
3791
+ {/* Action */}
3792
+ <Button variant="outlined" onClick={onClear}>
3793
+ Clear Search
3794
+ </Button>
3795
+ </div>
3796
+ );
3797
+ }
3798
+ ```
3799
+
3800
+ **Best practices:**
3801
+
3802
+ - Show the search query
3803
+ - Provide helpful suggestions
3804
+ - Allow clearing search
3805
+ - Keep tone friendly
3806
+ - Consider showing related results
3807
+
3808
+ **Accessibility:**
3809
+
3810
+ - Clear messaging
3811
+ - Suggestions are readable
3812
+ - Action is accessible
3813
+
3814
+ ---
3815
+
3816
+ ## Summary
3817
+
3818
+ This guide covers 25+ common UI patterns using the Discourser Design System. Each pattern demonstrates:
3819
+
3820
+ - **When to use**: Clear use cases and scenarios
3821
+ - **Components used**: Which design system components to combine
3822
+ - **Complete code examples**: Production-ready TypeScript/JSX
3823
+ - **Best practices**: Implementation guidelines
3824
+ - **Accessibility**: Inclusive design considerations
3825
+
3826
+ ### Pattern Categories
3827
+
3828
+ 1. **Forms (5 patterns)**: Vertical, horizontal, multi-step, validation, dependencies
3829
+ 2. **Navigation (4 patterns)**: Sidebar, top nav, tabs, breadcrumbs
3830
+ 3. **Feedback (4 patterns)**: Success, errors, confirmations, notifications
3831
+ 4. **Loading (4 patterns)**: Page load, partial load, button loading, infinite scroll
3832
+ 5. **Data Display (4 patterns)**: Card grids, lists with actions, avatars, accordions
3833
+ 6. **Search & Filter (3 patterns)**: Simple search, filtered search, search results
3834
+ 7. **Authentication (3 patterns)**: Login, signup, password reset
3835
+ 8. **Settings (2 patterns)**: Settings panel, profile settings
3836
+ 9. **Empty States (2 patterns)**: No data, no results
3837
+
3838
+ ### Using These Patterns
3839
+
3840
+ 1. **Identify your use case**: Find the pattern that matches your needs
3841
+ 2. **Review the example**: Understand the component structure
3842
+ 3. **Customize as needed**: Adapt the pattern to your specific requirements
3843
+ 4. **Follow best practices**: Apply the recommended guidelines
3844
+ 5. **Test accessibility**: Ensure your implementation is inclusive
3845
+
3846
+ ### Additional Resources
3847
+
3848
+ - **Component Guidelines**: See `guidelines/components/` for detailed component documentation
3849
+ - **Design Tokens**: See `guidelines/design-tokens/` for colors, spacing, typography
3850
+ - **Component Overview**: See `overview-components.md` for available components
3851
+
3852
+ For questions or contributions, visit the [Discourser Design System repository](https://github.com/Tasty-Maker-Studio/Discourser-Design-System).