mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
Merge 539d6ec53c into 7aa136da3b
This commit is contained in:
commit
4b0542d23e
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -54,6 +54,6 @@
|
||||
"classNames"
|
||||
],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(([^;]*)[\\);]", "[`'\"`]([^'\"`;]*)[`'\"`]"]
|
||||
["cva\\(([^;]*)[\\);]", "[`'\"`]([^'\"`;]*)[`'\"`]"], ["cn\\(([^;]*)[\\);]", "[`'\"`]([^'\"`;]*)[`'\"`]"]
|
||||
]
|
||||
}
|
||||
|
||||
@ -63,6 +63,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"sonner": "2.0.7",
|
||||
"tailwind-merge": "^2.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -73,5 +73,6 @@ export * from './primitives/popover'
|
||||
export * from './primitives/radioGroup'
|
||||
export * from './primitives/select'
|
||||
export * from './primitives/shadcn-io/dropzone'
|
||||
export * from './primitives/sonner'
|
||||
export * from './primitives/tabs'
|
||||
export * as Textarea from './primitives/textarea'
|
||||
|
||||
155
packages/ui/src/components/primitives/sonner.d.ts
vendored
Normal file
155
packages/ui/src/components/primitives/sonner.d.ts
vendored
Normal file
@ -0,0 +1,155 @@
|
||||
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
|
||||
|
||||
/**
|
||||
* Dismiss a toast notification by its ID
|
||||
* @param id - The ID of the toast to dismiss
|
||||
* @example
|
||||
* const toastId = toast.info({ title: 'Info' })
|
||||
* toast.dismiss(toastId)
|
||||
*/
|
||||
dismiss: (id: string | number) => void
|
||||
}
|
||||
|
||||
// Export types for external use
|
||||
export type { QuickLoadingProps, QuickToastProps, ToastButton, ToastLink, ToastProps, ToastType }
|
||||
565
packages/ui/src/components/primitives/sonner.tsx
Normal file
565
packages/ui/src/components/primitives/sonner.tsx
Normal file
@ -0,0 +1,565 @@
|
||||
import { cn } from '@cherrystudio/ui/utils'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { merge } from 'lodash'
|
||||
import { type ReactNode, type SVGProps, useId } from 'react'
|
||||
import type { Action, ExternalToast, ToastClassnames } from 'sonner'
|
||||
import { toast as sonnerToast, Toaster as Sonner, type ToasterProps } from 'sonner'
|
||||
|
||||
const InfoIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
<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)'
|
||||
}}></div>
|
||||
</foreignObject>
|
||||
<g filter="url(#filter0_dd_1669_13486)" data-figma-bg-blur-radius="4">
|
||||
<path
|
||||
d="M13.5714 23.0477H15.4762C15.4762 23.3002 15.3758 23.5425 15.1973 23.721C15.0186 23.8997 14.7764 24 14.5238 24C14.2712 24 14.0289 23.8997 13.8504 23.721C13.6718 23.5425 13.5714 23.3002 13.5714 23.0477ZM22.1429 16.9809V12.7429C22.1429 9.25714 19.2286 6.39047 15.4762 5.9619V4.95238C15.4762 4.69978 15.3758 4.45754 15.1973 4.27894C15.0186 4.10034 14.7764 4 14.5238 4C14.2712 4 14.0289 4.10034 13.8504 4.27894C13.6718 4.45754 13.5714 4.69978 13.5714 4.95238V5.9619C9.81905 6.39047 6.90477 9.25714 6.90477 12.7429V16.9809C6.3657 17.1414 5.89119 17.4682 5.54907 17.9147C5.20696 18.3613 5.01479 18.9043 5 19.4666C5.03217 20.1934 5.35079 20.8779 5.88617 21.3705C6.42157 21.8631 7.1302 22.1237 7.85714 22.0952H21.1905C21.9174 22.1237 22.6261 21.8631 23.1614 21.3705C23.6968 20.8779 24.0154 20.1934 24.0477 19.4666C24.0328 18.9043 23.8407 18.3613 23.4985 17.9147C23.1565 17.4682 22.682 17.1414 22.1429 16.9809Z"
|
||||
fill="url(#paint0_linear_1669_13486)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_dd_1669_13486"
|
||||
x="0"
|
||||
y="0"
|
||||
width="29.0476"
|
||||
height="30"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1669_13486" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feMorphology radius="2" operator="dilate" in="SourceAlpha" result="effect2_dropShadow_1669_13486" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1669_13486" result="effect2_dropShadow_1669_13486" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1669_13486" result="shape" />
|
||||
</filter>
|
||||
<clipPath id="bgblur_0_1669_13486_clip_path" transform="translate(0 0)">
|
||||
<path d="M13.5714 23.0477H15.4762C15.4762 23.3002 15.3758 23.5425 15.1973 23.721C15.0186 23.8997 14.7764 24 14.5238 24C14.2712 24 14.0289 23.8997 13.8504 23.721C13.6718 23.5425 13.5714 23.3002 13.5714 23.0477ZM22.1429 16.9809V12.7429C22.1429 9.25714 19.2286 6.39047 15.4762 5.9619V4.95238C15.4762 4.69978 15.3758 4.45754 15.1973 4.27894C15.0186 4.10034 14.7764 4 14.5238 4C14.2712 4 14.0289 4.10034 13.8504 4.27894C13.6718 4.45754 13.5714 4.69978 13.5714 4.95238V5.9619C9.81905 6.39047 6.90477 9.25714 6.90477 12.7429V16.9809C6.3657 17.1414 5.89119 17.4682 5.54907 17.9147C5.20696 18.3613 5.01479 18.9043 5 19.4666C5.03217 20.1934 5.35079 20.8779 5.88617 21.3705C6.42157 21.8631 7.1302 22.1237 7.85714 22.0952H21.1905C21.9174 22.1237 22.6261 21.8631 23.1614 21.3705C23.6968 20.8779 24.0154 20.1934 24.0477 19.4666C24.0328 18.9043 23.8407 18.3613 23.4985 17.9147C23.1565 17.4682 22.682 17.1414 22.1429 16.9809Z" />
|
||||
</clipPath>
|
||||
<linearGradient
|
||||
id="paint0_linear_1669_13486"
|
||||
x1="14.5238"
|
||||
y1="4"
|
||||
x2="14.5239"
|
||||
y2="39.5"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3B82F6" />
|
||||
<stop offset="1" stop-color="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const WarningIcon = ({ className, ...props }: SVGProps<SVGSVGElement>) => {
|
||||
// Remove colons to prevent ID recognition issues in some CSS environments
|
||||
const id = useId().replace(/:/g, '')
|
||||
const filterId = `filter_${id}`
|
||||
const maskId = `mask_${id}`
|
||||
const gradientId = `paint_${id}`
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
{...props}>
|
||||
<defs>
|
||||
<mask id={maskId}>
|
||||
{/* Show white part */}
|
||||
<rect width="24" height="24" fill="white" />
|
||||
{/* Clip black part */}
|
||||
<g fill="black">
|
||||
<path d="M12 17C12.5523 17 13 16.5523 13 16C13 15.4477 12.5523 15 12 15C11.4477 15 11 15.4477 11 16C11 16.5523 11.4477 17 12 17Z" />
|
||||
<path d="M12 13C11.7348 13 11.4804 12.8946 11.2929 12.7071C11.1054 12.5196 11 12.2652 11 12V8C11 7.73478 11.1054 7.48043 11.2929 7.29289C11.4804 7.10536 11.7348 7 12 7C12.2652 7 12.5196 7.10536 12.7071 7.29289C12.8946 7.48043 13 7.73478 13 8V12C13 12.2652 12.8946 12.5196 12.7071 12.7071C12.5196 12.8946 12.2652 13 12 13Z" />
|
||||
</g>
|
||||
</mask>
|
||||
<filter
|
||||
id={filterId}
|
||||
x="-3"
|
||||
y="-2"
|
||||
width="30"
|
||||
height="30"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
|
||||
</filter>
|
||||
<linearGradient id={gradientId} x1="12" y1="12" x2="12" y2="30.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#F59E0B" />
|
||||
<stop offset="1" stopColor="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g filter={`url(#${filterId})`}>
|
||||
<path
|
||||
d="M15.728 22H8.272C8.00681 21.9999 7.75249 21.8946 7.565 21.707L2.293 16.435C2.10545 16.2475 2.00006 15.9932 2 15.728V8.272C2.00006 8.00681 2.10545 7.75249 2.293 7.565L7.565 2.293C7.75249 2.10545 8.00681 2.00006 8.272 2H15.728C15.9932 2.00006 16.2475 2.10545 16.435 2.293L21.707 7.565C21.8946 7.75249 21.9999 8.00681 22 8.272V15.728C21.9999 15.9932 21.8946 16.2475 21.707 16.435L16.435 21.707C16.2475 21.8946 15.9932 21.9999 15.728 22Z"
|
||||
fill={`url(#${gradientId})`}
|
||||
mask={`url(#${maskId})`}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const SuccessIcon = ({ className, ...props }: SVGProps<SVGSVGElement>) => {
|
||||
const id = useId().replace(/:/g, '')
|
||||
const maskId = `mask_${id}`
|
||||
const filterId = `filter_${id}`
|
||||
const gradientId = `paint_${id}`
|
||||
const blurClipId = `blur_clip_${id}`
|
||||
|
||||
const checkPathData =
|
||||
'M17.3974 8.39243C17.6596 8.65461 17.6596 9.0797 17.3974 9.34187L11.1314 15.6078C11.0055 15.7338 10.8347 15.8045 10.6567 15.8045C10.4787 15.8045 10.3079 15.7338 10.182 15.6078L6.60142 12.0273C6.33924 11.7651 6.33924 11.3401 6.60142 11.0779C6.8636 10.8157 7.28868 10.8157 7.55086 11.0779L10.6567 14.1837L16.448 8.39243C16.7102 8.13026 17.1352 8.13026 17.3974 8.39243Z'
|
||||
|
||||
const polygonPathData =
|
||||
'M13.2121 2.57414C12.5853 1.80862 11.4146 1.80862 10.788 2.57414L9.90009 3.65856C9.83924 3.73288 9.73773 3.76009 9.64787 3.72614L8.33677 3.23092C7.41123 2.88134 6.39741 3.46667 6.23738 4.44301L6.0107 5.82606C5.99517 5.92086 5.92086 5.99516 5.82606 6.0107L4.44301 6.23738C3.46668 6.39741 2.88134 7.41122 3.23092 8.33676L3.72614 9.64787C3.76009 9.73773 3.73288 9.83924 3.65856 9.90009L2.57414 10.7879C1.80862 11.4147 1.80862 12.5854 2.57414 13.2121L3.65856 14.0999C3.73288 14.1608 3.76009 14.2623 3.72614 14.3522L3.23092 15.6633C2.88135 16.5888 3.46667 17.6026 4.44301 17.7627L5.82606 17.9893C5.92086 18.0049 5.99517 18.0792 6.0107 18.174L6.23738 19.557C6.39741 20.5333 7.41122 21.1186 8.33677 20.7691L9.64787 20.2739C9.73773 20.24 9.83924 20.2671 9.90009 20.3415L10.788 21.4259C11.4146 22.1914 12.5853 22.1914 13.2121 21.4259L14.0999 20.3415C14.1608 20.2671 14.2623 20.24 14.3521 20.2739L15.6633 20.7691C16.5888 21.1186 17.6027 20.5333 17.7626 19.557L17.9894 18.174C18.0049 18.0792 18.0791 18.0049 18.1739 17.9893L19.557 17.7627C20.5334 17.6026 21.1187 16.5888 20.7691 15.6633L20.2739 14.3522C20.2399 14.2623 20.2671 14.1608 20.3414 14.0999L21.4259 13.2121C22.1914 12.5854 22.1914 11.4147 21.4259 10.7879L20.3414 9.90009C20.2671 9.83924 20.2399 9.73773 20.2739 9.64787L20.7691 8.33676C21.1187 7.41122 20.5334 6.39741 19.557 6.23738L18.1739 6.0107C18.0791 5.99516 18.0791 5.92086 17.9894 5.82606L17.7626 4.44301C17.6027 3.46668 16.5888 2.88134 15.6633 3.23092L14.3521 3.72614C14.2623 3.76009 14.1608 3.73288 14.0999 3.65856L13.2121 2.57414Z'
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} {...props}>
|
||||
<defs>
|
||||
<mask
|
||||
id={maskId}
|
||||
style={{ maskType: 'luminance' }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24">
|
||||
{/* Show white part */}
|
||||
<rect width="24" height="24" fill="white" />
|
||||
{/* Clip black part */}
|
||||
<path d={checkPathData} fill="black" />
|
||||
</mask>
|
||||
|
||||
<clipPath id={blurClipId} transform="translate(3 2)">
|
||||
<path d={polygonPathData} />
|
||||
</clipPath>
|
||||
|
||||
<filter
|
||||
id={filterId}
|
||||
x="-3"
|
||||
y="-2"
|
||||
width="30"
|
||||
height="30"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feMorphology radius="2" operator="dilate" in="SourceAlpha" result="effect2_dropShadow" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape" />
|
||||
</filter>
|
||||
<linearGradient id={gradientId} x1="12" y1="7.5" x2="12" y2="41.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#3CD45A" />
|
||||
<stop offset="1" stopColor="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<g mask={`url(#${maskId})`}>
|
||||
<foreignObject x="-3" y="-2" width="30" height="30">
|
||||
<div
|
||||
style={{
|
||||
backdropFilter: 'blur(2px)',
|
||||
clipPath: `url(#${blurClipId})`,
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
</foreignObject>
|
||||
|
||||
<g filter={`url(#${filterId})`}>
|
||||
<path d={polygonPathData} fill={`url(#${gradientId})`} />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const ErrorIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g clip-path="url(#clip0_1669_13492)">
|
||||
<foreignObject x="-3" y="-2" width="30" height="28.8169">
|
||||
<div
|
||||
// xmlns="http://www.w3.org/1999/xhtml"
|
||||
style={{
|
||||
backdropFilter: 'blur(2px)',
|
||||
clipPath: 'url(#bgblur_1_1669_13492_clip_path)',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}}></div>
|
||||
</foreignObject>
|
||||
<g filter="url(#filter0_dd_1669_13492)" data-figma-bg-blur-radius="4">
|
||||
<path
|
||||
d="M21.709 17.3146L14.0873 3.2413C13.6682 2.47438 12.8697 2 12 2C11.1303 2 10.3318 2.47438 9.91272 3.2413L2.29101 17.3146C1.88778 18.0578 1.90359 18.9354 2.33844 19.6628C2.77329 20.3823 3.5323 20.8171 4.37828 20.8171H19.6217C20.4677 20.8171 21.2267 20.3823 21.6616 19.6628C22.0964 18.9354 22.1122 18.0578 21.709 17.3146ZM12 17.6546C11.5652 17.6546 11.2094 17.2988 11.2094 16.8639C11.2094 16.4291 11.5652 16.0733 12 16.0733C12.4348 16.0733 12.7906 16.4291 12.7906 16.8639C12.7906 17.2988 12.4348 17.6546 12 17.6546ZM12.7906 14.492C12.7906 14.9269 12.4348 15.2827 12 15.2827C11.5652 15.2827 11.2094 14.9269 11.2094 14.492V8.16695C11.2094 7.7321 11.5652 7.37632 12 7.37632C12.4348 7.37632 12.7906 7.7321 12.7906 8.16695V14.492Z"
|
||||
fill="url(#paint0_linear_1669_13492)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_dd_1669_13492"
|
||||
x="-3"
|
||||
y="-2"
|
||||
width="30"
|
||||
height="28.8169"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1669_13492" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feMorphology radius="2" operator="dilate" in="SourceAlpha" result="effect2_dropShadow_1669_13492" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1669_13492" result="effect2_dropShadow_1669_13492" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1669_13492" result="shape" />
|
||||
</filter>
|
||||
<clipPath id="bgblur_1_1669_13492_clip_path" transform="translate(3 2)">
|
||||
<path d="M21.709 17.3146L14.0873 3.2413C13.6682 2.47438 12.8697 2 12 2C11.1303 2 10.3318 2.47438 9.91272 3.2413L2.29101 17.3146C1.88778 18.0578 1.90359 18.9354 2.33844 19.6628C2.77329 20.3823 3.5323 20.8171 4.37828 20.8171H19.6217C20.4677 20.8171 21.2267 20.3823 21.6616 19.6628C22.0964 18.9354 22.1122 18.0578 21.709 17.3146ZM12 17.6546C11.5652 17.6546 11.2094 17.2988 11.2094 16.8639C11.2094 16.4291 11.5652 16.0733 12 16.0733C12.4348 16.0733 12.7906 16.4291 12.7906 16.8639C12.7906 17.2988 12.4348 17.6546 12 17.6546ZM12.7906 14.492C12.7906 14.9269 12.4348 15.2827 12 15.2827C11.5652 15.2827 11.2094 14.9269 11.2094 14.492V8.16695C11.2094 7.7321 11.5652 7.37632 12 7.37632C12.4348 7.37632 12.7906 7.7321 12.7906 8.16695V14.492Z" />
|
||||
</clipPath>
|
||||
<linearGradient id="paint0_linear_1669_13492" x1="12" y1="31.5" x2="12" y2="11" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" />
|
||||
<stop offset="0.97" stop-color="#DC2626" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1669_13492">
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
// 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<ToastData = unknown> {
|
||||
id: string | number
|
||||
type?: 'info' | 'warning' | 'error' | 'success' | 'loading' | 'custom'
|
||||
title: ReactNode
|
||||
description?: ReactNode
|
||||
colored?: boolean
|
||||
duration?: number
|
||||
dismissable?: boolean
|
||||
onDismiss?: () => void
|
||||
button?: Action | ReactNode
|
||||
promise?: Promise<ToastData>
|
||||
classNames?: ToastClassnames
|
||||
jsx?: (id: number | string) => React.ReactElement
|
||||
}
|
||||
|
||||
function toast(props: Omit<ToastProps, 'id'>) {
|
||||
const type = props.type ?? 'info'
|
||||
|
||||
const baseClassNames: ToastClassnames = {
|
||||
toast: cn(
|
||||
'flex rounded-xs p-4 bg-background border-border border-[0.5px] shadow-lg items-center',
|
||||
props.button ? 'gap-3' : 'gap-4',
|
||||
props.colored && type !== 'custom' && toastBgColorVariants({ type }),
|
||||
props.classNames?.toast
|
||||
),
|
||||
content: cn('flex flex-col', props.description && (props.button ? 'gap-1' : 'gap-2')),
|
||||
title: cn(
|
||||
'text-md font-medium leading-4.5',
|
||||
props.description === undefined && 'text-xs leading-3.5 tracking-normal',
|
||||
props.classNames?.title
|
||||
),
|
||||
description: cn('text-foreground-secondary text-xs leading-3.5 tracking-normal', props.classNames?.description),
|
||||
actionButton: cn(
|
||||
'py-1 px-2 rounded-3xs flex items-center h-7 max-h-7 bg-background-subtle border-[0.5px] border-border',
|
||||
'text-foreground text-sm leading-4 tracking-normal min-w-fit',
|
||||
props.colored && 'bg-white/10',
|
||||
props.classNames?.actionButton
|
||||
),
|
||||
icon: cn('size-6 min-w-6', props.description && 'self-start'),
|
||||
loader: cn('!static ![--size:24px]')
|
||||
}
|
||||
const { classNames: externalClassNames, ...rest } = props
|
||||
delete externalClassNames?.toast
|
||||
const classNames = merge(baseClassNames, externalClassNames)
|
||||
const data = {
|
||||
classNames,
|
||||
unstyled: true,
|
||||
description: rest.description,
|
||||
duration: rest.duration,
|
||||
action: rest.button,
|
||||
dismissible: rest.dismissable,
|
||||
onDismiss: rest.onDismiss
|
||||
} satisfies ExternalToast
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return sonnerToast.info(props.title, data)
|
||||
case 'warning':
|
||||
return sonnerToast.warning(props.title, data)
|
||||
case 'error':
|
||||
return sonnerToast.error(props.title, data)
|
||||
case 'success':
|
||||
return sonnerToast.success(props.title, data)
|
||||
case 'loading':
|
||||
const id = sonnerToast.loading(props.title, data)
|
||||
if (props.promise) {
|
||||
// Auto dismiss when promise is settled
|
||||
props.promise.finally(() => {
|
||||
sonnerToast.dismiss(id)
|
||||
})
|
||||
}
|
||||
return id
|
||||
default:
|
||||
console.warn('Using custom toast without a jsx.')
|
||||
return sonnerToast.custom(props.jsx ?? ((id) => <div id={String(id)}>{props.title}</div>))
|
||||
}
|
||||
}
|
||||
|
||||
interface QuickApiProps extends Omit<ToastProps, 'type' | 'id' | 'title'> {}
|
||||
|
||||
interface QuickLoadingProps extends QuickApiProps {
|
||||
promise: ToastProps['promise']
|
||||
}
|
||||
|
||||
toast.info = (message: ReactNode, data?: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'info',
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
toast.success = (message: ReactNode, data?: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'success',
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
toast.warning = (message: ReactNode, data?: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'warning',
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
toast.error = (message: ReactNode, data?: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'error',
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
toast.loading = (message: ReactNode, data: QuickLoadingProps) => {
|
||||
toast({
|
||||
type: 'loading',
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
toast.dismiss = (id: ToastProps['id']) => {
|
||||
sonnerToast.dismiss(id)
|
||||
}
|
||||
|
||||
// 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: 'backdrop-blur-md bg-blue-500/10 border-blue-500/20',
|
||||
warning: 'backdrop-blur-md bg-orange-500/10 border-orange-500/20',
|
||||
error: 'backdrop-blur-md bg-red-500/10 border-red-500/20',
|
||||
success: 'backdrop-blur-md bg-primary/10 border-primary/20',
|
||||
loading: 'backdrop-blur-none'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// function Toast({
|
||||
// id,
|
||||
// type,
|
||||
// title,
|
||||
// description,
|
||||
// coloredMessage,
|
||||
// colored: 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 (
|
||||
// <div
|
||||
// id={String(id)}
|
||||
// className={cn(
|
||||
// 'flex p-4 rounded-xs bg-background border-border border-[0.5px] 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"
|
||||
// // FIXME: missing hover/active style
|
||||
// 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>
|
||||
// )
|
||||
// }
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<Sonner
|
||||
className="toaster group"
|
||||
icons={{
|
||||
info: <InfoIcon />,
|
||||
success: <SuccessIcon />,
|
||||
warning: <WarningIcon />,
|
||||
error: <ErrorIcon />
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { toast, Toaster }
|
||||
574
packages/ui/stories/components/primitives/Sonner.stories.tsx
Normal file
574
packages/ui/stories/components/primitives/Sonner.stories.tsx
Normal file
@ -0,0 +1,574 @@
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { toast, Toaster } from '@cherrystudio/ui'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
interface PlaygroundArgs {
|
||||
type: 'info' | 'success' | 'warning' | 'error' | 'loading'
|
||||
title: string
|
||||
description: string
|
||||
colored: boolean
|
||||
duration: number
|
||||
withButton: boolean
|
||||
buttonLabel: string
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
// Playground
|
||||
export const Playground: StoryObj<PlaygroundArgs> = {
|
||||
args: {
|
||||
type: 'info',
|
||||
title: 'Notification Title',
|
||||
description: 'This is a description that provides more details about the notification.',
|
||||
colored: false,
|
||||
duration: 4000,
|
||||
withButton: false,
|
||||
buttonLabel: 'Action'
|
||||
},
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['info', 'success', 'warning', 'error', 'loading'],
|
||||
description: 'Type of toast notification'
|
||||
},
|
||||
title: {
|
||||
control: 'text',
|
||||
description: 'Main message of the toast'
|
||||
},
|
||||
description: {
|
||||
control: 'text',
|
||||
description: 'Optional detailed description'
|
||||
},
|
||||
colored: {
|
||||
control: 'boolean',
|
||||
description: 'Enable colored background'
|
||||
},
|
||||
duration: {
|
||||
control: { type: 'number', min: 1000, max: 10000, step: 1000 },
|
||||
description: 'Duration in milliseconds (use Infinity for persistent)'
|
||||
},
|
||||
withButton: {
|
||||
control: 'boolean',
|
||||
description: 'Show action button'
|
||||
},
|
||||
buttonLabel: {
|
||||
control: 'text',
|
||||
description: 'Label for the action button',
|
||||
if: { arg: 'withButton', truthy: true }
|
||||
}
|
||||
},
|
||||
render: (args: PlaygroundArgs) => {
|
||||
const handleToast = () => {
|
||||
const toastOptions: {
|
||||
description?: string
|
||||
colored: boolean
|
||||
duration: number
|
||||
button?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
promise?: Promise<void>
|
||||
} = {
|
||||
description: args.description || undefined,
|
||||
colored: args.colored,
|
||||
duration: args.duration
|
||||
}
|
||||
|
||||
if (args.withButton) {
|
||||
toastOptions.button = {
|
||||
label: args.buttonLabel || 'Action',
|
||||
onClick: () => toast.info('Button clicked!')
|
||||
}
|
||||
}
|
||||
|
||||
switch (args.type) {
|
||||
case 'info':
|
||||
toast.info(args.title, toastOptions)
|
||||
break
|
||||
case 'success':
|
||||
toast.success(args.title, toastOptions)
|
||||
break
|
||||
case 'warning':
|
||||
toast.warning(args.title, toastOptions)
|
||||
break
|
||||
case 'error':
|
||||
toast.error(args.title, toastOptions)
|
||||
break
|
||||
case 'loading':
|
||||
toast.loading(args.title, {
|
||||
...toastOptions,
|
||||
promise: new Promise<void>((resolve) => setTimeout(resolve, 2000))
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button onClick={handleToast}>Show Toast</Button>
|
||||
<div className="text-sm text-muted-foreground max-w-md">
|
||||
Use the controls panel below to customize the toast properties and click the button to preview.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Basic Toast Types
|
||||
export const Info: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info('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('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('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('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('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('Info Toast')}>Info</Button>
|
||||
<Button onClick={() => toast.success('Success Toast')}>Success</Button>
|
||||
<Button onClick={() => toast.warning('Warning Toast')}>Warning</Button>
|
||||
<Button onClick={() => toast.error('Error Toast')}>Error</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.loading('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('Event Created', {
|
||||
description: 'Your event has been created successfully. You can now share it with others.'
|
||||
})
|
||||
}>
|
||||
Show Toast with Description
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Custom Duration
|
||||
export const WithCustomDuration: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info('Quick message', {
|
||||
description: 'This will disappear in 1 second',
|
||||
duration: 1000
|
||||
})
|
||||
}>
|
||||
1 Second
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success('Normal duration', {
|
||||
description: 'This uses default duration (4 seconds)'
|
||||
})
|
||||
}>
|
||||
Default (4s)
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning('Important message', {
|
||||
description: 'This will stay for 10 seconds',
|
||||
duration: 10000
|
||||
})
|
||||
}>
|
||||
10 Seconds
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info('Persistent message', {
|
||||
description: 'This will stay until manually dismissed',
|
||||
duration: Number.POSITIVE_INFINITY
|
||||
})
|
||||
}>
|
||||
Infinite
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Action Button
|
||||
export const WithActionButton: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success('Changes Saved', {
|
||||
description: 'Your changes have been saved successfully.',
|
||||
button: {
|
||||
label: 'Undo',
|
||||
onClick: () => toast.info('Undoing changes...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Success with Action
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error('Update Failed', {
|
||||
description: 'Failed to update the record.',
|
||||
button: {
|
||||
label: 'Retry',
|
||||
onClick: () => toast.info('Retrying...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Error with Action
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info('Update Available', {
|
||||
description: 'A new version is ready to install.',
|
||||
button: {
|
||||
label: 'Update',
|
||||
onClick: () => toast.info('Starting update...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Info with Action
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Colored Background
|
||||
export const WithColoredBackground: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info('Information', {
|
||||
description: 'This toast has a colored background.',
|
||||
colored: true
|
||||
})
|
||||
}>
|
||||
Info Background
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success('Success!', {
|
||||
description: 'This toast has a colored background.',
|
||||
colored: true
|
||||
})
|
||||
}>
|
||||
Success Background
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning('Warning', {
|
||||
description: 'This toast has a colored background.',
|
||||
colored: true
|
||||
})
|
||||
}>
|
||||
Warning Background
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error('Error', {
|
||||
description: 'This toast has a colored background.',
|
||||
colored: true
|
||||
})
|
||||
}>
|
||||
Error Background
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Colored Background with Action
|
||||
export const ColoredBackgroundWithAction: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success('File Uploaded', {
|
||||
description: 'Your file has been uploaded successfully.',
|
||||
colored: true,
|
||||
button: {
|
||||
label: 'View',
|
||||
onClick: () => toast.info('Opening file...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Success with Button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning('Action Required', {
|
||||
description: 'Please review the changes.',
|
||||
colored: true,
|
||||
button: {
|
||||
label: 'Review',
|
||||
onClick: () => toast.info('Opening review...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Warning with Button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error('Update Failed', {
|
||||
description: 'Failed to update the record.',
|
||||
colored: true,
|
||||
button: {
|
||||
label: 'Retry',
|
||||
onClick: () => toast.info('Retrying...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Error with Button
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Multiple Toasts
|
||||
export const MultipleToasts: Story = {
|
||||
render: () => {
|
||||
const showMultiple = () => {
|
||||
toast.success('First notification', { description: 'This is the first message' })
|
||||
setTimeout(() => toast.info('Second notification', { description: 'This is the second message' }), 100)
|
||||
setTimeout(() => toast.warning('Third notification', { description: 'This is the third message' }), 200)
|
||||
setTimeout(() => toast.error('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('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('Saving file...', {
|
||||
promise
|
||||
})
|
||||
promise.then(() => {
|
||||
toast.success('File saved', {
|
||||
description: 'Your file has been saved successfully.',
|
||||
button: {
|
||||
label: 'View',
|
||||
onClick: () => toast.info('Opening file...')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
toast.success('Form submitted', {
|
||||
description: 'Your changes have been saved successfully.',
|
||||
button: {
|
||||
label: 'Undo',
|
||||
onClick: () => toast.info('Undoing changes...')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
toast.error('Failed to delete', {
|
||||
description: 'You do not have permission to delete this item.',
|
||||
button: {
|
||||
label: 'Retry',
|
||||
onClick: () => toast.info('Retrying...')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText('https://example.com')
|
||||
toast.success('Copied to clipboard', {
|
||||
description: 'The link has been copied to your clipboard.'
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
toast.info('Update available', {
|
||||
description: 'A new version of the application is ready to install.',
|
||||
colored: true,
|
||||
button: {
|
||||
label: 'Update Now',
|
||||
onClick: () => toast.info('Starting update...')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import '@renderer/databases'
|
||||
|
||||
import { Toaster } from '@cherrystudio/ui'
|
||||
import { preferenceService } from '@data/PreferenceService'
|
||||
import { loggerService } from '@logger'
|
||||
import store, { persistor } from '@renderer/store'
|
||||
@ -43,6 +44,7 @@ function App(): React.ReactElement {
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<Router />
|
||||
<Toaster />
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
|
||||
@ -186,43 +186,20 @@ export const closeToast = (key: string) => {
|
||||
getMessageApi().destroy(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all toast notifications
|
||||
*/
|
||||
export const closeAll = () => {
|
||||
getMessageApi().destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub functions for compatibility with previous toast API
|
||||
* These are no-ops since antd message doesn't expose a queue
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||
* Antd message doesn't expose a queue. Do not rely on this function.
|
||||
* @returns Empty toast queue stub
|
||||
*/
|
||||
export const getToastQueue = (): any => ({ toasts: [] })
|
||||
|
||||
/**
|
||||
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||
* Antd message doesn't track closing state. Do not rely on this function.
|
||||
* @param key - Toast key (unused)
|
||||
* @returns Always returns false
|
||||
*/
|
||||
export const isToastClosing = (key?: string): boolean => {
|
||||
key // unused
|
||||
return false
|
||||
export type ToastUtilities = {
|
||||
addToast: typeof addToast
|
||||
close: typeof closeToast
|
||||
error: typeof error
|
||||
success: typeof success
|
||||
warning: typeof warning
|
||||
info: typeof info
|
||||
loading: typeof loading
|
||||
}
|
||||
|
||||
export const getToastUtilities = () =>
|
||||
({
|
||||
getToastQueue,
|
||||
addToast,
|
||||
closeToast,
|
||||
closeAll,
|
||||
isToastClosing,
|
||||
close: closeToast,
|
||||
error,
|
||||
success,
|
||||
warning,
|
||||
|
||||
3
src/renderer/src/env.d.ts
vendored
3
src/renderer/src/env.d.ts
vendored
@ -1,10 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { ToastUtilities } from '@cherrystudio/ui'
|
||||
import type { HookAPI } from 'antd/es/modal/useModal'
|
||||
import type { NavigateFunction } from 'react-router-dom'
|
||||
|
||||
import type { ToastUtilities } from './components/TopView/toast'
|
||||
|
||||
interface ImportMetaEnv {
|
||||
VITE_RENDERER_INTEGRATED_MODEL: string
|
||||
}
|
||||
|
||||
@ -176,7 +176,7 @@ const ErrorDetailModal: React.FC<ErrorDetailModalProps> = ({ open, onClose, erro
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(errorText)
|
||||
window.toast.addToast({ title: t('message.copied') })
|
||||
window.toast.success({ title: t('message.copied') })
|
||||
}
|
||||
|
||||
const renderErrorDetails = (error?: SerializedError) => {
|
||||
|
||||
@ -83,8 +83,7 @@ export async function checkDataLimit() {
|
||||
}
|
||||
currentInterval = setInterval(check, CHECK_INTERVAL_WARNING)
|
||||
} else if (!shouldShowWarning && currentToastId) {
|
||||
// Dismiss toast when space is recovered
|
||||
window.toast.closeToast(currentToastId)
|
||||
window.toast.close(currentToastId)
|
||||
currentToastId = null
|
||||
|
||||
// Switch back to normal mode
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
"packages/extension-table-plus/**/*",
|
||||
"packages/mcp-trace/**/*",
|
||||
"packages/shared/**/*",
|
||||
"packages/ui/**/*",
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
11
yarn.lock
11
yarn.lock
@ -2255,6 +2255,7 @@ __metadata:
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
react-dropzone: "npm:^14.3.8"
|
||||
sonner: "npm:2.0.7"
|
||||
storybook: "npm:^10.0.5"
|
||||
styled-components: "npm:^6.1.15"
|
||||
tailwind-merge: "npm:^2.5.5"
|
||||
@ -27901,6 +27902,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sonner@npm:2.0.7":
|
||||
version: 2.0.7
|
||||
resolution: "sonner@npm:2.0.7"
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
checksum: 10c0/6966ab5e892ed6aab579a175e4a24f3b48747f0fc21cb68c3e33cb41caa7a0eebeb098c210545395e47a18d585eb8734ae7dd12d2bd18c8a3294a1ee73f997d9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "source-map-js@npm:1.2.1"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user