Files
Jyotisha/frontend/src/lib/notify.ts
T

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);
}