import { toast } from "sonner"; /** * The one way to tell the user how an action went (2026-09-30, product: 「报错和 * 提示不要占页面布局」). Floating toasts at the top centre, never an inline bar: * success and info leave after 3s, errors stay 6s and carry a close button. * Page states with their own retry (a failed chart, an empty list) stay in the * page; see DESIGN.md「提示与报错」. */ export const NOTICE_DURATION_MS = { success: 3_000, info: 3_000, error: 6_000 } as const; export type NoticeOptions = Readonly<{ /** Same id → the new message replaces the old one instead of stacking. */ id?: string; duration?: number; }>; /** The toaster's accessible name (see `components/ui/sonner.tsx`); also how it is found here. */ export const TOASTER_LABEL = "页面提示"; /** * A modal (反馈与投诉, 导出, 协议更新) sits in the browser's top layer, * above any z-index, so a toast raised from inside it would land under its * backdrop. While one is open the toaster joins the top layer as a manual * popover, re-shown on every toast so it stays above the newest dialog. */ export function liftToastsAboveModal() { if (typeof document === "undefined" || typeof document.querySelector !== "function") return; if (!document.querySelector("dialog:modal")) return; const host = document.querySelector(`section[aria-label^="${TOASTER_LABEL}"]`); if (!host || typeof host.showPopover !== "function") return; try { if (!host.hasAttribute("popover")) host.setAttribute("popover", "manual"); if (host.matches(":popover-open")) host.hidePopover(); host.showPopover(); } catch { // An older browser without popovers keeps the plain toaster. } } function options(tone: keyof typeof NOTICE_DURATION_MS, extra: NoticeOptions = {}) { return { duration: extra.duration ?? NOTICE_DURATION_MS[tone], ...(tone === "error" ? { closeButton: true } : {}), ...(extra.id ? { id: extra.id } : {}), }; } export function notifySuccess(message: string, extra?: NoticeOptions) { if (!message.trim()) return; liftToastsAboveModal(); toast.success(message, options("success", extra)); } export function notifyInfo(message: string, extra?: NoticeOptions) { if (!message.trim()) return; liftToastsAboveModal(); toast.info(message, options("info", extra)); } export function notifyError(message: string, extra?: NoticeOptions) { if (!message.trim()) return; liftToastsAboveModal(); toast.error(message, options("error", extra)); } export function dismissNotice(id: string) { toast.dismiss(id); }