@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.
- package/dist/fuma/base/custom-header.js +6 -3
- package/dist/fuma/base/custom-header.mjs +6 -3
- package/dist/main/alert-dialog/confirm-dialog.d.ts +5 -3
- package/dist/main/alert-dialog/confirm-dialog.js +7 -7
- package/dist/main/alert-dialog/confirm-dialog.mjs +8 -8
- package/dist/main/alert-dialog/dialog-loading-action.d.ts +12 -0
- package/dist/main/alert-dialog/dialog-loading-action.js +42 -0
- package/dist/main/alert-dialog/dialog-loading-action.mjs +40 -0
- package/dist/main/alert-dialog/high-priority-confirm-dialog.d.ts +5 -3
- package/dist/main/alert-dialog/high-priority-confirm-dialog.js +10 -4
- package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +11 -5
- package/dist/main/alert-dialog/index.d.ts +1 -0
- package/dist/main/alert-dialog/info-dialog.d.ts +4 -2
- package/dist/main/alert-dialog/info-dialog.js +6 -5
- package/dist/main/alert-dialog/info-dialog.mjs +7 -6
- package/dist/main/alert-dialog/undoable-confirm-dialog.d.ts +6 -4
- package/dist/main/alert-dialog/undoable-confirm-dialog.js +18 -17
- package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +19 -18
- package/dist/main/buttons/gradient-button.d.ts +3 -1
- package/dist/main/buttons/gradient-button.js +29 -3
- package/dist/main/buttons/gradient-button.mjs +29 -3
- package/dist/main/buttons/index.d.ts +1 -0
- package/dist/main/buttons/index.js +3 -0
- package/dist/main/buttons/index.mjs +1 -0
- package/dist/main/buttons/use-press-feedback.d.ts +18 -0
- package/dist/main/buttons/use-press-feedback.js +42 -0
- package/dist/main/buttons/use-press-feedback.mjs +39 -0
- package/dist/main/buttons/x-button.d.ts +3 -0
- package/dist/main/buttons/x-button.js +36 -6
- package/dist/main/buttons/x-button.mjs +36 -6
- package/dist/main/calendar/calendar-date-range-input.d.ts +17 -0
- package/dist/main/calendar/calendar-date-range-input.js +81 -0
- package/dist/main/calendar/calendar-date-range-input.mjs +79 -0
- package/dist/main/calendar/calendar-status-view.d.ts +23 -0
- package/dist/main/calendar/calendar-status-view.js +155 -0
- package/dist/main/calendar/calendar-status-view.mjs +153 -0
- package/dist/main/calendar/index.d.ts +3 -0
- package/dist/main/calendar/index.js +12 -0
- package/dist/main/calendar/index.mjs +4 -0
- package/dist/main/calendar/random-date-range-dialog.d.ts +15 -0
- package/dist/main/calendar/random-date-range-dialog.js +447 -0
- package/dist/main/calendar/random-date-range-dialog.mjs +445 -0
- package/package.json +8 -3
- package/src/fuma/base/custom-header.tsx +6 -3
- package/src/main/alert-dialog/confirm-dialog.tsx +52 -47
- package/src/main/alert-dialog/dialog-loading-action.tsx +63 -0
- package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +61 -48
- package/src/main/alert-dialog/index.ts +1 -0
- package/src/main/alert-dialog/info-dialog.tsx +50 -44
- package/src/main/alert-dialog/undoable-confirm-dialog.tsx +88 -82
- package/src/main/buttons/gradient-button.tsx +36 -3
- package/src/main/buttons/index.ts +1 -0
- package/src/main/buttons/use-press-feedback.ts +58 -0
- package/src/main/buttons/x-button.tsx +53 -11
- package/src/main/calendar/calendar-date-range-input.tsx +173 -0
- package/src/main/calendar/calendar-status-view.tsx +365 -0
- package/src/main/calendar/index.ts +5 -0
- 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:
|
|
23
|
-
onConfirm:
|
|
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 (!
|
|
51
|
+
if (!mounted) return dialogLoading;
|
|
48
52
|
|
|
49
53
|
const handleClose = () => {
|
|
50
54
|
onOpenChange(false);
|
|
51
55
|
};
|
|
52
56
|
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
className=
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
<
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
<AlertDialogDescription className={dialogDescriptionClass}>
|
|
124
|
+
{description}
|
|
125
|
+
</AlertDialogDescription>
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</
|
|
135
|
-
</
|
|
136
|
-
</
|
|
137
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
<
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
196
|
-
|
|
169
|
+
className={closeButtonClass}
|
|
170
|
+
onClick={handleClose}
|
|
171
|
+
aria-label="Close"
|
|
197
172
|
disabled={confirming}
|
|
198
173
|
>
|
|
199
|
-
<
|
|
200
|
-
{undoText}
|
|
174
|
+
<XIcon className="size-4" />
|
|
201
175
|
</button>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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={
|
|
214
|
-
|
|
197
|
+
onClick={() => {
|
|
198
|
+
void handleUndo();
|
|
199
|
+
}}
|
|
200
|
+
className={secondaryButtonClass}
|
|
201
|
+
disabled={confirming}
|
|
215
202
|
>
|
|
216
|
-
|
|
203
|
+
<Undo2Icon className="mr-1.5 size-4" />
|
|
204
|
+
{undoText}
|
|
217
205
|
</button>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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-
|
|
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={
|
|
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={
|
|
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>
|