Skip to content

Commit 6f5777c

Browse files
fix: omit optional keys instead of setting undefined to satisfy exactOptionalPropertyTypes (#1575)
Co-authored-by: 1nonlypiece <jagadeesh26062002@gmail.com>
1 parent 0ce41f6 commit 6f5777c

11 files changed

Lines changed: 1824 additions & 63 deletions

docs/TOAST_ACTIONS.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Toast Actions
2+
3+
`ToastProvider` supports one optional action per toast. Use it for a short follow-up such as undoing a reversible local change or sending the user to a related view.
4+
5+
```tsx
6+
const toast = useToast();
7+
8+
toast.success({
9+
title: 'Listing cancelled',
10+
description: 'The listing is no longer visible in the marketplace.',
11+
action: {
12+
label: 'Undo',
13+
onClick: () => restoreListing(),
14+
},
15+
});
16+
```
17+
18+
Actions dismiss their toast after the handler runs by default. Set `dismiss: false` when the toast should remain visible after the action.
19+
20+
```tsx
21+
toast.info({
22+
title: 'Export ready',
23+
action: {
24+
label: 'View',
25+
dismiss: false,
26+
onClick: () => router.push('/exports/latest'),
27+
},
28+
});
29+
```
30+
31+
Auto-dismiss timers continue to use each toast's configured `duration`, and they pause while the toast is hovered or contains keyboard focus.

docs/TOAST_HISTORY.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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.

docs/TOAST_SYSTEM.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Toast System
2+
3+
## Overview
4+
5+
A global, accessible toast notification system for consistent success, error, info, and warning feedback across the app.
6+
7+
## Usage
8+
9+
Wrap the app with `ToastProvider` in `src/app/layout.tsx`:
10+
11+
```tsx
12+
import { ToastProvider } from '@/components/toast/ToastProvider';
13+
14+
export default function RootLayout({ children }) {
15+
return (
16+
<html lang="en">
17+
<body>
18+
<ToastProvider>{children}</ToastProvider>
19+
</body>
20+
</html>
21+
);
22+
}
23+
```
24+
25+
Use `useToast` in client components:
26+
27+
```tsx
28+
'use client';
29+
import { useToast } from '@/components/toast/ToastProvider';
30+
31+
function Example() {
32+
const toast = useToast();
33+
34+
return (
35+
<button
36+
onClick={() =>
37+
toast.success({
38+
title: 'Saved',
39+
description: 'Your changes were saved successfully.',
40+
})
41+
}
42+
>
43+
Save
44+
</button>
45+
);
46+
}
47+
```
48+
49+
## API
50+
51+
- `success(options)`
52+
- `error(options)`
53+
- `info(options)`
54+
- `warning(options)`
55+
- `dismiss(id)`
56+
- `dismissAll()`
57+
58+
Options:
59+
- `title: string`
60+
- `description?: string`
61+
- `duration?: number` (ms; `0` disables auto-dismiss)
62+
63+
## Behavior
64+
65+
- Auto-dismiss after `duration` (default `5000` ms).
66+
- Pause on hover/focus; resume on leave/blur.
67+
- Max visible toasts: `5`; extra toasts are dropped from the visible queue.
68+
- Accessible live regions for screen readers.
69+
- Respects `prefers-reduced-motion`.
70+
71+
## Aria-live Announcer
72+
73+
`ToastProvider` renders two visually-hidden `aria-live` regions that announce toast text to screen readers as toasts are added.
74+
75+
| Severity | Region | `aria-live` |
76+
|----------|--------|-------------|
77+
| `error` | `[data-toast-announcer="assertive"]` | `assertive` |
78+
| `success`, `info`, `warning` | `[data-toast-announcer="polite"]` | `polite` |
79+
80+
The announced text is the toast `title` (and `description`, if present, joined with ``). When a new toast fires, the opposing region is cleared to avoid stale announcements. The regions use `aria-atomic="true"` so assistive tech reads the full updated text.
81+
82+
The regions are hidden from view using inline clip/overflow styles (equivalent to a `.sr-only` class) and are never interactive.
83+
84+
### Custom transport hook
85+
86+
To forward error records to an observability sink alongside announcements, see `src/lib/observability/reportError.ts` (wired into `src/app/error.tsx`).

0 commit comments

Comments
 (0)