@windrun-huaiin/third-ui 29.2.1 → 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.
Files changed (96) hide show
  1. package/dist/fuma/mdx/cheet-table.d.ts +13 -0
  2. package/dist/fuma/mdx/cheet-table.js +295 -0
  3. package/dist/fuma/mdx/cheet-table.mjs +293 -0
  4. package/dist/fuma/mdx/index.d.ts +1 -0
  5. package/dist/fuma/mdx/index.js +2 -0
  6. package/dist/fuma/mdx/index.mjs +1 -0
  7. package/dist/fuma/server/features/widgets.js +2 -0
  8. package/dist/fuma/server/features/widgets.mjs +2 -0
  9. package/dist/lib/fuma-schema-check-util.d.ts +1 -1
  10. package/dist/main/alert-dialog/confirm-dialog.js +1 -1
  11. package/dist/main/alert-dialog/confirm-dialog.mjs +2 -2
  12. package/dist/main/alert-dialog/dialog-loading-action.js +5 -2
  13. package/dist/main/alert-dialog/dialog-loading-action.mjs +5 -2
  14. package/dist/main/alert-dialog/dialog-styles.d.ts +4 -2
  15. package/dist/main/alert-dialog/dialog-styles.js +8 -4
  16. package/dist/main/alert-dialog/dialog-styles.mjs +7 -5
  17. package/dist/main/alert-dialog/high-priority-confirm-dialog.js +5 -5
  18. package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +6 -6
  19. package/dist/main/alert-dialog/info-dialog.js +1 -1
  20. package/dist/main/alert-dialog/info-dialog.mjs +2 -2
  21. package/dist/main/alert-dialog/undoable-confirm-dialog.js +2 -2
  22. package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +3 -3
  23. package/dist/main/anime/anime-beam-frame.d.ts +3 -0
  24. package/dist/main/anime/anime-beam-frame.js +63 -0
  25. package/dist/main/anime/anime-beam-frame.mjs +61 -0
  26. package/dist/main/anime/anime-spiral-loading.d.ts +10 -0
  27. package/dist/main/anime/anime-spiral-loading.js +77 -0
  28. package/dist/main/anime/anime-spiral-loading.mjs +75 -0
  29. package/dist/main/anime/index.d.ts +2 -0
  30. package/dist/main/anime/index.js +10 -0
  31. package/dist/main/anime/index.mjs +3 -0
  32. package/dist/main/beam-frame/animate.d.ts +3 -0
  33. package/dist/main/beam-frame/animate.js +63 -0
  34. package/dist/main/beam-frame/animate.mjs +61 -0
  35. package/dist/main/beam-frame/beam-frame.d.ts +4 -0
  36. package/dist/main/beam-frame/beam-frame.js +262 -0
  37. package/dist/main/beam-frame/beam-frame.mjs +258 -0
  38. package/dist/main/beam-frame/index.d.ts +4 -0
  39. package/dist/main/beam-frame/index.js +11 -0
  40. package/dist/main/beam-frame/index.mjs +3 -0
  41. package/dist/main/beam-frame/motion.d.ts +3 -0
  42. package/dist/main/beam-frame/motion.js +61 -0
  43. package/dist/main/beam-frame/motion.mjs +59 -0
  44. package/dist/main/beam-frame/share-config.d.ts +54 -0
  45. package/dist/main/beam-frame/share-config.js +161 -0
  46. package/dist/main/beam-frame/share-config.mjs +152 -0
  47. package/dist/main/beam-frame-config.d.ts +54 -0
  48. package/dist/main/beam-frame-config.js +161 -0
  49. package/dist/main/beam-frame-config.mjs +152 -0
  50. package/dist/main/calendar/random-date-range-dialog.js +177 -51
  51. package/dist/main/calendar/random-date-range-dialog.mjs +178 -52
  52. package/dist/main/cta.js +17 -1
  53. package/dist/main/cta.mjs +18 -2
  54. package/dist/main/delayed-img.d.ts +1 -1
  55. package/dist/main/delayed-img.js +8 -5
  56. package/dist/main/delayed-img.mjs +8 -5
  57. package/dist/main/info-tooltip.js +70 -9
  58. package/dist/main/info-tooltip.mjs +70 -9
  59. package/dist/main/loading-frame/index.d.ts +1 -0
  60. package/dist/main/loading.d.ts +2 -1
  61. package/dist/main/loading.js +64 -26
  62. package/dist/main/loading.mjs +64 -26
  63. package/dist/main/motion/index.d.ts +1 -0
  64. package/dist/main/motion/index.js +9 -0
  65. package/dist/main/motion/index.mjs +2 -0
  66. package/dist/main/motion/motion-beam-frame.d.ts +3 -0
  67. package/dist/main/motion/motion-beam-frame.js +61 -0
  68. package/dist/main/motion/motion-beam-frame.mjs +59 -0
  69. package/dist/main/snake-loading-frame.d.ts +7 -3
  70. package/dist/main/snake-loading-frame.js +44 -252
  71. package/dist/main/snake-loading-frame.mjs +46 -254
  72. package/package.json +16 -5
  73. package/src/fuma/mdx/cheet-table.tsx +650 -0
  74. package/src/fuma/mdx/index.ts +1 -0
  75. package/src/fuma/server/features/widgets.tsx +2 -0
  76. package/src/main/alert-dialog/confirm-dialog.tsx +2 -1
  77. package/src/main/alert-dialog/dialog-loading-action.tsx +7 -5
  78. package/src/main/alert-dialog/dialog-styles.ts +13 -3
  79. package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +26 -23
  80. package/src/main/alert-dialog/info-dialog.tsx +2 -1
  81. package/src/main/alert-dialog/undoable-confirm-dialog.tsx +18 -17
  82. package/src/main/anime/anime-beam-frame.tsx +128 -0
  83. package/src/main/anime/anime-spiral-loading.tsx +123 -0
  84. package/src/main/anime/index.ts +9 -0
  85. package/src/main/beam-frame-config.tsx +341 -0
  86. package/src/main/calendar/random-date-range-dialog.tsx +225 -69
  87. package/src/main/cta.tsx +50 -21
  88. package/src/main/delayed-img.tsx +9 -4
  89. package/src/main/info-tooltip.tsx +116 -20
  90. package/src/main/loading-frame/index.ts +4 -0
  91. package/src/main/loading.tsx +75 -24
  92. package/src/main/motion/index.ts +8 -0
  93. package/src/main/motion/motion-beam-frame.tsx +137 -0
  94. package/src/main/snake-loading-frame.tsx +95 -496
  95. package/src/styles/cta.css +21 -4
  96. package/src/styles/third-ui.css +0 -20
@@ -3,6 +3,7 @@
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>;
@@ -29,18 +30,19 @@ export function useDialogLoadingAction({
29
30
  action: DialogLoadingAction,
30
31
  handler?: DialogActionHandler
31
32
  ) => {
32
- onOpenChange(false);
33
-
34
33
  if (!handler) {
34
+ onOpenChange(false);
35
35
  return;
36
36
  }
37
37
 
38
38
  if (!loadingActions?.includes(action)) {
39
+ onOpenChange(false);
39
40
  await handler();
40
41
  return;
41
42
  }
42
43
 
43
44
  setLoading(true);
45
+ onOpenChange(false);
44
46
 
45
47
  try {
46
48
  await handler();
@@ -56,12 +58,12 @@ export function useDialogLoadingAction({
56
58
  <Loading className="h-full w-full" />
57
59
  </div>
58
60
  ) : (
59
- <div className="pointer-events-none fixed inset-0 z-10000 flex items-center justify-center p-4">
60
- <div className="pointer-events-auto overflow-hidden rounded-[28px] bg-neutral-50/58 shadow-[0_18px_56px_rgba(15,23,42,0.14)] backdrop-blur-md dark:bg-neutral-900/58 dark:shadow-[0_18px_56px_rgba(0,0,0,0.34)]">
61
+ <div className="fixed inset-0 z-10000">
62
+ <div className={dialogLoadingContentClass}>
61
63
  <Loading
62
64
  compact
63
65
  label="Loading"
64
- className="min-h-[250px] w-[min(22rem,calc(100vw-2rem))] bg-transparent"
66
+ className="min-h-[220px] w-fit max-w-full rounded-none bg-transparent px-0 py-0 dark:bg-transparent"
65
67
  labelClassName="text-foreground"
66
68
  />
67
69
  </div>
@@ -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";
@@ -71,7 +72,7 @@ export function HighPriorityConfirmDialog({
71
72
  <span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', themeBgColor, themeBorderColor)}>
72
73
  <FAQSIcon className={cn('size-5', themeIconColor)} />
73
74
  </span>
74
- <span className="min-w-0 truncate">{title}</span>
75
+ <span className={dialogTitleTextClass}>{title}</span>
75
76
  </h3>
76
77
  <button
77
78
  type="button"
@@ -82,28 +83,30 @@ export function HighPriorityConfirmDialog({
82
83
  <XIcon className="size-4" />
83
84
  </button>
84
85
  </div>
85
- <div className={dialogDescriptionClass}>
86
- {description}
87
- </div>
88
- <div className={dialogFooterClass}>
89
- <button
90
- type="button"
91
- onClick={() => {
92
- void runDialogAction('cancel', onCancel);
93
- }}
94
- className={secondaryButtonClass}
95
- >
96
- {cancelText}
97
- </button>
98
- <button
99
- type="button"
100
- onClick={() => {
101
- void runDialogAction('confirm', onConfirm);
102
- }}
103
- className={cn(primaryButtonClass, "hover:scale-105 active:scale-95")}
104
- >
105
- {confirmText}
106
- </button>
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>
107
110
  </div>
108
111
  </div>
109
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
 
@@ -109,7 +110,7 @@ export function InfoDialog({
109
110
  <span className={cn('inline-flex size-9 shrink-0 items-center justify-center rounded-full ring-1', typeClass.iconWrap)}>
110
111
  <Icon className={cn('size-5', typeClass.icon)} />
111
112
  </span>
112
- <span className="min-w-0 truncate">{title}</span>
113
+ <span className={dialogTitleTextClass}>{title}</span>
113
114
  </div>
114
115
  </AlertDialogTitle>
115
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';
@@ -163,7 +164,7 @@ export function UndoableConfirmDialog({
163
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">
164
165
  {pending ? <Trash2Icon className="size-5" /> : <CircleAlertIcon className="size-5" />}
165
166
  </span>
166
- <span className="min-w-0 truncate">{displayTitle}</span>
167
+ <span className={dialogTitleTextClass}>{displayTitle}</span>
167
168
  </div>
168
169
  </AlertDialogTitle>
169
170
  <button
@@ -199,7 +200,7 @@ export function UndoableConfirmDialog({
199
200
  onClick={() => {
200
201
  void handleUndo();
201
202
  }}
202
- className={secondaryButtonClass}
203
+ className={cn(secondaryButtonClass, 'w-full sm:w-auto')}
203
204
  disabled={confirming}
204
205
  >
205
206
  <Undo2Icon className="mr-1.5 size-4" />
@@ -207,21 +208,21 @@ export function UndoableConfirmDialog({
207
208
  </button>
208
209
  ) : (
209
210
  <>
210
- <button
211
- type="button"
212
- onClick={handleCancel}
213
- className={cancelButtonClass}
214
- >
215
- {cancelText}
216
- </button>
217
- <button
218
- type="button"
219
- onClick={startCountdown}
220
- className={confirmButtonClass}
221
- >
222
- {confirmText}
223
- </button>
224
- </>
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
+ </>
225
226
  )}
226
227
  </div>
227
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
+ }
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+
3
+ export {
4
+ AnimeBeamFrame,
5
+ type BeamFrameProps,
6
+ type BeamFrameTone,
7
+ } from './anime-beam-frame';
8
+
9
+ export { AnimeSpiralLoading, type AnimeSpiralLoadingProps } from './anime-spiral-loading';