From 8a9687f1906c643c2b09b9c90609081276bdcfbb Mon Sep 17 00:00:00 2001 From: icarus Date: Fri, 12 Dec 2025 18:45:48 +0800 Subject: [PATCH 01/17] feat(ui): add sonner toaster and next-themes support - Add sonner package for toast notifications with custom icons and styling - Add next-themes package for theme support - Create new Toaster component with theme-aware styling --- packages/ui/package.json | 2 + .../ui/src/components/primitives/sonner.tsx | 38 +++++++++++++++++++ yarn.lock | 22 +++++++++++ 3 files changed, 62 insertions(+) create mode 100644 packages/ui/src/components/primitives/sonner.tsx diff --git a/packages/ui/package.json b/packages/ui/package.json index 31e65d0bd4..32602956f0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -62,7 +62,9 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "lucide-react": "^0.545.0", + "next-themes": "^0.4.6", "react-dropzone": "^14.3.8", + "sonner": "^2.0.7", "tailwind-merge": "^2.5.5" }, "devDependencies": { diff --git a/packages/ui/src/components/primitives/sonner.tsx b/packages/ui/src/components/primitives/sonner.tsx new file mode 100644 index 0000000000..9f46e06d5c --- /dev/null +++ b/packages/ui/src/components/primitives/sonner.tsx @@ -0,0 +1,38 @@ +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react" +import { useTheme } from "next-themes" +import { Toaster as Sonner, type ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +export { Toaster } diff --git a/yarn.lock b/yarn.lock index e88e1bbe9a..053712671e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2252,9 +2252,11 @@ __metadata: framer-motion: "npm:^12.23.12" linguist-languages: "npm:^9.0.0" lucide-react: "npm:^0.545.0" + next-themes: "npm:^0.4.6" 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" @@ -23693,6 +23695,16 @@ __metadata: languageName: node linkType: hard +"next-themes@npm:^0.4.6": + version: 0.4.6 + resolution: "next-themes@npm:0.4.6" + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + checksum: 10c0/83590c11d359ce7e4ced14f6ea9dd7a691d5ce6843fe2dc520fc27e29ae1c535118478d03e7f172609c41b1ef1b8da6b8dd2d2acd6cd79cac1abbdbd5b99f2c4 + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -27901,6 +27913,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" From 621685e5b5e17b94ec497d35a1deea50781b20a1 Mon Sep 17 00:00:00 2001 From: icarus Date: Fri, 12 Dec 2025 19:11:18 +0800 Subject: [PATCH 02/17] feat(sonner): replace lucide icons with custom svg icons for toaster --- .../ui/src/components/primitives/sonner.tsx | 305 ++++++++++++++++-- 1 file changed, 286 insertions(+), 19 deletions(-) diff --git a/packages/ui/src/components/primitives/sonner.tsx b/packages/ui/src/components/primitives/sonner.tsx index 9f46e06d5c..a07294e5e6 100644 --- a/packages/ui/src/components/primitives/sonner.tsx +++ b/packages/ui/src/components/primitives/sonner.tsx @@ -1,33 +1,300 @@ -import { - CircleCheckIcon, - InfoIcon, - Loader2Icon, - OctagonXIcon, - TriangleAlertIcon, -} from "lucide-react" -import { useTheme } from "next-themes" -import { Toaster as Sonner, type ToasterProps } from "sonner" +import { Loader2Icon } from 'lucide-react' +import { useTheme } from 'next-themes' +import type { SVGProps } from 'react' +import { Toaster as Sonner, type ToasterProps } from 'sonner' + +const InfoIcon = ({ className }: SVGProps) => ( + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+) + +const WarningIcon = ({ className }: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + +const SuccessIcon = ({ className }: SVGProps) => ( + + + + + + +
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+) + +const ErrorIcon = ({ className }: SVGProps) => ( + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+) const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme() + const { theme = 'system' } = useTheme() return ( , + success: , info: , - warning: , - error: , - loading: , + warning: , + error: , + loading: }} style={ { - "--normal-bg": "var(--popover)", - "--normal-text": "var(--popover-foreground)", - "--normal-border": "var(--border)", - "--border-radius": "var(--radius)", + '--normal-bg': 'var(--popover)', + '--normal-text': 'var(--popover-foreground)', + '--normal-border': 'var(--border)', + '--border-radius': 'var(--radius)' } as React.CSSProperties } {...props} From b878315491aa8e243bd340d4b15263e0ea9b62c6 Mon Sep 17 00:00:00 2001 From: icarus Date: Fri, 12 Dec 2025 19:12:35 +0800 Subject: [PATCH 03/17] feat(components): export Toaster from sonner primitives --- packages/ui/src/components/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index c8ed30ea0a..a17c9cbf96 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 { Toaster } from './primitives/sonner' export * from './primitives/tabs' export * as Textarea from './primitives/textarea' From 17d9da6f4e44788986d64e4aca3769d09e2576e4 Mon Sep 17 00:00:00 2001 From: icarus Date: Fri, 12 Dec 2025 19:18:08 +0800 Subject: [PATCH 04/17] refactor(toast): remove deprecated toast utilities and update comment Remove deprecated toast API functions that were no-ops and update dataLimit comment to reflect current toast limitations --- src/renderer/src/components/TopView/toast.tsx | 42 ------------------- src/renderer/src/utils/dataLimit.ts | 4 +- 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/src/renderer/src/components/TopView/toast.tsx b/src/renderer/src/components/TopView/toast.tsx index 081a436892..0731a1b4bb 100644 --- a/src/renderer/src/components/TopView/toast.tsx +++ b/src/renderer/src/components/TopView/toast.tsx @@ -178,51 +178,9 @@ export const loading = (args: RequireSome): strin */ export const addToast = (config: ToastConfig) => info(config) -/** - * Close a specific toast notification by its key - * @param key - Toast key (string) - */ -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 const getToastUtilities = () => ({ - getToastQueue, addToast, - closeToast, - closeAll, - isToastClosing, error, success, warning, diff --git a/src/renderer/src/utils/dataLimit.ts b/src/renderer/src/utils/dataLimit.ts index 2c8db0697b..6e79732ab7 100644 --- a/src/renderer/src/utils/dataLimit.ts +++ b/src/renderer/src/utils/dataLimit.ts @@ -83,8 +83,8 @@ export async function checkDataLimit() { } currentInterval = setInterval(check, CHECK_INTERVAL_WARNING) } else if (!shouldShowWarning && currentToastId) { - // Dismiss toast when space is recovered - window.toast.closeToast(currentToastId) + // TODO: new toast component cannot be closed programmatically. add a dismiss button in the toast. + // window.toast.closeToast(currentToastId) currentToastId = null // Switch back to normal mode From 8794f2b3ac85720ecebfd1429f8c00df2e199c09 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 13 Dec 2025 00:41:52 +0800 Subject: [PATCH 05/17] 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 --- packages/ui/src/components/index.ts | 2 +- .../ui/src/components/primitives/sonner.d.ts | 146 +++++ .../ui/src/components/primitives/sonner.tsx | 245 ++++++-- .../components/primitives/Sonner.stories.tsx | 534 ++++++++++++++++++ 4 files changed, 885 insertions(+), 42 deletions(-) create mode 100644 packages/ui/src/components/primitives/sonner.d.ts create mode 100644 packages/ui/stories/components/primitives/Sonner.stories.tsx diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index a17c9cbf96..1cede94148 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -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' 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..3026be7915 --- /dev/null +++ b/packages/ui/src/components/primitives/sonner.d.ts @@ -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 +} + +/** + * 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 +} + +// 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 index a07294e5e6..c85b87ac40 100644 --- a/packages/ui/src/components/primitives/sonner.tsx +++ b/packages/ui/src/components/primitives/sonner.tsx @@ -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) => ( - - + +
@@ -129,19 +128,12 @@ const WarningIcon = ({ className }: SVGProps) => ( ) const SuccessIcon = ({ className }: SVGProps) => ( - - + + - +
) => ( ) -const Toaster = ({ ...props }: ToasterProps) => { - const { theme = 'system' } = useTheme() +const CloseIcon = ({ className }: SVGProps) => ( + + + +) +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 +} + +function toast(props: Omit) { + return sonnerToast.custom((id) => ) +} + +interface QuickApiProps extends Omit {} + +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 + case 'error': + return + case 'loading': + return + case 'success': + return + case 'warning': + return + } + }, [type]) + + const handleDismiss = useCallback(() => { + sonnerToast.dismiss(id) + onDismiss?.() + }, [id, onDismiss]) return ( - , - info: , - warning: , - error: , - loading: - }} - style={ - { - '--normal-bg': 'var(--popover)', - '--normal-text': 'var(--popover-foreground)', - '--normal-border': 'var(--border)', - '--border-radius': 'var(--radius)' - } as React.CSSProperties - } - {...props} - /> +
+ {dismissable && ( + + )} +
+ {icon} +
+
+ {title} +
+
+

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

+
+ {link && ( + // FIXME: missing typography/typography components/p/letter-spacing + + )} +
+
+ {button !== undefined && ( + + )} +
) } -export { Toaster } +const Toaster = ({ ...props }: ToasterProps) => { + return +} + +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..0c13d500a1 --- /dev/null +++ b/packages/ui/stories/components/primitives/Sonner.stories.tsx @@ -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 = { + 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 + +// 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 Colored Message +export const WithColoredMessage: Story = { + render: () => ( +
+ + +
+ ) +} + +// With Colored Background +export const WithColoredBackground: Story = { + render: () => ( +
+ + + + +
+ ) +} + +// Colored Background with Actions +export const ColoredBackgroundWithActions: Story = { + render: () => ( +
+ + + +
+ ) +} + +// With Action Button +export const WithActionButton: Story = { + render: () => ( +
+ +
+ ) +} + +// With Link +export const WithLink: Story = { + render: () => ( +
+ + +
+ ) +} + +// With Button and Link +export const WithButtonAndLink: Story = { + render: () => ( +
+ +
+ ) +} + +// Dismissable Toast +export const DismissableToast: Story = { + render: () => ( +
+ +
+ ) +} + +// 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 ( +
+ +
+ ) + } +} + +// 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 ( +
+ +
+ ) + } +} + +// 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: , + 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 ( +
+
+

File Operations

+
+ + +
+
+ +
+

Form Submissions

+
+ +
+
+ +
+

Error Handling

+
+ +
+
+ +
+

Updates & Notifications

+
+ +
+
+
+ ) + } +} From c8c67006afe5770f3b170448cebcb0a3c1720d17 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 13 Dec 2025 00:55:20 +0800 Subject: [PATCH 06/17] feat(sonner): add dismiss method to toast API Add the ability to dismiss specific toast notifications by their ID. This provides more control over toast management in the UI. --- packages/ui/src/components/primitives/sonner.d.ts | 9 +++++++++ packages/ui/src/components/primitives/sonner.tsx | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/packages/ui/src/components/primitives/sonner.d.ts b/packages/ui/src/components/primitives/sonner.d.ts index 3026be7915..1be1a8880c 100644 --- a/packages/ui/src/components/primitives/sonner.d.ts +++ b/packages/ui/src/components/primitives/sonner.d.ts @@ -140,6 +140,15 @@ interface toast { * }) */ 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 diff --git a/packages/ui/src/components/primitives/sonner.tsx b/packages/ui/src/components/primitives/sonner.tsx index c85b87ac40..14f2a32af5 100644 --- a/packages/ui/src/components/primitives/sonner.tsx +++ b/packages/ui/src/components/primitives/sonner.tsx @@ -345,6 +345,10 @@ toast.loading = (props: QuickLoadingProps) => { }) } +toast.dismiss = (id: ToastProps['id']) => { + sonnerToast.dismiss(id) +} + const toastColorVariants = cva(undefined, { variants: { type: { From 0e399fa4434eadf6a879d8b55863165fe59fe37f Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 13 Dec 2025 00:55:31 +0800 Subject: [PATCH 07/17] refactor(toast): update toast utilities and imports - Move ToastUtilities type to local component file - Add closeToast functionality to toast utilities - Update error block to use success toast method --- src/renderer/src/components/TopView/toast.tsx | 19 +++++++++++++++++++ src/renderer/src/env.d.ts | 3 ++- .../pages/home/Messages/Blocks/ErrorBlock.tsx | 2 +- src/renderer/src/utils/dataLimit.ts | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/TopView/toast.tsx b/src/renderer/src/components/TopView/toast.tsx index 0731a1b4bb..c9b1d56fca 100644 --- a/src/renderer/src/components/TopView/toast.tsx +++ b/src/renderer/src/components/TopView/toast.tsx @@ -178,9 +178,28 @@ export const loading = (args: RequireSome): strin */ export const addToast = (config: ToastConfig) => info(config) +/** + * Close a specific toast notification by its key + * @param key - Toast key (string) + */ +export const closeToast = (key: string) => { + getMessageApi().destroy(key) +} + +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 = () => ({ addToast, + 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 6e79732ab7..15fb794afb 100644 --- a/src/renderer/src/utils/dataLimit.ts +++ b/src/renderer/src/utils/dataLimit.ts @@ -84,7 +84,7 @@ export async function checkDataLimit() { currentInterval = setInterval(check, CHECK_INTERVAL_WARNING) } else if (!shouldShowWarning && currentToastId) { // TODO: new toast component cannot be closed programmatically. add a dismiss button in the toast. - // window.toast.closeToast(currentToastId) + window.toast.close(currentToastId) currentToastId = null // Switch back to normal mode From a29b0ef9dfee44f1a1a8666dcc8695ba18a6b501 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 13 Dec 2025 00:57:10 +0800 Subject: [PATCH 08/17] chore: remove next-themes dependency as it's no longer needed --- packages/ui/package.json | 1 - yarn.lock | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index 32602956f0..a6dfe5bbc2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -62,7 +62,6 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "lucide-react": "^0.545.0", - "next-themes": "^0.4.6", "react-dropzone": "^14.3.8", "sonner": "^2.0.7", "tailwind-merge": "^2.5.5" diff --git a/yarn.lock b/yarn.lock index 053712671e..e587f6abaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2252,7 +2252,6 @@ __metadata: framer-motion: "npm:^12.23.12" linguist-languages: "npm:^9.0.0" lucide-react: "npm:^0.545.0" - next-themes: "npm:^0.4.6" react: "npm:^19.0.0" react-dom: "npm:^19.0.0" react-dropzone: "npm:^14.3.8" @@ -23695,16 +23694,6 @@ __metadata: languageName: node linkType: hard -"next-themes@npm:^0.4.6": - version: 0.4.6 - resolution: "next-themes@npm:0.4.6" - peerDependencies: - react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - checksum: 10c0/83590c11d359ce7e4ced14f6ea9dd7a691d5ce6843fe2dc520fc27e29ae1c535118478d03e7f172609c41b1ef1b8da6b8dd2d2acd6cd79cac1abbdbd5b99f2c4 - languageName: node - linkType: hard - "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" From cfee61836fad3285bc0573f0b0b356c061bd213f Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 13 Dec 2025 00:58:31 +0800 Subject: [PATCH 09/17] fix: remove outdated toast comment and cleanup code The toast component now supports programmatic closing, making the TODO comment obsolete. Clean up the code by removing the unnecessary comment. --- src/renderer/src/utils/dataLimit.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/src/utils/dataLimit.ts b/src/renderer/src/utils/dataLimit.ts index 15fb794afb..e8bb3773f7 100644 --- a/src/renderer/src/utils/dataLimit.ts +++ b/src/renderer/src/utils/dataLimit.ts @@ -83,7 +83,6 @@ export async function checkDataLimit() { } currentInterval = setInterval(check, CHECK_INTERVAL_WARNING) } else if (!shouldShowWarning && currentToastId) { - // TODO: new toast component cannot be closed programmatically. add a dismiss button in the toast. window.toast.close(currentToastId) currentToastId = null From bb3230fc3db3dc07408ba449fbcb09b4a14f59a1 Mon Sep 17 00:00:00 2001 From: icarus Date: Sat, 13 Dec 2025 01:00:28 +0800 Subject: [PATCH 10/17] style(sonner): add todo comment for missing button styles --- packages/ui/src/components/primitives/sonner.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/primitives/sonner.tsx b/packages/ui/src/components/primitives/sonner.tsx index 14f2a32af5..026971de4f 100644 --- a/packages/ui/src/components/primitives/sonner.tsx +++ b/packages/ui/src/components/primitives/sonner.tsx @@ -451,6 +451,7 @@ function Toast({ {button !== undefined && (