diff --git a/src/components/GlobalSnackbar.tsx b/src/components/GlobalSnackbar.tsx deleted file mode 100644 index 096dee8..0000000 --- a/src/components/GlobalSnackbar.tsx +++ /dev/null @@ -1,372 +0,0 @@ -/** - * GlobalSnackbar - 全局 Snackbar 消息提示组件及 Provider - * - * 提供可复用的 Toast 消息提示功能,支持三种使用方式: - * 1. 作为受控组件使用:通过 props 控制显示状态 - * 2. 通过 useSnackbarState Hook 使用:在组件内部自动管理状态 - * 3. 通过 SnackbarProvider 和 useSnackbar Hook 使用:全局单例模式 - * - * NOTE: 项目同时使用 sonner 的 toast 进行简单的一次性提示。 - * 本组件适用于需要 severity 级别、Provider 上下文、自定义定位等高级场景。 - * 简单场景(如复制成功、操作提示)优先使用 `import { toast } from 'sonner'`。 - * - * @module GlobalSnackbar - * @version 1.1.0 - * - * @example - * ```tsx - * // 方式一:受控组件 - * setIsOpen(false)} - * severity="success" - * /> - * - * // 方式二:Hook 方式 (局部状态) - * const { snackbarProps, showMessage } = useSnackbarState(); - * showMessage('Hello!', { severity: 'info' }); - * - * // 方式三:Context 方式 (全局状态) - * // 在根组件包裹 Provider - * - * - * - * - * // 在子组件中使用 - * const { showMessage } = useSnackbar(); - * showMessage('Global Message'); - * ``` - */ - -import { - JSX, - useState, - useRef, - createContext, - useContext, - useEffect, - type ReactNode, - type SyntheticEvent, -} from 'react'; -import { CheckCircle, Info, AlertTriangle, XCircle } from 'lucide-react'; - -/** - * Snackbar 消息严重程度类型 - * @description 决定 Alert 组件的颜色和图标 - * - success: 绿色,成功提示 - * - info: 蓝色,信息提示 - * - warning: 橙色,警告提示 - * - error: 红色,错误提示 - */ -export type SnackbarSeverity = 'success' | 'info' | 'warning' | 'error'; - -/** - * GlobalSnackbar 组件的属性接口 - * @interface GlobalSnackbarProps - */ -export interface GlobalSnackbarProps { - /** 消息内容,要显示的提示文本 */ - message: string; - /** 是否显示 Snackbar */ - open: boolean; - /** 关闭回调函数 */ - onClose: () => void; - /** 消息级别,影响颜色和图标样式,默认 'info' */ - severity?: SnackbarSeverity; - /** 自动隐藏时间(毫秒),设为 0 则不自动关闭,默认 2000 */ - autoHideDuration?: number; - /** Snackbar 弹出位置,默认 { vertical: 'bottom', horizontal: 'center' } */ - anchorOrigin?: { - vertical: 'top' | 'bottom'; - horizontal: 'left' | 'center' | 'right'; - }; - /** 是否使用 Alert 组件包裹,false 则使用原生 Snackbar message,默认 true */ - showAlert?: boolean; - /** 是否隐藏 Alert 图标,默认 false */ - hideIcon?: boolean; - /** 自定义样式,透传给外层 Snackbar 组件 */ - sx?: React.CSSProperties; - /** 自定义样式,透传给内层 Alert 组件(仅 showAlert=true 时生效) */ - alertSx?: React.CSSProperties; -} - -/** - * showMessage 方法的选项配置 - * @interface SnackbarOptions - */ -export interface SnackbarOptions { - /** 消息级别:success | info | warning | error */ - severity?: SnackbarSeverity; - /** 自动隐藏时间(毫秒),设为 0 则不自动关闭 */ - autoHideDuration?: number; - /** 是否隐藏 Alert 图标 */ - hideIcon?: boolean; - /** 是否使用 Alert 组件包裹 */ - showAlert?: boolean; -} - -/** - * useSnackbarState Hook 的返回值类型 - * @interface UseSnackbarStateResult - */ -export interface UseSnackbarStateResult { - /** 传递给 GlobalSnackbar 组件的属性对象 */ - snackbarProps: GlobalSnackbarProps; - /** 显示消息的方法 */ - showMessage: (message: string, options?: SnackbarOptions) => void; - /** 关闭消息的方法 */ - closeMessage: () => void; -} - -/** - * GlobalSnackbar 组件的默认属性配置 - * @description 提供类型安全的默认值选择 - */ -const defaultProps: Required< - Pick< - GlobalSnackbarProps, - 'severity' | 'autoHideDuration' | 'anchorOrigin' | 'showAlert' | 'hideIcon' - > -> = { - severity: 'info', - autoHideDuration: 2000, - anchorOrigin: { vertical: 'bottom', horizontal: 'center' }, - showAlert: true, - hideIcon: false, -}; - -const severityConfig: Record< - SnackbarSeverity, - { icon: React.ElementType; bgClass: string; textClass: string } -> = { - success: { icon: CheckCircle, bgClass: 'bg-green-500', textClass: 'text-white' }, - info: { icon: Info, bgClass: 'bg-primary/100', textClass: 'text-white' }, - warning: { icon: AlertTriangle, bgClass: 'bg-amber-500', textClass: 'text-white' }, - error: { icon: XCircle, bgClass: 'bg-red-500', textClass: 'text-white' }, -}; - -/** - * GlobalSnackbar 组件 - * - * 全局消息提示的展示组件,支持受控和非受控两种使用模式。 - * - * @param {GlobalSnackbarProps} props - 组件属性 - * @returns {JSX.Element} - */ -export function GlobalSnackbar({ - message, - open, - onClose, - severity = defaultProps.severity, - autoHideDuration = defaultProps.autoHideDuration, - showAlert = defaultProps.showAlert, - hideIcon = defaultProps.hideIcon, -}: GlobalSnackbarProps): JSX.Element | null { - const timerRef = useRef | null>(null); - - useEffect(() => { - if (open && autoHideDuration > 0) { - timerRef.current = setTimeout(() => { - onClose(); - }, autoHideDuration); - return () => { - if (timerRef.current) clearTimeout(timerRef.current); - }; - } - return undefined; - }, [open, autoHideDuration, onClose]); - - if (!open) return null; - - const config = severityConfig[severity]; - const IconComponent = config.icon; - - return ( -
- {showAlert ? ( -
- {!hideIcon && } - {message} -
- ) : ( -
{message}
- )} -
- ); -} - -/** - * useSnackbarState - 消息提示的 Hook 方式 - * - * 提供状态管理的 Snackbar 功能,自动处理 open、message 等状态。 - * 适合在组件内部使用,无需额外的状态管理代码。 - * - * @param {SnackbarOptions} [initialOptions] - 初始配置选项 - * @returns {UseSnackbarStateResult} 包含 snackbarProps 和操作方法的对象 - * - * @description - * - 自动管理 Snackbar 的显示/隐藏状态 - * - 支持链式调用 showMessage - * - 合并初始选项和调用时选项 - * - * @example - * ```tsx - * function MyComponent() { - * const { snackbarProps, showMessage, closeMessage } = useSnackbarState({ - * severity: 'info', - * autoHideDuration: 3000, - * }); - * - * const handleSave = () => { - * // 业务逻辑... - * showMessage('保存成功!', { severity: 'success' }); - * }; - * - * return ( - * <> - * - * - * - * ); - * } - * ``` - */ -export function useSnackbarState(initialOptions?: SnackbarOptions): UseSnackbarStateResult { - // Snackbar 显示状态 - const [open, setOpen] = useState(false); - // 当前显示的消息内容 - const [message, setMessage] = useState(''); - // 消息配置选项 - const [options, setOptions] = useState(initialOptions || {}); - - /** - * 显示消息 - * - * @param {string} newMessage - 要显示的消息文本 - * @param {SnackbarOptions} [newOptions={}] - 新的配置选项 - * - * @description - * - 合并初始选项和新的调用选项 - * - 新选项会覆盖初始选项 - */ - const showMessage = (newMessage: string, newOptions: SnackbarOptions = {}) => { - setMessage(newMessage); - setOptions({ ...initialOptions, ...newOptions }); - setOpen(true); - }; - - /** - * 关闭消息 - * - * @description - * - 直接将 open 状态设置为 false - */ - const closeMessage = () => { - setOpen(false); - }; - - const handleClose = (_event?: SyntheticEvent | Event, reason?: string) => { - if (reason === 'clickaway') return; - closeMessage(); - }; - - /** - * 传递给 GlobalSnackbar 组件的属性 - * - * @description - * - 组合当前状态和选项为完整的组件 props - * - onClose 使用 handleClose 包装后的版本 - */ - const snackbarProps: GlobalSnackbarProps = { - message, - open, - onClose: handleClose, - severity: options.severity, - autoHideDuration: options.autoHideDuration, - hideIcon: options.hideIcon, - }; - - return { - snackbarProps, - showMessage, - closeMessage, - }; -} - -// --- Context & Provider --- - -/** - * Snackbar Context 的值类型定义 - */ -interface SnackbarContextValue { - showMessage: (message: string, options?: SnackbarOptions) => void; - closeMessage: () => void; -} - -const SnackbarContext = createContext(null); - -/** - * SnackbarProvider 组件的 props 类型 - */ -interface SnackbarProviderProps { - children: ReactNode; - initialOptions?: SnackbarOptions; -} - -/** - * SnackbarProvider 组件 - * - * 全局消息提示的 Provider 组件,需要包裹在应用根组件外层。 - */ -export function SnackbarProvider({ children, initialOptions }: SnackbarProviderProps): JSX.Element { - const { snackbarProps, showMessage, closeMessage } = useSnackbarState(initialOptions); - - return ( - - {children} - - - ); -} - -/** - * useSnackbar - 在子组件中获取 Snackbar 上下文的 Hook - * - * @param {SnackbarOptions} [options] - 钩子级别的默认配置(如 autoHideDuration) - * @returns {SnackbarContextValue} - 包含 showMessage 和 closeMessage 的对象 - * @throws {Error} - 如果不在 SnackbarProvider 内部调用,抛出错误 - * - * @description - * 选项合并策略: - * 1. 调用 showMessage 时传入的 callOptions 优先级最高 - * 2. useSnackbar(options) 传入的 Hook 级别配置次之 - * 3. SnackbarProvider(initialOptions) 传入的全局配置优先级最低 - */ -export function useSnackbar(options?: SnackbarOptions): SnackbarContextValue { - const context = useContext(SnackbarContext); - if (!context) { - throw new Error('useSnackbar must be used within SnackbarProvider'); - } - - // 包装 showMessage 以支持 Hook 级别的 initialOptions - const wrappedShowMessage = (message: string, callOptions?: SnackbarOptions) => { - // 采用防御性编程,确保 options 和 callOptions 为空时也能正常工作 - // 优先级:callOptions > options - const mergedOptions: SnackbarOptions = { - ...(options || {}), - ...(callOptions || {}), - }; - context.showMessage(message, mergedOptions); - }; - - return { - ...context, - showMessage: wrappedShowMessage, - }; -} - -export default GlobalSnackbar; diff --git a/src/components/ImageUploader.tsx b/src/components/ImageUploader.tsx index 0173353..6f3e1cc 100644 --- a/src/components/ImageUploader.tsx +++ b/src/components/ImageUploader.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef } from 'react'; import { Image, X } from 'lucide-react'; -import { useSnackbar } from '@/components/GlobalSnackbar'; +import { toast } from 'sonner'; import { useI18n } from '@/utils/chromeI18n'; interface ImageUploaderProps { @@ -30,7 +30,6 @@ const ImageUploader = ({ onDraggingChange, }: ImageUploaderProps) => { const { t } = useI18n('qrCode'); - const { showMessage } = useSnackbar(); const fileInputRef = useRef(null); const handleFileChange = useCallback( @@ -46,11 +45,8 @@ const ImageUploader = ({ URL.revokeObjectURL(previewUrl); } onClearFile(); - showMessage(t('qrCode:imageCleared'), { - severity: 'success', - autoHideDuration: 1000, - }); - }, [previewUrl, onClearFile, showMessage, t]); + toast.success(t('qrCode:imageCleared')); + }, [previewUrl, onClearFile, t]); const handleInputChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { @@ -90,13 +86,10 @@ const ImageUploader = ({ if (file) { try { handleFileChange(file); - showMessage(t('qrCode:imagePasted'), { severity: 'success', autoHideDuration: 1000 }); + toast.success(t('qrCode:imagePasted')); } catch (error) { console.error('处理粘贴图片失败:', error); - showMessage(t('qrCode:imagePasteError'), { - severity: 'error', - autoHideDuration: 3000, - }); + toast.error(t('qrCode:imagePasteError')); } } break; @@ -109,7 +102,7 @@ const ImageUploader = ({ return () => { document.removeEventListener('paste', handlePaste); }; - }, [showMessage, handleFileChange, t]); + }, [handleFileChange, t]); return (
{ - const mockOnClose = vi.fn(); - - beforeEach(() => { - vi.useFakeTimers(); - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - vi.useRealTimers(); - }); - - const defaultProps: GlobalSnackbarProps = { - message: '测试消息', - open: true, - onClose: mockOnClose, - }; - - describe('GlobalSnackbar UI 渲染', () => { - it('应渲染消息内容', () => { - render(); - expect(screen.getByText('测试消息')).toBeInTheDocument(); - }); - - it('当 showAlert 为 true 时应渲染带样式的提示', () => { - render(); - // 验证是否包含消息文本 - const alertElement = screen.getByText('测试消息'); - expect(alertElement).toBeInTheDocument(); - // 验证父元素有正确的样式类 - const parent = alertElement.parentElement; - expect(parent).toHaveClass('flex', 'items-center', 'gap-2'); - }); - - it('当 hideIcon 为 true 时不应渲染图标', () => { - render(); - // 图标使用 lucide-react 的 svg 元素 - const icon = document.querySelector('svg'); - expect(icon).not.toBeInTheDocument(); - }); - - it('应根据 severity 应用不同的样式', () => { - render(); - const message = screen.getByText('测试消息'); - const parent = message.parentElement; - expect(parent).toHaveClass('bg-red-500'); - }); - }); - - describe('useSnackbarState Hook 逻辑', () => { - it('应返回初始状态', () => { - const { result } = renderHook(() => useSnackbarState()); - expect(result.current.snackbarProps.open).toBe(false); - expect(result.current.snackbarProps.message).toBe(''); - }); - - it('showMessage 应更新状态', () => { - const { result } = renderHook(() => useSnackbarState()); - - act(() => { - result.current.showMessage('新消息'); - }); - - expect(result.current.snackbarProps.open).toBe(true); - expect(result.current.snackbarProps.message).toBe('新消息'); - }); - - it('closeMessage 应关闭消息', () => { - const { result } = renderHook(() => useSnackbarState()); - - act(() => { - result.current.showMessage('消息'); - }); - act(() => { - result.current.closeMessage(); - }); - - expect(result.current.snackbarProps.open).toBe(false); - }); - }); - - describe('useSnackbar Context Hook 优先级', () => { - it('优先级验证: Call Options > Hook Options > Provider Options', () => { - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - - const { result } = renderHook(() => useSnackbar({ severity: 'warning' }), { wrapper }); - - // 1. 测试 Hook Options 覆盖 Provider Options - act(() => { - result.current.showMessage('消息 1'); - }); - - expect(screen.getByText('消息 1')).toBeInTheDocument(); - - // 2. 测试 Call Options 覆盖 Hook Options - act(() => { - result.current.showMessage('消息 2', { severity: 'error' }); - }); - - expect(screen.getByText('消息 2')).toBeInTheDocument(); - }); - }); -}); diff --git a/src/components/__tests__/ImageUploader.test.tsx b/src/components/__tests__/ImageUploader.test.tsx index fcf48c0..e85ef52 100644 --- a/src/components/__tests__/ImageUploader.test.tsx +++ b/src/components/__tests__/ImageUploader.test.tsx @@ -13,10 +13,11 @@ const mockRevokeObjectURL = vi.fn(); Object.defineProperty(window.URL, 'createObjectURL', { value: mockCreateObjectURL }); Object.defineProperty(window.URL, 'revokeObjectURL', { value: mockRevokeObjectURL }); -vi.mock('@/components/GlobalSnackbar', () => ({ - useSnackbar: () => ({ - showMessage: vi.fn(), - }), +vi.mock('sonner', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, })); describe('ImageUploader 组件', () => { diff --git a/src/components/__tests__/RouterContainer.test.tsx b/src/components/__tests__/RouterContainer.test.tsx index 86d50e4..4ab1e39 100644 --- a/src/components/__tests__/RouterContainer.test.tsx +++ b/src/components/__tests__/RouterContainer.test.tsx @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { render } from '@testing-library/react'; import RouterContainer from '@/components/RouterContainer'; import { RouterProvider } from '@/providers/RouterProvider'; -import { SnackbarProvider } from '@/components/GlobalSnackbar'; import type { PageType } from '@/types/storage'; import React from 'react'; @@ -29,11 +28,7 @@ describe('RouterContainer 组件', () => { }); const renderWithProvider = (ui: React.ReactElement) => { - return render( - - {ui} - , - ); + return render({ui}); }; describe('渲染测试', () => { @@ -74,11 +69,7 @@ describe('RouterContainer 组件', () => { const { rerender } = renderWithProvider(); mockRouterValue.currentPage = 'timestamp'; - rerender( - - {} - , - ); + rerender({}); const box = document.querySelector('.page-transition-enter'); expect(box).toBeInTheDocument(); diff --git a/src/entrypoints/popup/App.tsx b/src/entrypoints/popup/App.tsx index cdec377..a245d48 100644 --- a/src/entrypoints/popup/App.tsx +++ b/src/entrypoints/popup/App.tsx @@ -2,7 +2,6 @@ import RouterProvider from '@/providers/RouterProvider'; import TopBar from '@/components/TopBar'; import RouterContainer from '@/components/RouterContainer'; import ErrorBoundary from '@/components/ErrorBoundary'; -import { SnackbarProvider } from '@/components/GlobalSnackbar'; import { getEntryPointType } from '@/config/features'; import { useMemo } from 'react'; @@ -30,14 +29,12 @@ export default function App() { visiblePagesKey={routerConfig.visiblePagesKey} pageOrderKey={routerConfig.pageOrderKey} > - -
- - - - -
-
+
+ + + + +
); } diff --git a/src/entrypoints/sidepanel/App.tsx b/src/entrypoints/sidepanel/App.tsx index 9709e84..80d156b 100644 --- a/src/entrypoints/sidepanel/App.tsx +++ b/src/entrypoints/sidepanel/App.tsx @@ -3,7 +3,6 @@ import RouterProvider from '@/providers/RouterProvider'; import TopBar from '@/components/TopBar'; import RouterContainer from '@/components/RouterContainer'; import ErrorBoundary from '@/components/ErrorBoundary'; -import { SnackbarProvider } from '@/components/GlobalSnackbar'; import { MessageAction, sendMessage } from '@/utils/messages'; export default function App() { @@ -16,14 +15,12 @@ export default function App() { return ( - -
- - - - -
-
+
+ + + + +
); } diff --git a/src/pages/QrCode/components/ParsePanel.tsx b/src/pages/QrCode/components/ParsePanel.tsx index bd84c56..a0a2aa6 100644 --- a/src/pages/QrCode/components/ParsePanel.tsx +++ b/src/pages/QrCode/components/ParsePanel.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react'; +import { toast } from 'sonner'; import TextInputArea from '@/components/TextInputArea'; import ImageUploader from '@/components/ImageUploader'; -import { useSnackbar } from '@/components/GlobalSnackbar'; import { useI18n } from '@/utils/chromeI18n'; import { useQrCodeContext } from '../contexts/QrCodeContext'; import { Label } from '@/components/ui/label'; @@ -9,7 +9,6 @@ import { cn } from '@/lib/utils'; export default function ParsePanel() { const { t } = useI18n('qrCode'); - const { showMessage } = useSnackbar(); const { parserState, setParserState, handleFileChange, handleClearFile } = useQrCodeContext(); // 全局粘贴事件监听 @@ -24,7 +23,7 @@ export default function ParsePanel() { const file = items[i].getAsFile(); if (file) { handleFileChange(file); - showMessage(t('qrCode:imagePasted'), { severity: 'success', autoHideDuration: 1000 }); + toast.success(t('qrCode:imagePasted')); } return; } @@ -38,14 +37,14 @@ export default function ParsePanel() { const blob = await response.blob(); const file = new File([blob], 'pasted-image.png', { type: blob.type }); handleFileChange(file); - showMessage(t('qrCode:imagePasted'), { severity: 'success', autoHideDuration: 1000 }); + toast.success(t('qrCode:imagePasted')); } catch (error) { console.error('处理 Base64 图片失败:', error); - showMessage(t('qrCode:imagePasteError'), { severity: 'error', autoHideDuration: 3000 }); + toast.error(t('qrCode:imagePasteError')); } } }, - [handleFileChange, showMessage, t], + [handleFileChange, t], ); useEffect(() => { diff --git a/src/pages/QrCode/hooks/useQrCode.ts b/src/pages/QrCode/hooks/useQrCode.ts index 706cefd..bedf5c3 100644 --- a/src/pages/QrCode/hooks/useQrCode.ts +++ b/src/pages/QrCode/hooks/useQrCode.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo, useState } from 'react'; import QRious from 'qrious'; -import { useSnackbar } from '@/components/GlobalSnackbar'; +import { toast } from 'sonner'; import { parseQrCodeFromFile } from '@/utils/qrCodeParser'; import { useContextMenuData } from '@/utils/useContextMenuData'; import { useI18n } from '@/utils/chromeI18n'; @@ -10,7 +10,6 @@ import type { QrCodeGeneratorState, QrCodeMode, QrCodeParserState } from '../typ export function useQrCode(): QrCodeContextValue { const { t } = useI18n('qrCode'); - const { showMessage } = useSnackbar(); const [mode, setMode] = useState('generate'); @@ -89,22 +88,22 @@ export function useQrCode(): QrCodeContextValue { if (result.success && result.data) { setParserState((prev) => ({ ...prev, decodedResult: result.data! })); - showMessage(t('qrCode:parseSuccess'), { severity: 'success', autoHideDuration: 1000 }); + toast.success(t('qrCode:parseSuccess')); } else { const errorMsg = result.error || t('qrCode:noQrDetected'); setParserState((prev) => ({ ...prev, parseError: errorMsg })); - showMessage(errorMsg, { severity: 'error', autoHideDuration: 3000 }); + toast.error(errorMsg); } } catch (error) { console.error('解析二维码失败:', error); const errorMsg = error instanceof Error ? error.message : t('qrCode:parseError'); setParserState((prev) => ({ ...prev, parseError: errorMsg })); - showMessage(errorMsg, { severity: 'error', autoHideDuration: 3000 }); + toast.error(errorMsg); } finally { setParserState((prev) => ({ ...prev, parsing: false })); } }, - [t, showMessage], + [t], ); const downloadQrCode = useCallback(() => { @@ -114,8 +113,8 @@ export function useQrCode(): QrCodeContextValue { link.href = qrCodeDataUrl; link.download = 'qrcode.png'; link.click(); - showMessage(t('qrCode:qrCodeDownloadSuccess'), { severity: 'success', autoHideDuration: 1000 }); - }, [qrCodeDataUrl, showMessage, t]); + toast.success(t('qrCode:qrCodeDownloadSuccess')); + }, [qrCodeDataUrl, t]); const copyQrCode = useCallback(async () => { if (!qrCodeDataUrl) return; @@ -130,12 +129,12 @@ export function useQrCode(): QrCodeContextValue { }), ]); - showMessage(t('qrCode:qrCodeCopySuccess'), { severity: 'success', autoHideDuration: 1000 }); + toast.success(t('qrCode:qrCodeCopySuccess')); } catch (error) { console.error('复制二维码失败:', error); - showMessage(t('qrCode:copyError'), { severity: 'error', autoHideDuration: 3000 }); + toast.error(t('qrCode:copyError')); } - }, [qrCodeDataUrl, showMessage, t]); + }, [qrCodeDataUrl, t]); const handleFileChange = useCallback( (file: File) => { diff --git a/src/pages/Timestamp/LiveClock.tsx b/src/pages/Timestamp/LiveClock.tsx index a314758..0ded535 100644 --- a/src/pages/Timestamp/LiveClock.tsx +++ b/src/pages/Timestamp/LiveClock.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Clock } from 'lucide-react'; +import { toast } from 'sonner'; import CopyButton from '@/components/CopyButton'; -import { useSnackbar } from '@/components/GlobalSnackbar'; import type { UnitType } from './constants'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; @@ -13,7 +13,6 @@ interface LiveClockProps extends React.HTMLAttributes { const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClockProps) => { const { t } = useI18n('timestamp'); - const { showMessage } = useSnackbar(); const onUseNowRef = useRef(onUseNow); const [currentDisplay, setCurrentDisplay] = useState(() => { @@ -45,8 +44,8 @@ const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClock const handleUseNow = useCallback(() => { onUseNowRef.current(currentDisplay.rawTime); - showMessage?.(t('timestamp:usedSuccess'), { severity: 'success' }); - }, [currentDisplay.rawTime, showMessage, t]); + toast.success(t('timestamp:usedSuccess')); + }, [currentDisplay.rawTime, t]); return (