@discourser/design-system 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +12 -4
  2. package/dist/styles.css +5126 -0
  3. package/guidelines/Guidelines.md +92 -41
  4. package/guidelines/components/accordion.md +732 -0
  5. package/guidelines/components/avatar.md +1015 -0
  6. package/guidelines/components/badge.md +728 -0
  7. package/guidelines/components/button.md +75 -40
  8. package/guidelines/components/card.md +84 -25
  9. package/guidelines/components/checkbox.md +671 -0
  10. package/guidelines/components/dialog.md +619 -31
  11. package/guidelines/components/drawer.md +1616 -0
  12. package/guidelines/components/heading.md +576 -0
  13. package/guidelines/components/icon-button.md +92 -37
  14. package/guidelines/components/input-addon.md +685 -0
  15. package/guidelines/components/input-group.md +830 -0
  16. package/guidelines/components/input.md +92 -37
  17. package/guidelines/components/popover.md +1271 -0
  18. package/guidelines/components/progress.md +836 -0
  19. package/guidelines/components/radio-group.md +852 -0
  20. package/guidelines/components/select.md +1662 -0
  21. package/guidelines/components/skeleton.md +802 -0
  22. package/guidelines/components/slider.md +911 -0
  23. package/guidelines/components/spinner.md +783 -0
  24. package/guidelines/components/switch.md +105 -38
  25. package/guidelines/components/tabs.md +1488 -0
  26. package/guidelines/components/textarea.md +495 -0
  27. package/guidelines/components/toast.md +784 -0
  28. package/guidelines/components/tooltip.md +912 -0
  29. package/guidelines/design-tokens/colors.md +309 -72
  30. package/guidelines/design-tokens/elevation.md +615 -45
  31. package/guidelines/design-tokens/spacing.md +654 -74
  32. package/guidelines/design-tokens/typography.md +432 -50
  33. package/guidelines/overview-components.md +60 -8
  34. package/guidelines/overview-imports.md +314 -0
  35. package/guidelines/overview-patterns.md +3852 -0
  36. package/package.json +4 -2
@@ -0,0 +1,784 @@
1
+ # Toast
2
+
3
+ **Purpose:** Temporary notification component that provides non-intrusive feedback to users about system events, actions, or status changes following Material Design 3 patterns.
4
+
5
+ ## When to Use This Component
6
+
7
+ Use Toast when you need to **provide temporary, non-intrusive feedback about system events or actions** that auto-dismiss after a few seconds.
8
+
9
+ ### Decision Tree
10
+
11
+ | Scenario | Use Toast? | Alternative | Reasoning |
12
+ | --------------------------------------- | ---------- | ----------------- | ------------------------------------------ |
13
+ | Success confirmation (saved, deleted) | ✅ Yes | - | Quick feedback that doesn't block workflow |
14
+ | Error notifications (network failed) | ✅ Yes | - | Informs user without interrupting |
15
+ | Loading state updates (upload complete) | ✅ Yes | - | Progress feedback with auto-dismiss |
16
+ | Critical errors requiring action | ❌ No | Dialog | Dialog forces acknowledgment |
17
+ | Persistent status information | ❌ No | Alert banner | Toast auto-dismisses, alerts stay |
18
+ | Complex forms with multiple errors | ❌ No | Inline validation | Toast is too brief for detailed feedback |
19
+
20
+ ### Component Comparison
21
+
22
+ ```typescript
23
+ // ✅ Toast - Success feedback
24
+ toaster.create({
25
+ title: 'Profile updated',
26
+ description: 'Your changes have been saved successfully.',
27
+ type: 'success',
28
+ duration: 3000,
29
+ });
30
+
31
+ // ❌ Don't use Toast for critical errors - Use Dialog
32
+ toaster.create({
33
+ title: 'Payment failed',
34
+ description: 'Your card was declined.',
35
+ type: 'error',
36
+ }); // User might miss this!
37
+
38
+ // ✅ Better: Use Dialog for critical actions
39
+ <Dialog.Root>
40
+ <Dialog.Content>
41
+ <Dialog.Title>Payment Failed</Dialog.Title>
42
+ <Dialog.Description>
43
+ Your card was declined. Please update your payment method.
44
+ </Dialog.Description>
45
+ <Dialog.Footer>
46
+ <Button>Update Payment</Button>
47
+ </Dialog.Footer>
48
+ </Dialog.Content>
49
+ </Dialog.Root>
50
+
51
+ // ❌ Don't use Toast for persistent info - Use Alert
52
+ toaster.create({
53
+ title: 'Maintenance scheduled',
54
+ description: 'System will be down tomorrow 2-4pm.',
55
+ type: 'warning',
56
+ duration: Infinity,
57
+ }); // Toast placement isn't ideal for persistent info
58
+
59
+ // ✅ Better: Use Alert banner for persistent warnings
60
+ <Alert status="warning">
61
+ <Alert.Icon />
62
+ <Alert.Title>Maintenance Scheduled</Alert.Title>
63
+ <Alert.Description>
64
+ System will be down tomorrow 2-4pm.
65
+ </Alert.Description>
66
+ </Alert>
67
+
68
+ // ✅ Toast - Loading to success transition
69
+ const toastId = toaster.create({
70
+ title: 'Uploading file...',
71
+ type: 'loading',
72
+ });
73
+
74
+ // Later, update to success
75
+ toaster.update(toastId, {
76
+ title: 'Upload complete',
77
+ type: 'success',
78
+ duration: 3000,
79
+ });
80
+ ```
81
+
82
+ ## Import
83
+
84
+ ```typescript
85
+ import { Toaster, toaster } from '@discourser/design-system';
86
+ ```
87
+
88
+ ## Component Structure
89
+
90
+ The Toast system consists of two parts:
91
+
92
+ 1. **Toaster**: The container component that renders toast notifications (placed once in your app layout)
93
+ 2. **toaster**: The imperative API for creating and managing toast notifications
94
+
95
+ ### Basic Setup
96
+
97
+ ```typescript
98
+ // In your root layout or App component
99
+ import { Toaster } from '@discourser/design-system';
100
+
101
+ export default function Layout({ children }) {
102
+ return (
103
+ <>
104
+ {children}
105
+ <Toaster />
106
+ </>
107
+ );
108
+ }
109
+ ```
110
+
111
+ ## Toast Types
112
+
113
+ The Toast component supports 4 types, each with specific visual indicators:
114
+
115
+ | Type | Icon | Usage | When to Use |
116
+ | --------- | ----------- | ---------------------- | -------------------------------------------------------------- |
117
+ | `success` | CheckCircle | Successful operations | Form submissions, save confirmations, successful deletions |
118
+ | `error` | CircleX | Error states | Failed operations, validation errors, system errors |
119
+ | `warning` | CircleAlert | Warning messages | Cautionary information, potential issues, confirmations needed |
120
+ | `loading` | Spinner | In-progress operations | Async operations, file uploads, data fetching |
121
+
122
+ ### Visual Characteristics
123
+
124
+ - **success**: Green checkmark icon, positive feedback
125
+ - **error**: Red X icon, critical attention
126
+ - **warning**: Yellow alert icon, caution indicator
127
+ - **loading**: Animated spinner, ongoing process
128
+
129
+ ## Toaster Configuration
130
+
131
+ The toaster is created with the following default configuration:
132
+
133
+ | Option | Default | Description |
134
+ | ----------------- | -------------- | --------------------------------- |
135
+ | `placement` | `'bottom-end'` | Position of toast notifications |
136
+ | `pauseOnPageIdle` | `true` | Pause timers when page is idle |
137
+ | `overlap` | `true` | Stack toasts on top of each other |
138
+ | `max` | `5` | Maximum number of visible toasts |
139
+
140
+ ### Placement Options
141
+
142
+ - `top-start`, `top`, `top-end`
143
+ - `bottom-start`, `bottom`, `bottom-end`
144
+
145
+ ## Toaster API
146
+
147
+ ### Creating Toasts
148
+
149
+ ```typescript
150
+ // Basic toast with title only
151
+ toaster.create({
152
+ title: 'Action completed',
153
+ type: 'success',
154
+ });
155
+
156
+ // Toast with title and description
157
+ toaster.create({
158
+ title: 'Error occurred',
159
+ description: 'Unable to save your changes. Please try again.',
160
+ type: 'error',
161
+ });
162
+
163
+ // Toast with custom duration (in milliseconds)
164
+ toaster.create({
165
+ title: 'Processing...',
166
+ type: 'loading',
167
+ duration: 5000, // 5 seconds
168
+ });
169
+
170
+ // Toast with action button
171
+ toaster.create({
172
+ title: 'Item deleted',
173
+ description: 'The item has been removed.',
174
+ type: 'success',
175
+ action: {
176
+ label: 'Undo',
177
+ onClick: () => handleUndo(),
178
+ },
179
+ });
180
+
181
+ // Toast without auto-dismiss
182
+ toaster.create({
183
+ title: 'Important message',
184
+ description: 'This will stay until closed.',
185
+ type: 'warning',
186
+ duration: Infinity,
187
+ });
188
+
189
+ // Closable toast (shows close button)
190
+ toaster.create({
191
+ title: 'Notification',
192
+ type: 'success',
193
+ closable: true,
194
+ });
195
+ ```
196
+
197
+ ### Managing Toasts
198
+
199
+ ```typescript
200
+ // Create and store toast ID for later control
201
+ const toastId = toaster.create({
202
+ title: 'Processing...',
203
+ type: 'loading',
204
+ });
205
+
206
+ // Update existing toast
207
+ toaster.update(toastId, {
208
+ title: 'Success!',
209
+ type: 'success',
210
+ duration: 3000,
211
+ });
212
+
213
+ // Dismiss specific toast
214
+ toaster.dismiss(toastId);
215
+
216
+ // Dismiss all toasts
217
+ toaster.dismissAll();
218
+
219
+ // Remove specific toast (immediate, no animation)
220
+ toaster.remove(toastId);
221
+
222
+ // Check if toast exists
223
+ const exists = toaster.isVisible(toastId);
224
+
225
+ // Get toast details
226
+ const toast = toaster.getById(toastId);
227
+ ```
228
+
229
+ ## Toast Properties
230
+
231
+ | Property | Type | Default | Description |
232
+ | ------------- | ------------------------------------------------ | -------------- | ----------------------------------------------------- |
233
+ | `title` | `string` | Required | Main toast message |
234
+ | `description` | `string` | - | Additional details or context |
235
+ | `type` | `'success' \| 'error' \| 'warning' \| 'loading'` | - | Visual style and icon |
236
+ | `duration` | `number` | `5000` | Auto-dismiss duration in ms (Infinity for persistent) |
237
+ | `closable` | `boolean` | `false` | Show close button |
238
+ | `action` | `{ label: string, onClick: () => void }` | - | Action button configuration |
239
+ | `id` | `string` | Auto-generated | Unique toast identifier |
240
+
241
+ ## Examples
242
+
243
+ ### Success Toast
244
+
245
+ ```typescript
246
+ // Simple success message
247
+ toaster.create({
248
+ title: 'Profile updated',
249
+ type: 'success',
250
+ });
251
+
252
+ // Success with details
253
+ toaster.create({
254
+ title: 'Changes saved',
255
+ description: 'Your profile has been updated successfully.',
256
+ type: 'success',
257
+ duration: 4000,
258
+ });
259
+
260
+ // Success with undo action
261
+ toaster.create({
262
+ title: 'Item deleted',
263
+ description: 'The file has been moved to trash.',
264
+ type: 'success',
265
+ action: {
266
+ label: 'Undo',
267
+ onClick: () => restoreItem(),
268
+ },
269
+ });
270
+ ```
271
+
272
+ ### Error Toast
273
+
274
+ ```typescript
275
+ // Simple error
276
+ toaster.create({
277
+ title: 'Error saving changes',
278
+ type: 'error',
279
+ });
280
+
281
+ // Error with explanation
282
+ toaster.create({
283
+ title: 'Connection failed',
284
+ description:
285
+ 'Unable to connect to the server. Please check your internet connection.',
286
+ type: 'error',
287
+ duration: 7000,
288
+ });
289
+
290
+ // Error with retry action
291
+ toaster.create({
292
+ title: 'Upload failed',
293
+ description: 'The file could not be uploaded.',
294
+ type: 'error',
295
+ action: {
296
+ label: 'Retry',
297
+ onClick: () => retryUpload(),
298
+ },
299
+ closable: true,
300
+ });
301
+ ```
302
+
303
+ ### Warning Toast
304
+
305
+ ```typescript
306
+ // Simple warning
307
+ toaster.create({
308
+ title: 'Unsaved changes',
309
+ type: 'warning',
310
+ });
311
+
312
+ // Warning with details
313
+ toaster.create({
314
+ title: 'Storage almost full',
315
+ description: 'You have used 90% of your available storage.',
316
+ type: 'warning',
317
+ duration: 10000,
318
+ });
319
+
320
+ // Persistent warning
321
+ toaster.create({
322
+ title: 'Action required',
323
+ description: 'Please verify your email address to continue.',
324
+ type: 'warning',
325
+ duration: Infinity,
326
+ closable: true,
327
+ });
328
+ ```
329
+
330
+ ### Loading Toast
331
+
332
+ ```typescript
333
+ // Simple loading
334
+ const loadingToast = toaster.create({
335
+ title: 'Processing...',
336
+ type: 'loading',
337
+ });
338
+
339
+ // Loading with description
340
+ const uploadToast = toaster.create({
341
+ title: 'Uploading file',
342
+ description: 'Please wait while we upload your file.',
343
+ type: 'loading',
344
+ });
345
+
346
+ // Update to success when complete
347
+ setTimeout(() => {
348
+ toaster.update(uploadToast, {
349
+ title: 'Upload complete',
350
+ description: 'Your file has been uploaded successfully.',
351
+ type: 'success',
352
+ duration: 3000,
353
+ });
354
+ }, 3000);
355
+ ```
356
+
357
+ ## Common Patterns
358
+
359
+ ### Form Submission Feedback
360
+
361
+ ```typescript
362
+ async function handleSubmit(data: FormData) {
363
+ const toastId = toaster.create({
364
+ title: 'Saving changes...',
365
+ type: 'loading',
366
+ });
367
+
368
+ try {
369
+ await saveData(data);
370
+
371
+ toaster.update(toastId, {
372
+ title: 'Changes saved',
373
+ description: 'Your updates have been saved successfully.',
374
+ type: 'success',
375
+ duration: 3000,
376
+ });
377
+ } catch (error) {
378
+ toaster.update(toastId, {
379
+ title: 'Save failed',
380
+ description: error.message,
381
+ type: 'error',
382
+ duration: 5000,
383
+ });
384
+ }
385
+ }
386
+ ```
387
+
388
+ ### Async Operation with Progress
389
+
390
+ ```typescript
391
+ async function uploadFile(file: File) {
392
+ const toastId = toaster.create({
393
+ title: 'Uploading file...',
394
+ description: `${file.name}`,
395
+ type: 'loading',
396
+ });
397
+
398
+ try {
399
+ const result = await upload(file);
400
+
401
+ toaster.update(toastId, {
402
+ title: 'Upload complete',
403
+ description: `${file.name} has been uploaded.`,
404
+ type: 'success',
405
+ duration: 3000,
406
+ });
407
+
408
+ return result;
409
+ } catch (error) {
410
+ toaster.update(toastId, {
411
+ title: 'Upload failed',
412
+ description: `Could not upload ${file.name}. ${error.message}`,
413
+ type: 'error',
414
+ action: {
415
+ label: 'Retry',
416
+ onClick: () => uploadFile(file),
417
+ },
418
+ closable: true,
419
+ });
420
+ }
421
+ }
422
+ ```
423
+
424
+ ### Delete with Undo
425
+
426
+ ```typescript
427
+ function handleDelete(itemId: string) {
428
+ const item = getItem(itemId);
429
+
430
+ // Soft delete
431
+ markAsDeleted(itemId);
432
+
433
+ toaster.create({
434
+ title: 'Item deleted',
435
+ description: item.name,
436
+ type: 'success',
437
+ duration: 5000,
438
+ action: {
439
+ label: 'Undo',
440
+ onClick: () => {
441
+ restoreItem(itemId);
442
+ toaster.create({
443
+ title: 'Item restored',
444
+ type: 'success',
445
+ });
446
+ },
447
+ },
448
+ });
449
+
450
+ // Permanent delete after toast duration
451
+ setTimeout(() => {
452
+ permanentlyDelete(itemId);
453
+ }, 5000);
454
+ }
455
+ ```
456
+
457
+ ### Multiple Related Operations
458
+
459
+ ```typescript
460
+ async function batchOperation(items: Item[]) {
461
+ let successCount = 0;
462
+ let failCount = 0;
463
+
464
+ const toastId = toaster.create({
465
+ title: `Processing ${items.length} items...`,
466
+ type: 'loading',
467
+ });
468
+
469
+ for (const item of items) {
470
+ try {
471
+ await processItem(item);
472
+ successCount++;
473
+ } catch {
474
+ failCount++;
475
+ }
476
+ }
477
+
478
+ toaster.update(toastId, {
479
+ title: 'Batch operation complete',
480
+ description: `${successCount} succeeded, ${failCount} failed`,
481
+ type: failCount === 0 ? 'success' : 'warning',
482
+ duration: 5000,
483
+ });
484
+ }
485
+ ```
486
+
487
+ ### Session Timeout Warning
488
+
489
+ ```typescript
490
+ function showSessionWarning(secondsRemaining: number) {
491
+ const toastId = toaster.create({
492
+ title: 'Session expiring soon',
493
+ description: `Your session will expire in ${secondsRemaining} seconds.`,
494
+ type: 'warning',
495
+ duration: Infinity,
496
+ closable: true,
497
+ action: {
498
+ label: 'Extend Session',
499
+ onClick: async () => {
500
+ await extendSession();
501
+ toaster.dismiss(toastId);
502
+ toaster.create({
503
+ title: 'Session extended',
504
+ type: 'success',
505
+ });
506
+ },
507
+ },
508
+ });
509
+ }
510
+ ```
511
+
512
+ ## DO NOT
513
+
514
+ ```typescript
515
+ // ❌ Don't use toasts for critical errors that require user action
516
+ toaster.create({
517
+ title: 'Payment failed',
518
+ type: 'error',
519
+ }); // Use a modal dialog instead
520
+
521
+ // ❌ Don't stack multiple toasts for the same operation
522
+ onClick={() => {
523
+ toaster.create({ title: 'Starting...', type: 'loading' });
524
+ toaster.create({ title: 'Processing...', type: 'loading' });
525
+ }} // Update the same toast instead
526
+
527
+ // ❌ Don't use very short durations
528
+ toaster.create({
529
+ title: 'Saved',
530
+ type: 'success',
531
+ duration: 500, // Too fast to read
532
+ });
533
+
534
+ // ❌ Don't use toasts for complex information
535
+ toaster.create({
536
+ title: 'Error',
537
+ description: 'Very long error message with multiple paragraphs...',
538
+ type: 'error',
539
+ }); // Use a dialog or dedicated error page
540
+
541
+ // ❌ Don't create toasts without type
542
+ toaster.create({
543
+ title: 'Something happened',
544
+ }); // Always specify type for proper visual feedback
545
+
546
+ // ❌ Don't use action buttons for navigation
547
+ toaster.create({
548
+ title: 'Success',
549
+ action: {
550
+ label: 'Go to Dashboard',
551
+ onClick: () => router.push('/dashboard'),
552
+ },
553
+ }); // Toasts should not be primary navigation
554
+
555
+ // ✅ Use proper duration based on content length
556
+ toaster.create({
557
+ title: 'Saved',
558
+ type: 'success',
559
+ duration: 3000, // 3-5 seconds for simple messages
560
+ });
561
+
562
+ // ✅ Use update for state changes
563
+ const id = toaster.create({ title: 'Loading...', type: 'loading' });
564
+ toaster.update(id, { title: 'Done', type: 'success' });
565
+
566
+ // ✅ Use modals for critical information
567
+ <Dialog>
568
+ <Dialog.Title>Payment Failed</Dialog.Title>
569
+ <Dialog.Description>Your payment could not be processed...</Dialog.Description>
570
+ </Dialog>
571
+ ```
572
+
573
+ ## Accessibility
574
+
575
+ The Toast component follows WCAG 2.1 Level AA standards:
576
+
577
+ - **ARIA Role**: Uses `role="status"` for non-critical notifications
578
+ - **Live Regions**: Automatically announces toast content to screen readers
579
+ - **Keyboard Navigation**:
580
+ - Tab focuses close button and action button
581
+ - Enter/Space activates buttons
582
+ - Escape dismisses closable toasts
583
+ - **Focus Management**: Does not steal focus from current interaction
584
+ - **Color Independence**: Uses icons in addition to color for type indication
585
+ - **Timing**: Respects `prefers-reduced-motion` for animations
586
+
587
+ ### Accessibility Best Practices
588
+
589
+ ```typescript
590
+ // ✅ Provide clear, concise messages
591
+ toaster.create({
592
+ title: 'Profile updated successfully',
593
+ type: 'success',
594
+ });
595
+
596
+ // ✅ Include helpful descriptions for errors
597
+ toaster.create({
598
+ title: 'Connection error',
599
+ description: 'Please check your internet connection and try again.',
600
+ type: 'error',
601
+ });
602
+
603
+ // ✅ Use appropriate durations for content length
604
+ toaster.create({
605
+ title: 'Short message',
606
+ type: 'success',
607
+ duration: 3000, // 3 seconds
608
+ });
609
+
610
+ toaster.create({
611
+ title: 'Longer message',
612
+ description: 'Additional context that takes more time to read.',
613
+ type: 'warning',
614
+ duration: 7000, // 7 seconds
615
+ });
616
+
617
+ // ✅ Make important toasts closable
618
+ toaster.create({
619
+ title: 'Important update',
620
+ description: 'System maintenance scheduled for tonight.',
621
+ type: 'warning',
622
+ duration: Infinity,
623
+ closable: true,
624
+ });
625
+
626
+ // ✅ Provide meaningful action labels
627
+ toaster.create({
628
+ title: 'Item deleted',
629
+ type: 'success',
630
+ action: {
631
+ label: 'Undo deletion', // Descriptive, not just "Undo"
632
+ onClick: handleUndo,
633
+ },
634
+ });
635
+ ```
636
+
637
+ ## Duration Guidelines
638
+
639
+ | Content Type | Recommended Duration | Reasoning |
640
+ | ------------------------ | -------------------- | ------------------------------------------------ |
641
+ | Simple success message | 3-4 seconds | Quick confirmation, doesn't need long visibility |
642
+ | Error with description | 5-7 seconds | User needs time to read and understand |
643
+ | Warning message | 7-10 seconds | Important information, needs attention |
644
+ | Loading state | Infinity | Should be dismissed programmatically |
645
+ | Message with action | 5-10 seconds | User needs time to read and decide |
646
+ | Critical persistent info | Infinity + closable | Stays until user acknowledges |
647
+
648
+ **Formula**: Base 3 seconds + 1 second per 10 words in description
649
+
650
+ ## Type Selection Guide
651
+
652
+ | Scenario | Recommended Type | Reasoning |
653
+ | -------------------- | ---------------- | ----------------------------------------- |
654
+ | Form saved | `success` | Positive confirmation of completed action |
655
+ | Item created | `success` | New resource successfully created |
656
+ | Item deleted | `success` | Destructive action completed (with undo) |
657
+ | Validation error | `error` | User input needs correction |
658
+ | Network error | `error` | Operation failed due to connectivity |
659
+ | Server error | `error` | Backend operation failed |
660
+ | Unsaved changes | `warning` | User might lose data |
661
+ | Low storage | `warning` | Approaching limit, action recommended |
662
+ | Permission issue | `warning` | Limited functionality available |
663
+ | File uploading | `loading` | Async operation in progress |
664
+ | Data fetching | `loading` | Loading state for async operation |
665
+ | API call in progress | `loading` | Waiting for server response |
666
+
667
+ ## Responsive Considerations
668
+
669
+ ```typescript
670
+ // The Toaster component automatically adjusts for mobile
671
+ // It uses `insetInline={{ mdDown: '4' }}` for proper spacing
672
+
673
+ // Desktop: Toasts appear in bottom-end corner
674
+ // Mobile: Toasts have 4-unit horizontal padding for better visibility
675
+
676
+ // Ensure touch-friendly action buttons
677
+ toaster.create({
678
+ title: 'Action required',
679
+ type: 'warning',
680
+ action: {
681
+ label: 'Dismiss', // Short label for mobile
682
+ onClick: handleDismiss,
683
+ },
684
+ closable: true, // Provides alternative way to dismiss
685
+ });
686
+ ```
687
+
688
+ ## Testing
689
+
690
+ When testing Toast components:
691
+
692
+ ```typescript
693
+ import { render, screen, waitFor } from '@testing-library/react';
694
+ import userEvent from '@testing-library/user-event';
695
+ import { Toaster, toaster } from '@discourser/design-system';
696
+
697
+ test('creates and displays success toast', async () => {
698
+ render(<Toaster />);
699
+
700
+ toaster.create({
701
+ title: 'Success message',
702
+ type: 'success',
703
+ });
704
+
705
+ expect(await screen.findByText('Success message')).toBeInTheDocument();
706
+ });
707
+
708
+ test('updates loading toast to success', async () => {
709
+ render(<Toaster />);
710
+
711
+ const id = toaster.create({
712
+ title: 'Loading...',
713
+ type: 'loading',
714
+ });
715
+
716
+ expect(await screen.findByText('Loading...')).toBeInTheDocument();
717
+
718
+ toaster.update(id, {
719
+ title: 'Complete',
720
+ type: 'success',
721
+ });
722
+
723
+ expect(await screen.findByText('Complete')).toBeInTheDocument();
724
+ });
725
+
726
+ test('action button triggers callback', async () => {
727
+ const handleAction = vi.fn();
728
+ render(<Toaster />);
729
+
730
+ toaster.create({
731
+ title: 'Action needed',
732
+ type: 'warning',
733
+ action: {
734
+ label: 'Confirm',
735
+ onClick: handleAction,
736
+ },
737
+ });
738
+
739
+ const button = await screen.findByRole('button', { name: 'Confirm' });
740
+ await userEvent.click(button);
741
+
742
+ expect(handleAction).toHaveBeenCalledOnce();
743
+ });
744
+
745
+ test('dismisses toast when close button clicked', async () => {
746
+ render(<Toaster />);
747
+
748
+ toaster.create({
749
+ title: 'Dismissible',
750
+ type: 'success',
751
+ closable: true,
752
+ });
753
+
754
+ const closeButton = await screen.findByRole('button', { name: /close/i });
755
+ await userEvent.click(closeButton);
756
+
757
+ await waitFor(() => {
758
+ expect(screen.queryByText('Dismissible')).not.toBeInTheDocument();
759
+ });
760
+ });
761
+
762
+ test('automatically dismisses after duration', async () => {
763
+ render(<Toaster />);
764
+
765
+ toaster.create({
766
+ title: 'Auto dismiss',
767
+ type: 'success',
768
+ duration: 1000,
769
+ });
770
+
771
+ expect(await screen.findByText('Auto dismiss')).toBeInTheDocument();
772
+
773
+ await waitFor(() => {
774
+ expect(screen.queryByText('Auto dismiss')).not.toBeInTheDocument();
775
+ }, { timeout: 2000 });
776
+ });
777
+ ```
778
+
779
+ ## Related Components
780
+
781
+ - **Dialog**: For critical messages requiring explicit user acknowledgment
782
+ - **Alert**: For persistent, non-dismissible contextual messages within page content
783
+ - **Banner**: For system-wide announcements or important information
784
+ - **Snackbar**: Alternative term for Toast in some design systems