diff --git a/.vscode/settings.json b/.vscode/settings.json index 09d356c41b..7eeedccb16 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -54,6 +54,6 @@ "classNames" ], "tailwindCSS.experimental.classRegex": [ - ["cva\\(([^;]*)[\\);]", "[`'\"`]([^'\"`;]*)[`'\"`]"] + ["cva\\(([^;]*)[\\);]", "[`'\"`]([^'\"`;]*)[`'\"`]"], ["cn\\(([^;]*)[\\);]", "[`'\"`]([^'\"`;]*)[`'\"`]"] ] } diff --git a/packages/ui/package.json b/packages/ui/package.json index 31e65d0bd4..97658dac99 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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": { diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index c8ed30ea0a..1cede94148 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -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' diff --git a/packages/ui/src/components/primitives/sonner.d.ts b/packages/ui/src/components/primitives/sonner.d.ts new file mode 100644 index 0000000000..1be1a8880c --- /dev/null +++ b/packages/ui/src/components/primitives/sonner.d.ts @@ -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 +} + +/** + * Props for quick toast API methods (without type field) + */ +interface QuickToastProps extends Omit {} + +/** + * 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): 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 } diff --git a/packages/ui/src/components/primitives/sonner.tsx b/packages/ui/src/components/primitives/sonner.tsx new file mode 100644 index 0000000000..186d6a74bb --- /dev/null +++ b/packages/ui/src/components/primitives/sonner.tsx @@ -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) => ( + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+) + +const WarningIcon = ({ className, ...props }: SVGProps) => { + // 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 ( + + + + {/* Show white part */} + + {/* Clip black part */} + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +const SuccessIcon = ({ className, ...props }: SVGProps) => { + 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 ( + + + + {/* Show white part */} + + {/* Clip black part */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + ) +} + +const ErrorIcon = ({ className }: SVGProps) => ( + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+) + +// const CloseIcon = ({ className }: SVGProps) => ( +// +// +// +// ) +interface ToastProps { + 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 + classNames?: ToastClassnames + jsx?: (id: number | string) => React.ReactElement +} + +function toast(props: Omit) { + 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) =>
{props.title}
)) + } +} + +interface QuickApiProps extends Omit {} + +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 +// case 'error': +// return +// case 'loading': +// return +// case 'success': +// return +// case 'warning': +// return +// } +// }, [type]) + +// const handleDismiss = useCallback(() => { +// sonnerToast.dismiss(id) +// onDismiss?.() +// }, [id, onDismiss]) + +// return ( +//
+// {dismissable && ( +// +// )} +//
+// {icon} +//
+//
+// {title} +//
+//
+//

+// {coloredMessage && {coloredMessage} } +// {description} +//

+//
+// {link && ( +// // FIXME: missing typography/typography components/p/letter-spacing +// +// )} +//
+//
+// {button !== undefined && ( +// +// )} +//
+// ) +// } + +const Toaster = ({ ...props }: ToasterProps) => { + return ( + , + success: , + warning: , + error: + }} + {...props} + /> + ) +} + +export { toast, Toaster } diff --git a/packages/ui/stories/components/primitives/Sonner.stories.tsx b/packages/ui/stories/components/primitives/Sonner.stories.tsx new file mode 100644 index 0000000000..f561fd72d0 --- /dev/null +++ b/packages/ui/stories/components/primitives/Sonner.stories.tsx @@ -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 = { + 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) => ( +
+ + +
+ ) + ] +} + +export default meta +type Story = StoryObj + +// Playground +export const Playground: StoryObj = { + 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 + } = { + 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((resolve) => setTimeout(resolve, 2000)) + }) + break + } + } + + return ( +
+ +
+ Use the controls panel below to customize the toast properties and click the button to preview. +
+
+ ) + } +} + +// Basic Toast Types +export const Info: Story = { + render: () => ( +
+ +
+ ) +} + +export const Success: Story = { + render: () => ( +
+ +
+ ) +} + +export const ErrorToast: Story = { + render: () => ( +
+ +
+ ) +} + +export const Warning: Story = { + render: () => ( +
+ +
+ ) +} + +export const Loading: Story = { + render: () => { + const mockPromise = new Promise((resolve) => { + setTimeout(() => resolve('Data loaded'), 2000) + }) + + return ( +
+ +
+ ) + } +} + +// All Toast Types Together +export const AllTypes: Story = { + render: () => ( +
+ + + + + +
+ ) +} + +// With Description +export const WithDescription: Story = { + render: () => ( +
+ +
+ ) +} + +// With Custom Duration +export const WithCustomDuration: Story = { + render: () => ( +
+ + + + +
+ ) +} + +// With Action Button +export const WithActionButton: Story = { + render: () => ( +
+ + + +
+ ) +} + +// With Colored Background +export const WithColoredBackground: Story = { + render: () => ( +
+ + + + +
+ ) +} + +// Colored Background with Action +export const ColoredBackgroundWithAction: Story = { + render: () => ( +
+ + + +
+ ) +} + +// 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 ( +
+ +
+ ) + } +} + +// 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 ( +
+ +
+ ) + } +} + +// 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 ( +
+
+

File Operations

+
+ + +
+
+ +
+

Form Submissions

+
+ +
+
+ +
+

Error Handling

+
+ +
+
+ +
+

Updates & Notifications

+
+ +
+
+
+ ) + } +} diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index fb34a13a26..da8b801457 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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 { + diff --git a/src/renderer/src/components/TopView/toast.tsx b/src/renderer/src/components/TopView/toast.tsx index 081a436892..c9b1d56fca 100644 --- a/src/renderer/src/components/TopView/toast.tsx +++ b/src/renderer/src/components/TopView/toast.tsx @@ -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, diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index f4452a5c31..55ec1d82c9 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -1,10 +1,11 @@ /// 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 } diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx index 1cb445f12d..53fe890ed3 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx @@ -176,7 +176,7 @@ const ErrorDetailModal: React.FC = ({ 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) => { diff --git a/src/renderer/src/utils/dataLimit.ts b/src/renderer/src/utils/dataLimit.ts index 2c8db0697b..e8bb3773f7 100644 --- a/src/renderer/src/utils/dataLimit.ts +++ b/src/renderer/src/utils/dataLimit.ts @@ -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 diff --git a/tsconfig.web.json b/tsconfig.web.json index 8ca81be30b..9c5f48fb67 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -11,7 +11,6 @@ "packages/extension-table-plus/**/*", "packages/mcp-trace/**/*", "packages/shared/**/*", - "packages/ui/**/*", ], "compilerOptions": { "composite": true, diff --git a/yarn.lock b/yarn.lock index e88e1bbe9a..22df2fa4f5 100644 --- a/yarn.lock +++ b/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"