@windrun-huaiin/third-ui 29.2.1 → 30.1.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 (121) hide show
  1. package/dist/fuma/fuma-page-genarator.d.ts +2 -6
  2. package/dist/fuma/fuma-page-genarator.js +3 -2
  3. package/dist/fuma/fuma-page-genarator.mjs +3 -2
  4. package/dist/fuma/mdx/cheet-table.d.ts +13 -0
  5. package/dist/fuma/mdx/cheet-table.js +295 -0
  6. package/dist/fuma/mdx/cheet-table.mjs +293 -0
  7. package/dist/fuma/mdx/index.d.ts +1 -0
  8. package/dist/fuma/mdx/index.js +2 -0
  9. package/dist/fuma/mdx/index.mjs +1 -0
  10. package/dist/fuma/server/features/widgets.js +2 -0
  11. package/dist/fuma/server/features/widgets.mjs +2 -0
  12. package/dist/lib/fuma-schema-check-util.d.ts +1 -1
  13. package/dist/main/404-page.d.ts +12 -0
  14. package/dist/main/404-page.js +66 -0
  15. package/dist/main/404-page.mjs +64 -0
  16. package/dist/main/alert-dialog/confirm-dialog.js +1 -1
  17. package/dist/main/alert-dialog/confirm-dialog.mjs +2 -2
  18. package/dist/main/alert-dialog/dialog-loading-action.js +5 -2
  19. package/dist/main/alert-dialog/dialog-loading-action.mjs +5 -2
  20. package/dist/main/alert-dialog/dialog-styles.d.ts +4 -2
  21. package/dist/main/alert-dialog/dialog-styles.js +8 -4
  22. package/dist/main/alert-dialog/dialog-styles.mjs +7 -5
  23. package/dist/main/alert-dialog/high-priority-confirm-dialog.js +5 -5
  24. package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +6 -6
  25. package/dist/main/alert-dialog/info-dialog.js +1 -1
  26. package/dist/main/alert-dialog/info-dialog.mjs +2 -2
  27. package/dist/main/alert-dialog/undoable-confirm-dialog.js +2 -2
  28. package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +3 -3
  29. package/dist/main/anime/anime-404-page.d.ts +14 -0
  30. package/dist/main/anime/anime-404-page.js +197 -0
  31. package/dist/main/anime/anime-404-page.mjs +195 -0
  32. package/dist/main/anime/anime-beam-frame.d.ts +3 -0
  33. package/dist/main/anime/anime-beam-frame.js +63 -0
  34. package/dist/main/anime/anime-beam-frame.mjs +61 -0
  35. package/dist/main/anime/anime-not-found-page.d.ts +7 -0
  36. package/dist/main/anime/anime-not-found-page.js +142 -0
  37. package/dist/main/anime/anime-not-found-page.mjs +140 -0
  38. package/dist/main/anime/anime-spiral-loading.d.ts +10 -0
  39. package/dist/main/anime/anime-spiral-loading.js +77 -0
  40. package/dist/main/anime/anime-spiral-loading.mjs +75 -0
  41. package/dist/main/anime/index.d.ts +3 -0
  42. package/dist/main/anime/index.js +12 -0
  43. package/dist/main/anime/index.mjs +4 -0
  44. package/dist/main/beam-frame/animate.d.ts +3 -0
  45. package/dist/main/beam-frame/animate.js +63 -0
  46. package/dist/main/beam-frame/animate.mjs +61 -0
  47. package/dist/main/beam-frame/beam-frame.d.ts +4 -0
  48. package/dist/main/beam-frame/beam-frame.js +262 -0
  49. package/dist/main/beam-frame/beam-frame.mjs +258 -0
  50. package/dist/main/beam-frame/index.d.ts +4 -0
  51. package/dist/main/beam-frame/index.js +11 -0
  52. package/dist/main/beam-frame/index.mjs +3 -0
  53. package/dist/main/beam-frame/motion.d.ts +3 -0
  54. package/dist/main/beam-frame/motion.js +61 -0
  55. package/dist/main/beam-frame/motion.mjs +59 -0
  56. package/dist/main/beam-frame/share-config.d.ts +54 -0
  57. package/dist/main/beam-frame/share-config.js +161 -0
  58. package/dist/main/beam-frame/share-config.mjs +152 -0
  59. package/dist/main/beam-frame-config.d.ts +54 -0
  60. package/dist/main/beam-frame-config.js +161 -0
  61. package/dist/main/beam-frame-config.mjs +152 -0
  62. package/dist/main/calendar/random-date-range-dialog.js +177 -51
  63. package/dist/main/calendar/random-date-range-dialog.mjs +178 -52
  64. package/dist/main/cta.js +17 -1
  65. package/dist/main/cta.mjs +18 -2
  66. package/dist/main/delayed-img.d.ts +1 -1
  67. package/dist/main/delayed-img.js +8 -5
  68. package/dist/main/delayed-img.mjs +8 -5
  69. package/dist/main/index.d.ts +1 -0
  70. package/dist/main/index.js +2 -0
  71. package/dist/main/index.mjs +1 -0
  72. package/dist/main/info-tooltip.js +70 -9
  73. package/dist/main/info-tooltip.mjs +70 -9
  74. package/dist/main/loading-frame/index.d.ts +1 -0
  75. package/dist/main/loading.d.ts +2 -1
  76. package/dist/main/loading.js +64 -26
  77. package/dist/main/loading.mjs +64 -26
  78. package/dist/main/motion/creative-left-panel.d.ts +7 -0
  79. package/dist/main/motion/creative-left-panel.js +11 -0
  80. package/dist/main/motion/creative-left-panel.mjs +9 -0
  81. package/dist/main/motion/creative-right-panel.d.ts +7 -0
  82. package/dist/main/motion/creative-right-panel.js +11 -0
  83. package/dist/main/motion/creative-right-panel.mjs +9 -0
  84. package/dist/main/motion/index.d.ts +1 -0
  85. package/dist/main/motion/index.js +9 -0
  86. package/dist/main/motion/index.mjs +2 -0
  87. package/dist/main/motion/motion-beam-frame.d.ts +3 -0
  88. package/dist/main/motion/motion-beam-frame.js +61 -0
  89. package/dist/main/motion/motion-beam-frame.mjs +59 -0
  90. package/dist/main/snake-loading-frame.d.ts +7 -3
  91. package/dist/main/snake-loading-frame.js +45 -252
  92. package/dist/main/snake-loading-frame.mjs +47 -254
  93. package/package.json +16 -5
  94. package/src/fuma/fuma-page-genarator.tsx +2 -22
  95. package/src/fuma/mdx/cheet-table.tsx +650 -0
  96. package/src/fuma/mdx/index.ts +1 -0
  97. package/src/fuma/server/features/widgets.tsx +2 -0
  98. package/src/main/404-page.tsx +162 -0
  99. package/src/main/alert-dialog/confirm-dialog.tsx +2 -1
  100. package/src/main/alert-dialog/dialog-loading-action.tsx +7 -5
  101. package/src/main/alert-dialog/dialog-styles.ts +13 -3
  102. package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +26 -23
  103. package/src/main/alert-dialog/info-dialog.tsx +2 -1
  104. package/src/main/alert-dialog/undoable-confirm-dialog.tsx +18 -17
  105. package/src/main/anime/anime-404-page.tsx +344 -0
  106. package/src/main/anime/anime-beam-frame.tsx +128 -0
  107. package/src/main/anime/anime-spiral-loading.tsx +123 -0
  108. package/src/main/anime/index.ts +10 -0
  109. package/src/main/beam-frame-config.tsx +341 -0
  110. package/src/main/calendar/random-date-range-dialog.tsx +225 -69
  111. package/src/main/cta.tsx +50 -21
  112. package/src/main/delayed-img.tsx +9 -4
  113. package/src/main/index.ts +1 -0
  114. package/src/main/info-tooltip.tsx +116 -20
  115. package/src/main/loading-frame/index.ts +4 -0
  116. package/src/main/loading.tsx +75 -24
  117. package/src/main/motion/index.ts +8 -0
  118. package/src/main/motion/motion-beam-frame.tsx +137 -0
  119. package/src/main/snake-loading-frame.tsx +95 -496
  120. package/src/styles/cta.css +21 -4
  121. package/src/styles/third-ui.css +0 -20
@@ -0,0 +1,344 @@
1
+ 'use client';
2
+
3
+ import {
4
+ THEME_BUTTON_GRADIENT_CLASS_MAP,
5
+ themeBgColor,
6
+ themeButtonGradientClass,
7
+ themeIconColor,
8
+ themeSvgIconColor,
9
+ type SupportedThemeColor,
10
+ } from '@windrun-huaiin/base-ui/lib';
11
+ import { cn } from '@windrun-huaiin/lib/utils';
12
+ import { animate, createTimeline, stagger, type JSAnimation, type Timeline } from 'animejs';
13
+ import { useReducedMotion } from 'motion/react';
14
+ import { useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';
15
+
16
+ export interface AnimeNotFoundPageProps {
17
+ siteIcon: ReactNode;
18
+ homeUrl?: string;
19
+ className?: string;
20
+ compact?: boolean;
21
+ themeClass?: SupportedThemeColor;
22
+ themeColor?: string;
23
+ ambientAnimated?: boolean;
24
+ doorOpen?: boolean;
25
+ onDoorOpenChange?: (open: boolean) => void;
26
+ }
27
+
28
+ const THEME_BG_CLASS_MAP: Record<SupportedThemeColor, string> = {
29
+ 'text-purple-500': 'bg-purple-500/20',
30
+ 'text-orange-500': 'bg-orange-500/20',
31
+ 'text-indigo-500': 'bg-indigo-500/20',
32
+ 'text-emerald-500': 'bg-emerald-500/20',
33
+ 'text-rose-500': 'bg-rose-500/20',
34
+ };
35
+
36
+ const dust = Array.from({ length: 10 }, (_, index) => ({
37
+ id: index,
38
+ left: `${12 + index * 8}%`,
39
+ top: `${18 + (index % 5) * 13}%`,
40
+ size: 3 + (index % 3),
41
+ }));
42
+
43
+ export function AnimeNotFoundPage({
44
+ siteIcon,
45
+ homeUrl = process.env.NEXT_PUBLIC_BASE_URL || '/',
46
+ className,
47
+ compact = false,
48
+ themeClass,
49
+ themeColor,
50
+ ambientAnimated = true,
51
+ doorOpen,
52
+ onDoorOpenChange,
53
+ }: AnimeNotFoundPageProps) {
54
+ const rootRef = useRef<HTMLDivElement | null>(null);
55
+ const timelineRef = useRef<Timeline | null>(null);
56
+ const shimmerRef = useRef<JSAnimation | null>(null);
57
+ const doorAnimationRef = useRef<JSAnimation | null>(null);
58
+ const lightAnimationRef = useRef<JSAnimation | null>(null);
59
+ const handleAnimationRef = useRef<JSAnimation | null>(null);
60
+ const messageAnimationRef = useRef<JSAnimation | null>(null);
61
+ const isDoorOpenRef = useRef(doorOpen ?? false);
62
+ const prefersReducedMotion = useReducedMotion();
63
+ const activeThemeClass = themeClass ?? themeIconColor;
64
+ const activeGradientClass = themeClass ? THEME_BUTTON_GRADIENT_CLASS_MAP[themeClass] : themeButtonGradientClass;
65
+ const activeBgClass = themeClass ? THEME_BG_CLASS_MAP[themeClass] : themeBgColor;
66
+ const doorStyle = useMemo(
67
+ () => ({
68
+ '--not-found-theme': themeColor ?? themeSvgIconColor,
69
+ }) as React.CSSProperties,
70
+ [themeColor]
71
+ );
72
+
73
+ useEffect(() => {
74
+ const root = rootRef.current;
75
+
76
+ if (!root || prefersReducedMotion) {
77
+ return undefined;
78
+ }
79
+
80
+ const door = root.querySelector<HTMLElement>('[data-not-found-door]');
81
+ const light = root.querySelector<HTMLElement>('[data-not-found-light]');
82
+ const message = root.querySelector<HTMLElement>('[data-not-found-message]');
83
+ const plate = root.querySelector<HTMLElement>('[data-not-found-plate]');
84
+ const handle = root.querySelector<HTMLElement>('[data-not-found-handle]');
85
+ const dustNodes = Array.from(root.querySelectorAll<HTMLElement>('[data-not-found-dust]'));
86
+
87
+ if (!door || !light || !plate || !handle) {
88
+ return undefined;
89
+ }
90
+
91
+ door.style.transform = 'rotateY(-2deg) translateX(0)';
92
+ light.style.opacity = '0.2';
93
+ light.style.transform = 'scaleX(0.78)';
94
+ if (message) {
95
+ message.style.opacity = '0';
96
+ message.style.transform = 'translateY(8px)';
97
+ }
98
+
99
+ timelineRef.current?.revert();
100
+ shimmerRef.current?.revert();
101
+ timelineRef.current = null;
102
+ shimmerRef.current = null;
103
+
104
+ if (ambientAnimated) {
105
+ timelineRef.current = createTimeline({ loop: true })
106
+ .add(plate, {
107
+ translateY: [0, -3, 0],
108
+ scale: [1, 1.025, 1],
109
+ duration: 1400,
110
+ ease: 'inOutSine',
111
+ })
112
+ .add(plate, {
113
+ translateY: [0, -3, 0],
114
+ scale: [1, 1.025, 1],
115
+ duration: 1400,
116
+ ease: 'inOutSine',
117
+ }, '+=900')
118
+ .add(dustNodes, {
119
+ opacity: [0, 0.72, 0],
120
+ translateY: [14, -18],
121
+ translateX: (_target: unknown, index: number) => (index % 2 === 0 ? 10 : -10),
122
+ scale: [0.4, 1, 0.6],
123
+ duration: 1800,
124
+ delay: stagger(80),
125
+ ease: 'outSine',
126
+ }, '<+=200');
127
+
128
+ shimmerRef.current = animate(root.querySelectorAll<HTMLElement>('[data-not-found-shimmer]'), {
129
+ translateX: ['-120%', '120%'],
130
+ opacity: [0, 0.8, 0],
131
+ duration: 2400,
132
+ delay: stagger(160),
133
+ ease: 'inOutSine',
134
+ loop: true,
135
+ });
136
+ }
137
+
138
+ return () => {
139
+ timelineRef.current?.revert();
140
+ timelineRef.current = null;
141
+ shimmerRef.current?.revert();
142
+ shimmerRef.current = null;
143
+ doorAnimationRef.current?.revert();
144
+ doorAnimationRef.current = null;
145
+ lightAnimationRef.current?.revert();
146
+ lightAnimationRef.current = null;
147
+ handleAnimationRef.current?.revert();
148
+ handleAnimationRef.current = null;
149
+ messageAnimationRef.current?.revert();
150
+ messageAnimationRef.current = null;
151
+ };
152
+ }, [ambientAnimated, prefersReducedMotion]);
153
+
154
+ const setDoorOpen = useCallback((nextOpen: boolean) => {
155
+ const root = rootRef.current;
156
+
157
+ if (!root) {
158
+ return;
159
+ }
160
+
161
+ const door = root.querySelector<HTMLElement>('[data-not-found-door]');
162
+ const light = root.querySelector<HTMLElement>('[data-not-found-light]');
163
+ const handle = root.querySelector<HTMLElement>('[data-not-found-handle]');
164
+ const message = root.querySelector<HTMLElement>('[data-not-found-message]');
165
+
166
+ if (!door || !light || !handle) {
167
+ return;
168
+ }
169
+
170
+ isDoorOpenRef.current = nextOpen;
171
+
172
+ doorAnimationRef.current?.pause();
173
+ lightAnimationRef.current?.pause();
174
+ handleAnimationRef.current?.pause();
175
+ messageAnimationRef.current?.pause();
176
+ doorAnimationRef.current = null;
177
+ lightAnimationRef.current = null;
178
+ handleAnimationRef.current = null;
179
+ messageAnimationRef.current = null;
180
+
181
+ if (prefersReducedMotion) {
182
+ door.style.transform = nextOpen ? 'rotateY(-72deg) translateX(-12px)' : 'rotateY(-2deg) translateX(0)';
183
+ light.style.opacity = nextOpen ? '0.8' : '0.2';
184
+ light.style.transform = nextOpen ? 'scaleX(1.28)' : 'scaleX(0.78)';
185
+ if (message) {
186
+ message.style.opacity = nextOpen ? '1' : '0';
187
+ message.style.transform = nextOpen ? 'translateY(0)' : 'translateY(8px)';
188
+ }
189
+ return;
190
+ }
191
+
192
+ doorAnimationRef.current = animate(door, {
193
+ rotateY: nextOpen ? -72 : -2,
194
+ translateX: nextOpen ? -12 : 0,
195
+ duration: 1050,
196
+ ease: 'inOutCubic',
197
+ });
198
+ lightAnimationRef.current = animate(light, {
199
+ opacity: nextOpen ? 0.8 : 0.2,
200
+ scaleX: nextOpen ? 1.28 : 0.78,
201
+ duration: 1050,
202
+ ease: 'inOutSine',
203
+ });
204
+ handleAnimationRef.current = animate(handle, {
205
+ scale: [1, 1.05, 1],
206
+ duration: 520,
207
+ ease: 'inOutSine',
208
+ });
209
+ if (message) {
210
+ messageAnimationRef.current = animate(message, {
211
+ opacity: nextOpen ? [0, 1] : [1, 0],
212
+ translateY: nextOpen ? [10, 0] : [0, 4, 10],
213
+ scale: nextOpen ? [0.96, 1] : [1, 0.98],
214
+ duration: nextOpen ? 620 : 520,
215
+ delay: nextOpen ? 320 : 280,
216
+ ease: nextOpen ? 'outCubic' : 'inOutSine',
217
+ });
218
+ }
219
+ }, [prefersReducedMotion]);
220
+
221
+ useEffect(() => {
222
+ if (doorOpen === undefined || doorOpen === isDoorOpenRef.current) {
223
+ return;
224
+ }
225
+
226
+ setDoorOpen(doorOpen);
227
+ }, [doorOpen, setDoorOpen]);
228
+
229
+ const toggleDoor = () => {
230
+ const nextOpen = !isDoorOpenRef.current;
231
+
232
+ if (doorOpen !== undefined) {
233
+ onDoorOpenChange?.(nextOpen);
234
+ return;
235
+ }
236
+
237
+ setDoorOpen(nextOpen);
238
+ onDoorOpenChange?.(nextOpen);
239
+ };
240
+
241
+ return (
242
+ <div
243
+ ref={rootRef}
244
+ className={cn('relative flex w-full items-center justify-center overflow-hidden px-4 py-8', compact ? 'h-full min-h-full' : 'min-h-dvh', className)}
245
+ style={doorStyle}
246
+ >
247
+ <div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_50%_20%,rgba(255,255,255,0.75),transparent_34%),linear-gradient(180deg,rgba(250,250,250,0.96),rgba(244,244,245,0.72))] dark:bg-[radial-gradient(circle_at_50%_20%,rgba(255,255,255,0.08),transparent_34%),linear-gradient(180deg,rgba(24,24,27,0.96),rgba(9,9,11,0.92))]" />
248
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 -z-10 h-1/2 bg-[linear-gradient(180deg,transparent,rgba(0,0,0,0.05))] dark:bg-[linear-gradient(180deg,transparent,rgba(0,0,0,0.34))]" />
249
+
250
+ <div className="flex w-full max-w-3xl flex-col items-center gap-5">
251
+ <section className="text-center">
252
+ <h3 className={cn('whitespace-nowrap text-[clamp(2.15rem,8vw,3.4rem)] font-black leading-none tracking-normal bg-linear-to-r bg-clip-text text-transparent', activeGradientClass)}>
253
+ Page Not Found
254
+ </h3>
255
+ </section>
256
+
257
+ <section className="flex w-full justify-center">
258
+ <div className="relative aspect-[0.78] w-full max-w-[270px] sm:max-w-[315px] md:max-w-[330px] perspective-distant">
259
+ <div
260
+ data-not-found-light=""
261
+ className="absolute left-[14%] top-[7%] h-[86%] w-[72%] rounded-[28px] bg-[radial-gradient(circle_at_50%_45%,rgba(255,255,255,0.96),color-mix(in_srgb,var(--not-found-theme)_42%,transparent)_42%,transparent_72%)] opacity-25 blur-xl"
262
+ />
263
+ <div className="absolute inset-[4%] rounded-[32px] border border-black/10 bg-neutral-950/5 shadow-2xl shadow-black/10 dark:border-white/10 dark:bg-white/5" />
264
+ <div className="absolute inset-[8%] rounded-[26px] bg-[linear-gradient(180deg,rgba(255,255,255,0.88),rgba(228,228,231,0.86))] shadow-[inset_0_1px_0_rgba(255,255,255,0.85)] dark:bg-[linear-gradient(180deg,rgba(39,39,42,0.92),rgba(24,24,27,0.96))]" />
265
+ <p
266
+ data-not-found-message=""
267
+ className="pointer-events-none absolute right-[16%] top-[29%] z-2 max-w-[46%] text-right text-sm font-medium leading-6 text-muted-foreground opacity-100"
268
+ >
269
+ <span className="block">The page</span>
270
+ <span className="block">you&#39;re looking for</span>
271
+ <span className="block">doesn&#39;t exist</span>
272
+ </p>
273
+ <button
274
+ type="button"
275
+ className="absolute inset-[8%] z-1 rounded-[26px] outline-none focus-visible:ring-2 focus-visible:ring-(--not-found-theme)"
276
+ aria-label="Toggle the 404 door"
277
+ onClick={toggleDoor}
278
+ />
279
+
280
+ <div
281
+ data-not-found-door=""
282
+ className="absolute inset-[8%] z-10 origin-left rounded-[26px] border border-black/10 bg-[linear-gradient(145deg,rgba(255,255,255,0.92),rgba(212,212,216,0.9))] shadow-2xl shadow-black/20 will-change-transform dark:border-white/10 dark:bg-[linear-gradient(145deg,rgba(63,63,70,0.96),rgba(24,24,27,0.98))]"
283
+ >
284
+ <div className="absolute inset-4 overflow-hidden rounded-[20px]">
285
+ <div data-not-found-shimmer="" className="absolute inset-y-0 w-1/3 -skew-x-12 bg-white/35 blur-md dark:bg-white/12" />
286
+ <a
287
+ href={homeUrl}
288
+ className="absolute inset-x-5 bottom-5 flex h-[39%] flex-col items-center justify-center gap-2 rounded-2xl border border-black/10 bg-white/25 text-sm text-muted-foreground transition-opacity hover:opacity-80 dark:border-white/10 dark:bg-white/5"
289
+ >
290
+ <span className="inline-flex items-center gap-2">
291
+ {siteIcon}
292
+ <span>Woops!</span>
293
+ </span>
294
+ <span className={cn('text-xs font-semibold underline underline-offset-4', activeThemeClass, 'decoration-current')}>
295
+ Back to Homepage
296
+ </span>
297
+ </a>
298
+ </div>
299
+
300
+ <div
301
+ data-not-found-plate=""
302
+ className="absolute left-1/2 top-[18%] flex h-[88px] w-[156px] -translate-x-1/2 items-center justify-center overflow-hidden rounded-2xl border border-black/10 bg-white/86 shadow-lg shadow-black/10 dark:border-white/10 dark:bg-black/30"
303
+ >
304
+ <div data-not-found-shimmer="" className="absolute inset-y-0 w-1/2 -skew-x-12 bg-white/60 blur-md dark:bg-white/15" />
305
+ <span className={cn('relative text-5xl font-black tabular-nums bg-linear-to-r bg-clip-text text-transparent', activeGradientClass)}>
306
+ 404
307
+ </span>
308
+ </div>
309
+
310
+ <button
311
+ type="button"
312
+ data-not-found-handle=""
313
+ className="group absolute right-[1%] top-[39%] z-10 flex size-12 items-center justify-center rounded-full outline-none ring-offset-2 transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-(--not-found-theme)"
314
+ aria-label="Toggle the 404 door"
315
+ onClick={toggleDoor}
316
+ >
317
+ <span className="relative grid h-8 w-6 place-items-center rounded-full border border-black/10 bg-white/50 shadow-inner shadow-black/10 backdrop-blur-sm transform-[rotateY(18deg)] dark:border-white/15 dark:bg-black/25">
318
+ <span className="absolute size-10 rounded-full bg-(--not-found-theme) opacity-0 blur-md transition-opacity duration-300 group-hover:opacity-25" />
319
+ <span className="relative grid size-4 place-items-center rounded-full border border-black/10 bg-(--not-found-theme) shadow-lg shadow-black/25 dark:border-white/15">
320
+ <span className="absolute right-1 top-1 size-1 rounded-full bg-white/75" />
321
+ </span>
322
+ </span>
323
+ </button>
324
+ </div>
325
+
326
+ {dust.map(dot => (
327
+ <span
328
+ key={dot.id}
329
+ data-not-found-dust=""
330
+ className={cn('absolute rounded-full opacity-0', activeBgClass)}
331
+ style={{
332
+ left: dot.left,
333
+ top: dot.top,
334
+ width: dot.size,
335
+ height: dot.size,
336
+ }}
337
+ />
338
+ ))}
339
+ </div>
340
+ </section>
341
+ </div>
342
+ </div>
343
+ );
344
+ }
@@ -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,10 @@
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';
10
+ export { AnimeNotFoundPage, type AnimeNotFoundPageProps } from './anime-404-page';