@windrun-huaiin/third-ui 29.1.0 → 29.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/fuma/base/custom-header.js +6 -3
  2. package/dist/fuma/base/custom-header.mjs +6 -3
  3. package/dist/main/alert-dialog/confirm-dialog.d.ts +6 -3
  4. package/dist/main/alert-dialog/confirm-dialog.js +7 -7
  5. package/dist/main/alert-dialog/confirm-dialog.mjs +8 -8
  6. package/dist/main/alert-dialog/dialog-loading-action.d.ts +13 -0
  7. package/dist/main/alert-dialog/dialog-loading-action.js +42 -0
  8. package/dist/main/alert-dialog/dialog-loading-action.mjs +40 -0
  9. package/dist/main/alert-dialog/high-priority-confirm-dialog.d.ts +6 -3
  10. package/dist/main/alert-dialog/high-priority-confirm-dialog.js +10 -4
  11. package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +11 -5
  12. package/dist/main/alert-dialog/index.d.ts +1 -0
  13. package/dist/main/alert-dialog/info-dialog.d.ts +5 -2
  14. package/dist/main/alert-dialog/info-dialog.js +6 -5
  15. package/dist/main/alert-dialog/info-dialog.mjs +7 -6
  16. package/dist/main/alert-dialog/undoable-confirm-dialog.d.ts +7 -4
  17. package/dist/main/alert-dialog/undoable-confirm-dialog.js +18 -17
  18. package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +19 -18
  19. package/dist/main/buttons/gradient-button.d.ts +3 -1
  20. package/dist/main/buttons/gradient-button.js +29 -3
  21. package/dist/main/buttons/gradient-button.mjs +29 -3
  22. package/dist/main/buttons/index.d.ts +1 -0
  23. package/dist/main/buttons/index.js +3 -0
  24. package/dist/main/buttons/index.mjs +1 -0
  25. package/dist/main/buttons/use-press-feedback.d.ts +18 -0
  26. package/dist/main/buttons/use-press-feedback.js +42 -0
  27. package/dist/main/buttons/use-press-feedback.mjs +39 -0
  28. package/dist/main/buttons/x-button.d.ts +3 -0
  29. package/dist/main/buttons/x-button.js +36 -6
  30. package/dist/main/buttons/x-button.mjs +36 -6
  31. package/dist/main/calendar/calendar-date-range-input.d.ts +17 -0
  32. package/dist/main/calendar/calendar-date-range-input.js +81 -0
  33. package/dist/main/calendar/calendar-date-range-input.mjs +79 -0
  34. package/dist/main/calendar/calendar-status-view.d.ts +23 -0
  35. package/dist/main/calendar/calendar-status-view.js +155 -0
  36. package/dist/main/calendar/calendar-status-view.mjs +153 -0
  37. package/dist/main/calendar/index.d.ts +3 -0
  38. package/dist/main/calendar/index.js +12 -0
  39. package/dist/main/calendar/index.mjs +4 -0
  40. package/dist/main/calendar/random-date-range-dialog.d.ts +18 -0
  41. package/dist/main/calendar/random-date-range-dialog.js +451 -0
  42. package/dist/main/calendar/random-date-range-dialog.mjs +449 -0
  43. package/package.json +6 -1
  44. package/src/fuma/base/custom-header.tsx +6 -3
  45. package/src/main/alert-dialog/confirm-dialog.tsx +54 -47
  46. package/src/main/alert-dialog/dialog-loading-action.tsx +78 -0
  47. package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +63 -48
  48. package/src/main/alert-dialog/index.ts +1 -0
  49. package/src/main/alert-dialog/info-dialog.tsx +52 -44
  50. package/src/main/alert-dialog/undoable-confirm-dialog.tsx +90 -82
  51. package/src/main/buttons/gradient-button.tsx +36 -3
  52. package/src/main/buttons/index.ts +1 -0
  53. package/src/main/buttons/use-press-feedback.ts +58 -0
  54. package/src/main/buttons/x-button.tsx +53 -11
  55. package/src/main/calendar/calendar-date-range-input.tsx +173 -0
  56. package/src/main/calendar/calendar-status-view.tsx +365 -0
  57. package/src/main/calendar/index.ts +5 -0
  58. package/src/main/calendar/random-date-range-dialog.tsx +753 -0
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { Loading } from '../loading';
6
+
7
+ export type DialogLoadingAction = 'cancel' | 'confirm' | 'undo';
8
+ export type DialogActionHandler = () => void | Promise<void>;
9
+
10
+ interface UseDialogLoadingActionOptions {
11
+ loadingActions?: readonly DialogLoadingAction[];
12
+ loadingFullPage?: boolean;
13
+ onOpenChange: (open: boolean) => void;
14
+ }
15
+
16
+ export function useDialogLoadingAction({
17
+ loadingActions,
18
+ loadingFullPage = false,
19
+ onOpenChange,
20
+ }: UseDialogLoadingActionOptions) {
21
+ const [mounted, setMounted] = React.useState(false);
22
+ const [loading, setLoading] = React.useState(false);
23
+
24
+ React.useEffect(() => {
25
+ setMounted(true);
26
+ }, []);
27
+
28
+ const runDialogAction = React.useCallback(async (
29
+ action: DialogLoadingAction,
30
+ handler?: DialogActionHandler
31
+ ) => {
32
+ onOpenChange(false);
33
+
34
+ if (!handler) {
35
+ return;
36
+ }
37
+
38
+ if (!loadingActions?.includes(action)) {
39
+ await handler();
40
+ return;
41
+ }
42
+
43
+ setLoading(true);
44
+
45
+ try {
46
+ await handler();
47
+ } finally {
48
+ setLoading(false);
49
+ }
50
+ }, [loadingActions, onOpenChange]);
51
+
52
+ const dialogLoading = mounted && loading
53
+ ? createPortal(
54
+ loadingFullPage ? (
55
+ <div className="fixed inset-0 z-10000">
56
+ <Loading className="h-full w-full" />
57
+ </div>
58
+ ) : (
59
+ <div className="pointer-events-none fixed inset-0 z-10000 flex items-center justify-center p-4">
60
+ <div className="pointer-events-auto overflow-hidden rounded-[28px] bg-neutral-50/58 shadow-[0_18px_56px_rgba(15,23,42,0.14)] backdrop-blur-md dark:bg-neutral-900/58 dark:shadow-[0_18px_56px_rgba(0,0,0,0.34)]">
61
+ <Loading
62
+ compact
63
+ label="Loading"
64
+ className="min-h-[250px] w-[min(22rem,calc(100vw-2rem))] bg-transparent"
65
+ labelClassName="text-foreground"
66
+ />
67
+ </div>
68
+ </div>
69
+ ),
70
+ document.body
71
+ )
72
+ : null;
73
+
74
+ return {
75
+ dialogLoading,
76
+ runDialogAction,
77
+ };
78
+ }
@@ -15,16 +15,19 @@ import {
15
15
  secondaryButtonClass,
16
16
  } from "./dialog-styles";
17
17
  import { themeBgColor, themeBorderColor, themeIconColor } from "@windrun-huaiin/base-ui/lib";
18
+ import { DialogLoadingAction, DialogActionHandler, useDialogLoadingAction } from "./dialog-loading-action";
18
19
 
19
20
  interface HighPriorityConfirmDialogProps {
20
21
  open: boolean;
21
22
  onOpenChange: (open: boolean) => void;
22
- onCancel: () => void;
23
- onConfirm: () => void;
23
+ onCancel: DialogActionHandler;
24
+ onConfirm: DialogActionHandler;
24
25
  title: string;
25
26
  description: React.ReactNode;
26
27
  confirmText?: string;
27
28
  cancelText?: string;
29
+ loadingActions?: readonly DialogLoadingAction[];
30
+ loadingFullPage?: boolean;
28
31
  }
29
32
 
30
33
  export function HighPriorityConfirmDialog({
@@ -36,65 +39,77 @@ export function HighPriorityConfirmDialog({
36
39
  description,
37
40
  confirmText = "Confirm",
38
41
  cancelText = "Cancel",
42
+ loadingActions,
43
+ loadingFullPage,
39
44
  }: HighPriorityConfirmDialogProps) {
40
45
  const [mounted, setMounted] = useState(false);
46
+ const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, loadingFullPage, onOpenChange });
41
47
 
42
48
  useEffect(() => {
43
49
  // Ensure portal target exists and prevent hydration mismatch
44
50
  setTimeout(() => setMounted(true), 0);
45
51
  }, []);
46
52
 
47
- if (!open || !mounted) return null;
53
+ if (!mounted) return dialogLoading;
48
54
 
49
55
  const handleClose = () => {
50
56
  onOpenChange(false);
51
57
  };
52
58
 
53
- return createPortal(
54
- <div className="fixed inset-0 z-10000 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-300">
55
- <div
56
- className={cn(highPrioritySurfaceClass, "scale-100")}
57
- role="dialog"
58
- aria-modal="true"
59
- onClick={(e) => e.stopPropagation()}
60
- >
61
- <div className={dialogHeaderClass}>
62
- <h3 className={highPriorityTitleClass}>
63
- <span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', themeBgColor, themeBorderColor)}>
64
- <FAQSIcon className={cn('size-5', themeIconColor)} />
65
- </span>
66
- <span className="min-w-0 truncate">{title}</span>
67
- </h3>
68
- <button
69
- type="button"
70
- className={closeButtonClass}
71
- onClick={handleClose}
72
- aria-label="Close"
59
+ return (
60
+ <>
61
+ {open && createPortal(
62
+ <div className="fixed inset-0 z-10000 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-300">
63
+ <div
64
+ className={cn(highPrioritySurfaceClass, "scale-100")}
65
+ role="dialog"
66
+ aria-modal="true"
67
+ onClick={(e) => e.stopPropagation()}
73
68
  >
74
- <XIcon className="size-4" />
75
- </button>
76
- </div>
77
- <div className={dialogDescriptionClass}>
78
- {description}
79
- </div>
80
- <div className={dialogFooterClass}>
81
- <button
82
- type="button"
83
- onClick={onCancel}
84
- className={secondaryButtonClass}
85
- >
86
- {cancelText}
87
- </button>
88
- <button
89
- type="button"
90
- onClick={onConfirm}
91
- className={cn(primaryButtonClass, "hover:scale-105 active:scale-95")}
92
- >
93
- {confirmText}
94
- </button>
95
- </div>
96
- </div>
97
- </div>,
98
- document.body
69
+ <div className={dialogHeaderClass}>
70
+ <h3 className={highPriorityTitleClass}>
71
+ <span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', themeBgColor, themeBorderColor)}>
72
+ <FAQSIcon className={cn('size-5', themeIconColor)} />
73
+ </span>
74
+ <span className="min-w-0 truncate">{title}</span>
75
+ </h3>
76
+ <button
77
+ type="button"
78
+ className={closeButtonClass}
79
+ onClick={handleClose}
80
+ aria-label="Close"
81
+ >
82
+ <XIcon className="size-4" />
83
+ </button>
84
+ </div>
85
+ <div className={dialogDescriptionClass}>
86
+ {description}
87
+ </div>
88
+ <div className={dialogFooterClass}>
89
+ <button
90
+ type="button"
91
+ onClick={() => {
92
+ void runDialogAction('cancel', onCancel);
93
+ }}
94
+ className={secondaryButtonClass}
95
+ >
96
+ {cancelText}
97
+ </button>
98
+ <button
99
+ type="button"
100
+ onClick={() => {
101
+ void runDialogAction('confirm', onConfirm);
102
+ }}
103
+ className={cn(primaryButtonClass, "hover:scale-105 active:scale-95")}
104
+ >
105
+ {confirmText}
106
+ </button>
107
+ </div>
108
+ </div>
109
+ </div>,
110
+ document.body
111
+ )}
112
+ {dialogLoading}
113
+ </>
99
114
  );
100
115
  }
@@ -5,3 +5,4 @@ export * from './confirm-dialog';
5
5
  export * from './high-priority-confirm-dialog';
6
6
  export * from './info-dialog';
7
7
  export * from './undoable-confirm-dialog';
8
+ export type { DialogLoadingAction } from './dialog-loading-action';
@@ -25,6 +25,7 @@ import {
25
25
  dialogThemedOverlayClass,
26
26
  dialogTitleClass,
27
27
  } from './dialog-styles';
28
+ import { DialogLoadingAction, DialogActionHandler, useDialogLoadingAction } from './dialog-loading-action';
28
29
 
29
30
  export type InfoDialogType = 'info' | 'warn' | 'success' | 'error';
30
31
  type InfoDialogIcon = typeof BadgeInfoIcon;
@@ -36,7 +37,9 @@ interface InfoDialogProps {
36
37
  title: React.ReactNode;
37
38
  description: React.ReactNode;
38
39
  confirmText?: string;
39
- onConfirm?: () => void;
40
+ loadingActions?: readonly DialogLoadingAction[];
41
+ loadingFullPage?: boolean;
42
+ onConfirm?: DialogActionHandler;
40
43
  }
41
44
 
42
45
  const infoTypeClassMap: Record<InfoDialogType, {
@@ -83,57 +86,62 @@ export function InfoDialog({
83
86
  title,
84
87
  description,
85
88
  confirmText = 'OK',
89
+ loadingActions,
90
+ loadingFullPage,
86
91
  onConfirm,
87
92
  }: InfoDialogProps) {
88
93
  const typeClass = infoTypeClassMap[type];
89
94
  const Icon = typeClass.Icon;
90
95
  const handleClose = () => onOpenChange(false);
96
+ const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, loadingFullPage, onOpenChange });
91
97
 
92
98
  return (
93
- <AlertDialog open={open} onOpenChange={onOpenChange}>
94
- <AlertDialogContent
95
- className={cn(dialogContentClass, typeClass.content)}
96
- overlayClassName={dialogThemedOverlayClass}
97
- onOverlayClick={handleClose}
98
- >
99
- <div className={dialogHeaderClass}>
100
- <AlertDialogTitle asChild>
101
- <div className={dialogTitleClass}>
102
- <span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', typeClass.iconWrap)}>
103
- <Icon className={cn('size-5', typeClass.icon)} />
104
- </span>
105
- <span className="min-w-0 truncate">{title}</span>
106
- </div>
107
- </AlertDialogTitle>
108
- <button
109
- type="button"
110
- className={closeButtonClass}
111
- onClick={handleClose}
112
- aria-label="Close"
113
- >
114
- <XIcon className="size-4" />
115
- </button>
116
- </div>
99
+ <>
100
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
101
+ <AlertDialogContent
102
+ className={cn(dialogContentClass, typeClass.content)}
103
+ overlayClassName={dialogThemedOverlayClass}
104
+ onOverlayClick={handleClose}
105
+ >
106
+ <div className={dialogHeaderClass}>
107
+ <AlertDialogTitle asChild>
108
+ <div className={dialogTitleClass}>
109
+ <span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', typeClass.iconWrap)}>
110
+ <Icon className={cn('size-5', typeClass.icon)} />
111
+ </span>
112
+ <span className="min-w-0 truncate">{title}</span>
113
+ </div>
114
+ </AlertDialogTitle>
115
+ <button
116
+ type="button"
117
+ className={closeButtonClass}
118
+ onClick={handleClose}
119
+ aria-label="Close"
120
+ >
121
+ <XIcon className="size-4" />
122
+ </button>
123
+ </div>
117
124
 
118
- <AlertDialogDescription className={dialogDescriptionClass}>
119
- {description}
120
- </AlertDialogDescription>
125
+ <AlertDialogDescription className={dialogDescriptionClass}>
126
+ {description}
127
+ </AlertDialogDescription>
121
128
 
122
- <div className={dialogFooterClass}>
123
- <AlertDialogAction
124
- className={cn(
125
- 'inline-flex min-h-10 items-center justify-center rounded-full px-5 py-2 text-sm font-bold transition focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-60',
126
- typeClass.action
127
- )}
128
- onClick={() => {
129
- onOpenChange(false);
130
- onConfirm?.();
131
- }}
132
- >
133
- {confirmText}
134
- </AlertDialogAction>
135
- </div>
136
- </AlertDialogContent>
137
- </AlertDialog>
129
+ <div className={dialogFooterClass}>
130
+ <AlertDialogAction
131
+ className={cn(
132
+ 'inline-flex min-h-10 items-center justify-center rounded-full px-5 py-2 text-sm font-bold transition focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-60',
133
+ typeClass.action
134
+ )}
135
+ onClick={() => {
136
+ void runDialogAction('confirm', onConfirm);
137
+ }}
138
+ >
139
+ {confirmText}
140
+ </AlertDialogAction>
141
+ </div>
142
+ </AlertDialogContent>
143
+ </AlertDialog>
144
+ {dialogLoading}
145
+ </>
138
146
  );
139
147
  }
@@ -22,6 +22,7 @@ import {
22
22
  secondaryButtonClass,
23
23
  } from './dialog-styles';
24
24
  import type { ConfirmDialogEmphasis } from './confirm-dialog';
25
+ import { DialogLoadingAction, DialogActionHandler, useDialogLoadingAction } from './dialog-loading-action';
25
26
 
26
27
  export interface UndoableConfirmDialogProps {
27
28
  open: boolean;
@@ -35,9 +36,11 @@ export interface UndoableConfirmDialogProps {
35
36
  undoText?: string;
36
37
  emphasis?: ConfirmDialogEmphasis;
37
38
  countdownSeconds?: number;
38
- onCancel?: () => void;
39
- onConfirm: () => void | Promise<void>;
40
- onUndo?: () => void;
39
+ loadingActions?: readonly DialogLoadingAction[];
40
+ loadingFullPage?: boolean;
41
+ onCancel?: DialogActionHandler;
42
+ onConfirm: DialogActionHandler;
43
+ onUndo?: DialogActionHandler;
41
44
  }
42
45
 
43
46
  export function UndoableConfirmDialog({
@@ -52,6 +55,8 @@ export function UndoableConfirmDialog({
52
55
  undoText = 'Undo',
53
56
  emphasis = 'confirm',
54
57
  countdownSeconds = 5,
58
+ loadingActions,
59
+ loadingFullPage,
55
60
  onCancel,
56
61
  onConfirm,
57
62
  onUndo,
@@ -64,6 +69,7 @@ export function UndoableConfirmDialog({
64
69
  const intervalRef = React.useRef<number | null>(null);
65
70
  const cancelButtonClass = emphasis === 'cancel' ? dangerButtonClass : secondaryButtonClass;
66
71
  const confirmButtonClass = emphasis === 'cancel' ? secondaryButtonClass : dangerButtonClass;
72
+ const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, loadingFullPage, onOpenChange });
67
73
 
68
74
  const clearTimers = React.useCallback(() => {
69
75
  if (timeoutRef.current) {
@@ -100,12 +106,11 @@ export function UndoableConfirmDialog({
100
106
  setConfirming(true);
101
107
 
102
108
  try {
103
- await onConfirm();
104
- onOpenChange(false);
109
+ await runDialogAction('confirm', onConfirm);
105
110
  } finally {
106
111
  setConfirming(false);
107
112
  }
108
- }, [clearTimers, onConfirm, onOpenChange]);
113
+ }, [clearTimers, onConfirm, runDialogAction]);
109
114
 
110
115
  const startCountdown = () => {
111
116
  clearTimers();
@@ -123,102 +128,105 @@ export function UndoableConfirmDialog({
123
128
 
124
129
  const handleCancel = () => {
125
130
  resetState();
126
- onOpenChange(false);
127
- onCancel?.();
131
+ void runDialogAction('cancel', onCancel);
128
132
  };
129
133
  const handleClose = React.useCallback(() => {
130
134
  resetState();
131
135
  onOpenChange(false);
132
136
  }, [onOpenChange, resetState]);
133
137
 
134
- const handleUndo = () => {
138
+ const handleUndo = async () => {
135
139
  resetState();
136
- onOpenChange(false);
137
- onUndo?.();
140
+ await runDialogAction('undo', onUndo);
138
141
  };
139
142
 
140
143
  const displayTitle = pending ? pendingTitle ?? title : title;
141
144
  const displayDescription = pending ? pendingDescription ?? description : description;
142
145
  return (
143
- <AlertDialog open={open} onOpenChange={(nextOpen) => {
144
- if (!nextOpen) {
145
- handleClose();
146
- return;
147
- }
148
-
149
- onOpenChange(nextOpen);
150
- }}>
151
- <AlertDialogContent
152
- className={cn(dialogContentClass, 'border-red-300 dark:border-red-700')}
153
- overlayClassName={dialogThemedOverlayClass}
154
- onOverlayClick={pending ? undefined : handleClose}
155
- >
156
- <div className={dialogHeaderClass}>
157
- <AlertDialogTitle asChild>
158
- <div className={dialogTitleClass}>
159
- <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600 ring-1 ring-red-200 dark:bg-red-950 dark:text-red-300 dark:ring-red-900">
160
- {pending ? <Trash2Icon className="size-5" /> : <CircleAlertIcon className="size-5" />}
161
- </span>
162
- <span className="min-w-0 truncate">{displayTitle}</span>
163
- </div>
164
- </AlertDialogTitle>
165
- <button
166
- type="button"
167
- className={closeButtonClass}
168
- onClick={handleClose}
169
- aria-label="Close"
170
- disabled={confirming}
171
- >
172
- <XIcon className="size-4" />
173
- </button>
174
- </div>
175
-
176
- <AlertDialogDescription className={cn(dialogDescriptionClass, 'min-h-[44px]')}>
177
- <span>{displayDescription}</span>
178
- </AlertDialogDescription>
179
-
180
- <div className="flex h-12 items-center justify-center py-1">
181
- <div className="flex items-baseline justify-center gap-2">
182
- <span className={cn('text-4xl font-black leading-none tabular-nums', pending && 'animate-bounce', themeIconColor)}>
183
- {pending ? remainingSeconds : safeCountdownSeconds}
184
- </span>
185
- <span className={cn('text-sm font-bold', themeIconColor)}>
186
- s
187
- </span>
188
- </div>
189
- </div>
190
-
191
- <div className={cn(dialogFooterClass, 'min-h-[88px] sm:min-h-10 sm:items-center')}>
192
- {pending ? (
146
+ <>
147
+ <AlertDialog open={open} onOpenChange={(nextOpen) => {
148
+ if (!nextOpen) {
149
+ handleClose();
150
+ return;
151
+ }
152
+
153
+ onOpenChange(nextOpen);
154
+ }}>
155
+ <AlertDialogContent
156
+ className={cn(dialogContentClass, 'border-red-300 dark:border-red-700')}
157
+ overlayClassName={dialogThemedOverlayClass}
158
+ onOverlayClick={pending ? undefined : handleClose}
159
+ >
160
+ <div className={dialogHeaderClass}>
161
+ <AlertDialogTitle asChild>
162
+ <div className={dialogTitleClass}>
163
+ <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600 ring-1 ring-red-200 dark:bg-red-950 dark:text-red-300 dark:ring-red-900">
164
+ {pending ? <Trash2Icon className="size-5" /> : <CircleAlertIcon className="size-5" />}
165
+ </span>
166
+ <span className="min-w-0 truncate">{displayTitle}</span>
167
+ </div>
168
+ </AlertDialogTitle>
193
169
  <button
194
170
  type="button"
195
- onClick={handleUndo}
196
- className={secondaryButtonClass}
171
+ className={closeButtonClass}
172
+ onClick={handleClose}
173
+ aria-label="Close"
197
174
  disabled={confirming}
198
175
  >
199
- <Undo2Icon className="mr-1.5 size-4" />
200
- {undoText}
176
+ <XIcon className="size-4" />
201
177
  </button>
202
- ) : (
203
- <>
204
- <button
205
- type="button"
206
- onClick={handleCancel}
207
- className={cancelButtonClass}
208
- >
209
- {cancelText}
210
- </button>
178
+ </div>
179
+
180
+ <AlertDialogDescription className={cn(dialogDescriptionClass, 'min-h-[44px]')}>
181
+ <span>{displayDescription}</span>
182
+ </AlertDialogDescription>
183
+
184
+ <div className="flex h-12 items-center justify-center py-1">
185
+ <div className="flex items-baseline justify-center gap-2">
186
+ <span className={cn('text-4xl font-black leading-none tabular-nums', pending && 'animate-bounce', themeIconColor)}>
187
+ {pending ? remainingSeconds : safeCountdownSeconds}
188
+ </span>
189
+ <span className={cn('text-sm font-bold', themeIconColor)}>
190
+ s
191
+ </span>
192
+ </div>
193
+ </div>
194
+
195
+ <div className={cn(dialogFooterClass, 'min-h-[88px] sm:min-h-10 sm:items-center')}>
196
+ {pending ? (
211
197
  <button
212
198
  type="button"
213
- onClick={startCountdown}
214
- className={confirmButtonClass}
199
+ onClick={() => {
200
+ void handleUndo();
201
+ }}
202
+ className={secondaryButtonClass}
203
+ disabled={confirming}
215
204
  >
216
- {confirmText}
205
+ <Undo2Icon className="mr-1.5 size-4" />
206
+ {undoText}
217
207
  </button>
218
- </>
219
- )}
220
- </div>
221
- </AlertDialogContent>
222
- </AlertDialog>
208
+ ) : (
209
+ <>
210
+ <button
211
+ type="button"
212
+ onClick={handleCancel}
213
+ className={cancelButtonClass}
214
+ >
215
+ {cancelText}
216
+ </button>
217
+ <button
218
+ type="button"
219
+ onClick={startCountdown}
220
+ className={confirmButtonClass}
221
+ >
222
+ {confirmText}
223
+ </button>
224
+ </>
225
+ )}
226
+ </div>
227
+ </AlertDialogContent>
228
+ </AlertDialog>
229
+ {dialogLoading}
230
+ </>
223
231
  );
224
232
  }