@tribepad/themis 1.0.16 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/dist/elements/Accordion/Accordion.d.ts +3 -1
  2. package/dist/elements/Accordion/Accordion.d.ts.map +1 -1
  3. package/dist/elements/Accordion/index.js +1 -1
  4. package/dist/elements/Accordion/index.js.map +1 -1
  5. package/dist/elements/Accordion/index.mjs +1 -1
  6. package/dist/elements/Accordion/index.mjs.map +1 -1
  7. package/dist/elements/AlertDialog/index.js +1 -1
  8. package/dist/elements/AlertDialog/index.js.map +1 -1
  9. package/dist/elements/AlertDialog/index.mjs +1 -1
  10. package/dist/elements/AlertDialog/index.mjs.map +1 -1
  11. package/dist/elements/Breadcrumbs/index.js +1 -1
  12. package/dist/elements/Breadcrumbs/index.js.map +1 -1
  13. package/dist/elements/Breadcrumbs/index.mjs +1 -1
  14. package/dist/elements/Breadcrumbs/index.mjs.map +1 -1
  15. package/dist/elements/Button/Button.d.ts.map +1 -1
  16. package/dist/elements/Button/index.js +1 -1
  17. package/dist/elements/Button/index.js.map +1 -1
  18. package/dist/elements/Button/index.mjs +1 -1
  19. package/dist/elements/Button/index.mjs.map +1 -1
  20. package/dist/elements/Card/Card.d.ts +21 -1
  21. package/dist/elements/Card/Card.d.ts.map +1 -1
  22. package/dist/elements/Card/Card.types.d.ts +36 -0
  23. package/dist/elements/Card/Card.types.d.ts.map +1 -1
  24. package/dist/elements/Card/index.d.ts +10 -3
  25. package/dist/elements/Card/index.d.ts.map +1 -1
  26. package/dist/elements/Card/index.js +1 -1
  27. package/dist/elements/Card/index.js.map +1 -1
  28. package/dist/elements/Card/index.mjs +1 -1
  29. package/dist/elements/Card/index.mjs.map +1 -1
  30. package/dist/elements/Carousel/Carousel.d.ts.map +1 -1
  31. package/dist/elements/Carousel/index.js +1 -1
  32. package/dist/elements/Carousel/index.js.map +1 -1
  33. package/dist/elements/Carousel/index.mjs +1 -1
  34. package/dist/elements/Carousel/index.mjs.map +1 -1
  35. package/dist/elements/Chart/Chart.d.ts.map +1 -1
  36. package/dist/elements/Chart/Chart.types.d.ts +21 -4
  37. package/dist/elements/Chart/Chart.types.d.ts.map +1 -1
  38. package/dist/elements/Chart/ChartAreaSeries.d.ts +32 -0
  39. package/dist/elements/Chart/ChartAreaSeries.d.ts.map +1 -0
  40. package/dist/elements/Chart/ChartContext.d.ts +4 -1
  41. package/dist/elements/Chart/ChartContext.d.ts.map +1 -1
  42. package/dist/elements/Chart/ChartLegend.d.ts +2 -2
  43. package/dist/elements/Chart/ChartLegend.d.ts.map +1 -1
  44. package/dist/elements/Chart/ChartPieSeries.d.ts +28 -0
  45. package/dist/elements/Chart/ChartPieSeries.d.ts.map +1 -0
  46. package/dist/elements/Chart/ChartRadarSeries.d.ts +28 -0
  47. package/dist/elements/Chart/ChartRadarSeries.d.ts.map +1 -0
  48. package/dist/elements/Chart/ChartSVG.d.ts +7 -1
  49. package/dist/elements/Chart/ChartSVG.d.ts.map +1 -1
  50. package/dist/elements/Chart/ChartScatterSeries.d.ts +31 -0
  51. package/dist/elements/Chart/ChartScatterSeries.d.ts.map +1 -0
  52. package/dist/elements/Chart/index.js +1 -1
  53. package/dist/elements/Chart/index.js.map +1 -1
  54. package/dist/elements/Chart/index.mjs +1 -1
  55. package/dist/elements/Chart/index.mjs.map +1 -1
  56. package/dist/elements/Checkbox/index.js +1 -1
  57. package/dist/elements/Checkbox/index.js.map +1 -1
  58. package/dist/elements/Checkbox/index.mjs +1 -1
  59. package/dist/elements/Checkbox/index.mjs.map +1 -1
  60. package/dist/elements/ColorPicker/ColorPicker.d.ts +20 -0
  61. package/dist/elements/ColorPicker/ColorPicker.d.ts.map +1 -0
  62. package/dist/elements/ColorPicker/ColorPicker.styles.d.ts +74 -0
  63. package/dist/elements/ColorPicker/ColorPicker.styles.d.ts.map +1 -0
  64. package/dist/elements/ColorPicker/ColorPicker.types.d.ts +113 -0
  65. package/dist/elements/ColorPicker/ColorPicker.types.d.ts.map +1 -0
  66. package/dist/elements/ColorPicker/index.d.ts +5 -0
  67. package/dist/elements/ColorPicker/index.d.ts.map +1 -0
  68. package/dist/elements/ColorPicker/index.js +3 -0
  69. package/dist/elements/ColorPicker/index.js.map +1 -0
  70. package/dist/elements/ColorPicker/index.mjs +3 -0
  71. package/dist/elements/ColorPicker/index.mjs.map +1 -0
  72. package/dist/elements/Combobox/Combobox.d.ts +1 -1
  73. package/dist/elements/Combobox/Combobox.d.ts.map +1 -1
  74. package/dist/elements/Combobox/Combobox.styles.d.ts.map +1 -1
  75. package/dist/elements/Combobox/index.js +1 -1
  76. package/dist/elements/Combobox/index.js.map +1 -1
  77. package/dist/elements/Combobox/index.mjs +1 -1
  78. package/dist/elements/Combobox/index.mjs.map +1 -1
  79. package/dist/elements/DatePicker/DatePicker.d.ts.map +1 -1
  80. package/dist/elements/DatePicker/DatePicker.types.d.ts +6 -6
  81. package/dist/elements/DatePicker/index.js +1 -1
  82. package/dist/elements/DatePicker/index.js.map +1 -1
  83. package/dist/elements/DatePicker/index.mjs +1 -1
  84. package/dist/elements/DatePicker/index.mjs.map +1 -1
  85. package/dist/elements/Dropdown/Dropdown.d.ts.map +1 -1
  86. package/dist/elements/Dropdown/index.js +1 -1
  87. package/dist/elements/Dropdown/index.js.map +1 -1
  88. package/dist/elements/Dropdown/index.mjs +1 -1
  89. package/dist/elements/Dropdown/index.mjs.map +1 -1
  90. package/dist/elements/FileField/index.js +1 -1
  91. package/dist/elements/FileField/index.js.map +1 -1
  92. package/dist/elements/FileField/index.mjs +1 -1
  93. package/dist/elements/FileField/index.mjs.map +1 -1
  94. package/dist/elements/Modal/Modal.d.ts +2 -1
  95. package/dist/elements/Modal/Modal.d.ts.map +1 -1
  96. package/dist/elements/Modal/index.js +1 -1
  97. package/dist/elements/Modal/index.js.map +1 -1
  98. package/dist/elements/Modal/index.mjs +1 -1
  99. package/dist/elements/Modal/index.mjs.map +1 -1
  100. package/dist/elements/NumberField/NumberField.d.ts.map +1 -1
  101. package/dist/elements/NumberField/NumberField.types.d.ts +1 -1
  102. package/dist/elements/NumberField/NumberField.variants.d.ts.map +1 -1
  103. package/dist/elements/NumberField/index.js +1 -1
  104. package/dist/elements/NumberField/index.js.map +1 -1
  105. package/dist/elements/NumberField/index.mjs +1 -1
  106. package/dist/elements/NumberField/index.mjs.map +1 -1
  107. package/dist/elements/OTPInput/OTPInput.d.ts +1 -1
  108. package/dist/elements/OTPInput/index.js +1 -1
  109. package/dist/elements/OTPInput/index.js.map +1 -1
  110. package/dist/elements/OTPInput/index.mjs +1 -1
  111. package/dist/elements/OTPInput/index.mjs.map +1 -1
  112. package/dist/elements/Pagination/index.js +1 -1
  113. package/dist/elements/Pagination/index.js.map +1 -1
  114. package/dist/elements/Pagination/index.mjs +1 -1
  115. package/dist/elements/Pagination/index.mjs.map +1 -1
  116. package/dist/elements/Panel/index.js +1 -1
  117. package/dist/elements/Panel/index.js.map +1 -1
  118. package/dist/elements/Panel/index.mjs +1 -1
  119. package/dist/elements/Panel/index.mjs.map +1 -1
  120. package/dist/elements/PasswordField/index.js +1 -1
  121. package/dist/elements/PasswordField/index.js.map +1 -1
  122. package/dist/elements/PasswordField/index.mjs +1 -1
  123. package/dist/elements/PasswordField/index.mjs.map +1 -1
  124. package/dist/elements/Progress/index.js +1 -1
  125. package/dist/elements/Progress/index.js.map +1 -1
  126. package/dist/elements/Progress/index.mjs +1 -1
  127. package/dist/elements/Progress/index.mjs.map +1 -1
  128. package/dist/elements/RatingScale/index.js +1 -1
  129. package/dist/elements/RatingScale/index.js.map +1 -1
  130. package/dist/elements/RatingScale/index.mjs +1 -1
  131. package/dist/elements/RatingScale/index.mjs.map +1 -1
  132. package/dist/elements/Resizable/index.js +1 -1
  133. package/dist/elements/Resizable/index.js.map +1 -1
  134. package/dist/elements/Resizable/index.mjs +1 -1
  135. package/dist/elements/Resizable/index.mjs.map +1 -1
  136. package/dist/elements/SearchField/index.js +1 -1
  137. package/dist/elements/SearchField/index.js.map +1 -1
  138. package/dist/elements/SearchField/index.mjs +1 -1
  139. package/dist/elements/SearchField/index.mjs.map +1 -1
  140. package/dist/elements/Select/Select.d.ts.map +1 -1
  141. package/dist/elements/Select/Select.styles.d.ts.map +1 -1
  142. package/dist/elements/Select/index.js +1 -1
  143. package/dist/elements/Select/index.js.map +1 -1
  144. package/dist/elements/Select/index.mjs +1 -1
  145. package/dist/elements/Select/index.mjs.map +1 -1
  146. package/dist/elements/SituationalJudgement/index.js +1 -1
  147. package/dist/elements/SituationalJudgement/index.js.map +1 -1
  148. package/dist/elements/SituationalJudgement/index.mjs +1 -1
  149. package/dist/elements/SituationalJudgement/index.mjs.map +1 -1
  150. package/dist/elements/Table/index.js +1 -1
  151. package/dist/elements/Table/index.js.map +1 -1
  152. package/dist/elements/Table/index.mjs +1 -1
  153. package/dist/elements/Table/index.mjs.map +1 -1
  154. package/dist/elements/Tabs/index.js +1 -1
  155. package/dist/elements/Tabs/index.js.map +1 -1
  156. package/dist/elements/Tabs/index.mjs +1 -1
  157. package/dist/elements/Tabs/index.mjs.map +1 -1
  158. package/dist/elements/TextField/TextField.d.ts.map +1 -1
  159. package/dist/elements/TextField/TextField.styles.d.ts.map +1 -1
  160. package/dist/elements/TextField/index.js +1 -1
  161. package/dist/elements/TextField/index.js.map +1 -1
  162. package/dist/elements/TextField/index.mjs +1 -1
  163. package/dist/elements/TextField/index.mjs.map +1 -1
  164. package/dist/elements/TimeField/TimeField.d.ts.map +1 -1
  165. package/dist/elements/TimeField/TimeField.types.d.ts.map +1 -1
  166. package/dist/elements/TimeField/index.js +1 -1
  167. package/dist/elements/TimeField/index.js.map +1 -1
  168. package/dist/elements/TimeField/index.mjs +1 -1
  169. package/dist/elements/TimeField/index.mjs.map +1 -1
  170. package/dist/elements/Toast/Toast.d.ts +21 -43
  171. package/dist/elements/Toast/Toast.d.ts.map +1 -1
  172. package/dist/elements/Toast/Toast.types.d.ts +27 -1
  173. package/dist/elements/Toast/Toast.types.d.ts.map +1 -1
  174. package/dist/elements/Toast/Toaster.d.ts +0 -12
  175. package/dist/elements/Toast/Toaster.d.ts.map +1 -1
  176. package/dist/elements/Toast/index.d.ts +1 -1
  177. package/dist/elements/Toast/index.d.ts.map +1 -1
  178. package/dist/elements/Toast/index.js +1 -1
  179. package/dist/elements/Toast/index.js.map +1 -1
  180. package/dist/elements/Toast/index.mjs +1 -1
  181. package/dist/elements/Toast/index.mjs.map +1 -1
  182. package/dist/elements/Tooltip/index.js +1 -1
  183. package/dist/elements/Tooltip/index.js.map +1 -1
  184. package/dist/elements/Tooltip/index.mjs +1 -1
  185. package/dist/elements/Tooltip/index.mjs.map +1 -1
  186. package/dist/elements/index.d.ts +3 -1
  187. package/dist/elements/index.d.ts.map +1 -1
  188. package/dist/elements/index.js +1 -1
  189. package/dist/elements/index.js.map +1 -1
  190. package/dist/elements/index.mjs +1 -1
  191. package/dist/elements/index.mjs.map +1 -1
  192. package/dist/index.js +2 -2
  193. package/dist/index.js.map +1 -1
  194. package/dist/index.mjs +2 -2
  195. package/dist/index.mjs.map +1 -1
  196. package/dist/styles/animations.css +110 -0
  197. package/dist/styles/defaults.css +32 -0
  198. package/dist/styles/index.d.ts +1 -1
  199. package/dist/styles/index.d.ts.map +1 -1
  200. package/dist/styles/index.js +1 -1
  201. package/dist/styles/index.js.map +1 -1
  202. package/dist/styles/index.mjs +1 -1
  203. package/dist/styles/index.mjs.map +1 -1
  204. package/dist/styles/shared-variants.d.ts +21 -2
  205. package/dist/styles/shared-variants.d.ts.map +1 -1
  206. package/package.json +35 -34
  207. package/src/elements/Chart/Chart.stories.tsx +186 -2
  208. package/src/elements/ColorPicker/ColorPicker.stories.tsx +289 -0
  209. package/src/elements/Modal/Modal.stories.tsx +6 -9
  210. 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 demonstrating all variants, positions, and features
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 { Bell, CheckCircle2, AlertTriangle, Info, Download, Trash2 } from 'lucide-react';
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, ToastVariant } from './Toast.types';
24
+ import type { ToasterPosition } from './Toast.types';
18
25
 
19
26
  // =============================================================================
20
- // Story Wrapper Component
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-[400px] w-full p-8">
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 Configuration
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: `Accessible toast notification system built with React Aria toast primitives, CVA variant styling, and WCAG 2.2 AAA compliance.
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
- - 4 semantic variants (default, primary, destructive, accent)
69
- - 6 position options
70
- - Programmatic API via \`toast()\` function
71
- - Queue management with configurable limit
72
- - Swipe-to-dismiss on touch devices
73
- - Full keyboard navigation
74
- - Reduced motion support
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
- // Basic Stories
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
- <ToastStoryWrapper>
112
- <div className="flex flex-col items-start gap-4">
113
- <p className="text-sm text-[var(--muted-foreground)]">
114
- Click the button to show a toast notification.
115
- </p>
116
- <Button onPress={() => toast('This is a default toast notification')}>
117
- Show Toast
118
- </Button>
119
- </div>
120
- </ToastStoryWrapper>
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
- * All 4 variant styles in one view
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
- <ToastStoryWrapper>
130
- <div className="flex flex-col items-start gap-4">
131
- <p className="text-sm text-[var(--muted-foreground)]">
132
- Click each button to see different toast variants.
133
- </p>
134
- <div className="flex flex-wrap gap-4">
135
- <Button
136
- variant="outline"
137
- onPress={() => toast('Default notification', { variant: 'default' })}
138
- >
139
- Default
140
- </Button>
141
- <Button
142
- variant="default"
143
- onPress={() => toast('Primary notification', { variant: 'primary' })}
144
- >
145
- Primary
146
- </Button>
147
- <Button
148
- variant="destructive"
149
- onPress={() => toast('Destructive notification', { variant: 'destructive' })}
150
- >
151
- Destructive
152
- </Button>
153
- <Button
154
- variant="secondary"
155
- onPress={() => toast('Accent notification', { variant: 'accent' })}
156
- >
157
- Accent
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: 'Four semantic variants that map to theme tokens: default, primary, destructive, and accent.',
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
- // Custom Content Stories
200
+ // 3. Title + Description — structured content
174
201
  // =============================================================================
175
202
 
176
- /**
177
- * Toast with custom ReactNode content including icons and actions
178
- */
179
- export const WithCustomContent: Story = {
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
- <p className="text-sm text-[var(--muted-foreground)]">
184
- Toasts can contain custom content including icons and interactive elements.
185
- </p>
186
- <div className="flex flex-wrap gap-4">
187
- <Button
188
- variant="outline"
189
- onPress={() =>
190
- toast(
191
- <div className="flex items-center gap-2">
192
- <CheckCircle2 className="h-4 w-4 text-green-500" />
193
- <span>Changes saved successfully!</span>
194
- </div>
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: 'Custom ReactNode content allows icons, styled text, and interactive elements within toasts.',
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
- // Timing Stories
306
+ // 5. Custom Icons
249
307
  // =============================================================================
250
308
 
251
- /**
252
- * Auto-dismiss behavior with default 5 second timeout
253
- */
254
- export const AutoDismiss: Story = {
255
- render: () => (
256
- <ToastStoryWrapper>
257
- <div className="flex flex-col items-start gap-4">
258
- <p className="text-sm text-[var(--muted-foreground)]">
259
- Toasts auto-dismiss after 5 seconds (minimum for accessibility).
260
- </p>
261
- <Button onPress={() => toast('This will auto-dismiss in 5 seconds')}>
262
- Show Auto-Dismiss Toast
263
- </Button>
264
- </div>
265
- </ToastStoryWrapper>
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: 'Toasts enforce a minimum 5-second timeout for WCAG 2.2 accessibility compliance, giving users adequate time to read.',
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
- * Persistent toast that stays until manually dismissed
278
- */
279
- export const Persistent: Story = {
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
- <p className="text-sm text-[var(--muted-foreground)]">
284
- Set timeout to 0 for persistent toasts that require user action.
285
- </p>
286
- <Button
287
- onPress={() =>
288
- toast('This toast will stay until you dismiss it', { timeout: 0 })
289
- }
290
- >
291
- Show Persistent Toast
292
- </Button>
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 important notifications that require user acknowledgment.',
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 Stories
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 receives the toast key and dismissal reason.
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
- <Button
323
- onPress={() =>
324
- toast('Dismiss me to see the callback', {
325
- timeout: 0,
326
- onClose: (key, reason) => {
327
- setLog((prev) => [...prev, `Toast ${key.slice(0, 8)}... dismissed: ${reason}`]);
328
- },
329
- })
330
- }
331
- >
332
- Show Toast with Callback
333
- </Button>
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="mt-4 rounded border border-[var(--border)] bg-[var(--muted)] p-4">
336
- <p className="mb-2 text-sm font-medium">Callback Log:</p>
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
- <p key={i} className="text-xs font-mono">{entry}</p>
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: 'The `onClose` callback is invoked with the toast key and reason (`timeout`, `user`, or `programmatic`) for analytics and state management.',
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
- // Position Stories
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
- Select a position and click "Show Toast" to see it in action.
371
- </p>
372
- <div className="flex flex-wrap gap-2">
373
- {(['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'] as const).map(
374
- (pos) => (
375
- <Button
376
- key={pos}
377
- variant={position === pos ? 'default' : 'outline'}
378
- size="sm"
379
- onPress={() => setPosition(pos)}
380
- >
381
- {pos}
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 onPress={() => toast(`Toast at ${position}`, { timeout: 0 })}>
387
- Show Toast
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 position options allow placement at any corner or center edge of the viewport.',
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 Management Stories
590
+ // 10. Queue & Stacking
404
591
  // =============================================================================
405
592
 
406
- /**
407
- * Multiple toasts with queue management
408
- */
409
- export const MultipleToasts: Story = {
410
- render: () => (
411
- <ToastStoryWrapper>
412
- <div className="flex flex-col items-start gap-4">
413
- <p className="text-sm text-[var(--muted-foreground)]">
414
- Click rapidly to see multiple toasts stack and queue.
415
- </p>
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
- * Queue limit demonstration
444
- */
445
- export const QueueManagement: Story = {
446
- render: () => (
447
- <ToastStoryWrapper maxVisibleToasts={3}>
448
- <div className="flex flex-col items-start gap-4">
449
- <p className="text-sm text-[var(--muted-foreground)]">
450
- Default maxVisibleToasts is 3. Additional toasts are queued and shown when space is available.
451
- </p>
452
- <Button
453
- onPress={() => {
454
- for (let i = 1; i <= 6; i++) {
455
- toast(`Toast ${i} of 6`, { timeout: 0 });
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: 'A "+N more" indicator appears below visible toasts when additional notifications are queued, using `aria-live="polite"` for screen reader announcement.',
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
- * Clear All button demonstration
506
- */
507
- export const ClearAllButton: Story = {
508
- render: () => (
509
- <ToastStoryWrapper>
510
- <div className="flex flex-col items-start gap-4">
511
- <p className="text-sm text-[var(--muted-foreground)]">
512
- When 2+ toasts are visible, a "Clear All" button appears to dismiss all at once.
513
- </p>
514
- <Button
515
- onPress={() => {
516
- toast('First notification', { timeout: 0 });
517
- toast('Second notification', { timeout: 0 });
518
- toast('Third notification', { timeout: 0 });
519
- }}
520
- >
521
- Add 3 Toasts
522
- </Button>
523
- </div>
524
- </ToastStoryWrapper>
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 provides a quick way to dismiss all visible toasts. It appears only when 2+ toasts are visible.',
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 Stories
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-4">
546
- <div className="rounded border border-[var(--border)] bg-[var(--muted)] p-4">
547
- <p className="mb-2 font-medium">Keyboard Controls:</p>
548
- <ul className="space-y-1 text-sm">
549
- <li><kbd className="rounded bg-[var(--background)] px-1">F6</kbd> - Navigate to toast region</li>
550
- <li><kbd className="rounded bg-[var(--background)] px-1">Tab</kbd> - Move between toasts and buttons</li>
551
- <li><kbd className="rounded bg-[var(--background)] px-1">Escape</kbd> - Dismiss focused toast</li>
552
- <li><kbd className="rounded bg-[var(--background)] px-1">Enter</kbd> - Activate focused button</li>
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
- <Button onPress={() => toast('Try navigating with keyboard!', { timeout: 0 })}>
556
- Show Toast (then use F6 to focus)
557
- </Button>
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 keyboard navigation support for users who cannot use a mouse. The toast region is an ARIA landmark accessible via F6.',
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
- * Touch target size demonstration
572
- */
573
- export const TouchTarget: Story = {
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
- Close buttons have 44x44px minimum touch targets (WCAG 2.2 AAA compliant).
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('Close button has 44x44px touch target', { timeout: 0 })}>
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 interactive elements meet WCAG 2.2 AAA 44x44px minimum touch target requirement.',
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
- * Swipe-to-dismiss on touch devices
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 a toast (50px threshold).
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-4 text-sm">
606
- <p>Try on a touch device or enable touch emulation in DevTools.</p>
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!', { timeout: 0 })}>
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: 'Touch users can swipe toasts to the right to dismiss them. A 50px threshold prevents accidental dismissals.',
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
- * Reduced motion support
625
- */
626
- export const ReducedMotion: Story = {
627
- render: () => (
628
- <ToastStoryWrapper>
629
- <div className="flex flex-col items-start gap-4">
630
- <p className="text-sm text-[var(--muted-foreground)]">
631
- When <code>prefers-reduced-motion: reduce</code> is set, animations are disabled.
632
- </p>
633
- <div className="rounded border border-[var(--border)] bg-[var(--muted)] p-4 text-sm">
634
- <p>To test: Enable "Reduce motion" in your OS accessibility settings,</p>
635
- <p>or use DevTools RenderingEmulate CSS media feature → prefers-reduced-motion: reduce</p>
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
- <Button onPress={() => toast('This toast respects reduced motion preferences')}>
638
- Show Toast
639
- </Button>
640
- </div>
641
- </ToastStoryWrapper>
642
- ),
833
+ </ToastStoryWrapper>
834
+ );
835
+ },
643
836
  parameters: {
644
837
  docs: {
645
838
  description: {
646
- story: 'Toasts respect `prefers-reduced-motion` media query. When enabled, slide animations are disabled and toasts appear/disappear instantly.',
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
- // Theme Stories
846
+ // 16. Real-world: Undo Delete
654
847
  // =============================================================================
655
848
 
656
- /**
657
- * All themes demonstration
658
- */
659
- export const AllThemes: Story = {
660
- render: () => (
661
- <ToastStoryWrapper>
662
- <div className="flex flex-col items-start gap-4">
663
- <p className="text-sm text-[var(--muted-foreground)]">
664
- Toasts automatically adapt to the current theme via CSS custom properties.
665
- </p>
666
- <p className="text-sm text-[var(--muted-foreground)]">
667
- Use the theme switcher in the Storybook toolbar to see different themes.
668
- </p>
669
- <div className="flex flex-wrap gap-4">
670
- <Button
671
- variant="outline"
672
- onPress={() => toast('Default variant', { variant: 'default', timeout: 0 })}
673
- >
674
- Default
675
- </Button>
676
- <Button
677
- variant="default"
678
- onPress={() => toast('Primary variant', { variant: 'primary', timeout: 0 })}
679
- >
680
- Primary
681
- </Button>
682
- <Button
683
- variant="destructive"
684
- onPress={() => toast('Destructive variant', { variant: 'destructive', timeout: 0 })}
685
- >
686
- Destructive
687
- </Button>
688
- <Button
689
- variant="secondary"
690
- onPress={() => toast('Accent variant', { variant: 'accent', timeout: 0 })}
691
- >
692
- Accent
693
- </Button>
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
- </div>
696
- </ToastStoryWrapper>
697
- ),
897
+ </ToastStoryWrapper>
898
+ );
899
+ },
698
900
  parameters: {
699
901
  docs: {
700
902
  description: {
701
- story: 'Toast variants use semantic CSS tokens that automatically adapt to light, dark, high-contrast, and colorblind themes.',
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-World Example Stories
910
+ // 17. Real-world: Notification Feed
709
911
  // =============================================================================
710
912
 
711
- /**
712
- * File upload progress notification
713
- */
714
- export const FileUploadExample: Story = {
715
- render: () => (
716
- <ToastStoryWrapper>
717
- <div className="flex flex-col items-start gap-4">
718
- <p className="text-sm text-[var(--muted-foreground)]">
719
- Real-world example: File upload progress and completion notifications.
720
- </p>
721
- <div className="flex flex-wrap gap-4">
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
- * Delete confirmation notification
767
- */
768
- export const DeleteConfirmationExample: Story = {
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
- * Notification center pattern
814
- */
815
- export const NotificationCenterExample: Story = {
816
- render: () => (
817
- <ToastStoryWrapper>
818
- <div className="flex flex-col items-start gap-4">
819
- <p className="text-sm text-[var(--muted-foreground)]">
820
- Real-world example: Multiple notification types in a notification center pattern.
821
- </p>
822
- <Button
823
- onPress={() => {
824
- toast(
825
- <div className="flex items-center gap-2">
826
- <Bell className="h-4 w-4" />
827
- <span>New message from John Doe</span>
828
- </div>,
829
- { timeout: 0 }
830
- );
831
- setTimeout(() => {
832
- toast(
833
- <div className="flex items-center gap-2">
834
- <CheckCircle2 className="h-4 w-4 text-green-500" />
835
- <span>Interview scheduled for tomorrow</span>
836
- </div>,
837
- { variant: 'primary', timeout: 0 }
838
- );
839
- }, 500);
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: 'Example of multiple notification types appearing in sequence, demonstrating the queue and stack behavior.',
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 Story
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-6">
880
- <div className="rounded border border-[var(--border)] p-4">
881
- <h4 className="mb-2 font-medium">toast() Function</h4>
882
- <pre className="rounded bg-[var(--muted)] p-2 text-xs overflow-x-auto">
883
- {`// Simple string
884
- toast("Hello world!");
885
-
886
- // With options
887
- toast("Error occurred", {
888
- variant: "destructive",
889
- timeout: 0,
890
- onClose: (key, reason) => console.log(reason)
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
- // Custom content
894
- toast(<div className="flex items-center gap-2">
895
- <Icon /><span>Custom content</span>
896
- </div>);`}
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
- <h4 className="mb-2 font-medium">toastQueue Methods</h4>
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
- size="sm"
905
- variant="outline"
906
- onPress={() => {
907
- const key = toast('New toast', { timeout: 0 });
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
- size="sm"
915
- variant="outline"
916
- onPress={() => {
917
- if (keys.length > 0) {
918
- toastQueue.close(keys[keys.length - 1] ?? '');
919
- setKeys((prev) => prev.slice(0, -1));
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
- size="sm"
927
- variant="outline"
928
- onPress={() => {
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
- size="sm"
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: 'Interactive demonstration of the programmatic toast API and queue management methods.',
1064
+ story: 'Full API reference with live interactive buttons for every method.',
968
1065
  },
969
1066
  },
970
1067
  },