|
| 1 | +# Toast History |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +`ToastHistory` is a companion component to the existing `ToastProvider` that records every dismissed (or auto-expired) client toast into a bounded in-memory store and surfaces those entries as a reviewable notification-history panel. |
| 6 | + |
| 7 | +This bridges the gap between ephemeral toasts and the notifications center: users who miss a transient toast can open the history panel to review what happened. |
| 8 | + |
| 9 | +Server-derived notifications are deliberately kept separate — history entries carry `source: "toast"` so consuming UIs can filter them without ambiguity. |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Architecture |
| 14 | + |
| 15 | +``` |
| 16 | +ToastProvider (context) |
| 17 | +├── active toasts (visible queue, max 5) |
| 18 | +└── history store (dismissed toasts, max 50 entries) |
| 19 | + └── ToastHistoryEntry { id, severity, title, description, |
| 20 | + createdAt, dismissedAt, read, source } |
| 21 | +``` |
| 22 | + |
| 23 | +`ToastHistory` reads from the same context and renders the history list. `useToastHistoryUnreadCount` is a lightweight hook for badge counts in nav bars or notification-center icons. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Usage |
| 28 | + |
| 29 | +### Basic — drop into the notifications center |
| 30 | + |
| 31 | +```tsx |
| 32 | +import { ToastHistory } from '@/components/toast/ToastHistory'; |
| 33 | + |
| 34 | +// Inside any component rendered within ToastProvider: |
| 35 | +export function NotificationsPanel() { |
| 36 | + return ( |
| 37 | + <aside aria-label="Notifications panel"> |
| 38 | + <ToastHistory /> |
| 39 | + </aside> |
| 40 | + ); |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +### Limit rendered entries |
| 45 | + |
| 46 | +```tsx |
| 47 | +<ToastHistory maxEntries={10} /> |
| 48 | +``` |
| 49 | + |
| 50 | +### Show an unread badge in a nav icon |
| 51 | + |
| 52 | +```tsx |
| 53 | +import { useToastHistoryUnreadCount } from '@/components/toast/ToastHistory'; |
| 54 | + |
| 55 | +function NotificationsIcon() { |
| 56 | + const unread = useToastHistoryUnreadCount(); |
| 57 | + return ( |
| 58 | + <button type="button" aria-label={`Notifications${unread > 0 ? `, ${unread} unread` : ''}`}> |
| 59 | + <BellIcon /> |
| 60 | + {unread > 0 && <span aria-hidden="true">{unread}</span>} |
| 61 | + </button> |
| 62 | + ); |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +### Programmatic history access via `useToast` |
| 67 | + |
| 68 | +```tsx |
| 69 | +const { history, clearHistory, markHistoryRead, markAllHistoryRead } = useToast(); |
| 70 | +``` |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## API |
| 75 | + |
| 76 | +### `<ToastHistory>` Props |
| 77 | + |
| 78 | +| Prop | Type | Default | Description | |
| 79 | +|------|------|---------|-------------| |
| 80 | +| `maxEntries` | `number` | all entries | Maximum number of history items rendered. | |
| 81 | + |
| 82 | +### `useToastHistoryUnreadCount(): number` |
| 83 | + |
| 84 | +Returns the count of unread toast-history entries from context. |
| 85 | + |
| 86 | +### Context additions (`ToastContextValue`) |
| 87 | + |
| 88 | +| Property | Type | Description | |
| 89 | +|----------|------|-------------| |
| 90 | +| `history` | `ToastHistoryEntry[]` | Ordered (newest first) array of dismissed toasts. | |
| 91 | +| `clearHistory` | `() => void` | Remove all history entries. | |
| 92 | +| `markHistoryRead` | `(id: string) => void` | Mark a single entry as read. | |
| 93 | +| `markAllHistoryRead` | `() => void` | Mark all entries as read. | |
| 94 | + |
| 95 | +### `ToastHistoryEntry` shape |
| 96 | + |
| 97 | +```ts |
| 98 | +interface ToastHistoryEntry { |
| 99 | + id: string; |
| 100 | + severity: 'success' | 'error' | 'info' | 'warning'; |
| 101 | + title: string; |
| 102 | + description?: string; |
| 103 | + createdAt: number; // ms since epoch — when the toast first appeared |
| 104 | + dismissedAt: number; // ms since epoch — when it left the visible queue |
| 105 | + read: boolean; |
| 106 | + source: 'toast'; // always "toast" — never mixed with server notifications |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## Behavior |
| 113 | + |
| 114 | +- **Bounded store** — the history is capped at 50 entries (oldest are discarded first). |
| 115 | +- **No duplicates** — dismissing the same toast ID twice is a no-op in the store. |
| 116 | +- **Auto-expire recording** — toasts that auto-dismiss via their timer are recorded exactly as manual dismissals are. |
| 117 | +- **`dismissAll` recording** — all currently visible toasts are archived when `dismissAll` is called. |
| 118 | +- **Privacy-safe** — only `title`, `description`, `severity`, and timestamps are stored. No user-supplied action callbacks or sensitive payloads are retained. |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## Accessibility |
| 123 | + |
| 124 | +- The history panel is wrapped in a `<section aria-label="Notification history">` so keyboard users can jump to it with a landmarks shortcut. |
| 125 | +- The list uses `role="list"` with an `aria-label`. |
| 126 | +- Each list item has an `aria-label` describing severity, title, description, and dismissal time. |
| 127 | +- The unread badge uses `aria-label="N unread"` and is hidden from AT when count is zero. |
| 128 | +- "Mark read" and "Clear all" buttons have descriptive accessible labels. |
| 129 | +- No `prefers-reduced-motion` concerns — the panel is static (no animation). |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +## Related docs |
| 134 | + |
| 135 | +- [Toast System](./TOAST_SYSTEM.md) — the base toast provider, its API, and aria-live announcer details. |
| 136 | +- [Toast Actions](./TOAST_ACTIONS.md) — how to add interactive action buttons to toasts. |
0 commit comments