@windrun-huaiin/third-ui 30.0.0 → 31.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 (122) hide show
  1. package/README.md +109 -143
  2. package/dist/ai/ai-prompt-textarea.js +5 -5
  3. package/dist/ai/ai-prompt-textarea.mjs +5 -5
  4. package/dist/clerk/clerk-auth-appearance.d.ts +13 -0
  5. package/dist/clerk/clerk-auth-appearance.js +19 -0
  6. package/dist/clerk/clerk-auth-appearance.mjs +15 -0
  7. package/dist/clerk/clerk-auth-modal-appearance.d.ts +12 -0
  8. package/dist/clerk/clerk-auth-modal-appearance.js +17 -0
  9. package/dist/clerk/clerk-auth-modal-appearance.mjs +14 -0
  10. package/dist/clerk/clerk-page-context-generator.js +3 -3
  11. package/dist/clerk/clerk-page-context-generator.mjs +3 -3
  12. package/dist/clerk/clerk-page-generator.js +4 -4
  13. package/dist/clerk/clerk-page-generator.mjs +4 -4
  14. package/dist/clerk/clerk-user-client.js +2 -1
  15. package/dist/clerk/clerk-user-client.mjs +2 -1
  16. package/dist/clerk/fingerprint/fingerprint-client.d.ts +10 -10
  17. package/dist/clerk/fingerprint/fingerprint-client.js +20 -20
  18. package/dist/clerk/fingerprint/fingerprint-client.mjs +20 -20
  19. package/dist/clerk/fingerprint/fingerprint-provider.d.ts +3 -3
  20. package/dist/clerk/fingerprint/fingerprint-provider.js +8 -8
  21. package/dist/clerk/fingerprint/fingerprint-provider.mjs +8 -8
  22. package/dist/clerk/fingerprint/fingerprint-server.d.ts +12 -12
  23. package/dist/clerk/fingerprint/fingerprint-server.js +17 -17
  24. package/dist/clerk/fingerprint/fingerprint-server.mjs +17 -17
  25. package/dist/clerk/fingerprint/fingerprint-shared.d.ts +3 -3
  26. package/dist/clerk/fingerprint/fingerprint-shared.js +10 -10
  27. package/dist/clerk/fingerprint/fingerprint-shared.mjs +10 -10
  28. package/dist/clerk/fingerprint/types.d.ts +0 -1
  29. package/dist/clerk/fingerprint/use-fingerprint.js +7 -7
  30. package/dist/clerk/fingerprint/use-fingerprint.mjs +7 -7
  31. package/dist/clerk/signin-with-fingerprint-client.d.ts +2 -2
  32. package/dist/clerk/signin-with-fingerprint-client.js +7 -6
  33. package/dist/clerk/signin-with-fingerprint-client.mjs +7 -6
  34. package/dist/clerk/signup-button-with-fingerprint-client.js +6 -4
  35. package/dist/clerk/signup-button-with-fingerprint-client.mjs +6 -4
  36. package/dist/clerk/signup-with-fingerprint-client.d.ts +2 -2
  37. package/dist/clerk/signup-with-fingerprint-client.js +7 -6
  38. package/dist/clerk/signup-with-fingerprint-client.mjs +7 -6
  39. package/dist/fuma/fuma-page-genarator.d.ts +2 -6
  40. package/dist/fuma/fuma-page-genarator.js +3 -2
  41. package/dist/fuma/fuma-page-genarator.mjs +3 -2
  42. package/dist/fuma/heavy/mermaid.js +1 -1
  43. package/dist/fuma/heavy/mermaid.mjs +1 -1
  44. package/dist/fuma/site-x.js +0 -1
  45. package/dist/fuma/site-x.mjs +0 -1
  46. package/dist/main/404-page.d.ts +12 -0
  47. package/dist/main/404-page.js +66 -0
  48. package/dist/main/404-page.mjs +64 -0
  49. package/dist/main/anime/anime-404-page.d.ts +14 -0
  50. package/dist/main/anime/anime-404-page.js +197 -0
  51. package/dist/main/anime/anime-404-page.mjs +195 -0
  52. package/dist/main/anime/anime-not-found-page.d.ts +7 -0
  53. package/dist/main/anime/anime-not-found-page.js +142 -0
  54. package/dist/main/anime/anime-not-found-page.mjs +140 -0
  55. package/dist/main/anime/index.d.ts +1 -0
  56. package/dist/main/anime/index.js +2 -0
  57. package/dist/main/anime/index.mjs +1 -0
  58. package/dist/main/calendar/calendar-date-range-input.js +1 -1
  59. package/dist/main/calendar/calendar-date-range-input.mjs +1 -1
  60. package/dist/main/credit/types.d.ts +8 -8
  61. package/dist/main/index.d.ts +1 -0
  62. package/dist/main/index.js +2 -0
  63. package/dist/main/index.mjs +1 -0
  64. package/dist/main/money-price/index.d.ts +1 -1
  65. package/dist/main/money-price/money-price-button.js +10 -10
  66. package/dist/main/money-price/money-price-button.mjs +10 -10
  67. package/dist/main/money-price/money-price-config-util.d.ts +30 -30
  68. package/dist/main/money-price/money-price-config-util.js +48 -48
  69. package/dist/main/money-price/money-price-config-util.mjs +48 -48
  70. package/dist/main/money-price/money-price-interactive.js +30 -18
  71. package/dist/main/money-price/money-price-interactive.mjs +30 -18
  72. package/dist/main/money-price/money-price-types.d.ts +7 -1
  73. package/dist/main/money-price/money-price-types.js +2 -2
  74. package/dist/main/money-price/money-price-types.mjs +2 -2
  75. package/dist/main/money-price/server.d.ts +1 -1
  76. package/dist/main/motion/creative-left-panel.d.ts +7 -0
  77. package/dist/main/motion/creative-left-panel.js +11 -0
  78. package/dist/main/motion/creative-left-panel.mjs +9 -0
  79. package/dist/main/motion/creative-right-panel.d.ts +7 -0
  80. package/dist/main/motion/creative-right-panel.js +11 -0
  81. package/dist/main/motion/creative-right-panel.mjs +9 -0
  82. package/dist/main/pill-select/x-pill-select.js +2 -2
  83. package/dist/main/pill-select/x-pill-select.mjs +2 -2
  84. package/dist/main/server.d.ts +1 -1
  85. package/dist/main/snake-loading-frame.js +1 -0
  86. package/dist/main/snake-loading-frame.mjs +1 -0
  87. package/package.json +13 -7
  88. package/src/ai/ai-prompt-textarea.tsx +6 -6
  89. package/src/clerk/clerk-auth-appearance.ts +16 -0
  90. package/src/clerk/clerk-page-context-generator.tsx +3 -5
  91. package/src/clerk/clerk-page-generator.tsx +9 -8
  92. package/src/clerk/clerk-user-client.tsx +14 -5
  93. package/src/clerk/fingerprint/fingerprint-client.ts +20 -20
  94. package/src/clerk/fingerprint/fingerprint-provider.tsx +11 -11
  95. package/src/clerk/fingerprint/fingerprint-server.ts +17 -17
  96. package/src/clerk/fingerprint/fingerprint-shared.ts +10 -10
  97. package/src/clerk/fingerprint/types.ts +0 -1
  98. package/src/clerk/fingerprint/use-fingerprint.ts +7 -7
  99. package/src/clerk/signin-with-fingerprint-client.tsx +7 -7
  100. package/src/clerk/signup-button-with-fingerprint-client.tsx +7 -5
  101. package/src/clerk/signup-with-fingerprint-client.tsx +7 -7
  102. package/src/fuma/base/custom-home-layout.tsx +4 -4
  103. package/src/fuma/fuma-page-genarator.tsx +2 -22
  104. package/src/fuma/heavy/mermaid.tsx +1 -1
  105. package/src/fuma/site-x.tsx +0 -1
  106. package/src/main/404-page.tsx +162 -0
  107. package/src/main/anime/anime-404-page.tsx +344 -0
  108. package/src/main/anime/index.ts +1 -0
  109. package/src/main/calendar/calendar-date-range-input.tsx +1 -1
  110. package/src/main/credit/types.ts +8 -8
  111. package/src/main/gallery/gallery-mobile-swiper.tsx +0 -1
  112. package/src/main/gallery/gallery-server.tsx +2 -2
  113. package/src/main/index.ts +1 -0
  114. package/src/main/money-price/index.ts +2 -0
  115. package/src/main/money-price/money-price-button.tsx +10 -10
  116. package/src/main/money-price/money-price-config-util.ts +49 -49
  117. package/src/main/money-price/money-price-interactive.tsx +40 -20
  118. package/src/main/money-price/money-price-types.ts +21 -14
  119. package/src/main/money-price/server.ts +2 -0
  120. package/src/main/pill-select/x-pill-select.tsx +2 -2
  121. package/src/main/server.ts +3 -1
  122. package/src/styles/third-ui.css +8 -0
@@ -0,0 +1,162 @@
1
+ 'use client';
2
+
3
+ import { NotFoundIcon } from '@windrun-huaiin/base-ui/components/shared';
4
+ import {
5
+ THEME_BUTTON_GRADIENT_CLASS_MAP,
6
+ themeBgColor,
7
+ themeButtonGradientClass,
8
+ themeIconColor,
9
+ themeViaColor,
10
+ type SupportedThemeColor,
11
+ } from '@windrun-huaiin/base-ui/lib';
12
+ import { cn } from '@windrun-huaiin/lib/utils';
13
+ import { useEffect, useState, type ReactNode } from 'react';
14
+
15
+ interface NotFoundPageProps {
16
+ siteIcon: ReactNode;
17
+ errorIcon?: ReactNode;
18
+ className?: string;
19
+ compact?: boolean;
20
+ themeClass?: SupportedThemeColor;
21
+ animated?: boolean;
22
+ }
23
+
24
+ const THEME_BG_CLASS_MAP: Record<SupportedThemeColor, string> = {
25
+ 'text-purple-500': 'bg-purple-500/20',
26
+ 'text-orange-500': 'bg-orange-500/20',
27
+ 'text-indigo-500': 'bg-indigo-500/20',
28
+ 'text-emerald-500': 'bg-emerald-500/20',
29
+ 'text-rose-500': 'bg-rose-500/20',
30
+ };
31
+
32
+ const THEME_VIA_CLASS_MAP: Record<SupportedThemeColor, string> = {
33
+ 'text-purple-500': 'via-purple-500/20',
34
+ 'text-orange-500': 'via-orange-500/20',
35
+ 'text-indigo-500': 'via-indigo-500/20',
36
+ 'text-emerald-500': 'via-emerald-500/20',
37
+ 'text-rose-500': 'via-rose-500/20',
38
+ };
39
+
40
+ export function NotFoundPage({
41
+ siteIcon,
42
+ errorIcon,
43
+ className,
44
+ compact = false,
45
+ themeClass,
46
+ animated = true,
47
+ }: NotFoundPageProps) {
48
+ const [glitchText, setGlitchText] = useState('404');
49
+ const homeUrl = process.env.NEXT_PUBLIC_BASE_URL || '/';
50
+ const activeThemeClass = themeClass ?? themeIconColor;
51
+ const activeGradientClass = themeClass ? THEME_BUTTON_GRADIENT_CLASS_MAP[themeClass] : themeButtonGradientClass;
52
+ const activeBgClass = themeClass ? THEME_BG_CLASS_MAP[themeClass] : themeBgColor;
53
+ const activeViaClass = themeClass ? THEME_VIA_CLASS_MAP[themeClass] : themeViaColor;
54
+
55
+ useEffect(() => {
56
+ if (!animated) {
57
+ setGlitchText('404');
58
+ return undefined;
59
+ }
60
+
61
+ const glitchChars = ['4', '0', '4', '?', '#', '!', '*', '&', '%', '$'];
62
+
63
+ const interval = setInterval(() => {
64
+ if (Math.random() < 0.5) {
65
+ setGlitchText('404');
66
+ } else {
67
+ const randomChars = Array.from(
68
+ { length: 3 },
69
+ () => glitchChars[Math.floor(Math.random() * glitchChars.length)]
70
+ ).join('');
71
+ setGlitchText(randomChars);
72
+ }
73
+ }, 600);
74
+
75
+ return () => clearInterval(interval);
76
+ }, [animated]);
77
+
78
+ return (
79
+ <div className={cn('relative flex w-full flex-col items-center justify-center px-4 py-8', compact ? 'h-full min-h-full' : 'min-h-dvh', className)}>
80
+ <div className="text-center space-y-8 max-w-2xl">
81
+ <div className="relative flex justify-center">
82
+ <h1
83
+ className={cn(
84
+ 'text-8xl md:text-9xl font-bold bg-linear-to-r bg-clip-text text-transparent select-none',
85
+ activeGradientClass
86
+ )}
87
+ style={{
88
+ fontFamily: 'Montserrat, monospace',
89
+ textShadow: '0 0 30px rgba(172, 98, 253, 0.3)',
90
+ letterSpacing: '0.1em',
91
+ }}
92
+ >
93
+ {glitchText}
94
+ </h1>
95
+ <div className="absolute inset-0 pointer-events-none">
96
+ <div className={cn('h-full w-full bg-linear-to-b from-transparent to-transparent', animated && 'animate-pulse', activeViaClass)} />
97
+ </div>
98
+ </div>
99
+
100
+ <div className="space-y-4">
101
+ <h2 className="text-2xl md:text-3xl font-semibold text-foreground">
102
+ Page Not Found
103
+ </h2>
104
+ <p className="text-lg text-muted-foreground max-w-md mx-auto leading-relaxed">
105
+ The page you&#39;re looking for doesn&#39;t exist
106
+ </p>
107
+ <a
108
+ href={homeUrl}
109
+ className={cn(
110
+ 'inline-flex text-sm font-medium underline underline-offset-4 transition-opacity hover:opacity-80',
111
+ activeThemeClass,
112
+ 'decoration-current'
113
+ )}
114
+ >
115
+ Back to Homepage
116
+ </a>
117
+ </div>
118
+
119
+ <div className="flex justify-center items-center gap-8 pt-8 opacity-60">
120
+ <a
121
+ href={homeUrl}
122
+ className="flex items-center gap-2 text-sm text-muted-foreground transition-opacity hover:opacity-80"
123
+ >
124
+ {siteIcon}
125
+ <span>Woops!</span>
126
+ </a>
127
+ <div className={cn('w-2 h-2 rounded-full', animated && 'animate-ping', activeBgClass)} />
128
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
129
+ {errorIcon ?? <NotFoundIcon />}
130
+ <span>Error Code: 404</span>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <div className={cn('pointer-events-none inset-0 overflow-hidden -z-10', compact ? 'absolute' : 'fixed')}>
136
+ <div
137
+ className="absolute inset-0 opacity-[0.02] dark:opacity-[0.05]"
138
+ style={{
139
+ backgroundImage: `
140
+ linear-gradient(rgba(172, 98, 253, 0.1) 1px, transparent 1px),
141
+ linear-gradient(90deg, rgba(172, 98, 253, 0.1) 1px, transparent 1px)
142
+ `,
143
+ backgroundSize: '50px 50px',
144
+ }}
145
+ />
146
+
147
+ {Array.from({ length: 6 }).map((_, i) => (
148
+ <div
149
+ key={i}
150
+ className={cn('absolute w-2 h-2 rounded-full', animated && 'animate-bounce', activeBgClass)}
151
+ style={{
152
+ left: `${20 + i * 15}%`,
153
+ top: `${30 + (i % 3) * 20}%`,
154
+ animationDelay: `${i * 0.5}s`,
155
+ animationDuration: `${2 + i * 0.3}s`,
156
+ }}
157
+ />
158
+ ))}
159
+ </div>
160
+ </div>
161
+ );
162
+ }
@@ -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
+ }
@@ -7,3 +7,4 @@ export {
7
7
  } from './anime-beam-frame';
8
8
 
9
9
  export { AnimeSpiralLoading, type AnimeSpiralLoadingProps } from './anime-spiral-loading';
10
+ export { AnimeNotFoundPage, type AnimeNotFoundPageProps } from './anime-404-page';
@@ -29,7 +29,7 @@ export type CalendarDateRangeInputProps = {
29
29
 
30
30
  type DateRangeInputPressKey = 'clear';
31
31
 
32
- const DEFAULT_PLACEHOLDER = '滑动窗口日期';
32
+ const DEFAULT_PLACEHOLDER = 'Slide pick Date';
33
33
  const DEFAULT_RANGE_DAYS = 7;
34
34
  const CLEAR_PRESS_FEEDBACK_MS = 180;
35
35
 
@@ -47,21 +47,21 @@ export interface CreditCTAConfig {
47
47
  export type CreditBucketStatus = 'active' | 'expiringSoon' | 'expired';
48
48
 
49
49
  export interface CreditBucket {
50
- /** 业务方自定义的积分类型标识,用于映射翻译或埋点 */
50
+ /** Business-defined credit type identifier for translation mapping or analytics. */
51
51
  kind: string;
52
- /** 若提供则使用该名称,否则由组件根据 kind 使用默认翻译 */
52
+ /** Display name override; otherwise the component uses the default translation for kind. */
53
53
  label?: string;
54
- /** 当前积分余额 */
54
+ /** Current credit balance. */
55
55
  balance: number;
56
- /** 该类型积分的额度上限 */
56
+ /** Credit limit for this credit type. */
57
57
  limit: number;
58
- /** 可选状态标签,用于强调过期等状态 */
58
+ /** Optional status label for highlighting states such as expiration. */
59
59
  status?: CreditBucketStatus;
60
- /** 积分过期时间(本地时区字符串),用于组件内部推导状态 */
60
+ /** Credit expiration time as a local time string, used to derive component state. */
61
61
  expiresAt?: string;
62
- /** 进度百分比(0-100);未提供时组件按 balance/limit 计算 */
62
+ /** Progress percentage from 0 to 100; computed from balance/limit when omitted. */
63
63
  progressPercent?: number;
64
- /** 任何额外说明,如剩余天数、使用限制等 */
64
+ /** Additional details, such as remaining days or usage limits. */
65
65
  description?: string;
66
66
  }
67
67
 
@@ -19,7 +19,6 @@ const swiperThemeStyle = {
19
19
  export function GalleryMobileSwiper({ items }: Props) {
20
20
  return (
21
21
  <div className="block sm:hidden px-4">
22
- {/* 外层容器:强制 maxWidth,防止任何溢出 */}
23
22
  <div
24
23
  className="w-full overflow-hidden"
25
24
  style={{ maxWidth: "min(calc(100vw - 48px), 350px)", margin: "0 auto" }}
@@ -33,10 +33,10 @@ export async function Gallery({ locale, sectionClassName, button }: GalleryProps
33
33
  </h2>
34
34
  <p className="text-center max-w-2xl mx-auto mb-16">{data.description}</p>
35
35
 
36
- {/* 移动端轮播 */}
36
+ {/* Mobile swiper */}
37
37
  <GalleryMobileSwiper items={data.items} />
38
38
 
39
- {/* 桌面端网格 */}
39
+ {/* Desktop grid */}
40
40
  <GalleryDesktopGrid items={data.items} />
41
41
 
42
42
  {button && <div className="text-center mx-auto mt-12 max-w-[85vw]">{button}</div>}
package/src/main/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // Lightweight common client components.
4
4
  export * from './go-to-top';
5
+ export * from './404-page';
5
6
  export * from './loading';
6
7
  export * from './nprogress-bar';
7
8
  export * from './rich-text-expert'
@@ -5,6 +5,8 @@ export { MoneyPriceButton } from './money-price-button';
5
5
  export type {
6
6
  MoneyPriceConfig,
7
7
  MoneyPriceData,
8
+ MoneyPriceAnimeTone,
9
+ MoneyPriceStrictDiffAnime,
8
10
  InitUserContext,
9
11
  MoneyPriceInteractiveProps,
10
12
  MoneyPriceButtonProps,
@@ -44,9 +44,9 @@ export function MoneyPriceButton({
44
44
  return 0;
45
45
  };
46
46
 
47
- // OneTime 模式的按钮配置
47
+ // Button configuration for one-time mode.
48
48
  const getOnetimeButtonConfig = () => {
49
- // 匿名用户:所有卡片都显示登录按钮
49
+ // Anonymous users: show the sign-in button on every card.
50
50
  if (!isAuthenticated) {
51
51
  return {
52
52
  text: texts.getStarted,
@@ -57,12 +57,12 @@ export function MoneyPriceButton({
57
57
  }
58
58
 
59
59
  if (subscriptionStatus === UserState.Anonymous) {
60
- // 已登录但状态未知 视为 FreeUser
60
+ // Signed in but status unknown: treat as FreeUser.
61
61
  console.warn('Clerk is authed OK but user is anonymous!');
62
62
  return { text: '', disabled: true, hidden: true };
63
63
  }
64
64
 
65
- // 登录用户:OneTime 模式下所有卡片都显示购买积分按钮
65
+ // Signed-in users: show the buy-credits button on every card in one-time mode.
66
66
  return {
67
67
  text: texts.buyCredits || texts.upgrade,
68
68
  onClick: () => onAction(planKey, billingType),
@@ -71,9 +71,9 @@ export function MoneyPriceButton({
71
71
  };
72
72
  };
73
73
 
74
- // 订阅模式的按钮配置
74
+ // Button configuration for subscription mode.
75
75
  const getSubscriptionButtonConfig = () => {
76
- // 匿名用户
76
+ // Anonymous users.
77
77
  if (!isAuthenticated) {
78
78
  const getButtonText = () => {
79
79
  switch (planKey) {
@@ -96,7 +96,7 @@ export function MoneyPriceButton({
96
96
  };
97
97
  }
98
98
 
99
- // 已登录用户
99
+ // Signed-in users.
100
100
  switch (subscriptionStatus) {
101
101
  case UserState.FreeUser: {
102
102
  if (planTier === 'F1') {
@@ -115,7 +115,7 @@ export function MoneyPriceButton({
115
115
  }
116
116
 
117
117
  case UserState.ProUser: {
118
- // 不允许降级到 Free
118
+ // Do not allow downgrades to Free.
119
119
  if (planTier === 'F1') {
120
120
  return { hidden: true };
121
121
  }
@@ -209,13 +209,13 @@ export function MoneyPriceButton({
209
209
  }
210
210
 
211
211
  default:
212
- // 已登录但状态未知 视为 FreeUser
212
+ // Signed in but status unknown: treat as FreeUser.
213
213
  console.warn('Clerk is authed OK but user is anonymous!');
214
214
  return { text: '', disabled: true, hidden: true };
215
215
  }
216
216
  };
217
217
 
218
- // 主要的按钮配置函数
218
+ // Main button configuration function.
219
219
  const getButtonConfig = () => {
220
220
  if (billingType === 'onetime') {
221
221
  return getOnetimeButtonConfig();