Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
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 <dialog> (反馈与投诉, 导出, 协议更新) 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<HTMLElement>(`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);
|
|
}
|