@windrun-huaiin/third-ui 29.2.0 → 30.0.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/mdx/cheet-table.d.ts +13 -0
- package/dist/fuma/mdx/cheet-table.js +295 -0
- package/dist/fuma/mdx/cheet-table.mjs +293 -0
- package/dist/fuma/mdx/index.d.ts +1 -0
- package/dist/fuma/mdx/index.js +2 -0
- package/dist/fuma/mdx/index.mjs +1 -0
- package/dist/fuma/server/features/widgets.js +2 -0
- package/dist/fuma/server/features/widgets.mjs +2 -0
- package/dist/lib/fuma-schema-check-util.d.ts +1 -1
- package/dist/main/alert-dialog/confirm-dialog.d.ts +2 -1
- package/dist/main/alert-dialog/confirm-dialog.js +3 -3
- package/dist/main/alert-dialog/confirm-dialog.mjs +4 -4
- package/dist/main/alert-dialog/dialog-loading-action.d.ts +2 -1
- package/dist/main/alert-dialog/dialog-loading-action.js +6 -3
- package/dist/main/alert-dialog/dialog-loading-action.mjs +6 -3
- package/dist/main/alert-dialog/dialog-styles.d.ts +4 -2
- package/dist/main/alert-dialog/dialog-styles.js +8 -4
- package/dist/main/alert-dialog/dialog-styles.mjs +7 -5
- package/dist/main/alert-dialog/high-priority-confirm-dialog.d.ts +2 -1
- package/dist/main/alert-dialog/high-priority-confirm-dialog.js +7 -7
- package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +8 -8
- package/dist/main/alert-dialog/info-dialog.d.ts +2 -1
- package/dist/main/alert-dialog/info-dialog.js +3 -3
- package/dist/main/alert-dialog/info-dialog.mjs +4 -4
- package/dist/main/alert-dialog/undoable-confirm-dialog.d.ts +2 -1
- package/dist/main/alert-dialog/undoable-confirm-dialog.js +4 -4
- package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +5 -5
- package/dist/main/anime/anime-beam-frame.d.ts +3 -0
- package/dist/main/anime/anime-beam-frame.js +63 -0
- package/dist/main/anime/anime-beam-frame.mjs +61 -0
- package/dist/main/anime/anime-spiral-loading.d.ts +10 -0
- package/dist/main/anime/anime-spiral-loading.js +77 -0
- package/dist/main/anime/anime-spiral-loading.mjs +75 -0
- package/dist/main/anime/index.d.ts +2 -0
- package/dist/main/anime/index.js +10 -0
- package/dist/main/anime/index.mjs +3 -0
- package/dist/main/beam-frame/animate.d.ts +3 -0
- package/dist/main/beam-frame/animate.js +63 -0
- package/dist/main/beam-frame/animate.mjs +61 -0
- package/dist/main/beam-frame/beam-frame.d.ts +4 -0
- package/dist/main/beam-frame/beam-frame.js +262 -0
- package/dist/main/beam-frame/beam-frame.mjs +258 -0
- package/dist/main/beam-frame/index.d.ts +4 -0
- package/dist/main/beam-frame/index.js +11 -0
- package/dist/main/beam-frame/index.mjs +3 -0
- package/dist/main/beam-frame/motion.d.ts +3 -0
- package/dist/main/beam-frame/motion.js +61 -0
- package/dist/main/beam-frame/motion.mjs +59 -0
- package/dist/main/beam-frame/share-config.d.ts +54 -0
- package/dist/main/beam-frame/share-config.js +161 -0
- package/dist/main/beam-frame/share-config.mjs +152 -0
- package/dist/main/beam-frame-config.d.ts +54 -0
- package/dist/main/beam-frame-config.js +161 -0
- package/dist/main/beam-frame-config.mjs +152 -0
- package/dist/main/calendar/random-date-range-dialog.d.ts +5 -2
- package/dist/main/calendar/random-date-range-dialog.js +239 -109
- package/dist/main/calendar/random-date-range-dialog.mjs +242 -112
- package/dist/main/cta.js +17 -1
- package/dist/main/cta.mjs +18 -2
- package/dist/main/delayed-img.d.ts +1 -1
- package/dist/main/delayed-img.js +8 -5
- package/dist/main/delayed-img.mjs +8 -5
- package/dist/main/info-tooltip.js +70 -9
- package/dist/main/info-tooltip.mjs +70 -9
- package/dist/main/loading-frame/index.d.ts +1 -0
- package/dist/main/loading.d.ts +2 -1
- package/dist/main/loading.js +64 -26
- package/dist/main/loading.mjs +64 -26
- package/dist/main/motion/index.d.ts +1 -0
- package/dist/main/motion/index.js +9 -0
- package/dist/main/motion/index.mjs +2 -0
- package/dist/main/motion/motion-beam-frame.d.ts +3 -0
- package/dist/main/motion/motion-beam-frame.js +61 -0
- package/dist/main/motion/motion-beam-frame.mjs +59 -0
- package/dist/main/snake-loading-frame.d.ts +7 -3
- package/dist/main/snake-loading-frame.js +44 -252
- package/dist/main/snake-loading-frame.mjs +46 -254
- package/package.json +16 -5
- package/src/fuma/mdx/cheet-table.tsx +650 -0
- package/src/fuma/mdx/index.ts +1 -0
- package/src/fuma/server/features/widgets.tsx +2 -0
- package/src/main/alert-dialog/confirm-dialog.tsx +5 -2
- package/src/main/alert-dialog/dialog-loading-action.tsx +22 -5
- package/src/main/alert-dialog/dialog-styles.ts +13 -3
- package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +29 -24
- package/src/main/alert-dialog/info-dialog.tsx +5 -2
- package/src/main/alert-dialog/undoable-confirm-dialog.tsx +21 -18
- package/src/main/anime/anime-beam-frame.tsx +128 -0
- package/src/main/anime/anime-spiral-loading.tsx +123 -0
- package/src/main/anime/index.ts +9 -0
- package/src/main/beam-frame-config.tsx +341 -0
- package/src/main/calendar/random-date-range-dialog.tsx +242 -74
- package/src/main/cta.tsx +50 -21
- package/src/main/delayed-img.tsx +9 -4
- package/src/main/info-tooltip.tsx +116 -20
- package/src/main/loading-frame/index.ts +4 -0
- package/src/main/loading.tsx +75 -24
- package/src/main/motion/index.ts +8 -0
- package/src/main/motion/motion-beam-frame.tsx +137 -0
- package/src/main/snake-loading-frame.tsx +95 -496
- package/src/styles/cta.css +21 -4
- package/src/styles/third-ui.css +0 -20
|
@@ -3,17 +3,20 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { Loading } from '../loading';
|
|
6
|
+
import { dialogLoadingContentClass } from './dialog-styles';
|
|
6
7
|
|
|
7
8
|
export type DialogLoadingAction = 'cancel' | 'confirm' | 'undo';
|
|
8
9
|
export type DialogActionHandler = () => void | Promise<void>;
|
|
9
10
|
|
|
10
11
|
interface UseDialogLoadingActionOptions {
|
|
11
12
|
loadingActions?: readonly DialogLoadingAction[];
|
|
13
|
+
loadingFullPage?: boolean;
|
|
12
14
|
onOpenChange: (open: boolean) => void;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export function useDialogLoadingAction({
|
|
16
18
|
loadingActions,
|
|
19
|
+
loadingFullPage = false,
|
|
17
20
|
onOpenChange,
|
|
18
21
|
}: UseDialogLoadingActionOptions) {
|
|
19
22
|
const [mounted, setMounted] = React.useState(false);
|
|
@@ -27,18 +30,19 @@ export function useDialogLoadingAction({
|
|
|
27
30
|
action: DialogLoadingAction,
|
|
28
31
|
handler?: DialogActionHandler
|
|
29
32
|
) => {
|
|
30
|
-
onOpenChange(false);
|
|
31
|
-
|
|
32
33
|
if (!handler) {
|
|
34
|
+
onOpenChange(false);
|
|
33
35
|
return;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
if (!loadingActions?.includes(action)) {
|
|
39
|
+
onOpenChange(false);
|
|
37
40
|
await handler();
|
|
38
41
|
return;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
setLoading(true);
|
|
45
|
+
onOpenChange(false);
|
|
42
46
|
|
|
43
47
|
try {
|
|
44
48
|
await handler();
|
|
@@ -49,9 +53,22 @@ export function useDialogLoadingAction({
|
|
|
49
53
|
|
|
50
54
|
const dialogLoading = mounted && loading
|
|
51
55
|
? createPortal(
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
56
|
+
loadingFullPage ? (
|
|
57
|
+
<div className="fixed inset-0 z-10000">
|
|
58
|
+
<Loading className="h-full w-full" />
|
|
59
|
+
</div>
|
|
60
|
+
) : (
|
|
61
|
+
<div className="fixed inset-0 z-10000">
|
|
62
|
+
<div className={dialogLoadingContentClass}>
|
|
63
|
+
<Loading
|
|
64
|
+
compact
|
|
65
|
+
label="Loading"
|
|
66
|
+
className="min-h-[220px] w-fit max-w-full rounded-none bg-transparent px-0 py-0 dark:bg-transparent"
|
|
67
|
+
labelClassName="text-foreground"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
55
72
|
document.body
|
|
56
73
|
)
|
|
57
74
|
: null;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { cn } from '@windrun-huaiin/lib/utils';
|
|
13
13
|
|
|
14
14
|
export const dialogSurfaceClass = cn(
|
|
15
|
-
'w-[calc(100vw-2rem)] max-w-md rounded-2xl border bg-white p-5 text-neutral-950 shadow-2xl outline-none dark:bg-neutral-950 dark:text-neutral-50',
|
|
15
|
+
'min-h-[240px] w-[calc(100vw-2rem)] max-w-md rounded-2xl border bg-white p-5 text-neutral-950 shadow-2xl outline-none dark:bg-neutral-950 dark:text-neutral-50',
|
|
16
16
|
'border-neutral-200 dark:border-neutral-800'
|
|
17
17
|
);
|
|
18
18
|
|
|
@@ -26,13 +26,22 @@ export const dialogContentClass = cn(
|
|
|
26
26
|
dialogSurfaceClass
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
+
export const dialogLoadingContentClass = cn(
|
|
30
|
+
'fixed left-1/2 top-1/2 z-10000 -translate-x-1/2 -translate-y-1/2',
|
|
31
|
+
dialogSurfaceClass,
|
|
32
|
+
'flex items-center justify-center overflow-hidden animate-in fade-in-0 zoom-in-95 duration-150'
|
|
33
|
+
);
|
|
34
|
+
|
|
29
35
|
export const dialogHeaderClass = 'flex items-start justify-between gap-4';
|
|
30
36
|
|
|
31
37
|
export const dialogTitleClass =
|
|
32
|
-
'flex min-w-0 items-center gap-2 text-lg font-bold leading-tight text-neutral-950 dark:text-neutral-50';
|
|
38
|
+
'flex min-w-0 flex-1 items-center gap-2 text-lg font-bold leading-tight text-neutral-950 dark:text-neutral-50';
|
|
39
|
+
|
|
40
|
+
export const dialogTitleTextClass =
|
|
41
|
+
'min-w-0 flex-1 line-clamp-2 break-words';
|
|
33
42
|
|
|
34
43
|
export const dialogDescriptionClass =
|
|
35
|
-
'mt-3 text-sm font-medium leading-relaxed text-neutral-600 dark:text-neutral-300';
|
|
44
|
+
'mt-3 break-words text-sm font-medium leading-relaxed text-neutral-600 dark:text-neutral-300';
|
|
36
45
|
|
|
37
46
|
export const dialogFooterClass = 'mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end';
|
|
38
47
|
|
|
@@ -67,6 +76,7 @@ export const highPriorityTitleClass = cn(
|
|
|
67
76
|
|
|
68
77
|
export const highPrioritySurfaceClass = cn(
|
|
69
78
|
dialogSurfaceClass,
|
|
79
|
+
'flex flex-col',
|
|
70
80
|
'backdrop-blur-md ring-4 animate-in zoom-in-95 duration-300',
|
|
71
81
|
themeBorderColor,
|
|
72
82
|
themeRingColor
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
dialogHeaderClass,
|
|
12
12
|
highPrioritySurfaceClass,
|
|
13
13
|
highPriorityTitleClass,
|
|
14
|
+
dialogTitleTextClass,
|
|
14
15
|
primaryButtonClass,
|
|
15
16
|
secondaryButtonClass,
|
|
16
17
|
} from "./dialog-styles";
|
|
@@ -27,6 +28,7 @@ interface HighPriorityConfirmDialogProps {
|
|
|
27
28
|
confirmText?: string;
|
|
28
29
|
cancelText?: string;
|
|
29
30
|
loadingActions?: readonly DialogLoadingAction[];
|
|
31
|
+
loadingFullPage?: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function HighPriorityConfirmDialog({
|
|
@@ -39,9 +41,10 @@ export function HighPriorityConfirmDialog({
|
|
|
39
41
|
confirmText = "Confirm",
|
|
40
42
|
cancelText = "Cancel",
|
|
41
43
|
loadingActions,
|
|
44
|
+
loadingFullPage,
|
|
42
45
|
}: HighPriorityConfirmDialogProps) {
|
|
43
46
|
const [mounted, setMounted] = useState(false);
|
|
44
|
-
const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, onOpenChange });
|
|
47
|
+
const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, loadingFullPage, onOpenChange });
|
|
45
48
|
|
|
46
49
|
useEffect(() => {
|
|
47
50
|
// Ensure portal target exists and prevent hydration mismatch
|
|
@@ -69,7 +72,7 @@ export function HighPriorityConfirmDialog({
|
|
|
69
72
|
<span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', themeBgColor, themeBorderColor)}>
|
|
70
73
|
<FAQSIcon className={cn('size-5', themeIconColor)} />
|
|
71
74
|
</span>
|
|
72
|
-
<span className=
|
|
75
|
+
<span className={dialogTitleTextClass}>{title}</span>
|
|
73
76
|
</h3>
|
|
74
77
|
<button
|
|
75
78
|
type="button"
|
|
@@ -80,28 +83,30 @@ export function HighPriorityConfirmDialog({
|
|
|
80
83
|
<XIcon className="size-4" />
|
|
81
84
|
</button>
|
|
82
85
|
</div>
|
|
83
|
-
<div className=
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
87
|
+
<div className={dialogDescriptionClass}>
|
|
88
|
+
{description}
|
|
89
|
+
</div>
|
|
90
|
+
<div className={cn(dialogFooterClass, 'mt-auto')}>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => {
|
|
94
|
+
void runDialogAction('cancel', onCancel);
|
|
95
|
+
}}
|
|
96
|
+
className={secondaryButtonClass}
|
|
97
|
+
>
|
|
98
|
+
{cancelText}
|
|
99
|
+
</button>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() => {
|
|
103
|
+
void runDialogAction('confirm', onConfirm);
|
|
104
|
+
}}
|
|
105
|
+
className={cn(primaryButtonClass, "hover:scale-105 active:scale-95")}
|
|
106
|
+
>
|
|
107
|
+
{confirmText}
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
105
110
|
</div>
|
|
106
111
|
</div>
|
|
107
112
|
</div>,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
dialogHeaderClass,
|
|
25
25
|
dialogThemedOverlayClass,
|
|
26
26
|
dialogTitleClass,
|
|
27
|
+
dialogTitleTextClass,
|
|
27
28
|
} from './dialog-styles';
|
|
28
29
|
import { DialogLoadingAction, DialogActionHandler, useDialogLoadingAction } from './dialog-loading-action';
|
|
29
30
|
|
|
@@ -38,6 +39,7 @@ interface InfoDialogProps {
|
|
|
38
39
|
description: React.ReactNode;
|
|
39
40
|
confirmText?: string;
|
|
40
41
|
loadingActions?: readonly DialogLoadingAction[];
|
|
42
|
+
loadingFullPage?: boolean;
|
|
41
43
|
onConfirm?: DialogActionHandler;
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -86,12 +88,13 @@ export function InfoDialog({
|
|
|
86
88
|
description,
|
|
87
89
|
confirmText = 'OK',
|
|
88
90
|
loadingActions,
|
|
91
|
+
loadingFullPage,
|
|
89
92
|
onConfirm,
|
|
90
93
|
}: InfoDialogProps) {
|
|
91
94
|
const typeClass = infoTypeClassMap[type];
|
|
92
95
|
const Icon = typeClass.Icon;
|
|
93
96
|
const handleClose = () => onOpenChange(false);
|
|
94
|
-
const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, onOpenChange });
|
|
97
|
+
const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, loadingFullPage, onOpenChange });
|
|
95
98
|
|
|
96
99
|
return (
|
|
97
100
|
<>
|
|
@@ -107,7 +110,7 @@ export function InfoDialog({
|
|
|
107
110
|
<span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', typeClass.iconWrap)}>
|
|
108
111
|
<Icon className={cn('size-5', typeClass.icon)} />
|
|
109
112
|
</span>
|
|
110
|
-
<span className=
|
|
113
|
+
<span className={dialogTitleTextClass}>{title}</span>
|
|
111
114
|
</div>
|
|
112
115
|
</AlertDialogTitle>
|
|
113
116
|
<button
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
dialogHeaderClass,
|
|
20
20
|
dialogThemedOverlayClass,
|
|
21
21
|
dialogTitleClass,
|
|
22
|
+
dialogTitleTextClass,
|
|
22
23
|
secondaryButtonClass,
|
|
23
24
|
} from './dialog-styles';
|
|
24
25
|
import type { ConfirmDialogEmphasis } from './confirm-dialog';
|
|
@@ -37,6 +38,7 @@ export interface UndoableConfirmDialogProps {
|
|
|
37
38
|
emphasis?: ConfirmDialogEmphasis;
|
|
38
39
|
countdownSeconds?: number;
|
|
39
40
|
loadingActions?: readonly DialogLoadingAction[];
|
|
41
|
+
loadingFullPage?: boolean;
|
|
40
42
|
onCancel?: DialogActionHandler;
|
|
41
43
|
onConfirm: DialogActionHandler;
|
|
42
44
|
onUndo?: DialogActionHandler;
|
|
@@ -55,6 +57,7 @@ export function UndoableConfirmDialog({
|
|
|
55
57
|
emphasis = 'confirm',
|
|
56
58
|
countdownSeconds = 5,
|
|
57
59
|
loadingActions,
|
|
60
|
+
loadingFullPage,
|
|
58
61
|
onCancel,
|
|
59
62
|
onConfirm,
|
|
60
63
|
onUndo,
|
|
@@ -67,7 +70,7 @@ export function UndoableConfirmDialog({
|
|
|
67
70
|
const intervalRef = React.useRef<number | null>(null);
|
|
68
71
|
const cancelButtonClass = emphasis === 'cancel' ? dangerButtonClass : secondaryButtonClass;
|
|
69
72
|
const confirmButtonClass = emphasis === 'cancel' ? secondaryButtonClass : dangerButtonClass;
|
|
70
|
-
const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, onOpenChange });
|
|
73
|
+
const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, loadingFullPage, onOpenChange });
|
|
71
74
|
|
|
72
75
|
const clearTimers = React.useCallback(() => {
|
|
73
76
|
if (timeoutRef.current) {
|
|
@@ -161,7 +164,7 @@ export function UndoableConfirmDialog({
|
|
|
161
164
|
<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
165
|
{pending ? <Trash2Icon className="size-5" /> : <CircleAlertIcon className="size-5" />}
|
|
163
166
|
</span>
|
|
164
|
-
<span className=
|
|
167
|
+
<span className={dialogTitleTextClass}>{displayTitle}</span>
|
|
165
168
|
</div>
|
|
166
169
|
</AlertDialogTitle>
|
|
167
170
|
<button
|
|
@@ -197,7 +200,7 @@ export function UndoableConfirmDialog({
|
|
|
197
200
|
onClick={() => {
|
|
198
201
|
void handleUndo();
|
|
199
202
|
}}
|
|
200
|
-
className={secondaryButtonClass}
|
|
203
|
+
className={cn(secondaryButtonClass, 'w-full sm:w-auto')}
|
|
201
204
|
disabled={confirming}
|
|
202
205
|
>
|
|
203
206
|
<Undo2Icon className="mr-1.5 size-4" />
|
|
@@ -205,21 +208,21 @@ export function UndoableConfirmDialog({
|
|
|
205
208
|
</button>
|
|
206
209
|
) : (
|
|
207
210
|
<>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
onClick={handleCancel}
|
|
214
|
+
className={cn(cancelButtonClass, 'w-full sm:w-auto')}
|
|
215
|
+
>
|
|
216
|
+
{cancelText}
|
|
217
|
+
</button>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onClick={startCountdown}
|
|
221
|
+
className={cn(confirmButtonClass, 'w-full sm:w-auto')}
|
|
222
|
+
>
|
|
223
|
+
{confirmText}
|
|
224
|
+
</button>
|
|
225
|
+
</>
|
|
223
226
|
)}
|
|
224
227
|
</div>
|
|
225
228
|
</AlertDialogContent>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useId, useRef } from 'react';
|
|
4
|
+
import { type WAAPIAnimation, waapi } from 'animejs';
|
|
5
|
+
import { useReducedMotion } from 'motion/react';
|
|
6
|
+
import {
|
|
7
|
+
BASE_DURATION_SECONDS,
|
|
8
|
+
BeamFrameShell,
|
|
9
|
+
BeamSvgLayer,
|
|
10
|
+
normalizeDuration,
|
|
11
|
+
useInteractiveRunning,
|
|
12
|
+
useMeasuredFrameSize,
|
|
13
|
+
type BeamFrameProps,
|
|
14
|
+
type FrameSize,
|
|
15
|
+
} from '../beam-frame-config';
|
|
16
|
+
|
|
17
|
+
export type { BeamFrameProps, BeamFrameTone } from '../beam-frame-config';
|
|
18
|
+
|
|
19
|
+
function AnimeBeamLayer({
|
|
20
|
+
isRunning,
|
|
21
|
+
duration,
|
|
22
|
+
radius,
|
|
23
|
+
size,
|
|
24
|
+
}: {
|
|
25
|
+
isRunning: boolean;
|
|
26
|
+
duration: number;
|
|
27
|
+
radius?: number;
|
|
28
|
+
size: FrameSize;
|
|
29
|
+
}) {
|
|
30
|
+
const aroundBeamRef = useRef<SVGGElement | null>(null);
|
|
31
|
+
const animationRef = useRef<WAAPIAnimation | null>(null);
|
|
32
|
+
const hasStartedRef = useRef(false);
|
|
33
|
+
const auraGradientId = useId().replace(/:/g, '');
|
|
34
|
+
const softGlowFilterId = useId().replace(/:/g, '');
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const node = aroundBeamRef.current;
|
|
38
|
+
|
|
39
|
+
if (!node) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isRunning) {
|
|
44
|
+
hasStartedRef.current = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
node.style.opacity = isRunning || hasStartedRef.current ? 'var(--beam-frame-beam-opacity)' : '0';
|
|
48
|
+
|
|
49
|
+
if (!isRunning) {
|
|
50
|
+
animationRef.current?.pause();
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (animationRef.current) {
|
|
55
|
+
animationRef.current.speed = BASE_DURATION_SECONDS / duration;
|
|
56
|
+
animationRef.current.play();
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
animationRef.current = waapi.animate(node, {
|
|
61
|
+
strokeDashoffset: [0, -1],
|
|
62
|
+
loop: true,
|
|
63
|
+
duration: BASE_DURATION_SECONDS * 1000,
|
|
64
|
+
ease: 'linear',
|
|
65
|
+
});
|
|
66
|
+
animationRef.current.speed = BASE_DURATION_SECONDS / duration;
|
|
67
|
+
|
|
68
|
+
return undefined;
|
|
69
|
+
}, [duration, isRunning]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
return () => {
|
|
73
|
+
animationRef.current?.revert();
|
|
74
|
+
animationRef.current = null;
|
|
75
|
+
};
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<BeamSvgLayer
|
|
80
|
+
beamRef={aroundBeamRef}
|
|
81
|
+
auraGradientId={auraGradientId}
|
|
82
|
+
softGlowFilterId={softGlowFilterId}
|
|
83
|
+
radius={radius}
|
|
84
|
+
size={size}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function AnimeBeamFrame(props: BeamFrameProps) {
|
|
90
|
+
const {
|
|
91
|
+
children,
|
|
92
|
+
active = false,
|
|
93
|
+
interactive = true,
|
|
94
|
+
tone = 'theme',
|
|
95
|
+
duration = BASE_DURATION_SECONDS,
|
|
96
|
+
radius,
|
|
97
|
+
className,
|
|
98
|
+
} = props;
|
|
99
|
+
const prefersReducedMotion = useReducedMotion();
|
|
100
|
+
const { isRunning, interactionProps } = useInteractiveRunning(active, interactive);
|
|
101
|
+
const shouldRun = isRunning && !prefersReducedMotion;
|
|
102
|
+
const normalizedDuration = normalizeDuration(duration);
|
|
103
|
+
const { ref, size } = useMeasuredFrameSize<HTMLDivElement>();
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<BeamFrameShell
|
|
107
|
+
active={active}
|
|
108
|
+
interactive={interactive}
|
|
109
|
+
tone={tone}
|
|
110
|
+
duration={normalizedDuration}
|
|
111
|
+
radius={radius}
|
|
112
|
+
className={className}
|
|
113
|
+
isRunning={shouldRun}
|
|
114
|
+
interactionProps={interactionProps}
|
|
115
|
+
rootRef={ref}
|
|
116
|
+
renderBeam={() => (
|
|
117
|
+
<AnimeBeamLayer
|
|
118
|
+
isRunning={shouldRun}
|
|
119
|
+
duration={normalizedDuration}
|
|
120
|
+
radius={radius}
|
|
121
|
+
size={size}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</BeamFrameShell>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import { createTimeline, stagger, type Timeline, utils } from 'animejs';
|
|
5
|
+
import { cn } from '@windrun-huaiin/lib/utils';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DOT_COUNT = 2024;
|
|
8
|
+
const DEFAULT_DURATION = 10000;
|
|
9
|
+
const DEFAULT_DISTANCE_REM = 20;
|
|
10
|
+
const DEFAULT_DOT_SIZE_EM = 1;
|
|
11
|
+
const DEFAULT_FONT_SIZE = 20;
|
|
12
|
+
|
|
13
|
+
export interface AnimeSpiralLoadingProps {
|
|
14
|
+
className?: string;
|
|
15
|
+
dotCount?: number;
|
|
16
|
+
duration?: number;
|
|
17
|
+
distanceRem?: number;
|
|
18
|
+
dotSizeEm?: number;
|
|
19
|
+
fontSize?: number;
|
|
20
|
+
paused?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AnimeSpiralLoading({
|
|
24
|
+
className,
|
|
25
|
+
dotCount = DEFAULT_DOT_COUNT,
|
|
26
|
+
duration = DEFAULT_DURATION,
|
|
27
|
+
distanceRem = DEFAULT_DISTANCE_REM,
|
|
28
|
+
dotSizeEm = DEFAULT_DOT_SIZE_EM,
|
|
29
|
+
fontSize = DEFAULT_FONT_SIZE,
|
|
30
|
+
paused = false,
|
|
31
|
+
}: AnimeSpiralLoadingProps) {
|
|
32
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
33
|
+
const timelineRef = useRef<Timeline | null>(null);
|
|
34
|
+
const pausedRef = useRef(paused);
|
|
35
|
+
const safeDotCount = Math.max(1, Math.floor(dotCount));
|
|
36
|
+
const safeDuration = Math.max(1, duration);
|
|
37
|
+
const angle = useMemo(
|
|
38
|
+
() => (index: number) => utils.mapRange(index, 0, safeDotCount, 0, Math.PI * 100),
|
|
39
|
+
[safeDotCount]
|
|
40
|
+
);
|
|
41
|
+
const dots = useMemo(
|
|
42
|
+
() => Array.from({ length: safeDotCount }, (_, index) => {
|
|
43
|
+
const hue = utils.round((360 / safeDotCount) * index, 0);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: index,
|
|
47
|
+
background: `hsl(${hue}, 60%, 60%)`,
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
[safeDotCount]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
pausedRef.current = paused;
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const root = rootRef.current;
|
|
57
|
+
|
|
58
|
+
if (!root) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const dotNodes = Array.from(root.querySelectorAll<HTMLElement>('[data-anime-spiral-dot]'));
|
|
63
|
+
|
|
64
|
+
timelineRef.current?.revert();
|
|
65
|
+
timelineRef.current = createTimeline()
|
|
66
|
+
.add(dotNodes, {
|
|
67
|
+
x: (_target: unknown, i: number) => `${Math.sin(angle(i)) * distanceRem}rem`,
|
|
68
|
+
y: (_target: unknown, i: number) => `${Math.cos(angle(i)) * distanceRem}rem`,
|
|
69
|
+
scale: [0, 0.4, 0.2, 0.9, 0],
|
|
70
|
+
playbackEase: 'inOutSine',
|
|
71
|
+
loop: true,
|
|
72
|
+
duration: safeDuration,
|
|
73
|
+
}, stagger([0, safeDuration]))
|
|
74
|
+
.init()
|
|
75
|
+
.seek(safeDuration);
|
|
76
|
+
|
|
77
|
+
if (pausedRef.current) {
|
|
78
|
+
timelineRef.current.pause();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
timelineRef.current?.revert();
|
|
83
|
+
timelineRef.current = null;
|
|
84
|
+
};
|
|
85
|
+
}, [angle, distanceRem, safeDuration, dots]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const timeline = timelineRef.current;
|
|
89
|
+
|
|
90
|
+
if (!timeline) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (paused) {
|
|
95
|
+
timeline.pause();
|
|
96
|
+
} else {
|
|
97
|
+
timeline.play();
|
|
98
|
+
}
|
|
99
|
+
}, [paused]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
ref={rootRef}
|
|
104
|
+
className={cn('relative h-dvh w-full overflow-hidden', className)}
|
|
105
|
+
style={{ fontSize }}
|
|
106
|
+
aria-hidden="true"
|
|
107
|
+
>
|
|
108
|
+
{dots.map(dot => (
|
|
109
|
+
<div
|
|
110
|
+
key={dot.id}
|
|
111
|
+
data-anime-spiral-dot=""
|
|
112
|
+
className="absolute left-1/2 top-1/2 rounded-full"
|
|
113
|
+
style={{
|
|
114
|
+
width: `${dotSizeEm}em`,
|
|
115
|
+
height: `${dotSizeEm}em`,
|
|
116
|
+
margin: `${dotSizeEm / -2}em 0 0 ${dotSizeEm / -2}em`,
|
|
117
|
+
backgroundColor: dot.background,
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|