mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-30 15:59:09 +08:00
feat(sonner): implement custom toast component with enhanced features
- Add custom toast component with support for info, success, warning, error, and loading states - Implement action buttons, links, colored messages, and dismissable toasts - Add TypeScript type definitions and Storybook documentation - Export toast API for programmatic usage
This commit is contained in:
parent
17d9da6f4e
commit
8794f2b3ac
@ -73,6 +73,6 @@ export * from './primitives/popover'
|
||||
export * from './primitives/radioGroup'
|
||||
export * from './primitives/select'
|
||||
export * from './primitives/shadcn-io/dropzone'
|
||||
export { Toaster } from './primitives/sonner'
|
||||
export * from './primitives/sonner'
|
||||
export * from './primitives/tabs'
|
||||
export * as Textarea from './primitives/textarea'
|
||||
|
||||
146
packages/ui/src/components/primitives/sonner.d.ts
vendored
Normal file
146
packages/ui/src/components/primitives/sonner.d.ts
vendored
Normal file
@ -0,0 +1,146 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Toast type variants
|
||||
*/
|
||||
type ToastType = 'info' | 'warning' | 'error' | 'success' | 'loading'
|
||||
|
||||
/**
|
||||
* Button configuration for toast actions
|
||||
*/
|
||||
interface ToastButton {
|
||||
/** Icon to display in the button */
|
||||
icon?: ReactNode
|
||||
/** Button label text */
|
||||
label: string
|
||||
/** Click handler for the button */
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Link configuration for toast navigation
|
||||
*/
|
||||
interface ToastLink {
|
||||
/** Link label text */
|
||||
label: string
|
||||
/** URL to navigate to */
|
||||
href?: string
|
||||
/** Click handler for the link */
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Base toast properties
|
||||
*/
|
||||
interface ToastProps {
|
||||
/** Unique identifier for the toast */
|
||||
id: string | number
|
||||
/** Type of toast notification */
|
||||
type: ToastType
|
||||
/** Main title text */
|
||||
title: string
|
||||
/** Optional description text */
|
||||
description?: string
|
||||
/** Optional colored message text */
|
||||
coloredMessage?: string
|
||||
/** Whether to use colored background for the toast */
|
||||
coloredBackground?: boolean
|
||||
/** Whether the toast can be dismissed */
|
||||
dismissable?: boolean
|
||||
/** Callback when toast is dismissed */
|
||||
onDismiss?: () => void
|
||||
/** Optional action button */
|
||||
button?: ToastButton
|
||||
/** Optional navigation link */
|
||||
link?: ToastLink
|
||||
/** Promise to track for loading state */
|
||||
promise?: Promise<unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for quick toast API methods (without type field)
|
||||
*/
|
||||
interface QuickToastProps extends Omit<ToastProps, 'type' | 'id'> {}
|
||||
|
||||
/**
|
||||
* Props for loading toast (requires promise)
|
||||
*/
|
||||
interface QuickLoadingProps extends QuickToastProps {
|
||||
promise: ToastProps['promise']
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast notification interface with type-safe methods
|
||||
*/
|
||||
interface toast {
|
||||
/**
|
||||
* Display a custom toast notification
|
||||
* @param props - Toast configuration (must include type)
|
||||
* @returns Toast ID
|
||||
* @example
|
||||
* toast({
|
||||
* type: 'info',
|
||||
* title: 'Hello',
|
||||
* description: 'This is a toast'
|
||||
* })
|
||||
*/
|
||||
(props: Omit<ToastProps, 'id'>): string | number
|
||||
|
||||
/**
|
||||
* Display an info toast notification
|
||||
* @param props - Toast configuration (type is automatically set to 'info')
|
||||
* @example
|
||||
* toast.info({
|
||||
* title: 'Information',
|
||||
* description: 'This is an info message'
|
||||
* })
|
||||
*/
|
||||
info: (props: QuickToastProps) => void
|
||||
|
||||
/**
|
||||
* Display a success toast notification
|
||||
* @param props - Toast configuration (type is automatically set to 'success')
|
||||
* @example
|
||||
* toast.success({
|
||||
* title: 'Success!',
|
||||
* description: 'Operation completed successfully'
|
||||
* })
|
||||
*/
|
||||
success: (props: QuickToastProps) => void
|
||||
|
||||
/**
|
||||
* Display a warning toast notification
|
||||
* @param props - Toast configuration (type is automatically set to 'warning')
|
||||
* @example
|
||||
* toast.warning({
|
||||
* title: 'Warning',
|
||||
* description: 'Please be careful'
|
||||
* })
|
||||
*/
|
||||
warning: (props: QuickToastProps) => void
|
||||
|
||||
/**
|
||||
* Display an error toast notification
|
||||
* @param props - Toast configuration (type is automatically set to 'error')
|
||||
* @example
|
||||
* toast.error({
|
||||
* title: 'Error',
|
||||
* description: 'Something went wrong'
|
||||
* })
|
||||
*/
|
||||
error: (props: QuickToastProps) => void
|
||||
|
||||
/**
|
||||
* Display a loading toast notification with promise tracking
|
||||
* @param props - Toast configuration (type is automatically set to 'loading', requires promise)
|
||||
* @example
|
||||
* toast.loading({
|
||||
* title: 'Loading...',
|
||||
* promise: fetchData()
|
||||
* })
|
||||
*/
|
||||
loading: (props: QuickLoadingProps) => void
|
||||
}
|
||||
|
||||
// Export types for external use
|
||||
export type { QuickLoadingProps, QuickToastProps, ToastButton, ToastLink, ToastProps, ToastType }
|
||||
@ -1,18 +1,17 @@
|
||||
import { cn } from '@cherrystudio/ui/utils'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { Loader2Icon } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import type { SVGProps } from 'react'
|
||||
import { Toaster as Sonner, type ToasterProps } from 'sonner'
|
||||
import { type ReactNode, type SVGProps, useCallback, useMemo } from 'react'
|
||||
import { toast as sonnerToast, Toaster as Sonner, type ToasterProps } from 'sonner'
|
||||
|
||||
const InfoIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<foreignObject x="0" y="0" width="29.0476" height="30">
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<foreignObject x="0" y="0">
|
||||
<div
|
||||
// xmlns="http://www.w3.org/1999/xhtml"
|
||||
style={{
|
||||
backdropFilter: 'blur(2px)',
|
||||
clipPath: 'url(#bgblur_0_1669_13486_clip_path)',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
clipPath: 'url(#bgblur_0_1669_13486_clip_path)'
|
||||
}}></div>
|
||||
</foreignObject>
|
||||
<g filter="url(#filter0_dd_1669_13486)" data-figma-bg-blur-radius="4">
|
||||
@ -129,19 +128,12 @@ const WarningIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
)
|
||||
|
||||
const SuccessIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<mask
|
||||
id="mask0_1669_13491"
|
||||
style={{ maskType: 'luminance' }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<mask id="mask0_1669_13491" style={{ maskType: 'luminance' }} maskUnits="userSpaceOnUse" x="0" y="0">
|
||||
<path d="M24 0H0V24H24V0Z" fill="white" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_1669_13491)">
|
||||
<foreignObject x="-3" y="-2" width="30" height="30">
|
||||
<foreignObject x="-3" y="-2">
|
||||
<div
|
||||
// xmlns="http://www.w3.org/1999/xhtml"
|
||||
style={{
|
||||
@ -275,31 +267,202 @@ const ErrorIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = 'system' } = useTheme()
|
||||
const CloseIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" className={className}>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.4419 5.44194C15.686 5.19786 15.686 4.80214 15.4419 4.55806C15.1979 4.31398 14.8021 4.31398 14.5581 4.55806L10 9.11612L5.44194 4.55806C5.19786 4.31398 4.80214 4.31398 4.55806 4.55806C4.31398 4.80214 4.31398 5.19786 4.55806 5.44194L9.11612 10L4.55806 14.5581C4.31398 14.8021 4.31398 15.1979 4.55806 15.4419C4.80214 15.686 5.19786 15.686 5.44194 15.4419L10 10.8839L14.5581 15.4419C14.8021 15.686 15.1979 15.686 15.4419 15.4419C15.686 15.1979 15.686 14.8021 15.4419 14.5581L10.8839 10L15.4419 5.44194Z"
|
||||
fill="black"
|
||||
fill-opacity="0.4"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
interface ToastProps {
|
||||
id: string | number
|
||||
type: 'info' | 'warning' | 'error' | 'success' | 'loading'
|
||||
title: string
|
||||
description?: string
|
||||
coloredMessage?: string
|
||||
coloredBackground?: boolean
|
||||
dismissable?: boolean
|
||||
onDismiss?: () => void
|
||||
button?: {
|
||||
icon?: ReactNode
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
link?: {
|
||||
label: string
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
promise?: Promise<unknown>
|
||||
}
|
||||
|
||||
function toast(props: Omit<ToastProps, 'id'>) {
|
||||
return sonnerToast.custom((id) => <Toast id={id} {...props} />)
|
||||
}
|
||||
|
||||
interface QuickApiProps extends Omit<ToastProps, 'type' | 'id'> {}
|
||||
|
||||
interface QuickLoadingProps extends QuickApiProps {
|
||||
promise: ToastProps['promise']
|
||||
}
|
||||
|
||||
toast.info = (props: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'info',
|
||||
...props
|
||||
})
|
||||
}
|
||||
|
||||
toast.success = (props: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'success',
|
||||
...props
|
||||
})
|
||||
}
|
||||
|
||||
toast.warning = (props: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'warning',
|
||||
...props
|
||||
})
|
||||
}
|
||||
|
||||
toast.error = (props: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'error',
|
||||
...props
|
||||
})
|
||||
}
|
||||
|
||||
toast.loading = (props: QuickLoadingProps) => {
|
||||
toast({
|
||||
type: 'loading',
|
||||
...props
|
||||
})
|
||||
}
|
||||
|
||||
const toastColorVariants = cva(undefined, {
|
||||
variants: {
|
||||
type: {
|
||||
info: 'text-blue-500',
|
||||
warning: 'text-warning-base',
|
||||
error: 'text-error-base',
|
||||
success: 'text-success-base',
|
||||
loading: 'text-foreground-muted'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const toastBgColorVariants = cva(undefined, {
|
||||
variants: {
|
||||
type: {
|
||||
info: 'bg-blue-50 border-blue-400',
|
||||
warning: 'bg-warning-bg border-warning-base',
|
||||
error: 'bg-error-bg border-error-base',
|
||||
success: 'bg-success-bg border-success-base',
|
||||
loading: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function Toast({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
description,
|
||||
coloredMessage,
|
||||
coloredBackground,
|
||||
dismissable,
|
||||
onDismiss,
|
||||
button,
|
||||
link
|
||||
}: ToastProps) {
|
||||
const icon = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return <InfoIcon className="size-6" />
|
||||
case 'error':
|
||||
return <ErrorIcon className="size-6" />
|
||||
case 'loading':
|
||||
return <Loader2Icon className="size-6 animate-spin" />
|
||||
case 'success':
|
||||
return <SuccessIcon className="size-6" />
|
||||
case 'warning':
|
||||
return <WarningIcon className="size-6" />
|
||||
}
|
||||
}, [type])
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
sonnerToast.dismiss(id)
|
||||
onDismiss?.()
|
||||
}, [id, onDismiss])
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps['theme']}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <SuccessIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <WarningIcon className="size-4" />,
|
||||
error: <ErrorIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />
|
||||
}}
|
||||
style={
|
||||
{
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
'--border-radius': 'var(--radius)'
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
<div
|
||||
id={String(id)}
|
||||
className={cn(
|
||||
'flex p-4 rounded-xs bg-background items-center shadow-lg',
|
||||
coloredBackground && toastBgColorVariants({ type })
|
||||
)}
|
||||
aria-label="Toast">
|
||||
{dismissable && (
|
||||
<button type="button" aria-label="Dismiss the toast" onClick={handleDismiss}>
|
||||
<CloseIcon className="size-5 absolute top-[5px] right-1.5" />
|
||||
</button>
|
||||
)}
|
||||
<div className={cn('flex items-start flex-1', button !== undefined ? 'gap-3' : 'gap-4')}>
|
||||
{icon}
|
||||
<div className="cs-toast-content flex flex-col gap-1">
|
||||
<div className="cs-toast-title font-medium leading-4.5" role="heading">
|
||||
{title}
|
||||
</div>
|
||||
<div className="cs-toast-description">
|
||||
<p className="text-foreground-secondary text-xs leading-3.5 tracking-normal">
|
||||
{coloredMessage && <span className={toastColorVariants({ type })}>{coloredMessage} </span>}
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
{link && (
|
||||
// FIXME: missing typography/typography components/p/letter-spacing
|
||||
<div className="cs-toast-link text-foreground-muted text-xs leading-3.5 tracking-normal">
|
||||
<a
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
className={cn(
|
||||
'underline decoration-foreground-muted cursor-pointer',
|
||||
'hover:text-foreground-secondary',
|
||||
// FIXME: missing active style in design
|
||||
'active:text-black'
|
||||
)}>
|
||||
{link.label}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{button !== undefined && (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'py-1 px-2 rounded-3xs flex items-center h-7 bg-background-subtle border-[0.5px] border-border',
|
||||
'text-foreground text-sm leading-4 tracking-normal',
|
||||
button.icon !== undefined && 'gap-2'
|
||||
)}
|
||||
onClick={button.onClick}>
|
||||
<div>{button.icon}</div>
|
||||
<div>{button.label}</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return <Sonner className="toaster group" {...props} />
|
||||
}
|
||||
|
||||
export { toast, Toaster }
|
||||
|
||||
534
packages/ui/stories/components/primitives/Sonner.stories.tsx
Normal file
534
packages/ui/stories/components/primitives/Sonner.stories.tsx
Normal file
@ -0,0 +1,534 @@
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { toast, Toaster } from '@cherrystudio/ui'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { RefreshCwIcon } from 'lucide-react'
|
||||
|
||||
const meta: Meta<typeof Toaster> = {
|
||||
title: 'Components/Primitives/Sonner',
|
||||
component: Toaster,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'A custom toast notification component built on sonner. Features custom icons, action buttons, links, and support for info, success, warning, error, and loading states.'
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex min-h-[400px] w-full items-center justify-center">
|
||||
<Story />
|
||||
<Toaster />
|
||||
</div>
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Basic Toast Types
|
||||
export const Info: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'Information',
|
||||
description: 'This is an informational message.'
|
||||
})
|
||||
}>
|
||||
Show Info Toast
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Success!',
|
||||
description: 'Operation completed successfully.'
|
||||
})
|
||||
}>
|
||||
Show Success Toast
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ErrorToast: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error({
|
||||
title: 'Error',
|
||||
description: 'Something went wrong. Please try again.'
|
||||
})
|
||||
}>
|
||||
Show Error Toast
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Warning: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Warning',
|
||||
description: 'Please be careful with this action.'
|
||||
})
|
||||
}>
|
||||
Show Warning Toast
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
render: () => {
|
||||
const mockPromise = new Promise((resolve) => {
|
||||
setTimeout(() => resolve('Data loaded'), 2000)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.loading({
|
||||
title: 'Loading...',
|
||||
description: 'Please wait while we process your request.',
|
||||
promise: mockPromise
|
||||
})
|
||||
}>
|
||||
Show Loading Toast
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// All Toast Types Together
|
||||
export const AllTypes: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={() => toast.info({ title: 'Info Toast' })}>Info</Button>
|
||||
<Button onClick={() => toast.success({ title: 'Success Toast' })}>Success</Button>
|
||||
<Button onClick={() => toast.warning({ title: 'Warning Toast' })}>Warning</Button>
|
||||
<Button onClick={() => toast.error({ title: 'Error Toast' })}>Error</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.loading({
|
||||
title: 'Loading Toast',
|
||||
promise: new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
})
|
||||
}>
|
||||
Loading
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Description
|
||||
export const WithDescription: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Event Created',
|
||||
description: 'Your event has been created successfully. You can now share it with others.'
|
||||
})
|
||||
}>
|
||||
Show Toast with Description
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Colored Message
|
||||
export const WithColoredMessage: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'System Update',
|
||||
coloredMessage: 'New version available!',
|
||||
description: 'Click the button to update now.'
|
||||
})
|
||||
}>
|
||||
Info with Colored Message
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Disk Space Low',
|
||||
coloredMessage: '95% used',
|
||||
description: 'Please free up some space.'
|
||||
})
|
||||
}>
|
||||
Warning with Colored Message
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Colored Background
|
||||
export const WithColoredBackground: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'Information',
|
||||
description: 'This toast has a colored background.',
|
||||
coloredBackground: true
|
||||
})
|
||||
}>
|
||||
Info Background
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Success!',
|
||||
description: 'This toast has a colored background.',
|
||||
coloredBackground: true
|
||||
})
|
||||
}>
|
||||
Success Background
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Warning',
|
||||
description: 'This toast has a colored background.',
|
||||
coloredBackground: true
|
||||
})
|
||||
}>
|
||||
Warning Background
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error({
|
||||
title: 'Error',
|
||||
description: 'This toast has a colored background.',
|
||||
coloredBackground: true
|
||||
})
|
||||
}>
|
||||
Error Background
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Colored Background with Actions
|
||||
export const ColoredBackgroundWithActions: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'File Uploaded',
|
||||
description: 'Your file has been uploaded successfully.',
|
||||
coloredBackground: true,
|
||||
button: {
|
||||
label: 'View',
|
||||
onClick: () => toast.info({ title: 'Opening file...' })
|
||||
}
|
||||
})
|
||||
}>
|
||||
Success with Button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Action Required',
|
||||
description: 'Please review the changes.',
|
||||
coloredBackground: true,
|
||||
link: {
|
||||
label: 'Review',
|
||||
onClick: () => toast.info({ title: 'Opening review...' })
|
||||
}
|
||||
})
|
||||
}>
|
||||
Warning with Link
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error({
|
||||
title: 'Update Failed',
|
||||
description: 'Failed to update the record.',
|
||||
coloredBackground: true,
|
||||
button: {
|
||||
icon: <RefreshCwIcon className="h-4 w-4" />,
|
||||
label: 'Retry',
|
||||
onClick: () => toast.info({ title: 'Retrying...' })
|
||||
},
|
||||
link: {
|
||||
label: 'Learn More',
|
||||
onClick: () => toast.info({ title: 'Opening help...' })
|
||||
}
|
||||
})
|
||||
}>
|
||||
Error with Button & Link
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Action Button
|
||||
export const WithActionButton: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Changes Saved',
|
||||
description: 'Your changes have been saved successfully.',
|
||||
button: {
|
||||
icon: <RefreshCwIcon className="h-4 w-4" />,
|
||||
label: 'Undo',
|
||||
onClick: () => toast.info({ title: 'Undoing changes...' })
|
||||
}
|
||||
})
|
||||
}>
|
||||
Show Toast with Action Button
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Link
|
||||
export const WithLink: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'Update Available',
|
||||
description: 'A new version is ready to install.',
|
||||
link: {
|
||||
label: 'View Details',
|
||||
onClick: () => toast.info({ title: 'Opening details...' })
|
||||
}
|
||||
})
|
||||
}>
|
||||
Toast with Click Handler
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Documentation Updated',
|
||||
description: 'Check out the new features.',
|
||||
link: {
|
||||
label: 'Read More',
|
||||
href: 'https://example.com',
|
||||
onClick: () => console.log('Link clicked')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Toast with Link
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Button and Link
|
||||
export const WithButtonAndLink: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Action Required',
|
||||
description: 'Please review the changes before proceeding.',
|
||||
button: {
|
||||
icon: <RefreshCwIcon className="h-4 w-4" />,
|
||||
label: 'Review',
|
||||
onClick: () => toast.info({ title: 'Opening review...' })
|
||||
},
|
||||
link: {
|
||||
label: 'Learn More',
|
||||
onClick: () => toast.info({ title: 'Opening documentation...' })
|
||||
}
|
||||
})
|
||||
}>
|
||||
Show Toast with Button and Link
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Dismissable Toast
|
||||
export const DismissableToast: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'Dismissable Toast',
|
||||
description: 'You can close this toast by clicking the X button.',
|
||||
dismissable: true,
|
||||
onDismiss: () => console.log('Toast dismissed')
|
||||
})
|
||||
}>
|
||||
Show Dismissable Toast
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Multiple Toasts
|
||||
export const MultipleToasts: Story = {
|
||||
render: () => {
|
||||
const showMultiple = () => {
|
||||
toast.success({ title: 'First notification', description: 'This is the first message' })
|
||||
setTimeout(() => toast.info({ title: 'Second notification', description: 'This is the second message' }), 100)
|
||||
setTimeout(() => toast.warning({ title: 'Third notification', description: 'This is the third message' }), 200)
|
||||
setTimeout(() => toast.error({ title: 'Fourth notification', description: 'This is the fourth message' }), 300)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button onClick={showMultiple}>Show Multiple Toasts</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Promise Example
|
||||
export const PromiseExample: Story = {
|
||||
render: () => {
|
||||
const handleAsyncOperation = () => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
Math.random() > 0.5 ? resolve({ name: 'John Doe' }) : reject(new Error('Failed to fetch data'))
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
toast.loading({
|
||||
title: 'Fetching data...',
|
||||
description: 'Please wait while we load your information.',
|
||||
promise
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button onClick={handleAsyncOperation}>Show Promise Toast (Random Result)</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Real World Examples
|
||||
export const RealWorldExamples: Story = {
|
||||
render: () => {
|
||||
const handleFileSave = () => {
|
||||
const promise = new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
toast.loading({
|
||||
title: 'Saving file...',
|
||||
promise
|
||||
})
|
||||
promise.then(() => {
|
||||
toast.success({
|
||||
title: 'File saved',
|
||||
description: 'Your file has been saved successfully.'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
toast.success({
|
||||
title: 'Form submitted',
|
||||
description: 'Your changes have been saved successfully.',
|
||||
button: {
|
||||
label: 'View',
|
||||
onClick: () => toast.info({ title: 'Opening form...' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
toast.error({
|
||||
title: 'Failed to delete',
|
||||
description: 'You do not have permission to delete this item.',
|
||||
button: {
|
||||
icon: <RefreshCwIcon className="h-4 w-4" />,
|
||||
label: 'Retry',
|
||||
onClick: () => toast.info({ title: 'Retrying...' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText('https://example.com')
|
||||
toast.success({
|
||||
title: 'Copied to clipboard',
|
||||
description: 'The link has been copied to your clipboard.'
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
toast.info({
|
||||
title: 'Update available',
|
||||
description: 'A new version of the application is ready to install.',
|
||||
button: {
|
||||
label: 'Update Now',
|
||||
onClick: () => toast.info({ title: 'Starting update...' })
|
||||
},
|
||||
link: {
|
||||
label: 'Release Notes',
|
||||
onClick: () => toast.info({ title: 'Opening release notes...' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">File Operations</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleFileSave}>Save File</Button>
|
||||
<Button variant="outline" onClick={handleCopy}>
|
||||
Copy Link
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Form Submissions</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleFormSubmit}>Submit Form</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Error Handling</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="destructive" onClick={handleDelete}>
|
||||
Delete Item
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Updates & Notifications</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleUpdate}>Show Update</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user