1import X from "lucide-solid/icons/x";
2import { createSignal, Show } from "solid-js";
3import { reason } from "../api.ts";
4
5interface Shown {
6 text: string;
7 action?: { label: string; run: () => Promise<unknown> };
8}
9
10const [shown, setShown] = createSignal<Shown>();
11let timer: ReturnType<typeof setTimeout> | undefined;
12
13const dismiss = () => {
14 clearTimeout(timer);
15 setShown();
16};
17const linger = () => {
18 clearTimeout(timer);
19 timer = setTimeout(dismiss, 8000);
20};
21
22/** Replaces the current toast; `action` is for undo and the like, and its failure replaces the toast with the reason. */
23export function toast(text: string, action?: Shown["action"]) {
24 setShown({ text, action });
25 linger();
26}
27
28/** The one live region toasts appear in; mount once. */
29export function Toasts() {
30 const [busy, setBusy] = createSignal(false);
31 const run = async (action: NonNullable<Shown["action"]>) => {
32 setBusy(true);
33 try {
34 await action.run();
35 dismiss();
36 } catch (failure) {
37 toast(reason(failure));
38 } finally {
39 setBusy(false);
40 }
41 };
42 return (
43 <div class="toasts" role="status" onPointerEnter={() => clearTimeout(timer)} onPointerLeave={() => shown() && linger()}>
44 <Show when={shown()} keyed>
45 {(current) => (
46 <div class="toast">
47 <span>{current.text}</span>
48 <Show when={current.action}>
49 {(action) => (
50 <button class="button small" disabled={busy()} aria-busy={busy()} onClick={() => run(action())}>
51 {action().label}
52 </button>
53 )}
54 </Show>
55 <button class="close" aria-label="Dismiss" onClick={dismiss}><X size={14} /></button>
56 </div>
57 )}
58 </Show>
59 </div>
60 );
61}