@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.
- package/README.md +12 -4
- package/dist/styles.css +5126 -0
- package/guidelines/Guidelines.md +92 -41
- package/guidelines/components/accordion.md +732 -0
- package/guidelines/components/avatar.md +1015 -0
- package/guidelines/components/badge.md +728 -0
- package/guidelines/components/button.md +75 -40
- package/guidelines/components/card.md +84 -25
- package/guidelines/components/checkbox.md +671 -0
- package/guidelines/components/dialog.md +619 -31
- package/guidelines/components/drawer.md +1616 -0
- package/guidelines/components/heading.md +576 -0
- package/guidelines/components/icon-button.md +92 -37
- package/guidelines/components/input-addon.md +685 -0
- package/guidelines/components/input-group.md +830 -0
- package/guidelines/components/input.md +92 -37
- package/guidelines/components/popover.md +1271 -0
- package/guidelines/components/progress.md +836 -0
- package/guidelines/components/radio-group.md +852 -0
- package/guidelines/components/select.md +1662 -0
- package/guidelines/components/skeleton.md +802 -0
- package/guidelines/components/slider.md +911 -0
- package/guidelines/components/spinner.md +783 -0
- package/guidelines/components/switch.md +105 -38
- package/guidelines/components/tabs.md +1488 -0
- package/guidelines/components/textarea.md +495 -0
- package/guidelines/components/toast.md +784 -0
- package/guidelines/components/tooltip.md +912 -0
- package/guidelines/design-tokens/colors.md +309 -72
- package/guidelines/design-tokens/elevation.md +615 -45
- package/guidelines/design-tokens/spacing.md +654 -74
- package/guidelines/design-tokens/typography.md +432 -50
- package/guidelines/overview-components.md +60 -8
- package/guidelines/overview-imports.md +314 -0
- package/guidelines/overview-patterns.md +3852 -0
- 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
|