@windrun-huaiin/third-ui 29.1.0 → 29.2.0

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 +5 -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 +12 -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 +5 -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 +4 -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 +6 -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 +15 -0
  41. package/dist/main/calendar/random-date-range-dialog.js +447 -0
  42. package/dist/main/calendar/random-date-range-dialog.mjs +445 -0
  43. package/package.json +8 -3
  44. package/src/fuma/base/custom-header.tsx +6 -3
  45. package/src/main/alert-dialog/confirm-dialog.tsx +52 -47
  46. package/src/main/alert-dialog/dialog-loading-action.tsx +63 -0
  47. package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +61 -48
  48. package/src/main/alert-dialog/index.ts +1 -0
  49. package/src/main/alert-dialog/info-dialog.tsx +50 -44
  50. package/src/main/alert-dialog/undoable-confirm-dialog.tsx +88 -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 +741 -0
@@ -0,0 +1,63 @@
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
+ onOpenChange: (open: boolean) => void;
13
+ }
14
+
15
+ export function useDialogLoadingAction({
16
+ loadingActions,
17
+ onOpenChange,
18
+ }: UseDialogLoadingActionOptions) {
19
+ const [mounted, setMounted] = React.useState(false);
20
+ const [loading, setLoading] = React.useState(false);
21
+
22
+ React.useEffect(() => {
23
+ setMounted(true);
24
+ }, []);
25
+
26
+ const runDialogAction = React.useCallback(async (
27
+ action: DialogLoadingAction,
28
+ handler?: DialogActionHandler
29
+ ) => {
30
+ onOpenChange(false);
31
+
32
+ if (!handler) {
33
+ return;
34
+ }
35
+
36
+ if (!loadingActions?.includes(action)) {
37
+ await handler();
38
+ return;
39
+ }
40
+
41
+ setLoading(true);
42
+
43
+ try {
44
+ await handler();
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ }, [loadingActions, onOpenChange]);
49
+
50
+ const dialogLoading = mounted && loading
51
+ ? createPortal(
52
+ <div className="fixed inset-0 z-10000">
53
+ <Loading className="h-full w-full" />
54
+ </div>,
55
+ document.body
56
+ )
57
+ : null;
58
+
59
+ return {
60
+ dialogLoading,
61
+ runDialogAction,
62
+ };
63
+ }
@@ -15,16 +15,18 @@ 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[];
28
30
  }
29
31
 
30
32
  export function HighPriorityConfirmDialog({
@@ -36,65 +38,76 @@ export function HighPriorityConfirmDialog({
36
38
  description,
37
39
  confirmText = "Confirm",
38
40
  cancelText = "Cancel",
41
+ loadingActions,
39
42
  }: HighPriorityConfirmDialogProps) {
40
43
  const [mounted, setMounted] = useState(false);
44
+ const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, onOpenChange });
41
45
 
42
46
  useEffect(() => {
43
47
  // Ensure portal target exists and prevent hydration mismatch
44
48
  setTimeout(() => setMounted(true), 0);
45
49
  }, []);
46
50
 
47
- if (!open || !mounted) return null;
51
+ if (!mounted) return dialogLoading;
48
52
 
49
53
  const handleClose = () => {
50
54
  onOpenChange(false);
51
55
  };
52
56
 
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"
57
+ return (
58
+ <>
59
+ {open && createPortal(
60
+ <div className="fixed inset-0 z-10000 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-300">
61
+ <div
62
+ className={cn(highPrioritySurfaceClass, "scale-100")}
63
+ role="dialog"
64
+ aria-modal="true"
65
+ onClick={(e) => e.stopPropagation()}
73
66
  >
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
67
+ <div className={dialogHeaderClass}>
68
+ <h3 className={highPriorityTitleClass}>
69
+ <span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', themeBgColor, themeBorderColor)}>
70
+ <FAQSIcon className={cn('size-5', themeIconColor)} />
71
+ </span>
72
+ <span className="min-w-0 truncate">{title}</span>
73
+ </h3>
74
+ <button
75
+ type="button"
76
+ className={closeButtonClass}
77
+ onClick={handleClose}
78
+ aria-label="Close"
79
+ >
80
+ <XIcon className="size-4" />
81
+ </button>
82
+ </div>
83
+ <div className={dialogDescriptionClass}>
84
+ {description}
85
+ </div>
86
+ <div className={dialogFooterClass}>
87
+ <button
88
+ type="button"
89
+ onClick={() => {
90
+ void runDialogAction('cancel', onCancel);
91
+ }}
92
+ className={secondaryButtonClass}
93
+ >
94
+ {cancelText}
95
+ </button>
96
+ <button
97
+ type="button"
98
+ onClick={() => {
99
+ void runDialogAction('confirm', onConfirm);
100
+ }}
101
+ className={cn(primaryButtonClass, "hover:scale-105 active:scale-95")}
102
+ >
103
+ {confirmText}
104
+ </button>
105
+ </div>
106
+ </div>
107
+ </div>,
108
+ document.body
109
+ )}
110
+ {dialogLoading}
111
+ </>
99
112
  );
100
113
  }
@@ -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,8 @@ interface InfoDialogProps {
36
37
  title: React.ReactNode;
37
38
  description: React.ReactNode;
38
39
  confirmText?: string;
39
- onConfirm?: () => void;
40
+ loadingActions?: readonly DialogLoadingAction[];
41
+ onConfirm?: DialogActionHandler;
40
42
  }
41
43
 
42
44
  const infoTypeClassMap: Record<InfoDialogType, {
@@ -83,57 +85,61 @@ export function InfoDialog({
83
85
  title,
84
86
  description,
85
87
  confirmText = 'OK',
88
+ loadingActions,
86
89
  onConfirm,
87
90
  }: InfoDialogProps) {
88
91
  const typeClass = infoTypeClassMap[type];
89
92
  const Icon = typeClass.Icon;
90
93
  const handleClose = () => onOpenChange(false);
94
+ const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, onOpenChange });
91
95
 
92
96
  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>
97
+ <>
98
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
99
+ <AlertDialogContent
100
+ className={cn(dialogContentClass, typeClass.content)}
101
+ overlayClassName={dialogThemedOverlayClass}
102
+ onOverlayClick={handleClose}
103
+ >
104
+ <div className={dialogHeaderClass}>
105
+ <AlertDialogTitle asChild>
106
+ <div className={dialogTitleClass}>
107
+ <span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', typeClass.iconWrap)}>
108
+ <Icon className={cn('size-5', typeClass.icon)} />
109
+ </span>
110
+ <span className="min-w-0 truncate">{title}</span>
111
+ </div>
112
+ </AlertDialogTitle>
113
+ <button
114
+ type="button"
115
+ className={closeButtonClass}
116
+ onClick={handleClose}
117
+ aria-label="Close"
118
+ >
119
+ <XIcon className="size-4" />
120
+ </button>
121
+ </div>
117
122
 
118
- <AlertDialogDescription className={dialogDescriptionClass}>
119
- {description}
120
- </AlertDialogDescription>
123
+ <AlertDialogDescription className={dialogDescriptionClass}>
124
+ {description}
125
+ </AlertDialogDescription>
121
126
 
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>
127
+ <div className={dialogFooterClass}>
128
+ <AlertDialogAction
129
+ className={cn(
130
+ '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',
131
+ typeClass.action
132
+ )}
133
+ onClick={() => {
134
+ void runDialogAction('confirm', onConfirm);
135
+ }}
136
+ >
137
+ {confirmText}
138
+ </AlertDialogAction>
139
+ </div>
140
+ </AlertDialogContent>
141
+ </AlertDialog>
142
+ {dialogLoading}
143
+ </>
138
144
  );
139
145
  }
@@ -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,10 @@ 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
+ onCancel?: DialogActionHandler;
41
+ onConfirm: DialogActionHandler;
42
+ onUndo?: DialogActionHandler;
41
43
  }
42
44
 
43
45
  export function UndoableConfirmDialog({
@@ -52,6 +54,7 @@ export function UndoableConfirmDialog({
52
54
  undoText = 'Undo',
53
55
  emphasis = 'confirm',
54
56
  countdownSeconds = 5,
57
+ loadingActions,
55
58
  onCancel,
56
59
  onConfirm,
57
60
  onUndo,
@@ -64,6 +67,7 @@ export function UndoableConfirmDialog({
64
67
  const intervalRef = React.useRef<number | null>(null);
65
68
  const cancelButtonClass = emphasis === 'cancel' ? dangerButtonClass : secondaryButtonClass;
66
69
  const confirmButtonClass = emphasis === 'cancel' ? secondaryButtonClass : dangerButtonClass;
70
+ const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, onOpenChange });
67
71
 
68
72
  const clearTimers = React.useCallback(() => {
69
73
  if (timeoutRef.current) {
@@ -100,12 +104,11 @@ export function UndoableConfirmDialog({
100
104
  setConfirming(true);
101
105
 
102
106
  try {
103
- await onConfirm();
104
- onOpenChange(false);
107
+ await runDialogAction('confirm', onConfirm);
105
108
  } finally {
106
109
  setConfirming(false);
107
110
  }
108
- }, [clearTimers, onConfirm, onOpenChange]);
111
+ }, [clearTimers, onConfirm, runDialogAction]);
109
112
 
110
113
  const startCountdown = () => {
111
114
  clearTimers();
@@ -123,102 +126,105 @@ export function UndoableConfirmDialog({
123
126
 
124
127
  const handleCancel = () => {
125
128
  resetState();
126
- onOpenChange(false);
127
- onCancel?.();
129
+ void runDialogAction('cancel', onCancel);
128
130
  };
129
131
  const handleClose = React.useCallback(() => {
130
132
  resetState();
131
133
  onOpenChange(false);
132
134
  }, [onOpenChange, resetState]);
133
135
 
134
- const handleUndo = () => {
136
+ const handleUndo = async () => {
135
137
  resetState();
136
- onOpenChange(false);
137
- onUndo?.();
138
+ await runDialogAction('undo', onUndo);
138
139
  };
139
140
 
140
141
  const displayTitle = pending ? pendingTitle ?? title : title;
141
142
  const displayDescription = pending ? pendingDescription ?? description : description;
142
143
  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 ? (
144
+ <>
145
+ <AlertDialog open={open} onOpenChange={(nextOpen) => {
146
+ if (!nextOpen) {
147
+ handleClose();
148
+ return;
149
+ }
150
+
151
+ onOpenChange(nextOpen);
152
+ }}>
153
+ <AlertDialogContent
154
+ className={cn(dialogContentClass, 'border-red-300 dark:border-red-700')}
155
+ overlayClassName={dialogThemedOverlayClass}
156
+ onOverlayClick={pending ? undefined : handleClose}
157
+ >
158
+ <div className={dialogHeaderClass}>
159
+ <AlertDialogTitle asChild>
160
+ <div className={dialogTitleClass}>
161
+ <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">
162
+ {pending ? <Trash2Icon className="size-5" /> : <CircleAlertIcon className="size-5" />}
163
+ </span>
164
+ <span className="min-w-0 truncate">{displayTitle}</span>
165
+ </div>
166
+ </AlertDialogTitle>
193
167
  <button
194
168
  type="button"
195
- onClick={handleUndo}
196
- className={secondaryButtonClass}
169
+ className={closeButtonClass}
170
+ onClick={handleClose}
171
+ aria-label="Close"
197
172
  disabled={confirming}
198
173
  >
199
- <Undo2Icon className="mr-1.5 size-4" />
200
- {undoText}
174
+ <XIcon className="size-4" />
201
175
  </button>
202
- ) : (
203
- <>
204
- <button
205
- type="button"
206
- onClick={handleCancel}
207
- className={cancelButtonClass}
208
- >
209
- {cancelText}
210
- </button>
176
+ </div>
177
+
178
+ <AlertDialogDescription className={cn(dialogDescriptionClass, 'min-h-[44px]')}>
179
+ <span>{displayDescription}</span>
180
+ </AlertDialogDescription>
181
+
182
+ <div className="flex h-12 items-center justify-center py-1">
183
+ <div className="flex items-baseline justify-center gap-2">
184
+ <span className={cn('text-4xl font-black leading-none tabular-nums', pending && 'animate-bounce', themeIconColor)}>
185
+ {pending ? remainingSeconds : safeCountdownSeconds}
186
+ </span>
187
+ <span className={cn('text-sm font-bold', themeIconColor)}>
188
+ s
189
+ </span>
190
+ </div>
191
+ </div>
192
+
193
+ <div className={cn(dialogFooterClass, 'min-h-[88px] sm:min-h-10 sm:items-center')}>
194
+ {pending ? (
211
195
  <button
212
196
  type="button"
213
- onClick={startCountdown}
214
- className={confirmButtonClass}
197
+ onClick={() => {
198
+ void handleUndo();
199
+ }}
200
+ className={secondaryButtonClass}
201
+ disabled={confirming}
215
202
  >
216
- {confirmText}
203
+ <Undo2Icon className="mr-1.5 size-4" />
204
+ {undoText}
217
205
  </button>
218
- </>
219
- )}
220
- </div>
221
- </AlertDialogContent>
222
- </AlertDialog>
206
+ ) : (
207
+ <>
208
+ <button
209
+ type="button"
210
+ onClick={handleCancel}
211
+ className={cancelButtonClass}
212
+ >
213
+ {cancelText}
214
+ </button>
215
+ <button
216
+ type="button"
217
+ onClick={startCountdown}
218
+ className={confirmButtonClass}
219
+ >
220
+ {confirmText}
221
+ </button>
222
+ </>
223
+ )}
224
+ </div>
225
+ </AlertDialogContent>
226
+ </AlertDialog>
227
+ {dialogLoading}
228
+ </>
223
229
  );
224
230
  }
@@ -12,9 +12,14 @@ import {
12
12
  } from "@windrun-huaiin/base-ui/lib";
13
13
  import Link from "next/link";
14
14
  import React, { useState } from 'react';
15
+ import { PressFeedback, resolvePressFeedbackMode, usePressFeedback } from './use-press-feedback';
15
16
 
16
17
  type GradientButtonVariant = 'default' | 'soft' | 'subtle';
17
18
 
19
+ const PRESS_FEEDBACK_MS = 180;
20
+ const gradientPressSubtleClass = 'translate-y-px scale-[0.98] shadow-inner brightness-95';
21
+ const gradientPressSolidClass = 'translate-y-[2px] scale-[0.96] shadow-[inset_0_2px_4px_rgba(15,23,42,0.22)] brightness-90';
22
+
18
23
  export interface GradientButtonProps {
19
24
  title: React.ReactNode;
20
25
  icon?: React.ReactNode;
@@ -30,6 +35,7 @@ export interface GradientButtonProps {
30
35
  loadingText?: React.ReactNode;
31
36
  preventDoubleClick?: boolean;
32
37
  variant?: GradientButtonVariant;
38
+ pressFeedback?: PressFeedback;
33
39
  }
34
40
 
35
41
  export function GradientButton({
@@ -47,8 +53,11 @@ export function GradientButton({
47
53
  preventDoubleClick = true,
48
54
  iconClassName,
49
55
  variant = 'default',
56
+ pressFeedback,
50
57
  }: GradientButtonProps) {
51
58
  const [isLoading, setIsLoading] = useState(false);
59
+ const pressMode = resolvePressFeedbackMode(pressFeedback);
60
+ const { pressedKey, flash, getPressProps } = usePressFeedback<'root'>(PRESS_FEEDBACK_MS);
52
61
  const actualLoadingText = loadingText || title?.toString().trim() || 'Loading...';
53
62
 
54
63
  const defaultIconClass = "h-4 w-4";
@@ -94,6 +103,12 @@ export function GradientButton({
94
103
  };
95
104
 
96
105
  const isDisabled = disabled || isLoading;
106
+ const isPressed = pressMode !== 'none' && pressedKey === 'root' && !disabled;
107
+ const pressClassName = isPressed
108
+ ? pressMode === 'solid'
109
+ ? gradientPressSolidClass
110
+ : gradientPressSubtleClass
111
+ : null;
97
112
  const displayTitle = isLoading ? actualLoadingText : title;
98
113
  const iconProvided = icon !== undefined;
99
114
 
@@ -163,11 +178,13 @@ export function GradientButton({
163
178
  const buttonClassName = cn(
164
179
  baseButtonStyles,
165
180
  variantClassName,
166
- 'text-base font-bold transition-all duration-300 rounded-full',
181
+ 'text-base font-bold transition-[transform,background-color,filter,box-shadow,border-color,color] duration-300 rounded-full',
167
182
  alignmentClass,
183
+ pressClassName,
168
184
  isDisabled && 'opacity-50 cursor-not-allowed',
169
185
  className,
170
186
  );
187
+ const pressProps = pressMode !== 'none' && !isDisabled ? getPressProps('root') : {};
171
188
 
172
189
  return (
173
190
  <div className={`flex flex-row gap-3 ${getAlignmentClass()}`}>
@@ -175,8 +192,14 @@ export function GradientButton({
175
192
  <button
176
193
  type="button"
177
194
  className={buttonClassName}
178
- onClick={handleClick}
195
+ onClick={(event) => {
196
+ if (!isDisabled && pressMode !== 'none') {
197
+ flash('root');
198
+ }
199
+ handleClick(event);
200
+ }}
179
201
  disabled={isDisabled}
202
+ {...pressProps}
180
203
  >
181
204
  {buttonContent}
182
205
  </button>
@@ -185,8 +208,18 @@ export function GradientButton({
185
208
  href={href || "#"}
186
209
  className={cn(buttonClassName, "no-underline hover:no-underline")}
187
210
  {...(openInNewTab ? { target: "_blank", rel: preserveReferrer ? 'noopener' : 'noopener noreferrer' } : {})}
188
- onClick={isDisabled ? (e) => e.preventDefault() : undefined}
211
+ onClick={(event) => {
212
+ if (isDisabled) {
213
+ event.preventDefault();
214
+ return;
215
+ }
216
+
217
+ if (pressMode !== 'none') {
218
+ flash('root');
219
+ }
220
+ }}
189
221
  aria-disabled={isDisabled}
222
+ {...pressProps}
190
223
  >
191
224
  {buttonContent}
192
225
  </Link>