@windrun-huaiin/third-ui 30.0.0 → 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.
@@ -7,6 +7,7 @@ var react = require('motion/react');
7
7
  var utils = require('@windrun-huaiin/lib/utils');
8
8
  var animeBeamFrame = require('./anime/anime-beam-frame.js');
9
9
  require('animejs');
10
+ require('./anime/anime-404-page.js');
10
11
 
11
12
  const DEFAULT_THEME_COLOR = '#3b82f6';
12
13
  const EXIT_DURATION_MS = 260;
@@ -5,6 +5,7 @@ import { motion } from 'motion/react';
5
5
  import { cn } from '@windrun-huaiin/lib/utils';
6
6
  import { AnimeBeamFrame } from './anime/anime-beam-frame.mjs';
7
7
  import 'animejs';
8
+ import './anime/anime-404-page.mjs';
8
9
 
9
10
  const DEFAULT_THEME_COLOR = '#3b82f6';
10
11
  const EXIT_DURATION_MS = 260;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "30.0.0",
3
+ "version": "30.1.0",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "exports": {
6
6
  "./clerk": {
@@ -243,9 +243,9 @@
243
243
  "tslib": "^2.8.1",
244
244
  "unified": "^11.0.5",
245
245
  "zod": "^4.3.6",
246
- "@windrun-huaiin/contracts": "^30.0.0",
247
- "@windrun-huaiin/base-ui": "^30.0.0",
248
- "@windrun-huaiin/lib": "^30.0.0"
246
+ "@windrun-huaiin/base-ui": "^30.1.0",
247
+ "@windrun-huaiin/lib": "^30.0.0",
248
+ "@windrun-huaiin/contracts": "^30.0.0"
249
249
  },
250
250
  "peerDependencies": {
251
251
  "clsx": "^2.1.1",
@@ -3,6 +3,7 @@ import { ReactNode, ReactElement, cloneElement, type CSSProperties } from 'react
3
3
  import { TocFooterWrapper } from './mdx/toc-footer-wrapper';
4
4
  import type { LLMCopyButtonProps, LLMCopyButton } from './mdx/toc-base';
5
5
  import { getAsNeededLocalizedUrl } from '@windrun-huaiin/lib/utils';
6
+ import { notFound } from 'next/navigation';
6
7
  import { PortableClerkTOC, PortableClerkTOCTitle } from './mdx/toc-clerk-portable';
7
8
  import { themeSvgIconColor } from '@windrun-huaiin/base-ui/lib';
8
9
 
@@ -36,14 +37,6 @@ interface FumaPageParams {
36
37
  * The copy button component, must be LLMCopyButton
37
38
  */
38
39
  copyButtonComponent?: ReactElement<LLMCopyButtonProps, typeof LLMCopyButton>;
39
- /*
40
- * The site icon component to use in NotFoundPage
41
- */
42
- siteIcon: ReactNode;
43
- /*
44
- * The fallback page component to use when the page is not found
45
- */
46
- FallbackPage: React.ComponentType<{ siteIcon: ReactNode }>;
47
40
  /*
48
41
  * Supported locales for generating alternates metadata, defaults to ['en']
49
42
  */
@@ -89,8 +82,6 @@ export function createFumaPage({
89
82
  mdxSourceDir,
90
83
  githubBaseUrl,
91
84
  copyButtonComponent,
92
- siteIcon,
93
- FallbackPage,
94
85
  supportedLocales = ['en'],
95
86
  showBreadcrumb = true,
96
87
  showTableOfContent = true,
@@ -138,18 +129,7 @@ export function createFumaPage({
138
129
  totalElapsedMs: durationMs(pageStartedAt),
139
130
  });
140
131
  if (!page) {
141
- return (
142
- <DocsPage
143
- full
144
- breadcrumb={{ enabled: false }}
145
- footer={{ enabled: false }}
146
- tableOfContent={{ enabled: false }}
147
- tableOfContentPopover={{ enabled: false }}
148
- className="max-w-none px-0 py-0"
149
- >
150
- <FallbackPage siteIcon={siteIcon} />
151
- </DocsPage>
152
- );
132
+ notFound();
153
133
  }
154
134
 
155
135
  const path = githubBaseUrl ? `${mdxSourceDir}/${page.path}` : undefined;
@@ -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';
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'