@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
package/src/main/delayed-img.tsx
CHANGED
|
@@ -25,12 +25,13 @@ export function DelayedImg({
|
|
|
25
25
|
wrapperClassName,
|
|
26
26
|
placeholderClassName,
|
|
27
27
|
className,
|
|
28
|
+
onError,
|
|
28
29
|
onLoad,
|
|
29
30
|
...imageProps
|
|
30
31
|
}: DelayedImgProps) {
|
|
31
32
|
const shouldDelay = ENV_DELAY_ENABLED && ENV_DELAY_MS > 0
|
|
32
33
|
const [isMounted, setIsMounted] = useState(!shouldDelay)
|
|
33
|
-
const [
|
|
34
|
+
const [isSettled, setIsSettled] = useState(false)
|
|
34
35
|
|
|
35
36
|
useEffect(() => {
|
|
36
37
|
if (!shouldDelay || isMounted) {
|
|
@@ -46,7 +47,7 @@ export function DelayedImg({
|
|
|
46
47
|
|
|
47
48
|
return (
|
|
48
49
|
<div className={cn("relative", wrapperClassName)}>
|
|
49
|
-
{(!isMounted || !
|
|
50
|
+
{(!isMounted || !isSettled) && (
|
|
50
51
|
<SnakeLoadingFrame
|
|
51
52
|
shape="rounded-rect"
|
|
52
53
|
loading
|
|
@@ -68,13 +69,17 @@ export function DelayedImg({
|
|
|
68
69
|
<Image
|
|
69
70
|
{...imageProps}
|
|
70
71
|
alt={alt}
|
|
72
|
+
onError={(event) => {
|
|
73
|
+
setIsSettled(true)
|
|
74
|
+
onError?.(event)
|
|
75
|
+
}}
|
|
71
76
|
onLoad={(event) => {
|
|
72
|
-
|
|
77
|
+
setIsSettled(true)
|
|
73
78
|
onLoad?.(event)
|
|
74
79
|
}}
|
|
75
80
|
className={cn(
|
|
76
81
|
"transition duration-300",
|
|
77
|
-
|
|
82
|
+
isSettled ? "opacity-100" : "opacity-0",
|
|
78
83
|
className,
|
|
79
84
|
)}
|
|
80
85
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
4
5
|
import { CircleQuestionMarkIcon } from '@windrun-huaiin/base-ui/icons';
|
|
5
6
|
import { themeBorderColor, themeIconColor, themeRingColor } from '@windrun-huaiin/base-ui/lib';
|
|
6
7
|
import { cn } from '@windrun-huaiin/lib/utils';
|
|
@@ -12,6 +13,67 @@ type InfoTooltipProps = {
|
|
|
12
13
|
desktopSide?: 'right' | 'bottom';
|
|
13
14
|
};
|
|
14
15
|
|
|
16
|
+
type TooltipPosition = {
|
|
17
|
+
left: number;
|
|
18
|
+
top: number;
|
|
19
|
+
maxWidth: number;
|
|
20
|
+
side: 'bottom' | 'inline';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const TOOLTIP_MARGIN = 12;
|
|
24
|
+
const TOOLTIP_GAP = 8;
|
|
25
|
+
const TOOLTIP_MAX_WIDTH = 288;
|
|
26
|
+
|
|
27
|
+
function clamp(value: number, min: number, max: number) {
|
|
28
|
+
return Math.min(Math.max(value, min), max);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getTooltipPosition(
|
|
32
|
+
target: HTMLElement,
|
|
33
|
+
align: NonNullable<InfoTooltipProps['align']>,
|
|
34
|
+
desktopSide: NonNullable<InfoTooltipProps['desktopSide']>,
|
|
35
|
+
): TooltipPosition {
|
|
36
|
+
const rect = target.getBoundingClientRect();
|
|
37
|
+
const viewportWidth = window.innerWidth;
|
|
38
|
+
const viewportHeight = window.innerHeight;
|
|
39
|
+
const maxWidth = Math.min(
|
|
40
|
+
TOOLTIP_MAX_WIDTH,
|
|
41
|
+
Math.max(160, viewportWidth - TOOLTIP_MARGIN * 2),
|
|
42
|
+
);
|
|
43
|
+
const useInlineSide = desktopSide === 'right' && viewportWidth >= 768;
|
|
44
|
+
|
|
45
|
+
if (useInlineSide) {
|
|
46
|
+
const rightSpace = viewportWidth - rect.right - TOOLTIP_GAP - TOOLTIP_MARGIN;
|
|
47
|
+
const leftSpace = rect.left - TOOLTIP_GAP - TOOLTIP_MARGIN;
|
|
48
|
+
const placeRight = rightSpace >= Math.min(220, maxWidth) || rightSpace >= leftSpace;
|
|
49
|
+
const preferredLeft = placeRight
|
|
50
|
+
? rect.right + TOOLTIP_GAP
|
|
51
|
+
: rect.left - maxWidth - TOOLTIP_GAP;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
left: clamp(preferredLeft, TOOLTIP_MARGIN, viewportWidth - maxWidth - TOOLTIP_MARGIN),
|
|
55
|
+
top: clamp(
|
|
56
|
+
rect.top + rect.height / 2,
|
|
57
|
+
TOOLTIP_MARGIN + 40,
|
|
58
|
+
viewportHeight - TOOLTIP_MARGIN - 40,
|
|
59
|
+
),
|
|
60
|
+
maxWidth,
|
|
61
|
+
side: 'inline',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const preferredLeft = align === 'start'
|
|
66
|
+
? rect.left
|
|
67
|
+
: rect.right - maxWidth;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
left: clamp(preferredLeft, TOOLTIP_MARGIN, viewportWidth - maxWidth - TOOLTIP_MARGIN),
|
|
71
|
+
top: Math.min(rect.bottom + TOOLTIP_GAP, viewportHeight - TOOLTIP_MARGIN),
|
|
72
|
+
maxWidth,
|
|
73
|
+
side: 'bottom',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
15
77
|
export function InfoTooltip({
|
|
16
78
|
content,
|
|
17
79
|
className,
|
|
@@ -20,7 +82,17 @@ export function InfoTooltip({
|
|
|
20
82
|
}: InfoTooltipProps) {
|
|
21
83
|
const normalizedContent = content.trim();
|
|
22
84
|
const containerRef = useRef<HTMLSpanElement | null>(null);
|
|
85
|
+
const tooltipRef = useRef<HTMLSpanElement | null>(null);
|
|
23
86
|
const [open, setOpen] = useState(false);
|
|
87
|
+
const [position, setPosition] = useState<TooltipPosition | null>(null);
|
|
88
|
+
|
|
89
|
+
const updatePosition = () => {
|
|
90
|
+
if (!containerRef.current) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setPosition(getTooltipPosition(containerRef.current, align, desktopSide));
|
|
95
|
+
};
|
|
24
96
|
|
|
25
97
|
useEffect(() => {
|
|
26
98
|
function handlePointerDown(event: MouseEvent | TouchEvent) {
|
|
@@ -29,7 +101,11 @@ export function InfoTooltip({
|
|
|
29
101
|
}
|
|
30
102
|
|
|
31
103
|
const target = event.target;
|
|
32
|
-
if (
|
|
104
|
+
if (
|
|
105
|
+
target instanceof Node &&
|
|
106
|
+
!containerRef.current.contains(target) &&
|
|
107
|
+
!tooltipRef.current?.contains(target)
|
|
108
|
+
) {
|
|
33
109
|
setOpen(false);
|
|
34
110
|
}
|
|
35
111
|
}
|
|
@@ -43,6 +119,23 @@ export function InfoTooltip({
|
|
|
43
119
|
};
|
|
44
120
|
}, []);
|
|
45
121
|
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!open) {
|
|
124
|
+
setPosition(null);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
updatePosition();
|
|
129
|
+
|
|
130
|
+
window.addEventListener('resize', updatePosition);
|
|
131
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
window.removeEventListener('resize', updatePosition);
|
|
135
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
136
|
+
};
|
|
137
|
+
}, [align, desktopSide, open]);
|
|
138
|
+
|
|
46
139
|
if (!normalizedContent) {
|
|
47
140
|
return null;
|
|
48
141
|
}
|
|
@@ -50,7 +143,7 @@ export function InfoTooltip({
|
|
|
50
143
|
return (
|
|
51
144
|
<span
|
|
52
145
|
ref={containerRef}
|
|
53
|
-
className={cn('
|
|
146
|
+
className={cn('inline-flex h-5 w-5 shrink-0 align-middle', className)}
|
|
54
147
|
onMouseLeave={() => setOpen(false)}
|
|
55
148
|
>
|
|
56
149
|
<button
|
|
@@ -76,24 +169,27 @@ export function InfoTooltip({
|
|
|
76
169
|
>
|
|
77
170
|
<CircleQuestionMarkIcon className="h-4 w-4" />
|
|
78
171
|
</button>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
172
|
+
{open && position
|
|
173
|
+
? createPortal(
|
|
174
|
+
<span
|
|
175
|
+
ref={tooltipRef}
|
|
176
|
+
className={cn(
|
|
177
|
+
'pointer-events-none fixed z-50 max-h-[calc(100vh-1.5rem)] overflow-y-auto rounded-2xl border bg-white/95 px-3 py-2 text-xs leading-5 text-slate-600 shadow-xl backdrop-blur-sm dark:bg-slate-950/95 dark:text-slate-300',
|
|
178
|
+
position.side === 'inline' && '-translate-y-1/2',
|
|
179
|
+
themeBorderColor,
|
|
180
|
+
)}
|
|
181
|
+
style={{
|
|
182
|
+
left: position.left,
|
|
183
|
+
top: position.top,
|
|
184
|
+
width: position.maxWidth,
|
|
185
|
+
}}
|
|
186
|
+
role="tooltip"
|
|
187
|
+
>
|
|
188
|
+
{normalizedContent}
|
|
189
|
+
</span>,
|
|
190
|
+
document.body,
|
|
191
|
+
)
|
|
192
|
+
: null}
|
|
97
193
|
</span>
|
|
98
194
|
);
|
|
99
195
|
}
|
package/src/main/loading.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
4
|
import { cn } from '@windrun-huaiin/lib/utils';
|
|
4
5
|
import { themeSvgIconColor } from '@windrun-huaiin/base-ui/lib';
|
|
6
|
+
import { animate, type JSAnimation } from 'animejs';
|
|
5
7
|
|
|
6
8
|
const NUM_ROWS = 15;
|
|
7
9
|
const NUM_COLS = 15;
|
|
@@ -66,6 +68,7 @@ interface LoadingProps {
|
|
|
66
68
|
className?: string;
|
|
67
69
|
label?: string;
|
|
68
70
|
labelClassName?: string;
|
|
71
|
+
paused?: boolean;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export function Loading({
|
|
@@ -74,32 +77,82 @@ export function Loading({
|
|
|
74
77
|
className,
|
|
75
78
|
label = 'Loading...',
|
|
76
79
|
labelClassName,
|
|
80
|
+
paused = false,
|
|
77
81
|
}: LoadingProps = {}) {
|
|
78
|
-
const
|
|
79
|
-
const
|
|
82
|
+
const gridRef = useRef<HTMLDivElement | null>(null);
|
|
83
|
+
const animationRef = useRef<JSAnimation | null>(null);
|
|
84
|
+
const pausedRef = useRef(paused);
|
|
85
|
+
const colors = useMemo(() => createLoadingPalette(themeColor), [themeColor]);
|
|
80
86
|
const centerX = (NUM_COLS - 1) / 2;
|
|
81
87
|
const centerY = (NUM_ROWS - 1) / 2;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
88
|
+
const dots = useMemo(() => {
|
|
89
|
+
const nextDots = [];
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < NUM_ROWS; i++) {
|
|
92
|
+
for (let j = 0; j < NUM_COLS; j++) {
|
|
93
|
+
const distance = Math.sqrt(Math.pow(i - centerY, 2) + Math.pow(j - centerX, 2));
|
|
94
|
+
nextDots.push({
|
|
95
|
+
id: `${i}-${j}`,
|
|
96
|
+
row: i,
|
|
97
|
+
col: j,
|
|
98
|
+
delay: distance * STAGGER_DELAY_FACTOR,
|
|
99
|
+
color: colors[Math.floor(distance) % colors.length],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
96
102
|
}
|
|
97
|
-
|
|
103
|
+
|
|
104
|
+
return nextDots;
|
|
105
|
+
}, [centerX, centerY, colors]);
|
|
98
106
|
|
|
99
107
|
// Calculate the total width and height of the dot container
|
|
100
108
|
const containerWidth = (NUM_COLS - 1) * SPACING + DOT_SIZE;
|
|
101
109
|
const containerHeight = (NUM_ROWS - 1) * SPACING + DOT_SIZE;
|
|
102
110
|
|
|
111
|
+
pausedRef.current = paused;
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const grid = gridRef.current;
|
|
115
|
+
|
|
116
|
+
if (!grid) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const dotNodes = Array.from(grid.querySelectorAll<HTMLElement>('[data-loading-dot]'));
|
|
121
|
+
|
|
122
|
+
animationRef.current?.revert();
|
|
123
|
+
animationRef.current = animate(dotNodes, {
|
|
124
|
+
opacity: [0, 1, 0.7, 0],
|
|
125
|
+
scale: [0.2, 1.2, 0.8, 0.2],
|
|
126
|
+
duration: ANIMATION_DURATION * 1000,
|
|
127
|
+
delay: (target?: unknown) => Number((target as HTMLElement | undefined)?.dataset.loadingDelay ?? 0) * 1000,
|
|
128
|
+
ease: 'inOutSine',
|
|
129
|
+
loop: true,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (pausedRef.current) {
|
|
133
|
+
animationRef.current.pause();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
animationRef.current?.revert();
|
|
138
|
+
animationRef.current = null;
|
|
139
|
+
};
|
|
140
|
+
}, [dots]);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const animation = animationRef.current;
|
|
144
|
+
|
|
145
|
+
if (!animation) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (paused) {
|
|
150
|
+
animation.pause();
|
|
151
|
+
} else {
|
|
152
|
+
animation.play();
|
|
153
|
+
}
|
|
154
|
+
}, [paused]);
|
|
155
|
+
|
|
103
156
|
return (
|
|
104
157
|
<div
|
|
105
158
|
className={cn(
|
|
@@ -109,6 +162,7 @@ export function Loading({
|
|
|
109
162
|
)}
|
|
110
163
|
>
|
|
111
164
|
<div
|
|
165
|
+
ref={gridRef}
|
|
112
166
|
style={{
|
|
113
167
|
width: containerWidth,
|
|
114
168
|
height: containerHeight,
|
|
@@ -120,6 +174,8 @@ export function Loading({
|
|
|
120
174
|
{dots.map(dot => (
|
|
121
175
|
<div
|
|
122
176
|
key={dot.id}
|
|
177
|
+
data-loading-dot=""
|
|
178
|
+
data-loading-delay={dot.delay}
|
|
123
179
|
style={{
|
|
124
180
|
position: 'absolute',
|
|
125
181
|
left: dot.col * SPACING,
|
|
@@ -128,13 +184,8 @@ export function Loading({
|
|
|
128
184
|
height: DOT_SIZE,
|
|
129
185
|
backgroundColor: dot.color,
|
|
130
186
|
borderRadius: '50%',
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
animationTimingFunction: 'cubic-bezier(0.45, 0, 0.55, 1)',
|
|
134
|
-
animationIterationCount: 'infinite',
|
|
135
|
-
animationDelay: `${dot.delay}s`,
|
|
136
|
-
opacity: 0,
|
|
137
|
-
transform: 'scale(0)',
|
|
187
|
+
opacity: 0.35,
|
|
188
|
+
transform: 'scale(0.2)',
|
|
138
189
|
}}
|
|
139
190
|
/>
|
|
140
191
|
))}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useId, useRef } from 'react';
|
|
4
|
+
import { animate, useReducedMotion } from 'motion/react';
|
|
5
|
+
import {
|
|
6
|
+
BASE_DURATION_SECONDS,
|
|
7
|
+
BeamFrameShell,
|
|
8
|
+
BeamSvgLayer,
|
|
9
|
+
normalizeDuration,
|
|
10
|
+
useInteractiveRunning,
|
|
11
|
+
useMeasuredFrameSize,
|
|
12
|
+
type BeamFrameProps,
|
|
13
|
+
type FrameSize,
|
|
14
|
+
} from '../beam-frame-config';
|
|
15
|
+
|
|
16
|
+
export type { BeamFrameProps, BeamFrameTone } from '../beam-frame-config';
|
|
17
|
+
|
|
18
|
+
type PlaybackControls = {
|
|
19
|
+
speed: number;
|
|
20
|
+
play: () => void;
|
|
21
|
+
pause: () => void;
|
|
22
|
+
stop: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function MotionAroundBeam({
|
|
26
|
+
isRunning,
|
|
27
|
+
duration,
|
|
28
|
+
radius,
|
|
29
|
+
size,
|
|
30
|
+
}: {
|
|
31
|
+
isRunning: boolean;
|
|
32
|
+
duration: number;
|
|
33
|
+
radius?: number;
|
|
34
|
+
size: FrameSize;
|
|
35
|
+
}) {
|
|
36
|
+
const auraGradientId = useId().replace(/:/g, '');
|
|
37
|
+
const softGlowFilterId = useId().replace(/:/g, '');
|
|
38
|
+
const beamGroupRef = useRef<SVGGElement | null>(null);
|
|
39
|
+
const controlsRef = useRef<PlaybackControls | null>(null);
|
|
40
|
+
const hasStartedRef = useRef(false);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const node = beamGroupRef.current;
|
|
44
|
+
|
|
45
|
+
if (!node) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isRunning) {
|
|
50
|
+
hasStartedRef.current = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
node.style.opacity = isRunning || hasStartedRef.current ? 'var(--beam-frame-beam-opacity)' : '0';
|
|
54
|
+
|
|
55
|
+
if (!isRunning) {
|
|
56
|
+
controlsRef.current?.pause();
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (controlsRef.current) {
|
|
61
|
+
controlsRef.current.speed = BASE_DURATION_SECONDS / duration;
|
|
62
|
+
controlsRef.current.play();
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
controlsRef.current = animate(
|
|
67
|
+
node,
|
|
68
|
+
{ strokeDashoffset: [0, -1] },
|
|
69
|
+
{
|
|
70
|
+
duration: BASE_DURATION_SECONDS,
|
|
71
|
+
repeat: Infinity,
|
|
72
|
+
ease: 'linear',
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
controlsRef.current.speed = BASE_DURATION_SECONDS / duration;
|
|
76
|
+
|
|
77
|
+
return undefined;
|
|
78
|
+
}, [duration, isRunning]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
return () => {
|
|
82
|
+
controlsRef.current?.stop();
|
|
83
|
+
controlsRef.current = null;
|
|
84
|
+
};
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<BeamSvgLayer
|
|
89
|
+
beamRef={beamGroupRef}
|
|
90
|
+
auraGradientId={auraGradientId}
|
|
91
|
+
softGlowFilterId={softGlowFilterId}
|
|
92
|
+
radius={radius}
|
|
93
|
+
size={size}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function MotionBeamFrame(props: BeamFrameProps) {
|
|
99
|
+
const {
|
|
100
|
+
children,
|
|
101
|
+
active = false,
|
|
102
|
+
interactive = true,
|
|
103
|
+
tone = 'theme',
|
|
104
|
+
duration = BASE_DURATION_SECONDS,
|
|
105
|
+
radius,
|
|
106
|
+
className,
|
|
107
|
+
} = props;
|
|
108
|
+
const prefersReducedMotion = useReducedMotion();
|
|
109
|
+
const { isRunning, interactionProps } = useInteractiveRunning(active, interactive);
|
|
110
|
+
const shouldRun = isRunning && !prefersReducedMotion;
|
|
111
|
+
const normalizedDuration = normalizeDuration(duration);
|
|
112
|
+
const { ref, size } = useMeasuredFrameSize<HTMLDivElement>();
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<BeamFrameShell
|
|
116
|
+
active={active}
|
|
117
|
+
interactive={interactive}
|
|
118
|
+
tone={tone}
|
|
119
|
+
duration={normalizedDuration}
|
|
120
|
+
radius={radius}
|
|
121
|
+
className={className}
|
|
122
|
+
isRunning={shouldRun}
|
|
123
|
+
interactionProps={interactionProps}
|
|
124
|
+
rootRef={ref}
|
|
125
|
+
renderBeam={() => (
|
|
126
|
+
<MotionAroundBeam
|
|
127
|
+
isRunning={shouldRun}
|
|
128
|
+
duration={normalizedDuration}
|
|
129
|
+
radius={radius}
|
|
130
|
+
size={size}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</BeamFrameShell>
|
|
136
|
+
);
|
|
137
|
+
}
|