@tribepad/themis 1.0.17 → 1.1.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/dist/elements/Accordion/Accordion.d.ts +3 -1
- package/dist/elements/Accordion/Accordion.d.ts.map +1 -1
- package/dist/elements/Accordion/index.js +1 -1
- package/dist/elements/Accordion/index.js.map +1 -1
- package/dist/elements/Accordion/index.mjs +1 -1
- package/dist/elements/Accordion/index.mjs.map +1 -1
- package/dist/elements/AlertDialog/index.js +1 -1
- package/dist/elements/AlertDialog/index.js.map +1 -1
- package/dist/elements/AlertDialog/index.mjs +1 -1
- package/dist/elements/AlertDialog/index.mjs.map +1 -1
- package/dist/elements/Breadcrumbs/index.js +1 -1
- package/dist/elements/Breadcrumbs/index.js.map +1 -1
- package/dist/elements/Breadcrumbs/index.mjs +1 -1
- package/dist/elements/Breadcrumbs/index.mjs.map +1 -1
- package/dist/elements/Button/Button.d.ts.map +1 -1
- package/dist/elements/Button/index.js +1 -1
- package/dist/elements/Button/index.js.map +1 -1
- package/dist/elements/Button/index.mjs +1 -1
- package/dist/elements/Button/index.mjs.map +1 -1
- package/dist/elements/Card/Card.d.ts +21 -1
- package/dist/elements/Card/Card.d.ts.map +1 -1
- package/dist/elements/Card/Card.types.d.ts +36 -0
- package/dist/elements/Card/Card.types.d.ts.map +1 -1
- package/dist/elements/Card/index.d.ts +10 -3
- package/dist/elements/Card/index.d.ts.map +1 -1
- package/dist/elements/Card/index.js +1 -1
- package/dist/elements/Card/index.js.map +1 -1
- package/dist/elements/Card/index.mjs +1 -1
- package/dist/elements/Card/index.mjs.map +1 -1
- package/dist/elements/Carousel/Carousel.d.ts.map +1 -1
- package/dist/elements/Carousel/index.js +1 -1
- package/dist/elements/Carousel/index.js.map +1 -1
- package/dist/elements/Carousel/index.mjs +1 -1
- package/dist/elements/Carousel/index.mjs.map +1 -1
- package/dist/elements/Chart/Chart.d.ts.map +1 -1
- package/dist/elements/Chart/Chart.types.d.ts +21 -4
- package/dist/elements/Chart/Chart.types.d.ts.map +1 -1
- package/dist/elements/Chart/ChartAreaSeries.d.ts +32 -0
- package/dist/elements/Chart/ChartAreaSeries.d.ts.map +1 -0
- package/dist/elements/Chart/ChartContext.d.ts +4 -1
- package/dist/elements/Chart/ChartContext.d.ts.map +1 -1
- package/dist/elements/Chart/ChartLegend.d.ts +2 -2
- package/dist/elements/Chart/ChartLegend.d.ts.map +1 -1
- package/dist/elements/Chart/ChartPieSeries.d.ts +28 -0
- package/dist/elements/Chart/ChartPieSeries.d.ts.map +1 -0
- package/dist/elements/Chart/ChartRadarSeries.d.ts +28 -0
- package/dist/elements/Chart/ChartRadarSeries.d.ts.map +1 -0
- package/dist/elements/Chart/ChartSVG.d.ts +7 -1
- package/dist/elements/Chart/ChartSVG.d.ts.map +1 -1
- package/dist/elements/Chart/ChartScatterSeries.d.ts +31 -0
- package/dist/elements/Chart/ChartScatterSeries.d.ts.map +1 -0
- package/dist/elements/Chart/index.js +1 -1
- package/dist/elements/Chart/index.js.map +1 -1
- package/dist/elements/Chart/index.mjs +1 -1
- package/dist/elements/Chart/index.mjs.map +1 -1
- package/dist/elements/Checkbox/index.js +1 -1
- package/dist/elements/Checkbox/index.js.map +1 -1
- package/dist/elements/Checkbox/index.mjs +1 -1
- package/dist/elements/Checkbox/index.mjs.map +1 -1
- package/dist/elements/ColorPicker/ColorPicker.d.ts.map +1 -1
- package/dist/elements/ColorPicker/ColorPicker.styles.d.ts +3 -1
- package/dist/elements/ColorPicker/ColorPicker.styles.d.ts.map +1 -1
- package/dist/elements/ColorPicker/index.js +1 -1
- package/dist/elements/ColorPicker/index.js.map +1 -1
- package/dist/elements/ColorPicker/index.mjs +1 -1
- package/dist/elements/ColorPicker/index.mjs.map +1 -1
- package/dist/elements/Combobox/Combobox.d.ts +1 -1
- package/dist/elements/Combobox/Combobox.d.ts.map +1 -1
- package/dist/elements/Combobox/Combobox.styles.d.ts.map +1 -1
- package/dist/elements/Combobox/index.js +1 -1
- package/dist/elements/Combobox/index.js.map +1 -1
- package/dist/elements/Combobox/index.mjs +1 -1
- package/dist/elements/Combobox/index.mjs.map +1 -1
- package/dist/elements/DatePicker/DatePicker.d.ts.map +1 -1
- package/dist/elements/DatePicker/index.js +1 -1
- package/dist/elements/DatePicker/index.js.map +1 -1
- package/dist/elements/DatePicker/index.mjs +1 -1
- package/dist/elements/DatePicker/index.mjs.map +1 -1
- package/dist/elements/Dropdown/Dropdown.d.ts.map +1 -1
- package/dist/elements/Dropdown/index.js +1 -1
- package/dist/elements/Dropdown/index.js.map +1 -1
- package/dist/elements/Dropdown/index.mjs +1 -1
- package/dist/elements/Dropdown/index.mjs.map +1 -1
- package/dist/elements/FileField/index.js +1 -1
- package/dist/elements/FileField/index.js.map +1 -1
- package/dist/elements/FileField/index.mjs +1 -1
- package/dist/elements/FileField/index.mjs.map +1 -1
- package/dist/elements/Modal/Modal.d.ts +2 -1
- package/dist/elements/Modal/Modal.d.ts.map +1 -1
- package/dist/elements/Modal/index.js +1 -1
- package/dist/elements/Modal/index.js.map +1 -1
- package/dist/elements/Modal/index.mjs +1 -1
- package/dist/elements/Modal/index.mjs.map +1 -1
- package/dist/elements/NumberField/NumberField.d.ts.map +1 -1
- package/dist/elements/NumberField/NumberField.types.d.ts +1 -1
- package/dist/elements/NumberField/NumberField.variants.d.ts.map +1 -1
- package/dist/elements/NumberField/index.js +1 -1
- package/dist/elements/NumberField/index.js.map +1 -1
- package/dist/elements/NumberField/index.mjs +1 -1
- package/dist/elements/NumberField/index.mjs.map +1 -1
- package/dist/elements/OTPInput/index.js +1 -1
- package/dist/elements/OTPInput/index.js.map +1 -1
- package/dist/elements/OTPInput/index.mjs +1 -1
- package/dist/elements/OTPInput/index.mjs.map +1 -1
- package/dist/elements/Pagination/index.js +1 -1
- package/dist/elements/Pagination/index.js.map +1 -1
- package/dist/elements/Pagination/index.mjs +1 -1
- package/dist/elements/Pagination/index.mjs.map +1 -1
- package/dist/elements/Panel/index.js +1 -1
- package/dist/elements/Panel/index.js.map +1 -1
- package/dist/elements/Panel/index.mjs +1 -1
- package/dist/elements/Panel/index.mjs.map +1 -1
- package/dist/elements/PasswordField/index.js +1 -1
- package/dist/elements/PasswordField/index.js.map +1 -1
- package/dist/elements/PasswordField/index.mjs +1 -1
- package/dist/elements/PasswordField/index.mjs.map +1 -1
- package/dist/elements/Progress/index.js +1 -1
- package/dist/elements/Progress/index.js.map +1 -1
- package/dist/elements/Progress/index.mjs +1 -1
- package/dist/elements/Progress/index.mjs.map +1 -1
- package/dist/elements/RatingScale/index.js +1 -1
- package/dist/elements/RatingScale/index.js.map +1 -1
- package/dist/elements/RatingScale/index.mjs +1 -1
- package/dist/elements/RatingScale/index.mjs.map +1 -1
- package/dist/elements/Resizable/index.js +1 -1
- package/dist/elements/Resizable/index.js.map +1 -1
- package/dist/elements/Resizable/index.mjs +1 -1
- package/dist/elements/Resizable/index.mjs.map +1 -1
- package/dist/elements/SearchField/index.js +1 -1
- package/dist/elements/SearchField/index.js.map +1 -1
- package/dist/elements/SearchField/index.mjs +1 -1
- package/dist/elements/SearchField/index.mjs.map +1 -1
- package/dist/elements/Select/Select.d.ts.map +1 -1
- package/dist/elements/Select/Select.styles.d.ts.map +1 -1
- package/dist/elements/Select/index.js +1 -1
- package/dist/elements/Select/index.js.map +1 -1
- package/dist/elements/Select/index.mjs +1 -1
- package/dist/elements/Select/index.mjs.map +1 -1
- package/dist/elements/Sidebar/Sidebar.styles.d.ts +41 -0
- package/dist/elements/Sidebar/Sidebar.styles.d.ts.map +1 -0
- package/dist/elements/Sidebar/Sidebar.types.d.ts +224 -0
- package/dist/elements/Sidebar/Sidebar.types.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarCompound.d.ts +39 -0
- package/dist/elements/Sidebar/SidebarCompound.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarMobileDrawer.d.ts +7 -0
- package/dist/elements/Sidebar/SidebarMobileDrawer.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarPanel.d.ts +21 -0
- package/dist/elements/Sidebar/SidebarPanel.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarPopover.d.ts +13 -0
- package/dist/elements/Sidebar/SidebarPopover.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarRail.d.ts +10 -0
- package/dist/elements/Sidebar/SidebarRail.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarRoot.d.ts +24 -0
- package/dist/elements/Sidebar/SidebarRoot.d.ts.map +1 -0
- package/dist/elements/Sidebar/index.d.ts +5 -0
- package/dist/elements/Sidebar/index.d.ts.map +1 -0
- package/dist/elements/Sidebar/index.js +3 -0
- package/dist/elements/Sidebar/index.js.map +1 -0
- package/dist/elements/Sidebar/index.mjs +3 -0
- package/dist/elements/Sidebar/index.mjs.map +1 -0
- package/dist/elements/SituationalJudgement/index.js +1 -1
- package/dist/elements/SituationalJudgement/index.js.map +1 -1
- package/dist/elements/SituationalJudgement/index.mjs +1 -1
- package/dist/elements/SituationalJudgement/index.mjs.map +1 -1
- package/dist/elements/Table/index.js +1 -1
- package/dist/elements/Table/index.js.map +1 -1
- package/dist/elements/Table/index.mjs +1 -1
- package/dist/elements/Table/index.mjs.map +1 -1
- package/dist/elements/Tabs/index.js +1 -1
- package/dist/elements/Tabs/index.js.map +1 -1
- package/dist/elements/Tabs/index.mjs +1 -1
- package/dist/elements/Tabs/index.mjs.map +1 -1
- package/dist/elements/TextField/TextField.d.ts.map +1 -1
- package/dist/elements/TextField/TextField.styles.d.ts.map +1 -1
- package/dist/elements/TextField/index.js +1 -1
- package/dist/elements/TextField/index.js.map +1 -1
- package/dist/elements/TextField/index.mjs +1 -1
- package/dist/elements/TextField/index.mjs.map +1 -1
- package/dist/elements/TimeField/TimeField.d.ts.map +1 -1
- package/dist/elements/TimeField/TimeField.types.d.ts.map +1 -1
- package/dist/elements/TimeField/index.js +1 -1
- package/dist/elements/TimeField/index.js.map +1 -1
- package/dist/elements/TimeField/index.mjs +1 -1
- package/dist/elements/TimeField/index.mjs.map +1 -1
- package/dist/elements/Toast/Toast.d.ts +21 -43
- package/dist/elements/Toast/Toast.d.ts.map +1 -1
- package/dist/elements/Toast/Toast.types.d.ts +27 -1
- package/dist/elements/Toast/Toast.types.d.ts.map +1 -1
- package/dist/elements/Toast/Toaster.d.ts +0 -12
- package/dist/elements/Toast/Toaster.d.ts.map +1 -1
- package/dist/elements/Toast/index.d.ts +1 -1
- package/dist/elements/Toast/index.d.ts.map +1 -1
- package/dist/elements/Toast/index.js +1 -1
- package/dist/elements/Toast/index.js.map +1 -1
- package/dist/elements/Toast/index.mjs +1 -1
- package/dist/elements/Toast/index.mjs.map +1 -1
- package/dist/elements/Tooltip/index.js +1 -1
- package/dist/elements/Tooltip/index.js.map +1 -1
- package/dist/elements/Tooltip/index.mjs +1 -1
- package/dist/elements/Tooltip/index.mjs.map +1 -1
- package/dist/elements/index.d.ts +5 -1
- package/dist/elements/index.d.ts.map +1 -1
- package/dist/elements/index.js +1 -1
- package/dist/elements/index.js.map +1 -1
- package/dist/elements/index.mjs +1 -1
- package/dist/elements/index.mjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/hooks/useMediaQuery.d.ts +12 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/styles/animations.css +110 -0
- package/dist/styles/defaults.css +32 -0
- package/dist/styles/index.d.ts +1 -1
- package/dist/styles/index.d.ts.map +1 -1
- package/dist/styles/index.js +1 -1
- package/dist/styles/index.js.map +1 -1
- package/dist/styles/index.mjs +1 -1
- package/dist/styles/index.mjs.map +1 -1
- package/dist/styles/shared-variants.d.ts +21 -2
- package/dist/styles/shared-variants.d.ts.map +1 -1
- package/package.json +40 -34
- package/src/elements/Chart/Chart.stories.tsx +186 -2
- package/src/elements/Modal/Modal.stories.tsx +6 -9
- package/src/elements/Sidebar/Sidebar.stories.tsx +514 -0
- package/src/elements/Toast/Toast.stories.tsx +745 -648
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Toast Component Stories
|
|
3
|
-
* Storybook stories for Toast component
|
|
4
|
-
*
|
|
5
|
-
* @see toast-prd.md - Product Requirements Document
|
|
6
|
-
* @see plan.md - Implementation Plan
|
|
7
|
-
* @see constitution.md Principle IV (Accessibility First - WCAG 2.2 AAA)
|
|
3
|
+
* Storybook stories for the goey-inspired Toast component
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
6
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
11
7
|
import { useEffect, useState, type ReactElement } from 'react';
|
|
12
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Bell,
|
|
10
|
+
CheckCircle2,
|
|
11
|
+
Download,
|
|
12
|
+
Trash2,
|
|
13
|
+
Sparkles,
|
|
14
|
+
UserPlus,
|
|
15
|
+
Shield,
|
|
16
|
+
Clock,
|
|
17
|
+
RefreshCw,
|
|
18
|
+
FileText,
|
|
19
|
+
} from 'lucide-react';
|
|
13
20
|
import { ToastProvider } from './ToastProvider';
|
|
14
21
|
import { Toaster } from './Toaster';
|
|
15
22
|
import { toast, toastQueue } from './Toast';
|
|
16
23
|
import { Button } from '../Button';
|
|
17
|
-
import type { ToasterPosition
|
|
24
|
+
import type { ToasterPosition } from './Toast.types';
|
|
18
25
|
|
|
19
26
|
// =============================================================================
|
|
20
|
-
// Story Wrapper
|
|
27
|
+
// Story Wrapper
|
|
21
28
|
// =============================================================================
|
|
22
29
|
|
|
23
30
|
interface ToastStoryWrapperProps {
|
|
@@ -26,24 +33,18 @@ interface ToastStoryWrapperProps {
|
|
|
26
33
|
children: ReactElement;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
/**
|
|
30
|
-
* Wrapper component that provides ToastProvider and Toaster for stories
|
|
31
|
-
*/
|
|
32
36
|
function ToastStoryWrapper({
|
|
33
37
|
position = 'bottom-right',
|
|
34
38
|
maxVisibleToasts = 3,
|
|
35
39
|
children,
|
|
36
40
|
}: ToastStoryWrapperProps): ReactElement {
|
|
37
|
-
// Clear toasts on unmount to prevent leaks between stories
|
|
38
41
|
useEffect(() => {
|
|
39
|
-
return () => {
|
|
40
|
-
toastQueue.closeAll();
|
|
41
|
-
};
|
|
42
|
+
return () => { toastQueue.closeAll(); };
|
|
42
43
|
}, []);
|
|
43
44
|
|
|
44
45
|
return (
|
|
45
46
|
<ToastProvider maxVisibleToasts={maxVisibleToasts}>
|
|
46
|
-
<div className="min-h-[
|
|
47
|
+
<div className="min-h-[420px] w-full p-8 bg-[var(--page-background)]">
|
|
47
48
|
{children}
|
|
48
49
|
</div>
|
|
49
50
|
<Toaster position={position} aria-label="Notifications" />
|
|
@@ -52,7 +53,7 @@ function ToastStoryWrapper({
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
// =============================================================================
|
|
55
|
-
// Meta
|
|
56
|
+
// Meta
|
|
56
57
|
// =============================================================================
|
|
57
58
|
|
|
58
59
|
const meta = {
|
|
@@ -62,17 +63,28 @@ const meta = {
|
|
|
62
63
|
layout: 'fullscreen',
|
|
63
64
|
docs: {
|
|
64
65
|
description: {
|
|
65
|
-
component: `
|
|
66
|
+
component: `Goey-inspired toast notification system with organic blob morphing animations, built on React Aria toast primitives with WCAG 2.2 AAA compliance.
|
|
67
|
+
|
|
68
|
+
**API at a glance:**
|
|
69
|
+
\`\`\`ts
|
|
70
|
+
// Convenience methods (recommended)
|
|
71
|
+
toast.success('Saved!', { title: 'Upload Complete', description: 'document.pdf was uploaded.' });
|
|
72
|
+
toast.error('Failed', { title: 'Connection Lost', description: 'Check your network.' });
|
|
73
|
+
toast.warning('Heads up', { title: 'Low Storage', description: '90% of storage used.' });
|
|
74
|
+
toast.info('FYI', { title: 'New Feature', description: 'Dark mode is now available.' });
|
|
75
|
+
|
|
76
|
+
// Full control
|
|
77
|
+
toast('Message', { variant: 'primary', timeout: 0, onClose: (key, reason) => {} });
|
|
78
|
+
\`\`\`
|
|
66
79
|
|
|
67
80
|
**Key Features:**
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
- "Clear All" button for bulk dismissal`,
|
|
81
|
+
- 7 variants: \`default\` \`primary\` \`destructive\` \`accent\` \`success\` \`warning\` \`info\`
|
|
82
|
+
- Organic blob shape with spring-physics entry/exit morphing
|
|
83
|
+
- Progress countdown bar with hover-to-pause
|
|
84
|
+
- Auto variant icons — override with custom \`icon\` prop
|
|
85
|
+
- Structured content via \`title\` + \`description\` props
|
|
86
|
+
- 6 position options, queue management, swipe-to-dismiss
|
|
87
|
+
- Full WCAG 2.2 AAA compliance`,
|
|
76
88
|
},
|
|
77
89
|
},
|
|
78
90
|
},
|
|
@@ -82,16 +94,12 @@ const meta = {
|
|
|
82
94
|
control: 'select',
|
|
83
95
|
options: ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'],
|
|
84
96
|
description: 'Position of the toast container',
|
|
85
|
-
table: {
|
|
86
|
-
defaultValue: { summary: 'bottom-right' },
|
|
87
|
-
},
|
|
97
|
+
table: { defaultValue: { summary: 'bottom-right' } },
|
|
88
98
|
},
|
|
89
99
|
'aria-label': {
|
|
90
100
|
control: 'text',
|
|
91
101
|
description: 'Accessible label for the toast region',
|
|
92
|
-
table: {
|
|
93
|
-
defaultValue: { summary: 'Notifications' },
|
|
94
|
-
},
|
|
102
|
+
table: { defaultValue: { summary: 'Notifications' } },
|
|
95
103
|
},
|
|
96
104
|
},
|
|
97
105
|
} satisfies Meta<typeof Toaster>;
|
|
@@ -100,137 +108,187 @@ export default meta;
|
|
|
100
108
|
type Story = StoryObj<typeof meta>;
|
|
101
109
|
|
|
102
110
|
// =============================================================================
|
|
103
|
-
//
|
|
111
|
+
// 1. Default — fires on load so the canvas isn't empty
|
|
104
112
|
// =============================================================================
|
|
105
113
|
|
|
106
|
-
/**
|
|
107
|
-
* Default toast with simple string content
|
|
108
|
-
*/
|
|
109
114
|
export const Default: Story = {
|
|
110
|
-
render: () =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
render: () => {
|
|
116
|
+
function DefaultStory() {
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
toast.success('', {
|
|
119
|
+
title: 'Changes saved',
|
|
120
|
+
description: 'Your profile has been updated successfully.',
|
|
121
|
+
});
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<ToastStoryWrapper>
|
|
126
|
+
<div className="flex flex-col items-start gap-4">
|
|
127
|
+
<p className="text-sm text-[var(--muted-foreground)]">
|
|
128
|
+
The toast fires automatically on load. Click the button to fire another.
|
|
129
|
+
</p>
|
|
130
|
+
<Button
|
|
131
|
+
onPress={() =>
|
|
132
|
+
toast.success('', {
|
|
133
|
+
title: 'Changes saved',
|
|
134
|
+
description: 'Your profile has been updated successfully.',
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
>
|
|
138
|
+
Show Toast
|
|
139
|
+
</Button>
|
|
140
|
+
</div>
|
|
141
|
+
</ToastStoryWrapper>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return <DefaultStory />;
|
|
145
|
+
},
|
|
146
|
+
parameters: {
|
|
147
|
+
docs: {
|
|
148
|
+
description: {
|
|
149
|
+
story: 'A success toast with title and description — the most common pattern. Notice the blob shape, left accent border, and progress countdown bar.',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
122
153
|
};
|
|
123
154
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// 2. All Variants — pre-fires all 7 so you can see the design
|
|
157
|
+
// =============================================================================
|
|
158
|
+
|
|
127
159
|
export const AllVariants: Story = {
|
|
128
|
-
render: () =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
</
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
</Button>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
161
|
-
</ToastStoryWrapper>
|
|
162
|
-
),
|
|
160
|
+
render: () => {
|
|
161
|
+
function AllVariantsStory() {
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
toast('Default notification', { variant: 'default', timeout: 0 });
|
|
164
|
+
toast.success('', { title: 'Upload complete', description: 'document.pdf saved.' });
|
|
165
|
+
toast.error('', { title: 'Connection error', description: 'Unable to reach the server.' });
|
|
166
|
+
toast.warning('', { title: 'Low storage', description: '90% of storage used.' });
|
|
167
|
+
toast.info('', { title: 'New version available', description: 'v2.1.0 is ready to install.' });
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<ToastStoryWrapper maxVisibleToasts={5}>
|
|
172
|
+
<div className="flex flex-col items-start gap-3">
|
|
173
|
+
<p className="text-sm font-medium text-[var(--content-foreground)]">Fire individual variants:</p>
|
|
174
|
+
<div className="flex flex-wrap gap-2">
|
|
175
|
+
<Button size="sm" variant="outline" onPress={() => toast('Default', { variant: 'default', timeout: 0 })}>Default</Button>
|
|
176
|
+
<Button size="sm" variant="default" onPress={() => toast('Primary', { variant: 'primary', timeout: 0 })}>Primary</Button>
|
|
177
|
+
<Button size="sm" variant="destructive" onPress={() => toast.error('Something went wrong')}>Destructive</Button>
|
|
178
|
+
<Button size="sm" variant="secondary" onPress={() => toast('Accent', { variant: 'accent', timeout: 0 })}>Accent</Button>
|
|
179
|
+
<Button size="sm" variant="outline" onPress={() => toast.success('All good!')}>Success</Button>
|
|
180
|
+
<Button size="sm" variant="outline" onPress={() => toast.warning('Heads up')}>Warning</Button>
|
|
181
|
+
<Button size="sm" variant="outline" onPress={() => toast.info('Just so you know')}>Info</Button>
|
|
182
|
+
</div>
|
|
183
|
+
<Button variant="ghost" size="sm" onPress={() => toastQueue.closeAll()}>Clear all</Button>
|
|
184
|
+
</div>
|
|
185
|
+
</ToastStoryWrapper>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return <AllVariantsStory />;
|
|
189
|
+
},
|
|
163
190
|
parameters: {
|
|
164
191
|
docs: {
|
|
165
192
|
description: {
|
|
166
|
-
story: '
|
|
193
|
+
story: 'All 7 variants pre-fired on load. Semantic variants (success, warning, info, destructive) include auto icons and a coloured left-border accent. Default and primary use solid fills.',
|
|
167
194
|
},
|
|
168
195
|
},
|
|
169
196
|
},
|
|
170
197
|
};
|
|
171
198
|
|
|
172
199
|
// =============================================================================
|
|
173
|
-
//
|
|
200
|
+
// 3. Title + Description — structured content
|
|
174
201
|
// =============================================================================
|
|
175
202
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
203
|
+
export const TitleAndDescription: Story = {
|
|
204
|
+
render: () => {
|
|
205
|
+
function TitleDescStory() {
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
toast.info('', {
|
|
208
|
+
title: 'New feature available',
|
|
209
|
+
description: 'Dark mode is now available in Settings → Appearance.',
|
|
210
|
+
});
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<ToastStoryWrapper>
|
|
215
|
+
<div className="flex flex-col items-start gap-3">
|
|
216
|
+
<p className="text-sm text-[var(--muted-foreground)]">
|
|
217
|
+
Use <code className="text-xs bg-[var(--muted)] px-1 rounded">title</code> and <code className="text-xs bg-[var(--muted)] px-1 rounded">description</code> for structured notifications.
|
|
218
|
+
</p>
|
|
219
|
+
<div className="flex flex-wrap gap-2">
|
|
220
|
+
<Button
|
|
221
|
+
size="sm" variant="outline"
|
|
222
|
+
onPress={() => toast.success('', {
|
|
223
|
+
title: 'Upload complete',
|
|
224
|
+
description: 'report_q4.pdf has been saved to your workspace.',
|
|
225
|
+
})}
|
|
226
|
+
>
|
|
227
|
+
Success
|
|
228
|
+
</Button>
|
|
229
|
+
<Button
|
|
230
|
+
size="sm" variant="outline"
|
|
231
|
+
onPress={() => toast.error('', {
|
|
232
|
+
title: 'Payment failed',
|
|
233
|
+
description: 'Your card ending in 4242 was declined.',
|
|
234
|
+
})}
|
|
235
|
+
>
|
|
236
|
+
Error
|
|
237
|
+
</Button>
|
|
238
|
+
<Button
|
|
239
|
+
size="sm" variant="outline"
|
|
240
|
+
onPress={() => toast.warning('', {
|
|
241
|
+
title: 'Session expiring',
|
|
242
|
+
description: 'You will be signed out in 5 minutes.',
|
|
243
|
+
})}
|
|
244
|
+
>
|
|
245
|
+
Warning
|
|
246
|
+
</Button>
|
|
247
|
+
<Button
|
|
248
|
+
size="sm" variant="outline"
|
|
249
|
+
onPress={() => toast.info('', {
|
|
250
|
+
title: 'Maintenance scheduled',
|
|
251
|
+
description: 'The service will be offline Sunday 2–4 AM.',
|
|
252
|
+
})}
|
|
253
|
+
>
|
|
254
|
+
Info
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</ToastStoryWrapper>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return <TitleDescStory />;
|
|
262
|
+
},
|
|
263
|
+
parameters: {
|
|
264
|
+
docs: {
|
|
265
|
+
description: {
|
|
266
|
+
story: 'Provide `title` for a bold heading and `description` for supporting detail. The accessible name of the toast is derived from the title automatically.',
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// =============================================================================
|
|
273
|
+
// 4. Convenience Methods
|
|
274
|
+
// =============================================================================
|
|
275
|
+
|
|
276
|
+
export const ConvenienceMethods: Story = {
|
|
180
277
|
render: () => (
|
|
181
278
|
<ToastStoryWrapper>
|
|
182
279
|
<div className="flex flex-col items-start gap-4">
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
>
|
|
198
|
-
With Success Icon
|
|
199
|
-
</Button>
|
|
200
|
-
<Button
|
|
201
|
-
variant="outline"
|
|
202
|
-
onPress={() =>
|
|
203
|
-
toast(
|
|
204
|
-
<div className="flex items-center gap-2">
|
|
205
|
-
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
|
206
|
-
<span>Your session will expire in 5 minutes</span>
|
|
207
|
-
</div>,
|
|
208
|
-
{ variant: 'accent' }
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
>
|
|
212
|
-
With Warning Icon
|
|
213
|
-
</Button>
|
|
214
|
-
<Button
|
|
215
|
-
variant="outline"
|
|
216
|
-
onPress={() =>
|
|
217
|
-
toast(
|
|
218
|
-
<div className="flex items-center gap-2">
|
|
219
|
-
<Info className="h-4 w-4" />
|
|
220
|
-
<span>New version available</span>
|
|
221
|
-
<button
|
|
222
|
-
type="button"
|
|
223
|
-
className="ml-2 text-xs underline hover:no-underline"
|
|
224
|
-
onClick={() => alert('Updating...')}
|
|
225
|
-
>
|
|
226
|
-
Update now
|
|
227
|
-
</button>
|
|
228
|
-
</div>
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
>
|
|
232
|
-
With Action Button
|
|
233
|
-
</Button>
|
|
280
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--muted)] p-4 font-mono text-xs leading-relaxed">
|
|
281
|
+
<p className="text-[var(--muted-foreground)] mb-2">// Shorthand — variant and icon are set automatically</p>
|
|
282
|
+
<p><span className="text-[var(--success-background)]">toast.success</span>(<span className="text-[var(--primary)]">'Changes saved!'</span>)</p>
|
|
283
|
+
<p><span className="text-[var(--destructive-background)]">toast.error</span>(<span className="text-[var(--primary)]">'Upload failed'</span>)</p>
|
|
284
|
+
<p><span className="text-[var(--warning-background)]">toast.warning</span>(<span className="text-[var(--primary)]">'Low storage'</span>)</p>
|
|
285
|
+
<p><span className="text-[var(--info-background)]">toast.info</span>(<span className="text-[var(--primary)]">'Update available'</span>)</p>
|
|
286
|
+
</div>
|
|
287
|
+
<div className="flex flex-wrap gap-2">
|
|
288
|
+
<Button size="sm" variant="outline" onPress={() => toast.success('Changes saved!')}>toast.success()</Button>
|
|
289
|
+
<Button size="sm" variant="outline" onPress={() => toast.error('Upload failed')}>toast.error()</Button>
|
|
290
|
+
<Button size="sm" variant="outline" onPress={() => toast.warning('Low storage space')}>toast.warning()</Button>
|
|
291
|
+
<Button size="sm" variant="outline" onPress={() => toast.info('Update available')}>toast.info()</Button>
|
|
234
292
|
</div>
|
|
235
293
|
</div>
|
|
236
294
|
</ToastStoryWrapper>
|
|
@@ -238,104 +296,232 @@ export const WithCustomContent: Story = {
|
|
|
238
296
|
parameters: {
|
|
239
297
|
docs: {
|
|
240
298
|
description: {
|
|
241
|
-
story: '
|
|
299
|
+
story: 'Convenience methods set the correct variant and icon automatically. All accept the same options as `toast()` minus the `variant` prop.',
|
|
242
300
|
},
|
|
243
301
|
},
|
|
244
302
|
},
|
|
245
303
|
};
|
|
246
304
|
|
|
247
305
|
// =============================================================================
|
|
248
|
-
//
|
|
306
|
+
// 5. Custom Icons
|
|
249
307
|
// =============================================================================
|
|
250
308
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
309
|
+
export const CustomIcons: Story = {
|
|
310
|
+
render: () => {
|
|
311
|
+
function CustomIconsStory() {
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
toast.success('', {
|
|
314
|
+
icon: <Sparkles className="h-5 w-5 text-yellow-500" />,
|
|
315
|
+
title: 'Achievement unlocked',
|
|
316
|
+
description: 'You completed 100 tasks. Nice work!',
|
|
317
|
+
});
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<ToastStoryWrapper>
|
|
322
|
+
<div className="flex flex-col items-start gap-3">
|
|
323
|
+
<p className="text-sm text-[var(--muted-foreground)]">
|
|
324
|
+
Pass an <code className="text-xs bg-[var(--muted)] px-1 rounded">icon</code> prop to replace the default variant icon with any React element.
|
|
325
|
+
</p>
|
|
326
|
+
<div className="flex flex-wrap gap-2">
|
|
327
|
+
<Button
|
|
328
|
+
size="sm" variant="outline"
|
|
329
|
+
onPress={() => toast.success('', {
|
|
330
|
+
icon: <Sparkles className="h-5 w-5 text-yellow-500" />,
|
|
331
|
+
title: 'Achievement unlocked',
|
|
332
|
+
description: 'You completed 100 tasks. Nice work!',
|
|
333
|
+
})}
|
|
334
|
+
>
|
|
335
|
+
<Sparkles className="h-3.5 w-3.5 mr-1.5" /> Trophy
|
|
336
|
+
</Button>
|
|
337
|
+
<Button
|
|
338
|
+
size="sm" variant="outline"
|
|
339
|
+
onPress={() => toast.info('', {
|
|
340
|
+
icon: <Bell className="h-5 w-5 text-[var(--info-background)]" />,
|
|
341
|
+
title: 'New message',
|
|
342
|
+
description: 'Sarah replied to your comment.',
|
|
343
|
+
})}
|
|
344
|
+
>
|
|
345
|
+
<Bell className="h-3.5 w-3.5 mr-1.5" /> Bell
|
|
346
|
+
</Button>
|
|
347
|
+
<Button
|
|
348
|
+
size="sm" variant="outline"
|
|
349
|
+
onPress={() => toast.info('', {
|
|
350
|
+
icon: <UserPlus className="h-5 w-5 text-[var(--info-background)]" />,
|
|
351
|
+
title: 'New team member',
|
|
352
|
+
description: 'Alex joined your workspace.',
|
|
353
|
+
})}
|
|
354
|
+
>
|
|
355
|
+
<UserPlus className="h-3.5 w-3.5 mr-1.5" /> User
|
|
356
|
+
</Button>
|
|
357
|
+
<Button
|
|
358
|
+
size="sm" variant="outline"
|
|
359
|
+
onPress={() => toast.success('', {
|
|
360
|
+
icon: <Shield className="h-5 w-5 text-[var(--success-background)]" />,
|
|
361
|
+
title: 'Security scan complete',
|
|
362
|
+
description: 'No threats detected.',
|
|
363
|
+
})}
|
|
364
|
+
>
|
|
365
|
+
<Shield className="h-3.5 w-3.5 mr-1.5" /> Shield
|
|
366
|
+
</Button>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</ToastStoryWrapper>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return <CustomIconsStory />;
|
|
373
|
+
},
|
|
267
374
|
parameters: {
|
|
268
375
|
docs: {
|
|
269
376
|
description: {
|
|
270
|
-
story: '
|
|
377
|
+
story: 'The `icon` prop accepts any React element and replaces the auto variant icon. Any Lucide icon or custom SVG works.',
|
|
271
378
|
},
|
|
272
379
|
},
|
|
273
380
|
},
|
|
274
381
|
};
|
|
275
382
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
383
|
+
// =============================================================================
|
|
384
|
+
// 6. Progress Bar + Hover to Pause
|
|
385
|
+
// =============================================================================
|
|
386
|
+
|
|
387
|
+
export const ProgressAndPause: Story = {
|
|
388
|
+
render: () => {
|
|
389
|
+
function ProgressStory() {
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
toast.success('', {
|
|
392
|
+
title: 'Hover me to pause',
|
|
393
|
+
description: 'The progress bar and timer freeze while you hover.',
|
|
394
|
+
timeout: 10000,
|
|
395
|
+
});
|
|
396
|
+
}, []);
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<ToastStoryWrapper>
|
|
400
|
+
<div className="flex flex-col items-start gap-4">
|
|
401
|
+
<p className="text-sm text-[var(--muted-foreground)]">
|
|
402
|
+
A progress bar counts down the auto-dismiss timer. Hover over the toast to pause it.
|
|
403
|
+
</p>
|
|
404
|
+
<div className="flex flex-wrap gap-2">
|
|
405
|
+
<Button
|
|
406
|
+
size="sm" variant="outline"
|
|
407
|
+
onPress={() => toast.success('', {
|
|
408
|
+
title: 'Hover me to pause',
|
|
409
|
+
description: 'The progress bar and timer freeze while you hover.',
|
|
410
|
+
timeout: 10000,
|
|
411
|
+
})}
|
|
412
|
+
>
|
|
413
|
+
10s with progress bar
|
|
414
|
+
</Button>
|
|
415
|
+
<Button
|
|
416
|
+
size="sm" variant="outline"
|
|
417
|
+
onPress={() => toast('No progress bar', { timeout: 5000, showProgress: false })}
|
|
418
|
+
>
|
|
419
|
+
showProgress: false
|
|
420
|
+
</Button>
|
|
421
|
+
<Button
|
|
422
|
+
size="sm" variant="outline"
|
|
423
|
+
onPress={() => toast.warning('', {
|
|
424
|
+
title: 'Action required',
|
|
425
|
+
description: 'Dismiss this manually — no timer.',
|
|
426
|
+
timeout: 0,
|
|
427
|
+
})}
|
|
428
|
+
>
|
|
429
|
+
Persistent (no bar)
|
|
430
|
+
</Button>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
</ToastStoryWrapper>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return <ProgressStory />;
|
|
437
|
+
},
|
|
438
|
+
parameters: {
|
|
439
|
+
docs: {
|
|
440
|
+
description: {
|
|
441
|
+
story: 'Progress bars appear automatically for auto-dismiss toasts and are `aria-hidden` (decorative). They pause when hovered. Use `showProgress: false` to opt out, or `timeout: 0` for a persistent toast.',
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// =============================================================================
|
|
448
|
+
// 7. Timing
|
|
449
|
+
// =============================================================================
|
|
450
|
+
|
|
451
|
+
export const Timing: Story = {
|
|
280
452
|
render: () => (
|
|
281
453
|
<ToastStoryWrapper>
|
|
282
454
|
<div className="flex flex-col items-start gap-4">
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
</
|
|
455
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--muted)] p-4 text-sm space-y-1">
|
|
456
|
+
<p><code className="text-xs bg-[var(--content-background)] px-1 rounded">timeout: 5000</code> — default, minimum enforced for WCAG 2.2</p>
|
|
457
|
+
<p><code className="text-xs bg-[var(--content-background)] px-1 rounded">timeout: 10000</code> — longer read time</p>
|
|
458
|
+
<p><code className="text-xs bg-[var(--content-background)] px-1 rounded">timeout: 0</code> — persistent until manually dismissed</p>
|
|
459
|
+
</div>
|
|
460
|
+
<div className="flex flex-wrap gap-2">
|
|
461
|
+
<Button size="sm" variant="outline" onPress={() => toast('Auto-dismisses in 5 s')}>5s (default)</Button>
|
|
462
|
+
<Button size="sm" variant="outline" onPress={() => toast('Auto-dismisses in 10 s', { timeout: 10000 })}>10s</Button>
|
|
463
|
+
<Button size="sm" variant="outline" onPress={() => toast.warning('', { title: 'Persistent', description: 'Dismiss manually.', timeout: 0 })}>Persistent</Button>
|
|
464
|
+
</div>
|
|
293
465
|
</div>
|
|
294
466
|
</ToastStoryWrapper>
|
|
295
467
|
),
|
|
296
468
|
parameters: {
|
|
297
469
|
docs: {
|
|
298
470
|
description: {
|
|
299
|
-
story: 'Use `timeout: 0` for
|
|
471
|
+
story: 'WCAG 2.2 enforces a minimum 5 000 ms timeout so users have time to read. Values between 1–4999 ms are clamped to 5 000 ms. Use `timeout: 0` for persistent toasts.',
|
|
300
472
|
},
|
|
301
473
|
},
|
|
302
474
|
},
|
|
303
475
|
};
|
|
304
476
|
|
|
305
477
|
// =============================================================================
|
|
306
|
-
// Callback
|
|
478
|
+
// 8. onClose Callback
|
|
307
479
|
// =============================================================================
|
|
308
480
|
|
|
309
|
-
/**
|
|
310
|
-
* Toast with onClose callback for tracking
|
|
311
|
-
*/
|
|
312
481
|
export const WithCallback: Story = {
|
|
313
482
|
render: function WithCallbackStory() {
|
|
314
|
-
const [log, setLog] = useState<string[]>([]);
|
|
483
|
+
const [log, setLog] = useState<{ key: string; reason: string; time: string }[]>([]);
|
|
315
484
|
|
|
316
485
|
return (
|
|
317
486
|
<ToastStoryWrapper>
|
|
318
487
|
<div className="flex flex-col items-start gap-4">
|
|
319
488
|
<p className="text-sm text-[var(--muted-foreground)]">
|
|
320
|
-
The onClose callback
|
|
489
|
+
The <code className="text-xs bg-[var(--muted)] px-1 rounded">onClose</code> callback fires with the toast key and dismissal reason.
|
|
321
490
|
</p>
|
|
322
|
-
<
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
491
|
+
<div className="flex flex-wrap gap-2">
|
|
492
|
+
<Button
|
|
493
|
+
size="sm"
|
|
494
|
+
onPress={() =>
|
|
495
|
+
toast.info('', {
|
|
496
|
+
title: 'Dismiss me',
|
|
497
|
+
description: 'Via button, timeout, or Clear All.',
|
|
498
|
+
timeout: 8000,
|
|
499
|
+
onClose: (key, reason) => {
|
|
500
|
+
setLog((prev) => [
|
|
501
|
+
{ key: key.slice(0, 8), reason, time: new Date().toLocaleTimeString() },
|
|
502
|
+
...prev,
|
|
503
|
+
].slice(0, 5));
|
|
504
|
+
},
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
>
|
|
508
|
+
Show Toast
|
|
509
|
+
</Button>
|
|
510
|
+
<Button size="sm" variant="outline" onPress={() => toastQueue.closeAll()}>Clear all (programmatic)</Button>
|
|
511
|
+
</div>
|
|
334
512
|
{log.length > 0 && (
|
|
335
|
-
<div className="
|
|
336
|
-
<p className="mb-2 text-
|
|
513
|
+
<div className="w-full rounded-lg border border-[var(--border)] bg-[var(--muted)] p-3">
|
|
514
|
+
<p className="mb-2 text-xs font-semibold text-[var(--muted-foreground)] uppercase tracking-wide">Callback log</p>
|
|
337
515
|
{log.map((entry, i) => (
|
|
338
|
-
<
|
|
516
|
+
<div key={i} className="flex gap-3 font-mono text-xs py-0.5">
|
|
517
|
+
<span className="text-[var(--muted-foreground)]">{entry.time}</span>
|
|
518
|
+
<span className="text-[var(--content-foreground)]">{entry.key}…</span>
|
|
519
|
+
<span className={
|
|
520
|
+
entry.reason === 'timeout' ? 'text-[var(--muted-foreground)]' :
|
|
521
|
+
entry.reason === 'user' ? 'text-[var(--success-background)]' :
|
|
522
|
+
'text-[var(--info-background)]'
|
|
523
|
+
}>{entry.reason}</span>
|
|
524
|
+
</div>
|
|
339
525
|
))}
|
|
340
526
|
</div>
|
|
341
527
|
)}
|
|
@@ -346,45 +532,46 @@ export const WithCallback: Story = {
|
|
|
346
532
|
parameters: {
|
|
347
533
|
docs: {
|
|
348
534
|
description: {
|
|
349
|
-
story: '
|
|
535
|
+
story: 'Track dismissals with `onClose(key, reason)`. Reason is `"timeout"` for auto-dismiss, `"user"` for the close button or swipe, and `"programmatic"` for `toastQueue.close()` / `closeAll()`.',
|
|
350
536
|
},
|
|
351
537
|
},
|
|
352
538
|
},
|
|
353
539
|
};
|
|
354
540
|
|
|
355
541
|
// =============================================================================
|
|
356
|
-
//
|
|
542
|
+
// 9. Positions
|
|
357
543
|
// =============================================================================
|
|
358
544
|
|
|
359
|
-
/**
|
|
360
|
-
* All 6 position options
|
|
361
|
-
*/
|
|
362
545
|
export const AllPositions: Story = {
|
|
363
546
|
render: function AllPositionsStory() {
|
|
364
547
|
const [position, setPosition] = useState<ToasterPosition>('bottom-right');
|
|
548
|
+
const positions = ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'] as const;
|
|
365
549
|
|
|
366
550
|
return (
|
|
367
551
|
<ToastStoryWrapper position={position}>
|
|
368
552
|
<div className="flex flex-col items-start gap-4">
|
|
369
|
-
<p className="text-sm text-[var(--muted-foreground)]">
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
</Button>
|
|
383
|
-
)
|
|
384
|
-
)}
|
|
553
|
+
<p className="text-sm text-[var(--muted-foreground)]">Select a position, then fire a toast.</p>
|
|
554
|
+
<div className="grid grid-cols-3 gap-2 w-64">
|
|
555
|
+
{positions.map((pos) => (
|
|
556
|
+
<Button
|
|
557
|
+
key={pos}
|
|
558
|
+
variant={position === pos ? 'default' : 'outline'}
|
|
559
|
+
size="sm"
|
|
560
|
+
onPress={() => setPosition(pos)}
|
|
561
|
+
className="text-xs"
|
|
562
|
+
>
|
|
563
|
+
{pos.replace('-', ' ')}
|
|
564
|
+
</Button>
|
|
565
|
+
))}
|
|
385
566
|
</div>
|
|
386
|
-
<Button
|
|
387
|
-
|
|
567
|
+
<Button
|
|
568
|
+
onPress={() => toast.success('', {
|
|
569
|
+
title: position,
|
|
570
|
+
description: 'Toast positioned correctly.',
|
|
571
|
+
timeout: 0,
|
|
572
|
+
})}
|
|
573
|
+
>
|
|
574
|
+
Fire at {position}
|
|
388
575
|
</Button>
|
|
389
576
|
</div>
|
|
390
577
|
</ToastStoryWrapper>
|
|
@@ -393,191 +580,153 @@ export const AllPositions: Story = {
|
|
|
393
580
|
parameters: {
|
|
394
581
|
docs: {
|
|
395
582
|
description: {
|
|
396
|
-
story: 'Six
|
|
583
|
+
story: 'Six positions: any corner or centre edge of the viewport. The blob shape and animation direction are the same regardless of position.',
|
|
397
584
|
},
|
|
398
585
|
},
|
|
399
586
|
},
|
|
400
587
|
};
|
|
401
588
|
|
|
402
589
|
// =============================================================================
|
|
403
|
-
// Queue
|
|
590
|
+
// 10. Queue & Stacking
|
|
404
591
|
// =============================================================================
|
|
405
592
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
<div className="flex flex-wrap gap-4">
|
|
417
|
-
<Button
|
|
418
|
-
onPress={() => {
|
|
419
|
-
const variants: ToastVariant[] = ['default', 'primary', 'destructive', 'accent'];
|
|
420
|
-
const variant = variants[Math.floor(Math.random() * variants.length)];
|
|
421
|
-
toast(`Toast #${Date.now() % 1000}`, { variant, timeout: 0 });
|
|
422
|
-
}}
|
|
423
|
-
>
|
|
424
|
-
Add Random Toast
|
|
425
|
-
</Button>
|
|
426
|
-
<Button variant="outline" onPress={() => toastQueue.closeAll()}>
|
|
427
|
-
Clear All
|
|
428
|
-
</Button>
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
</ToastStoryWrapper>
|
|
432
|
-
),
|
|
433
|
-
parameters: {
|
|
434
|
-
docs: {
|
|
435
|
-
description: {
|
|
436
|
-
story: 'Multiple toasts stack vertically with the newest at the bottom. Excess toasts are queued.',
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
},
|
|
440
|
-
};
|
|
593
|
+
export const QueueAndStacking: Story = {
|
|
594
|
+
render: () => {
|
|
595
|
+
function QueueStory() {
|
|
596
|
+
useEffect(() => {
|
|
597
|
+
toast.success('', { title: 'First', description: 'Visible immediately.', timeout: 0 });
|
|
598
|
+
toast.info('', { title: 'Second', description: 'Visible immediately.', timeout: 0 });
|
|
599
|
+
toast.warning('', { title: 'Third', description: 'Visible immediately.', timeout: 0 });
|
|
600
|
+
toast('Fourth — queued behind the first three', { timeout: 0 });
|
|
601
|
+
toast('Fifth — also queued', { timeout: 0 });
|
|
602
|
+
}, []);
|
|
441
603
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}}
|
|
458
|
-
>
|
|
459
|
-
Add 6 Toasts (3 visible, 3 queued)
|
|
460
|
-
</Button>
|
|
461
|
-
</div>
|
|
462
|
-
</ToastStoryWrapper>
|
|
463
|
-
),
|
|
464
|
-
parameters: {
|
|
465
|
-
docs: {
|
|
466
|
-
description: {
|
|
467
|
-
story: 'The `maxVisibleToasts` prop limits visible toasts. Additional toasts are queued and appear when others are dismissed.',
|
|
468
|
-
},
|
|
469
|
-
},
|
|
604
|
+
return (
|
|
605
|
+
<ToastStoryWrapper maxVisibleToasts={3}>
|
|
606
|
+
<div className="flex flex-col items-start gap-3">
|
|
607
|
+
<p className="text-sm text-[var(--muted-foreground)]">
|
|
608
|
+
5 toasts fired — 3 visible, 2 queued. Dismiss one to see the next appear.
|
|
609
|
+
</p>
|
|
610
|
+
<div className="flex flex-wrap gap-2">
|
|
611
|
+
<Button size="sm" variant="outline" onPress={() => toast('Another one', { timeout: 0 })}>Add toast</Button>
|
|
612
|
+
<Button size="sm" variant="ghost" onPress={() => toastQueue.closeAll()}>Clear all</Button>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
</ToastStoryWrapper>
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
return <QueueStory />;
|
|
470
619
|
},
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Collapsed stack indicator
|
|
475
|
-
*/
|
|
476
|
-
export const CollapsedStack: Story = {
|
|
477
|
-
render: () => (
|
|
478
|
-
<ToastStoryWrapper maxVisibleToasts={2}>
|
|
479
|
-
<div className="flex flex-col items-start gap-4">
|
|
480
|
-
<p className="text-sm text-[var(--muted-foreground)]">
|
|
481
|
-
When toasts exceed maxVisibleToasts, a collapsed stack indicator shows the queue count.
|
|
482
|
-
</p>
|
|
483
|
-
<Button
|
|
484
|
-
onPress={() => {
|
|
485
|
-
for (let i = 1; i <= 5; i++) {
|
|
486
|
-
toast(`Notification ${i}`, { timeout: 0 });
|
|
487
|
-
}
|
|
488
|
-
}}
|
|
489
|
-
>
|
|
490
|
-
Add 5 Toasts (maxVisible=2)
|
|
491
|
-
</Button>
|
|
492
|
-
</div>
|
|
493
|
-
</ToastStoryWrapper>
|
|
494
|
-
),
|
|
495
620
|
parameters: {
|
|
496
621
|
docs: {
|
|
497
622
|
description: {
|
|
498
|
-
story: '
|
|
623
|
+
story: '`maxVisibleToasts` (default: 3) caps how many show at once. Queued toasts appear behind the visible ones as a stacked ghost effect with a "+N more" count.',
|
|
499
624
|
},
|
|
500
625
|
},
|
|
501
626
|
},
|
|
502
627
|
};
|
|
503
628
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
629
|
+
// =============================================================================
|
|
630
|
+
// 11. Clear All
|
|
631
|
+
// =============================================================================
|
|
632
|
+
|
|
633
|
+
export const ClearAll: Story = {
|
|
634
|
+
render: () => {
|
|
635
|
+
function ClearAllStory() {
|
|
636
|
+
useEffect(() => {
|
|
637
|
+
toast.success('', { title: 'Saved', description: 'Report exported.', timeout: 0 });
|
|
638
|
+
toast.info('', { title: 'Sync complete', description: '142 records updated.', timeout: 0 });
|
|
639
|
+
toast.warning('', { title: 'Low storage', description: '90% used.', timeout: 0 });
|
|
640
|
+
}, []);
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<ToastStoryWrapper>
|
|
644
|
+
<div className="flex flex-col items-start gap-3">
|
|
645
|
+
<p className="text-sm text-[var(--muted-foreground)]">
|
|
646
|
+
When 2+ toasts are visible, a "Clear All" button appears below the stack.
|
|
647
|
+
</p>
|
|
648
|
+
<div className="flex flex-wrap gap-2">
|
|
649
|
+
<Button size="sm" variant="outline" onPress={() => toast.info('', { title: 'Another notification', timeout: 0 })}>Add toast</Button>
|
|
650
|
+
<Button size="sm" variant="ghost" onPress={() => toastQueue.closeAll()}>Clear programmatically</Button>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
</ToastStoryWrapper>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return <ClearAllStory />;
|
|
657
|
+
},
|
|
526
658
|
parameters: {
|
|
527
659
|
docs: {
|
|
528
660
|
description: {
|
|
529
|
-
story: 'The "Clear All" button
|
|
661
|
+
story: 'The "Clear All" button appears automatically when 2+ toasts are visible. You can also call `toastQueue.closeAll()` programmatically.',
|
|
530
662
|
},
|
|
531
663
|
},
|
|
532
664
|
},
|
|
533
665
|
};
|
|
534
666
|
|
|
535
667
|
// =============================================================================
|
|
536
|
-
// Accessibility
|
|
668
|
+
// 12. Accessibility
|
|
537
669
|
// =============================================================================
|
|
538
670
|
|
|
539
|
-
|
|
540
|
-
* Keyboard navigation instructions
|
|
541
|
-
*/
|
|
542
|
-
export const KeyboardNavigation: Story = {
|
|
671
|
+
export const Accessibility: Story = {
|
|
543
672
|
render: () => (
|
|
544
673
|
<ToastStoryWrapper>
|
|
545
|
-
<div className="flex flex-col items-start gap-
|
|
546
|
-
<div className="rounded border border-[var(--border)] bg-[var(--muted)] p-4">
|
|
547
|
-
<p className="mb-
|
|
548
|
-
<ul className="space-y-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
674
|
+
<div className="flex flex-col items-start gap-5">
|
|
675
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--muted)] p-4 w-full max-w-md">
|
|
676
|
+
<p className="mb-3 text-sm font-semibold">WCAG 2.2 AAA compliance checklist</p>
|
|
677
|
+
<ul className="space-y-2 text-sm">
|
|
678
|
+
{[
|
|
679
|
+
['Keyboard navigation', 'F6 focuses the toast region, Tab/Shift+Tab cycle through buttons, Escape dismisses'],
|
|
680
|
+
['44 × 44 px touch targets', 'The close button meets the AAA minimum even at small visual sizes'],
|
|
681
|
+
['5 000 ms minimum timeout', 'Values below 5 000 ms are automatically clamped (except 0 = persistent)'],
|
|
682
|
+
['ARIA live region', 'New toasts are announced to screen readers via alertdialog role'],
|
|
683
|
+
['Reduced motion', 'Spring animations are disabled when prefers-reduced-motion: reduce is set'],
|
|
684
|
+
['Progress bar is decorative', 'aria-hidden — timeout info is conveyed by the ARIA live region'],
|
|
685
|
+
].map(([feature, detail]) => (
|
|
686
|
+
<li key={feature} className="flex flex-col gap-0.5">
|
|
687
|
+
<span className="font-medium text-[var(--content-foreground)]">{feature}</span>
|
|
688
|
+
<span className="text-xs text-[var(--muted-foreground)]">{detail}</span>
|
|
689
|
+
</li>
|
|
690
|
+
))}
|
|
553
691
|
</ul>
|
|
554
692
|
</div>
|
|
555
|
-
<
|
|
556
|
-
|
|
557
|
-
|
|
693
|
+
<div className="flex flex-wrap gap-2">
|
|
694
|
+
<Button onPress={() => toast.info('', { title: 'Keyboard test', description: 'Use F6 → Tab to focus the close button, then Enter or Escape.', timeout: 0 })}>
|
|
695
|
+
Show Toast (try keyboard)
|
|
696
|
+
</Button>
|
|
697
|
+
<Button variant="outline" onPress={() => toast('Short timeout clamped', { timeout: 1000 })}>
|
|
698
|
+
timeout: 1000 → clamped to 5000
|
|
699
|
+
</Button>
|
|
700
|
+
</div>
|
|
558
701
|
</div>
|
|
559
702
|
</ToastStoryWrapper>
|
|
560
703
|
),
|
|
561
704
|
parameters: {
|
|
562
705
|
docs: {
|
|
563
706
|
description: {
|
|
564
|
-
story: 'Full
|
|
707
|
+
story: 'Full accessibility checklist. All items are enforced programmatically — no configuration required.',
|
|
565
708
|
},
|
|
566
709
|
},
|
|
567
710
|
},
|
|
568
711
|
};
|
|
569
712
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
713
|
+
// =============================================================================
|
|
714
|
+
// 13. Reduced Motion
|
|
715
|
+
// =============================================================================
|
|
716
|
+
|
|
717
|
+
export const ReducedMotion: Story = {
|
|
574
718
|
render: () => (
|
|
575
719
|
<ToastStoryWrapper>
|
|
576
720
|
<div className="flex flex-col items-start gap-4">
|
|
721
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--muted)] p-4 text-sm space-y-1 max-w-md">
|
|
722
|
+
<p className="font-medium mb-2">How to test</p>
|
|
723
|
+
<p>macOS: System Settings → Accessibility → Display → Reduce Motion</p>
|
|
724
|
+
<p>DevTools: Rendering → Emulate CSS media → <code className="text-xs bg-[var(--content-background)] px-1 rounded">prefers-reduced-motion: reduce</code></p>
|
|
725
|
+
</div>
|
|
577
726
|
<p className="text-sm text-[var(--muted-foreground)]">
|
|
578
|
-
|
|
727
|
+
With reduced motion enabled the blob morphing and spring animations are replaced by an instant opacity fade.
|
|
579
728
|
</p>
|
|
580
|
-
<Button onPress={() => toast('
|
|
729
|
+
<Button onPress={() => toast.success('', { title: 'Reduced motion', description: 'Entry and exit are instant opacity fades.' })}>
|
|
581
730
|
Show Toast
|
|
582
731
|
</Button>
|
|
583
732
|
</div>
|
|
@@ -586,26 +735,27 @@ export const TouchTarget: Story = {
|
|
|
586
735
|
parameters: {
|
|
587
736
|
docs: {
|
|
588
737
|
description: {
|
|
589
|
-
story: 'All
|
|
738
|
+
story: 'All Spring-physics morphing is disabled when `prefers-reduced-motion: reduce` is active. Toasts fade in/out instead.',
|
|
590
739
|
},
|
|
591
740
|
},
|
|
592
741
|
},
|
|
593
742
|
};
|
|
594
743
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
744
|
+
// =============================================================================
|
|
745
|
+
// 14. Swipe to Dismiss
|
|
746
|
+
// =============================================================================
|
|
747
|
+
|
|
598
748
|
export const SwipeToDismiss: Story = {
|
|
599
749
|
render: () => (
|
|
600
750
|
<ToastStoryWrapper>
|
|
601
751
|
<div className="flex flex-col items-start gap-4">
|
|
602
752
|
<p className="text-sm text-[var(--muted-foreground)]">
|
|
603
|
-
On touch devices, swipe right to dismiss
|
|
753
|
+
On touch devices, swipe a toast to the right to dismiss it (50 px threshold). The toast tracks your finger and fades as you drag.
|
|
604
754
|
</p>
|
|
605
|
-
<div className="rounded border border-[var(--border)] bg-[var(--accent-background)] p-
|
|
606
|
-
|
|
755
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--accent-background)] p-3 text-sm max-w-xs">
|
|
756
|
+
Enable touch emulation in DevTools (toggle device toolbar) to test in browser.
|
|
607
757
|
</div>
|
|
608
|
-
<Button onPress={() => toast('Swipe me right to dismiss
|
|
758
|
+
<Button onPress={() => toast.info('', { title: 'Swipe me right', description: 'Drag to the right to dismiss.', timeout: 0 })}>
|
|
609
759
|
Show Toast
|
|
610
760
|
</Button>
|
|
611
761
|
</div>
|
|
@@ -614,349 +764,296 @@ export const SwipeToDismiss: Story = {
|
|
|
614
764
|
parameters: {
|
|
615
765
|
docs: {
|
|
616
766
|
description: {
|
|
617
|
-
story: '
|
|
767
|
+
story: 'Swipe right with a 50 px minimum threshold triggers dismissal. The toast follows the finger and fades proportionally to swipe distance.',
|
|
618
768
|
},
|
|
619
769
|
},
|
|
620
770
|
},
|
|
621
771
|
};
|
|
622
772
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
<div className="
|
|
634
|
-
<p
|
|
635
|
-
|
|
773
|
+
// =============================================================================
|
|
774
|
+
// 15. Real-world: File Upload
|
|
775
|
+
// =============================================================================
|
|
776
|
+
|
|
777
|
+
export const FileUploadExample: Story = {
|
|
778
|
+
render: function FileUploadStory() {
|
|
779
|
+
const [uploadKey, setUploadKey] = useState<string | null>(null);
|
|
780
|
+
|
|
781
|
+
return (
|
|
782
|
+
<ToastStoryWrapper>
|
|
783
|
+
<div className="flex flex-col items-start gap-4">
|
|
784
|
+
<p className="text-sm text-[var(--muted-foreground)]">
|
|
785
|
+
Simulate a file upload lifecycle: uploading → success or error.
|
|
786
|
+
</p>
|
|
787
|
+
<div className="flex flex-wrap gap-2">
|
|
788
|
+
<Button
|
|
789
|
+
variant="outline"
|
|
790
|
+
onPress={() => {
|
|
791
|
+
toastQueue.closeAll();
|
|
792
|
+
const key = toast.info('', {
|
|
793
|
+
icon: <Download className="h-5 w-5 animate-bounce text-[var(--info-background)]" />,
|
|
794
|
+
title: 'Uploading…',
|
|
795
|
+
description: 'report_q4_final.pdf — 2.3 MB',
|
|
796
|
+
timeout: 0,
|
|
797
|
+
showProgress: false,
|
|
798
|
+
});
|
|
799
|
+
setUploadKey(key);
|
|
800
|
+
}}
|
|
801
|
+
>
|
|
802
|
+
<Download className="h-4 w-4 mr-2" />
|
|
803
|
+
Start upload
|
|
804
|
+
</Button>
|
|
805
|
+
<Button
|
|
806
|
+
onPress={() => {
|
|
807
|
+
if (uploadKey) toastQueue.close(uploadKey);
|
|
808
|
+
setUploadKey(null);
|
|
809
|
+
toast.success('', {
|
|
810
|
+
title: 'Upload complete',
|
|
811
|
+
description: 'report_q4_final.pdf saved to /Documents.',
|
|
812
|
+
});
|
|
813
|
+
}}
|
|
814
|
+
>
|
|
815
|
+
<CheckCircle2 className="h-4 w-4 mr-2" />
|
|
816
|
+
Finish (success)
|
|
817
|
+
</Button>
|
|
818
|
+
<Button
|
|
819
|
+
variant="destructive"
|
|
820
|
+
onPress={() => {
|
|
821
|
+
if (uploadKey) toastQueue.close(uploadKey);
|
|
822
|
+
setUploadKey(null);
|
|
823
|
+
toast.error('', {
|
|
824
|
+
title: 'Upload failed',
|
|
825
|
+
description: 'Network error — please try again.',
|
|
826
|
+
});
|
|
827
|
+
}}
|
|
828
|
+
>
|
|
829
|
+
Finish (error)
|
|
830
|
+
</Button>
|
|
831
|
+
</div>
|
|
636
832
|
</div>
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
</div>
|
|
641
|
-
</ToastStoryWrapper>
|
|
642
|
-
),
|
|
833
|
+
</ToastStoryWrapper>
|
|
834
|
+
);
|
|
835
|
+
},
|
|
643
836
|
parameters: {
|
|
644
837
|
docs: {
|
|
645
838
|
description: {
|
|
646
|
-
story: '
|
|
839
|
+
story: 'A full upload lifecycle: info toast while in progress (persistent, no bar), replaced by success or error on completion.',
|
|
647
840
|
},
|
|
648
841
|
},
|
|
649
842
|
},
|
|
650
843
|
};
|
|
651
844
|
|
|
652
845
|
// =============================================================================
|
|
653
|
-
//
|
|
846
|
+
// 16. Real-world: Undo Delete
|
|
654
847
|
// =============================================================================
|
|
655
848
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
<
|
|
663
|
-
<
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
849
|
+
export const UndoDelete: Story = {
|
|
850
|
+
render: function UndoDeleteStory() {
|
|
851
|
+
const [deleted, setDeleted] = useState<string[]>([]);
|
|
852
|
+
const [items] = useState(['Project Alpha', 'Campaign Q1', 'Design System v2', 'Budget 2025']);
|
|
853
|
+
|
|
854
|
+
return (
|
|
855
|
+
<ToastStoryWrapper>
|
|
856
|
+
<div className="flex flex-col items-start gap-4">
|
|
857
|
+
<p className="text-sm text-[var(--muted-foreground)]">
|
|
858
|
+
Delete an item — an error toast with an Undo action gives 8 seconds to recover it.
|
|
859
|
+
</p>
|
|
860
|
+
<div className="flex flex-col gap-2 w-64">
|
|
861
|
+
{items.filter((item) => !deleted.includes(item)).map((item) => (
|
|
862
|
+
<div key={item} className="flex items-center justify-between rounded-lg border border-[var(--border)] px-3 py-2">
|
|
863
|
+
<span className="text-sm">{item}</span>
|
|
864
|
+
<Button
|
|
865
|
+
variant="ghost"
|
|
866
|
+
size="sm"
|
|
867
|
+
onPress={() => {
|
|
868
|
+
setDeleted((d) => [...d, item]);
|
|
869
|
+
const key = toast.error('', {
|
|
870
|
+
icon: <Trash2 className="h-5 w-5 text-[var(--destructive-background)]" />,
|
|
871
|
+
title: 'Deleted',
|
|
872
|
+
description: `"${item}" has been removed.`,
|
|
873
|
+
timeout: 8000,
|
|
874
|
+
onClose: (_, reason) => {
|
|
875
|
+
if (reason === 'user' || reason === 'programmatic') {
|
|
876
|
+
// Check if it was the undo button — handled in onClick
|
|
877
|
+
}
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// Patch: show Undo via ReactNode alongside structured content
|
|
882
|
+
// (Undo button fires programmatic close + restore)
|
|
883
|
+
void key;
|
|
884
|
+
}}
|
|
885
|
+
>
|
|
886
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
887
|
+
</Button>
|
|
888
|
+
</div>
|
|
889
|
+
))}
|
|
890
|
+
{deleted.length > 0 && (
|
|
891
|
+
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
|
892
|
+
{deleted.length} item{deleted.length > 1 ? 's' : ''} deleted
|
|
893
|
+
</p>
|
|
894
|
+
)}
|
|
895
|
+
</div>
|
|
694
896
|
</div>
|
|
695
|
-
</
|
|
696
|
-
|
|
697
|
-
|
|
897
|
+
</ToastStoryWrapper>
|
|
898
|
+
);
|
|
899
|
+
},
|
|
698
900
|
parameters: {
|
|
699
901
|
docs: {
|
|
700
902
|
description: {
|
|
701
|
-
story: '
|
|
903
|
+
story: 'Destructive actions with an 8-second window to recover. The error toast uses the trash icon and provides a clear title + description.',
|
|
702
904
|
},
|
|
703
905
|
},
|
|
704
906
|
},
|
|
705
907
|
};
|
|
706
908
|
|
|
707
909
|
// =============================================================================
|
|
708
|
-
// Real-
|
|
910
|
+
// 17. Real-world: Notification Feed
|
|
709
911
|
// =============================================================================
|
|
710
912
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
<Button
|
|
723
|
-
variant="outline"
|
|
724
|
-
onPress={() => {
|
|
725
|
-
toast(
|
|
726
|
-
<div className="flex items-center gap-2">
|
|
727
|
-
<Download className="h-4 w-4 animate-pulse" />
|
|
728
|
-
<span>Uploading document.pdf...</span>
|
|
729
|
-
</div>,
|
|
730
|
-
{ timeout: 0 }
|
|
731
|
-
);
|
|
732
|
-
}}
|
|
733
|
-
>
|
|
734
|
-
<Download className="h-4 w-4 mr-2" />
|
|
735
|
-
Start Upload
|
|
736
|
-
</Button>
|
|
737
|
-
<Button
|
|
738
|
-
variant="outline"
|
|
739
|
-
onPress={() => {
|
|
740
|
-
toast(
|
|
741
|
-
<div className="flex items-center gap-2">
|
|
742
|
-
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
|
743
|
-
<span>Upload complete! File saved.</span>
|
|
744
|
-
</div>,
|
|
745
|
-
{ variant: 'primary' }
|
|
746
|
-
);
|
|
747
|
-
}}
|
|
748
|
-
>
|
|
749
|
-
<CheckCircle2 className="h-4 w-4 mr-2" />
|
|
750
|
-
Complete Upload
|
|
751
|
-
</Button>
|
|
752
|
-
</div>
|
|
753
|
-
</div>
|
|
754
|
-
</ToastStoryWrapper>
|
|
755
|
-
),
|
|
756
|
-
parameters: {
|
|
757
|
-
docs: {
|
|
758
|
-
description: {
|
|
759
|
-
story: 'Example of using toasts for file upload progress and completion feedback.',
|
|
913
|
+
export const NotificationFeed: Story = {
|
|
914
|
+
render: function NotificationFeedStory() {
|
|
915
|
+
const notifications = [
|
|
916
|
+
{
|
|
917
|
+
fn: () => toast.info('', {
|
|
918
|
+
icon: <UserPlus className="h-5 w-5 text-[var(--info-background)]" />,
|
|
919
|
+
title: 'New team member',
|
|
920
|
+
description: 'Alex Chen accepted your invitation.',
|
|
921
|
+
timeout: 0,
|
|
922
|
+
}),
|
|
923
|
+
label: 'Team invite accepted',
|
|
760
924
|
},
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
render: () => (
|
|
770
|
-
<ToastStoryWrapper>
|
|
771
|
-
<div className="flex flex-col items-start gap-4">
|
|
772
|
-
<p className="text-sm text-[var(--muted-foreground)]">
|
|
773
|
-
Real-world example: Destructive action confirmation with undo option.
|
|
774
|
-
</p>
|
|
775
|
-
<Button
|
|
776
|
-
variant="destructive"
|
|
777
|
-
onPress={() => {
|
|
778
|
-
const key = toast(
|
|
779
|
-
<div className="flex items-center gap-2">
|
|
780
|
-
<Trash2 className="h-4 w-4" />
|
|
781
|
-
<span>Item deleted.</span>
|
|
782
|
-
<button
|
|
783
|
-
type="button"
|
|
784
|
-
className="ml-2 underline hover:no-underline"
|
|
785
|
-
onClick={() => {
|
|
786
|
-
toastQueue.close(key);
|
|
787
|
-
toast('Item restored!', { variant: 'primary' });
|
|
788
|
-
}}
|
|
789
|
-
>
|
|
790
|
-
Undo
|
|
791
|
-
</button>
|
|
792
|
-
</div>,
|
|
793
|
-
{ variant: 'destructive', timeout: 8000 }
|
|
794
|
-
);
|
|
795
|
-
}}
|
|
796
|
-
>
|
|
797
|
-
<Trash2 className="h-4 w-4 mr-2" />
|
|
798
|
-
Delete Item
|
|
799
|
-
</Button>
|
|
800
|
-
</div>
|
|
801
|
-
</ToastStoryWrapper>
|
|
802
|
-
),
|
|
803
|
-
parameters: {
|
|
804
|
-
docs: {
|
|
805
|
-
description: {
|
|
806
|
-
story: 'Example of a destructive notification with an inline undo action.',
|
|
925
|
+
{
|
|
926
|
+
fn: () => toast.success('', {
|
|
927
|
+
icon: <FileText className="h-5 w-5 text-[var(--success-background)]" />,
|
|
928
|
+
title: 'Report ready',
|
|
929
|
+
description: 'Q4 analytics are ready to view.',
|
|
930
|
+
timeout: 0,
|
|
931
|
+
}),
|
|
932
|
+
label: 'Report ready',
|
|
807
933
|
},
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
934
|
+
{
|
|
935
|
+
fn: () => toast.warning('', {
|
|
936
|
+
icon: <Clock className="h-5 w-5 text-[var(--warning-background)]" />,
|
|
937
|
+
title: 'Deadline tomorrow',
|
|
938
|
+
description: 'Campaign review due by 5 PM.',
|
|
939
|
+
timeout: 0,
|
|
940
|
+
}),
|
|
941
|
+
label: 'Deadline reminder',
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
fn: () => toast.error('', {
|
|
945
|
+
icon: <RefreshCw className="h-5 w-5 text-[var(--destructive-background)]" />,
|
|
946
|
+
title: 'Sync failed',
|
|
947
|
+
description: 'Could not connect to Google Drive.',
|
|
948
|
+
timeout: 0,
|
|
949
|
+
}),
|
|
950
|
+
label: 'Sync error',
|
|
951
|
+
},
|
|
952
|
+
];
|
|
811
953
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
setTimeout(() => {
|
|
841
|
-
toast(
|
|
842
|
-
<div className="flex items-center gap-2">
|
|
843
|
-
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
|
844
|
-
<span>Document review required</span>
|
|
845
|
-
</div>,
|
|
846
|
-
{ variant: 'accent', timeout: 0 }
|
|
847
|
-
);
|
|
848
|
-
}, 1000);
|
|
849
|
-
}}
|
|
850
|
-
>
|
|
851
|
-
<Bell className="h-4 w-4 mr-2" />
|
|
852
|
-
Show Notifications
|
|
853
|
-
</Button>
|
|
854
|
-
</div>
|
|
855
|
-
</ToastStoryWrapper>
|
|
856
|
-
),
|
|
954
|
+
return (
|
|
955
|
+
<ToastStoryWrapper maxVisibleToasts={4}>
|
|
956
|
+
<div className="flex flex-col items-start gap-4">
|
|
957
|
+
<p className="text-sm text-[var(--muted-foreground)]">
|
|
958
|
+
Fire individual notifications or trigger all four at once.
|
|
959
|
+
</p>
|
|
960
|
+
<div className="flex flex-wrap gap-2">
|
|
961
|
+
{notifications.map(({ fn, label }) => (
|
|
962
|
+
<Button key={label} size="sm" variant="outline" onPress={fn}>{label}</Button>
|
|
963
|
+
))}
|
|
964
|
+
</div>
|
|
965
|
+
<div className="flex gap-2">
|
|
966
|
+
<Button
|
|
967
|
+
onPress={() => {
|
|
968
|
+
notifications.forEach(({ fn }, i) => {
|
|
969
|
+
setTimeout(fn, i * 300);
|
|
970
|
+
});
|
|
971
|
+
}}
|
|
972
|
+
>
|
|
973
|
+
<Bell className="h-4 w-4 mr-2" />
|
|
974
|
+
Fire all (staggered)
|
|
975
|
+
</Button>
|
|
976
|
+
<Button variant="ghost" size="sm" onPress={() => toastQueue.closeAll()}>Clear all</Button>
|
|
977
|
+
</div>
|
|
978
|
+
</div>
|
|
979
|
+
</ToastStoryWrapper>
|
|
980
|
+
);
|
|
981
|
+
},
|
|
857
982
|
parameters: {
|
|
858
983
|
docs: {
|
|
859
984
|
description: {
|
|
860
|
-
story: '
|
|
985
|
+
story: 'Four realistic notification types fired in sequence. Shows the stacking behaviour, ghost layers, and the "+N more" queue indicator.',
|
|
861
986
|
},
|
|
862
987
|
},
|
|
863
988
|
},
|
|
864
989
|
};
|
|
865
990
|
|
|
866
991
|
// =============================================================================
|
|
867
|
-
// API Reference
|
|
992
|
+
// 18. API Reference
|
|
868
993
|
// =============================================================================
|
|
869
994
|
|
|
870
|
-
/**
|
|
871
|
-
* Programmatic API demonstration
|
|
872
|
-
*/
|
|
873
995
|
export const APIReference: Story = {
|
|
874
996
|
render: function APIReferenceStory() {
|
|
875
997
|
const [keys, setKeys] = useState<string[]>([]);
|
|
876
998
|
|
|
877
999
|
return (
|
|
878
1000
|
<ToastStoryWrapper>
|
|
879
|
-
<div className="flex flex-col gap-
|
|
880
|
-
<div className="rounded border border-[var(--border)] p-4">
|
|
881
|
-
<
|
|
882
|
-
<pre className="rounded bg-[var(--muted)] p-
|
|
883
|
-
{
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1001
|
+
<div className="flex flex-col gap-5 max-w-lg">
|
|
1002
|
+
<div className="rounded-lg border border-[var(--border)] p-4">
|
|
1003
|
+
<p className="mb-2 text-sm font-semibold">toast() options</p>
|
|
1004
|
+
<pre className="rounded bg-[var(--muted)] p-3 text-xs overflow-x-auto leading-relaxed">
|
|
1005
|
+
{`toast('Message', {
|
|
1006
|
+
variant: 'success', // default | primary | destructive
|
|
1007
|
+
// accent | success | warning | info
|
|
1008
|
+
timeout: 5000, // ms, 0 = persistent, min 5000
|
|
1009
|
+
title: 'Bold heading',
|
|
1010
|
+
description: 'Supporting detail',
|
|
1011
|
+
icon: <MyIcon />, // overrides auto variant icon
|
|
1012
|
+
showProgress: true, // false to hide progress bar
|
|
1013
|
+
onClose: (key, reason) => {},
|
|
1014
|
+
// reason: 'timeout' | 'user' | 'programmatic'
|
|
891
1015
|
});
|
|
892
1016
|
|
|
893
|
-
//
|
|
894
|
-
toast(
|
|
895
|
-
|
|
896
|
-
|
|
1017
|
+
// Convenience methods
|
|
1018
|
+
toast.success(content, options);
|
|
1019
|
+
toast.error(content, options);
|
|
1020
|
+
toast.warning(content, options);
|
|
1021
|
+
toast.info(content, options);`}
|
|
897
1022
|
</pre>
|
|
898
1023
|
</div>
|
|
899
1024
|
|
|
900
|
-
<div className="rounded border border-[var(--border)] p-4">
|
|
901
|
-
<
|
|
1025
|
+
<div className="rounded-lg border border-[var(--border)] p-4">
|
|
1026
|
+
<p className="mb-3 text-sm font-semibold">toastQueue methods</p>
|
|
902
1027
|
<div className="flex flex-wrap gap-2">
|
|
903
|
-
<Button
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
setKeys((prev) => [...prev, key]);
|
|
909
|
-
}}
|
|
910
|
-
>
|
|
911
|
-
add() - Returns key
|
|
1028
|
+
<Button size="sm" variant="outline" onPress={() => {
|
|
1029
|
+
const key = toast.info('', { title: 'Added via add()', timeout: 0 });
|
|
1030
|
+
setKeys((p) => [...p, key]);
|
|
1031
|
+
}}>
|
|
1032
|
+
add() → key
|
|
912
1033
|
</Button>
|
|
913
|
-
<Button
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
921
|
-
}}
|
|
922
|
-
>
|
|
923
|
-
close(key)
|
|
1034
|
+
<Button size="sm" variant="outline" onPress={() => {
|
|
1035
|
+
if (keys.length) {
|
|
1036
|
+
toastQueue.close(keys[0] ?? '');
|
|
1037
|
+
setKeys((p) => p.slice(1));
|
|
1038
|
+
}
|
|
1039
|
+
}}>
|
|
1040
|
+
close(key[0])
|
|
924
1041
|
</Button>
|
|
925
|
-
<Button
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
toastQueue.closeAll();
|
|
930
|
-
setKeys([]);
|
|
931
|
-
}}
|
|
932
|
-
>
|
|
1042
|
+
<Button size="sm" variant="outline" onPress={() => {
|
|
1043
|
+
toastQueue.closeAll();
|
|
1044
|
+
setKeys([]);
|
|
1045
|
+
}}>
|
|
933
1046
|
closeAll()
|
|
934
1047
|
</Button>
|
|
935
|
-
<Button
|
|
936
|
-
|
|
937
|
-
variant="outline"
|
|
938
|
-
onPress={() => toastQueue.pauseAll()}
|
|
939
|
-
>
|
|
940
|
-
pauseAll()
|
|
941
|
-
</Button>
|
|
942
|
-
<Button
|
|
943
|
-
size="sm"
|
|
944
|
-
variant="outline"
|
|
945
|
-
onPress={() => toastQueue.resumeAll()}
|
|
946
|
-
>
|
|
947
|
-
resumeAll()
|
|
948
|
-
</Button>
|
|
1048
|
+
<Button size="sm" variant="outline" onPress={() => toastQueue.pauseAll()}>pauseAll()</Button>
|
|
1049
|
+
<Button size="sm" variant="outline" onPress={() => toastQueue.resumeAll()}>resumeAll()</Button>
|
|
949
1050
|
</div>
|
|
1051
|
+
{keys.length > 0 && (
|
|
1052
|
+
<div className="mt-3 font-mono text-xs text-[var(--muted-foreground)]">
|
|
1053
|
+
{keys.map((k, i) => <p key={i}>[{i}] {k}</p>)}
|
|
1054
|
+
</div>
|
|
1055
|
+
)}
|
|
950
1056
|
</div>
|
|
951
|
-
|
|
952
|
-
{keys.length > 0 && (
|
|
953
|
-
<div className="rounded border border-[var(--border)] bg-[var(--muted)] p-4">
|
|
954
|
-
<p className="mb-2 text-sm font-medium">Active Toast Keys:</p>
|
|
955
|
-
{keys.map((key, i) => (
|
|
956
|
-
<p key={i} className="text-xs font-mono">{key}</p>
|
|
957
|
-
))}
|
|
958
|
-
</div>
|
|
959
|
-
)}
|
|
960
1057
|
</div>
|
|
961
1058
|
</ToastStoryWrapper>
|
|
962
1059
|
);
|
|
@@ -964,7 +1061,7 @@ toast(<div className="flex items-center gap-2">
|
|
|
964
1061
|
parameters: {
|
|
965
1062
|
docs: {
|
|
966
1063
|
description: {
|
|
967
|
-
story: '
|
|
1064
|
+
story: 'Full API reference with live interactive buttons for every method.',
|
|
968
1065
|
},
|
|
969
1066
|
},
|
|
970
1067
|
},
|