1import type { ConfirmDialogOptions, ConfirmDialogVariant } from "@t3tools/contracts";
2
3export type ConfirmDialogState =
4 | { readonly status: "idle" }
5 | {
6 readonly status: "confirming";
7 readonly message: string;
8 readonly variant: ConfirmDialogVariant;
9 }
10 | {
11 readonly status: "closing";
12 readonly message: string;
13 readonly variant: ConfirmDialogVariant;
14 };
15
16type PendingConfirmation = {
17 readonly message: string;
18 readonly variant: ConfirmDialogVariant;
19 readonly resolve: (confirmed: boolean) => void;
20};
21
22const idleState: ConfirmDialogState = { status: "idle" };
23let state: ConfirmDialogState = idleState;
24let activeConfirmation: PendingConfirmation | null = null;
25let queuedConfirmations: PendingConfirmation[] = [];
26let registeredHostCount = 0;
27const listeners = new Set<() => void>();
28
29function publish(next: ConfirmDialogState): void {
30 state = next;
31 for (const listener of listeners) {
32 listener();
33 }
34}
35
36function resolvePendingConfirmations(confirmed: boolean): void {
37 activeConfirmation?.resolve(confirmed);
38 for (const confirmation of queuedConfirmations) {
39 confirmation.resolve(confirmed);
40 }
41 activeConfirmation = null;
42 queuedConfirmations = [];
43}
44
45export function readConfirmDialogState(): ConfirmDialogState {
46 return state;
47}
48
49export function subscribeConfirmDialog(listener: () => void): () => void {
50 listeners.add(listener);
51 return () => {
52 listeners.delete(listener);
53 };
54}
55
56/**
57 * Registers the renderer host that can present themed confirmations. The
58 * returned cleanup function also cancels any request left without a host.
59 */
60export function registerConfirmDialogHost(): () => void {
61 registeredHostCount += 1;
62 let registered = true;
63
64 return () => {
65 if (!registered) return;
66 registered = false;
67 registeredHostCount = Math.max(0, registeredHostCount - 1);
68
69 if (registeredHostCount === 0) {
70 resolvePendingConfirmations(false);
71 publish(idleState);
72 }
73 };
74}
75
76/**
77 * Requests a themed confirmation when a host is mounted. An undefined result
78 * means no themed host is currently available.
79 */
80export function requestConfirmDialog(
81 message: string,
82 options?: ConfirmDialogOptions,
83): Promise<boolean> | undefined {
84 if (registeredHostCount === 0) return undefined;
85
86 const confirmation = new Promise<boolean>((resolve) => {
87 const pending = {
88 message,
89 variant: options?.variant ?? "default",
90 resolve,
91 } satisfies PendingConfirmation;
92 if (activeConfirmation || state.status === "closing") {
93 queuedConfirmations.push(pending);
94 return;
95 }
96
97 activeConfirmation = pending;
98 publish({ status: "confirming", message, variant: pending.variant });
99 });
100
101 return confirmation;
102}
103
104/** True while a confirmation is shown, closing, or waiting to be shown. */
105export function isConfirmDialogActive(): boolean {
106 return state.status !== "idle" || activeConfirmation !== null || queuedConfirmations.length > 0;
107}
108
109export function respondToConfirmDialog(confirmed: boolean): void {
110 if (state.status !== "confirming" || !activeConfirmation) return;
111
112 const confirmation = activeConfirmation;
113 activeConfirmation = null;
114 confirmation.resolve(confirmed);
115 publish({ status: "closing", message: state.message, variant: state.variant });
116}
117
118export function completeConfirmDialogClose(): void {
119 if (state.status !== "closing") return;
120
121 const next = queuedConfirmations.shift();
122 if (!next) {
123 publish(idleState);
124 return;
125 }
126
127 activeConfirmation = next;
128 publish({ status: "confirming", message: next.message, variant: next.variant });
129}
130
131export function resetConfirmDialogForTests(): void {
132 resolvePendingConfirmations(false);
133 registeredHostCount = 0;
134 publish(idleState);
135 listeners.clear();
136}