Files
myeasycms-v2/apps/dev-tool/app/components/components/sonner-story.tsx
Giancarlo Buomprisco ad427365c9 Storybook (#328)
* feat(docs): add interactive examples and API references for Button, Card, and LoadingFallback components
- Updated dependencies
- Set `retries` to a fixed value of 3 for consistent test retries across environments.
- Increased `timeout` from 60 seconds to 120 seconds to allow more time for tests to complete.
- Reduced `expect` timeout from 10 seconds to 5 seconds for quicker feedback on assertions.
2025-08-22 07:35:44 +08:00

1115 lines
40 KiB
TypeScript

'use client';
import {
AlertCircleIcon,
CheckCircleIcon,
DownloadIcon,
InfoIcon,
SaveIcon,
SendIcon,
TrashIcon,
UploadIcon,
UserPlusIcon,
XCircleIcon,
} from 'lucide-react';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Toaster, toast } from '@kit/ui/sonner';
import { Switch } from '@kit/ui/switch';
import { Textarea } from '@kit/ui/textarea';
import {
generateImportStatement,
generatePropsString,
useStoryControls,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
interface SonnerStoryControls {
position:
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| 'top-center'
| 'bottom-center';
variant: 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading';
duration: number;
dismissible: boolean;
richColors: boolean;
expand: boolean;
customTitle: string;
customDescription: string;
}
export default function SonnerStory() {
const { controls, updateControl } = useStoryControls<SonnerStoryControls>({
position: 'bottom-right',
variant: 'default',
duration: 4000,
dismissible: true,
richColors: true,
expand: false,
customTitle: 'Custom notification',
customDescription: 'This is a custom toast notification message.',
});
const generateCode = () => {
const toasterPropsString = generatePropsString(
{
position: controls.position,
richColors: controls.richColors,
expand: controls.expand,
},
{
position: 'bottom-right',
richColors: false,
expand: false,
},
);
const toastOptions = generatePropsString(
{
description:
controls.customDescription !==
'This is a custom toast notification message.'
? `"${controls.customDescription}"`
: undefined,
duration: controls.duration !== 4000 ? controls.duration : undefined,
dismissible: !controls.dismissible ? false : undefined,
},
{},
);
const imports = generateImportStatement(
['toast', 'Toaster'],
'@kit/ui/sonner',
);
const toastCall =
controls.variant === 'default'
? `toast('${controls.customTitle}'${toastOptions ? `, {${toastOptions} }` : ''})`
: `toast.${controls.variant}('${controls.customTitle}'${toastOptions ? `, {${toastOptions} }` : ''})`;
return `${imports}\n\n// Add Toaster to your app root\nfunction App() {\n return (\n <>\n {/* Your app content */}\n <Toaster${toasterPropsString} />\n </>\n );\n}\n\n// Use anywhere in your components\nfunction MyComponent() {\n const showToast = () => {\n ${toastCall};\n };\n\n return <button onClick={showToast}>Show Toast</button>;\n}`;
};
const showToast = () => {
const options = {
duration: controls.duration,
dismissible: controls.dismissible,
};
switch (controls.variant) {
case 'success':
toast.success(controls.customTitle, {
description: controls.customDescription,
...options,
});
break;
case 'error':
toast.error(controls.customTitle, {
description: controls.customDescription,
...options,
});
break;
case 'warning':
toast.warning(controls.customTitle, {
description: controls.customDescription,
...options,
});
break;
case 'info':
toast.info(controls.customTitle, {
description: controls.customDescription,
...options,
});
break;
case 'loading':
toast.loading(controls.customTitle, {
description: controls.customDescription,
...options,
});
break;
default:
toast(controls.customTitle, {
description: controls.customDescription,
...options,
});
}
};
const controlsContent = (
<Card>
<CardHeader>
<CardTitle>Sonner Controls</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-2 block text-sm font-medium">Position</label>
<Select
value={controls.position}
onValueChange={(value: SonnerStoryControls['position']) =>
updateControl('position', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="top-left">Top Left</SelectItem>
<SelectItem value="top-center">Top Center</SelectItem>
<SelectItem value="top-right">Top Right</SelectItem>
<SelectItem value="bottom-left">Bottom Left</SelectItem>
<SelectItem value="bottom-center">Bottom Center</SelectItem>
<SelectItem value="bottom-right">Bottom Right</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="mb-2 block text-sm font-medium">Variant</label>
<Select
value={controls.variant}
onValueChange={(value: SonnerStoryControls['variant']) =>
updateControl('variant', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="success">Success</SelectItem>
<SelectItem value="error">Error</SelectItem>
<SelectItem value="warning">Warning</SelectItem>
<SelectItem value="info">Info</SelectItem>
<SelectItem value="loading">Loading</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<label className="mb-2 block text-sm font-medium">
Duration (ms): {controls.duration}
</label>
<input
type="range"
min="1000"
max="10000"
step="500"
value={controls.duration}
onChange={(e) => updateControl('duration', Number(e.target.value))}
className="w-full"
/>
</div>
<div className="flex flex-wrap gap-4">
<div className="flex items-center space-x-2">
<Switch
id="dismissible"
checked={controls.dismissible}
onCheckedChange={(checked) =>
updateControl('dismissible', checked)
}
/>
<label htmlFor="dismissible" className="text-sm">
Dismissible
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="richColors"
checked={controls.richColors}
onCheckedChange={(checked) =>
updateControl('richColors', checked)
}
/>
<label htmlFor="richColors" className="text-sm">
Rich Colors
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="expand"
checked={controls.expand}
onCheckedChange={(checked) => updateControl('expand', checked)}
/>
<label htmlFor="expand" className="text-sm">
Expand
</label>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="customTitle">Custom Title</Label>
<Input
id="customTitle"
value={controls.customTitle}
onChange={(e) => updateControl('customTitle', e.target.value)}
placeholder="Enter toast title..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="customDescription">Custom Description</Label>
<Textarea
id="customDescription"
value={controls.customDescription}
onChange={(e) => updateControl('customDescription', e.target.value)}
placeholder="Enter toast description..."
rows={3}
/>
</div>
</CardContent>
</Card>
);
const previewContent = (
<Card>
<CardHeader>
<CardTitle>Sonner Preview</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<Label className="mb-3 block text-base font-semibold">
Test Toast Notification
</Label>
<div className="flex flex-col gap-3">
<Button onClick={showToast} size="sm">
Show {controls.variant} Toast
</Button>
<div className="text-muted-foreground text-sm">
<p>
<strong>Position:</strong> {controls.position}
</p>
<p>
<strong>Duration:</strong> {controls.duration}ms
</p>
<p>
<strong>Dismissible:</strong>{' '}
{controls.dismissible ? 'Yes' : 'No'}
</p>
</div>
</div>
</div>
<div>
<Label className="mb-3 block text-base font-semibold">
Quick Actions
</Label>
<div className="grid grid-cols-2 gap-2 md:grid-cols-3">
<Button
variant="outline"
size="sm"
onClick={() =>
toast.success('Success!', {
description: 'Operation completed successfully.',
})
}
>
<CheckCircleIcon className="mr-2 h-4 w-4" />
Success
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
toast.error('Error!', {
description: 'Something went wrong.',
})
}
>
<XCircleIcon className="mr-2 h-4 w-4" />
Error
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
toast.warning('Warning!', {
description: 'Please check your input.',
})
}
>
<AlertCircleIcon className="mr-2 h-4 w-4" />
Warning
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
toast.info('Info', {
description: 'Here is some information.',
})
}
>
<InfoIcon className="mr-2 h-4 w-4" />
Info
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const id = toast.loading('Loading...', {
description: 'Please wait...',
});
setTimeout(() => {
toast.success('Done!', {
id,
description: 'Loading completed.',
});
}, 2000);
}}
>
Loading
</Button>
<Button
variant="outline"
size="sm"
onClick={() => toast.dismiss()}
>
Dismiss All
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
);
return (
<>
<ComponentStoryLayout
preview={previewContent}
controls={controlsContent}
generatedCode={generateCode()}
previewTitle="Interactive Sonner Toasts"
previewDescription="Toast notifications with various styles and configurations"
controlsTitle="Configuration"
controlsDescription="Customize toast appearance and behavior"
examples={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Basic Toast Types</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Simple Messages</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button
variant="outline"
size="sm"
onClick={() => toast('Simple message')}
className="w-full justify-start"
>
Basic Toast
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
toast('With description', {
description:
'Additional details about the notification.',
})
}
className="w-full justify-start"
>
With Description
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Status Messages</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button
variant="outline"
size="sm"
onClick={() => toast.success('Success message')}
className="w-full justify-start"
>
<CheckCircleIcon className="mr-2 h-4 w-4" />
Success
</Button>
<Button
variant="outline"
size="sm"
onClick={() => toast.error('Error message')}
className="w-full justify-start"
>
<XCircleIcon className="mr-2 h-4 w-4" />
Error
</Button>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Interactive Toasts</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>With Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button
variant="outline"
size="sm"
onClick={() =>
toast('File uploaded', {
description:
'Your file has been uploaded successfully.',
action: {
label: 'View',
onClick: () => console.log('View clicked'),
},
})
}
className="w-full justify-start"
>
<UploadIcon className="mr-2 h-4 w-4" />
Upload Complete
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
toast('Confirm deletion', {
description: 'This action cannot be undone.',
action: {
label: 'Delete',
onClick: () =>
toast.success('Deleted successfully'),
},
cancel: {
label: 'Cancel',
onClick: () => toast.info('Cancelled'),
},
})
}
className="w-full justify-start"
>
<TrashIcon className="mr-2 h-4 w-4" />
Delete Item
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Promise States</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button
variant="outline"
size="sm"
onClick={() => {
const promise = new Promise((resolve) =>
setTimeout(resolve, 2000),
);
toast.promise(promise, {
loading: 'Saving...',
success: 'Saved successfully!',
error: 'Failed to save',
});
}}
className="w-full justify-start"
>
<SaveIcon className="mr-2 h-4 w-4" />
Save Data
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const promise = new Promise((_, reject) =>
setTimeout(
() => reject(new Error('Network error')),
2000,
),
);
toast.promise(promise, {
loading: 'Sending...',
success: 'Sent successfully!',
error: 'Failed to send message',
});
}}
className="w-full justify-start"
>
<SendIcon className="mr-2 h-4 w-4" />
Send Message (Fail)
</Button>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Real-world Examples
</h3>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>User Management</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<Button
variant="outline"
size="sm"
onClick={() =>
toast.success('User invited', {
description: 'Invitation sent to john@example.com',
action: {
label: 'Undo',
onClick: () => toast.info('Invitation cancelled'),
},
})
}
className="justify-start"
>
<UserPlusIcon className="mr-2 h-4 w-4" />
Invite User
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
toast.warning('User role changed', {
description: 'John Doe is now an admin',
duration: 6000,
})
}
className="justify-start"
>
Role Update
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>File Operations</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
<Button
variant="outline"
size="sm"
onClick={() => {
const downloadPromise = new Promise((resolve) =>
setTimeout(resolve, 3000),
);
toast.promise(downloadPromise, {
loading: 'Preparing download...',
success: 'Download ready',
error: 'Download failed',
});
}}
className="justify-start"
>
<DownloadIcon className="mr-2 h-4 w-4" />
Download
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
toast.info('File shared', {
description: 'Link copied to clipboard',
duration: 3000,
})
}
className="justify-start"
>
Share File
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
toast.error('Access denied', {
description:
"You don't have permission to access this file",
duration: 5000,
})
}
className="justify-start"
>
Access Error
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Advanced Usage</h3>
<Card>
<CardHeader>
<CardTitle>Custom Styling & Updates</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<Button
variant="outline"
size="sm"
onClick={() => {
let count = 0;
const id = toast.loading('Processing...', {
description: `Step ${count + 1} of 5`,
});
const interval = setInterval(() => {
count++;
if (count < 5) {
toast.loading('Processing...', {
id,
description: `Step ${count + 1} of 5`,
});
} else {
clearInterval(interval);
toast.success('Process complete!', {
id,
description: 'All steps completed successfully',
});
}
}, 1000);
}}
className="justify-start"
>
Multi-step Process
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
toast('Custom styled toast', {
description: 'With custom className',
className: 'border-l-4 border-l-blue-500',
})
}
className="justify-start"
>
Custom Style
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
}
apiReference={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">Sonner Components</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Component</th>
<th className="p-2 text-left font-medium">Props</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">Toaster</td>
<td className="p-2 font-mono">
position, theme, richColors, expand
</td>
<td className="p-2">Toast container component</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">toast</td>
<td className="p-2 font-mono">message, options</td>
<td className="p-2">Function to trigger toasts</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Toast Functions</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Function</th>
<th className="p-2 text-left font-medium">Parameters</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">toast()</td>
<td className="p-2 font-mono">message, options?</td>
<td className="p-2">Basic toast notification</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">toast.success()</td>
<td className="p-2 font-mono">message, options?</td>
<td className="p-2">Success toast with green styling</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">toast.error()</td>
<td className="p-2 font-mono">message, options?</td>
<td className="p-2">Error toast with red styling</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">toast.warning()</td>
<td className="p-2 font-mono">message, options?</td>
<td className="p-2">Warning toast with yellow styling</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">toast.info()</td>
<td className="p-2 font-mono">message, options?</td>
<td className="p-2">Info toast with blue styling</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">toast.loading()</td>
<td className="p-2 font-mono">message, options?</td>
<td className="p-2">Loading toast with spinner</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">toast.promise()</td>
<td className="p-2 font-mono">promise, messages</td>
<td className="p-2">Promise-based toast states</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">toast.dismiss()</td>
<td className="p-2 font-mono">id?</td>
<td className="p-2">Dismiss toast(s)</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Toast Options</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Option</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">description</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Additional description text</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">duration</td>
<td className="p-2 font-mono">number</td>
<td className="p-2">4000</td>
<td className="p-2">Auto dismiss time in milliseconds</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">dismissible</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">true</td>
<td className="p-2">Allow manual dismissal</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">action</td>
<td className="p-2 font-mono">ActionButton</td>
<td className="p-2">-</td>
<td className="p-2">Primary action button</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">cancel</td>
<td className="p-2 font-mono">ActionButton</td>
<td className="p-2">-</td>
<td className="p-2">Cancel action button</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">id</td>
<td className="p-2 font-mono">string | number</td>
<td className="p-2">auto</td>
<td className="p-2">Unique identifier for updates</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">className</td>
<td className="p-2 font-mono">string</td>
<td className="p-2">-</td>
<td className="p-2">Additional CSS classes</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Toaster Props</h3>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b">
<th className="p-2 text-left font-medium">Prop</th>
<th className="p-2 text-left font-medium">Type</th>
<th className="p-2 text-left font-medium">Default</th>
<th className="p-2 text-left font-medium">Description</th>
</tr>
</thead>
<tbody className="text-sm">
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">position</td>
<td className="p-2 font-mono">Position</td>
<td className="p-2">'bottom-right'</td>
<td className="p-2">Toast container position</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">theme</td>
<td className="p-2 font-mono">
'light' | 'dark' | 'system'
</td>
<td className="p-2">'system'</td>
<td className="p-2">Theme preference</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">richColors</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Enable colored backgrounds</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">expand</td>
<td className="p-2 font-mono">boolean</td>
<td className="p-2">false</td>
<td className="p-2">Expand toasts by default</td>
</tr>
<tr className="border-border/50 border-b">
<td className="p-2 font-mono">visibleToasts</td>
<td className="p-2 font-mono">number</td>
<td className="p-2">3</td>
<td className="p-2">Number of visible toasts</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
}
usageGuidelines={
<div className="space-y-8">
<div>
<h3 className="mb-4 text-lg font-semibold">
Setup & Basic Usage
</h3>
<p className="text-muted-foreground mb-4 text-sm">
Sonner provides a simple and powerful toast notification system.
Add the Toaster component to your app root and use the toast
function to trigger notifications.
</p>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`import { Toaster, toast } from '@kit/ui/sonner';
// Add to your app root (layout.tsx or _app.tsx)
function App() {
return (
<>
{/* Your app content */}
<Toaster position="bottom-right" />
</>
);
}
// Use anywhere in your components
function MyComponent() {
const handleSave = async () => {
try {
await saveData();
toast.success('Saved successfully!');
} catch (error) {
toast.error('Failed to save', {
description: error.message,
});
}
};
return <button onClick={handleSave}>Save</button>;
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Promise Integration
</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`// Automatic loading/success/error states
const handleAsync = () => {
const promise = fetch('/api/data').then(res => res.json());
toast.promise(promise, {
loading: 'Fetching data...',
success: 'Data loaded successfully!',
error: 'Failed to load data',
});
};
// With custom promise handling
const handleUpload = async (file: File) => {
const uploadPromise = uploadFile(file);
toast.promise(uploadPromise, {
loading: 'Uploading file...',
success: (data) => \`File uploaded: \${data.filename}\`,
error: (err) => \`Upload failed: \${err.message}\`,
});
}`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Interactive Toasts</h3>
<div className="bg-muted/50 rounded-lg p-4">
<pre className="overflow-x-auto text-sm">
{`// With action buttons
toast('File uploaded', {
description: 'Your file has been uploaded successfully.',
action: {
label: 'View',
onClick: () => window.open('/files/123'),
},
});
// With confirmation
toast('Delete item?', {
description: 'This action cannot be undone.',
action: {
label: 'Delete',
onClick: () => {
deleteItem();
toast.success('Item deleted');
},
},
cancel: {
label: 'Cancel',
onClick: () => toast.dismiss(),
},
});
// Updating existing toasts
const loadingId = toast.loading('Processing...');
// Later update the same toast
toast.success('Processing complete!', { id: loadingId });`}
</pre>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Best Practices</h3>
<div className="space-y-4">
<div className="space-y-2 text-sm">
<h4 className="font-medium">When to Use Toasts</h4>
<p> Confirmation of user actions (save, delete, send)</p>
<p>
Non-critical errors that don't require immediate attention
</p>
<p>• Status updates for background processes</p>
<p>• Temporary information that expires automatically</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Toast Content Guidelines</h4>
<p>• Keep messages concise and scannable</p>
<p>
• Use action-oriented language ("Saved", "Sent", "Failed")
</p>
<p>• Provide context in descriptions when helpful</p>
<p>• Include recovery actions for error states</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">UX Considerations</h4>
<p>
• Don't overwhelm users with too many simultaneous toasts
</p>
<p>
Use appropriate variants (success, error, warning, info)
</p>
<p> Consider toast positioning based on your app layout</p>
<p> Provide dismiss options for persistent toasts</p>
</div>
<div className="space-y-2 text-sm">
<h4 className="font-medium">Accessibility</h4>
<p> Toasts are announced to screen readers automatically</p>
<p> Use semantic colors and icons for different states</p>
<p> Ensure sufficient contrast for text and backgrounds</p>
<p> Provide keyboard access to action buttons</p>
</div>
</div>
</div>
</div>
}
/>
<Toaster
position={controls.position}
richColors={controls.richColors}
expand={controls.expand}
/>
</>
);
}
export { SonnerStory };