100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import { failureMessage } from "$lib/api";
|
|||
|
|
import { isAlertEnabled, type AlertKind } from "$lib/alert-prefs";
|
||
|
|
import { toast, type ToastAction } from "$lib/components/ui/toast-state";
|
||
|
|
|
||
|
|
/** Brief confirmation — readable but not sticky. */
|
||
|
|
const SUCCESS_DURATION_MS = 4000;
|
||
|
|
/** Errors need more time to read. */
|
||
|
|
const ERROR_DURATION_MS = 7000;
|
||
|
|
/** Link actions need dwell time to click. */
|
||
|
|
const ACTION_DURATION_MS = 10000;
|
||
|
|
|
||
|
|
type NotifyOptions = {
|
||
|
|
description?: string;
|
||
|
|
action?: ToastAction;
|
||
|
|
actions?: ToastAction[];
|
||
|
|
duration?: number;
|
||
|
|
/**
|
||
|
|
* Operator alert category (P0-9). When set, toast is gated by local alert prefs.
|
||
|
|
* Omit for always-on UI feedback (forms, undo, validation).
|
||
|
|
*/
|
||
|
|
alert?: AlertKind;
|
||
|
|
};
|
||
|
|
|
||
|
|
export type NotifyApiErrorOptions = NotifyOptions & {
|
||
|
|
/** When set, used as toast title; failure message becomes the description. */
|
||
|
|
title?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
function hasLinkActions(opts: NotifyOptions): boolean {
|
||
|
|
return Boolean(opts.action || opts.actions?.length);
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveDuration(opts: NotifyOptions, fallbackMs: number): number {
|
||
|
|
if (opts.duration !== undefined) return opts.duration;
|
||
|
|
if (hasLinkActions(opts)) return ACTION_DURATION_MS;
|
||
|
|
return fallbackMs;
|
||
|
|
}
|
||
|
|
|
||
|
|
function shouldShowToast(opts: NotifyOptions): boolean {
|
||
|
|
if (!opts.alert) return true;
|
||
|
|
// Never mute toasts that carry onClick actions (e.g. Undo cancel).
|
||
|
|
const actions = opts.actions?.length ? opts.actions : opts.action ? [opts.action] : [];
|
||
|
|
if (actions.some((a) => typeof a.onClick === "function")) return true;
|
||
|
|
return isAlertEnabled(opts.alert);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Show a toast (and keep Alert strings in sync when callers also set local state). */
|
||
|
|
export function notifySuccess(title: string, descriptionOrOpts?: string | NotifyOptions) {
|
||
|
|
const opts =
|
||
|
|
typeof descriptionOrOpts === "string"
|
||
|
|
? { description: descriptionOrOpts }
|
||
|
|
: (descriptionOrOpts ?? {});
|
||
|
|
if (!shouldShowToast(opts)) return;
|
||
|
|
toast({
|
||
|
|
title,
|
||
|
|
description: opts.description,
|
||
|
|
variant: "default",
|
||
|
|
action: opts.action,
|
||
|
|
actions: opts.actions,
|
||
|
|
duration: resolveDuration(opts, SUCCESS_DURATION_MS)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function notifyError(title: string, descriptionOrOpts?: string | NotifyOptions) {
|
||
|
|
const opts =
|
||
|
|
typeof descriptionOrOpts === "string"
|
||
|
|
? { description: descriptionOrOpts }
|
||
|
|
: (descriptionOrOpts ?? {});
|
||
|
|
if (!shouldShowToast(opts)) return;
|
||
|
|
toast({
|
||
|
|
title,
|
||
|
|
description: opts.description,
|
||
|
|
variant: "destructive",
|
||
|
|
action: opts.action,
|
||
|
|
actions: opts.actions,
|
||
|
|
duration: resolveDuration(opts, ERROR_DURATION_MS)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Standard path for `api()` failures → toast (and optional page banner).
|
||
|
|
* Returns the message so callers can assign `error = notifyApiError(...)`.
|
||
|
|
* Message is always returned even when the toast is muted by alert prefs.
|
||
|
|
*/
|
||
|
|
export function notifyApiError(
|
||
|
|
err: unknown,
|
||
|
|
fallback: string,
|
||
|
|
opts?: NotifyApiErrorOptions
|
||
|
|
): string {
|
||
|
|
const message = failureMessage(err, fallback);
|
||
|
|
const { title, description, action, actions, duration, alert } = opts ?? {};
|
||
|
|
const toastOpts: NotifyOptions = { description, action, actions, duration, alert };
|
||
|
|
if (title !== undefined) {
|
||
|
|
notifyError(title, { ...toastOpts, description: description ?? message });
|
||
|
|
} else {
|
||
|
|
notifyError(message, toastOpts);
|
||
|
|
}
|
||
|
|
return message;
|
||
|
|
}
|