create-next-imagicma 0.0.1

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 (86) hide show
  1. package/README.md +42 -0
  2. package/bin/create-next-imagicma.mjs +220 -0
  3. package/package.json +19 -0
  4. package/template/.env.example +10 -0
  5. package/template/AGENTS.md +146 -0
  6. package/template/README.md +36 -0
  7. package/template/app/_components/DevPreviewShield.tsx +638 -0
  8. package/template/app/api/greeting/route.ts +27 -0
  9. package/template/app/error.tsx +93 -0
  10. package/template/app/favicon.ico +0 -0
  11. package/template/app/globals.css +145 -0
  12. package/template/app/hello/_components/HelloClient.tsx +94 -0
  13. package/template/app/hello/page.tsx +23 -0
  14. package/template/app/layout.tsx +29 -0
  15. package/template/app/page.tsx +49 -0
  16. package/template/app/providers.tsx +25 -0
  17. package/template/components/ui/accordion.tsx +58 -0
  18. package/template/components/ui/alert-dialog.tsx +141 -0
  19. package/template/components/ui/alert.tsx +61 -0
  20. package/template/components/ui/aspect-ratio.tsx +7 -0
  21. package/template/components/ui/avatar.tsx +51 -0
  22. package/template/components/ui/badge.tsx +40 -0
  23. package/template/components/ui/breadcrumb.tsx +117 -0
  24. package/template/components/ui/button.tsx +64 -0
  25. package/template/components/ui/calendar.tsx +72 -0
  26. package/template/components/ui/card.tsx +87 -0
  27. package/template/components/ui/carousel.tsx +262 -0
  28. package/template/components/ui/chart.tsx +365 -0
  29. package/template/components/ui/checkbox.tsx +30 -0
  30. package/template/components/ui/collapsible.tsx +11 -0
  31. package/template/components/ui/command.tsx +153 -0
  32. package/template/components/ui/context-menu.tsx +200 -0
  33. package/template/components/ui/dialog.tsx +122 -0
  34. package/template/components/ui/drawer.tsx +118 -0
  35. package/template/components/ui/dropdown-menu.tsx +200 -0
  36. package/template/components/ui/form.tsx +178 -0
  37. package/template/components/ui/hover-card.tsx +29 -0
  38. package/template/components/ui/input-otp.tsx +71 -0
  39. package/template/components/ui/input.tsx +25 -0
  40. package/template/components/ui/label.tsx +26 -0
  41. package/template/components/ui/menubar.tsx +256 -0
  42. package/template/components/ui/navigation-menu.tsx +130 -0
  43. package/template/components/ui/pagination.tsx +119 -0
  44. package/template/components/ui/popover.tsx +31 -0
  45. package/template/components/ui/progress.tsx +28 -0
  46. package/template/components/ui/radio-group.tsx +44 -0
  47. package/template/components/ui/resizable.tsx +45 -0
  48. package/template/components/ui/scroll-area.tsx +48 -0
  49. package/template/components/ui/select.tsx +160 -0
  50. package/template/components/ui/separator.tsx +31 -0
  51. package/template/components/ui/sheet.tsx +140 -0
  52. package/template/components/ui/sidebar.tsx +732 -0
  53. package/template/components/ui/skeleton.tsx +17 -0
  54. package/template/components/ui/slider.tsx +28 -0
  55. package/template/components/ui/switch.tsx +29 -0
  56. package/template/components/ui/table.tsx +119 -0
  57. package/template/components/ui/tabs.tsx +55 -0
  58. package/template/components/ui/textarea.tsx +24 -0
  59. package/template/components/ui/toast.tsx +129 -0
  60. package/template/components/ui/toaster.tsx +35 -0
  61. package/template/components/ui/toggle-group.tsx +61 -0
  62. package/template/components/ui/toggle.tsx +45 -0
  63. package/template/components/ui/tooltip.tsx +30 -0
  64. package/template/drizzle.config.ts +50 -0
  65. package/template/eslint.config.mjs +18 -0
  66. package/template/hooks/use-greeting.ts +15 -0
  67. package/template/hooks/use-mobile.ts +21 -0
  68. package/template/hooks/use-toast.ts +194 -0
  69. package/template/lib/queryClient.ts +59 -0
  70. package/template/lib/utils.ts +6 -0
  71. package/template/next.config.ts +8 -0
  72. package/template/package.json +81 -0
  73. package/template/pnpm-lock.yaml +6937 -0
  74. package/template/postcss.config.mjs +7 -0
  75. package/template/public/file.svg +1 -0
  76. package/template/public/globe.svg +1 -0
  77. package/template/public/next.svg +1 -0
  78. package/template/public/vercel.svg +1 -0
  79. package/template/public/window.svg +1 -0
  80. package/template/server/db.ts +24 -0
  81. package/template/server/storage.ts +41 -0
  82. package/template/shared/routes.ts +13 -0
  83. package/template/shared/schema.ts +17 -0
  84. package/template/tailwind.config.mjs +96 -0
  85. package/template/tsconfig.json +35 -0
  86. package/template/types/pg.d.ts +19 -0
@@ -0,0 +1,638 @@
1
+ "use client";
2
+
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+
5
+ type OverlayKind = "none" | "building" | "error";
6
+ type ErrorSource = "compile" | "server" | "runtime" | "unknown";
7
+
8
+ type ErrorInfo = {
9
+ source: ErrorSource;
10
+ message: string;
11
+ };
12
+
13
+ const SHOW_DELAY_MS = 120;
14
+ const MIN_VISIBLE_MS = 260;
15
+ const TOAST_VISIBLE_MS = 800;
16
+ const MAX_ERROR_LINES = 3;
17
+
18
+ function stripAnsi(input: string) {
19
+ return input.replace(/\u001b\[[0-9;]*m/g, "");
20
+ }
21
+
22
+ function toErrorMessage(value: unknown): string {
23
+ if (typeof value === "string") return value;
24
+ if (value instanceof Error) return value.message || "发生未知错误";
25
+ if (value && typeof value === "object" && "message" in value) {
26
+ const message = (value as { message?: unknown }).message;
27
+ if (typeof message === "string" && message.trim()) return message;
28
+ }
29
+ try {
30
+ return JSON.stringify(value);
31
+ } catch {
32
+ return "发生未知错误";
33
+ }
34
+ }
35
+
36
+ function formatErrorSnippet(message: string) {
37
+ const cleaned = stripAnsi(message).trim();
38
+ const lines = cleaned.split(/\r?\n/).filter(Boolean);
39
+ return lines.slice(0, MAX_ERROR_LINES).join("\n");
40
+ }
41
+
42
+ function getHmrWebSocketUrl() {
43
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
44
+ const host = window.location.host;
45
+ return `${protocol}//${host}/_next/webpack-hmr`;
46
+ }
47
+
48
+ function UILoader() {
49
+ return (
50
+ <div className="relative w-24 h-20 bg-white/90 dark:bg-zinc-800 rounded-lg shadow-xl border border-zinc-200 dark:border-zinc-700 overflow-hidden flex flex-col p-2 gap-2">
51
+ {/* Header simulate */}
52
+ <div className="flex gap-1 mb-1">
53
+ <div className="w-2 h-2 rounded-full bg-red-400" />
54
+ <div className="w-2 h-2 rounded-full bg-yellow-400" />
55
+ <div className="w-2 h-2 rounded-full bg-green-400" />
56
+ </div>
57
+ {/* Content lines simulate */}
58
+ <div className="w-full h-2 bg-zinc-200 dark:bg-zinc-700 rounded animate-[pulse_1s_ease-in-out_infinite]" />
59
+ <div className="w-3/4 h-2 bg-indigo-100 dark:bg-indigo-900/50 rounded animate-[pulse_1.5s_ease-in-out_infinite]" />
60
+ <div className="w-1/2 h-2 bg-zinc-200 dark:bg-zinc-700 rounded animate-[pulse_2s_ease-in-out_infinite]" />
61
+ {/* Shimmer effect overlay */}
62
+ <div className="absolute inset-0 -translate-x-full animate-[shimmer_1.5s_infinite] bg-gradient-to-r from-transparent via-white/40 to-transparent" />
63
+ <style
64
+ dangerouslySetInnerHTML={{
65
+ __html: `@keyframes shimmer { 100% { transform: translateX(100%); } }`,
66
+ }}
67
+ />
68
+ </div>
69
+ );
70
+ }
71
+
72
+ function RefreshCwIcon({ className }: { className?: string }) {
73
+ return (
74
+ <svg
75
+ xmlns="http://www.w3.org/2000/svg"
76
+ width="12"
77
+ height="12"
78
+ viewBox="0 0 24 24"
79
+ fill="none"
80
+ stroke="currentColor"
81
+ strokeWidth="2"
82
+ strokeLinecap="round"
83
+ strokeLinejoin="round"
84
+ className={className}
85
+ aria-hidden
86
+ >
87
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
88
+ <path d="M3 3v5h5" />
89
+ </svg>
90
+ );
91
+ }
92
+
93
+ const HMR_EXIT_DURATION_MS = 400;
94
+
95
+ function HMRStatusOverlay({
96
+ isVisible,
97
+ message = "正在更新预览…",
98
+ }: {
99
+ isVisible: boolean;
100
+ message?: string;
101
+ }) {
102
+ const exitUntilRef = useRef<number | null>(null);
103
+ const prevVisibleRef = useRef(false);
104
+ const [, setTick] = useState(0);
105
+
106
+ if (isVisible) {
107
+ prevVisibleRef.current = true;
108
+ exitUntilRef.current = null;
109
+ } else {
110
+ if (prevVisibleRef.current && exitUntilRef.current === null) {
111
+ // eslint-disable-next-line react-hooks/purity -- intentional ref mutation for exit timing
112
+ exitUntilRef.current = Date.now() + HMR_EXIT_DURATION_MS;
113
+ }
114
+
115
+ prevVisibleRef.current = false;
116
+ }
117
+
118
+ useEffect(() => {
119
+ if (isVisible || exitUntilRef.current === null) return;
120
+ const timer = setTimeout(
121
+ () => setTick((n) => n + 1),
122
+ HMR_EXIT_DURATION_MS
123
+ );
124
+ return () => clearTimeout(timer);
125
+ }, [isVisible]);
126
+ // eslint-disable-next-line react-hooks/purity -- intentional ref mutation for exit timing
127
+ const shouldRender =isVisible || (exitUntilRef.current !== null && Date.now() < exitUntilRef.current);
128
+ if (!shouldRender) {
129
+ exitUntilRef.current = null;
130
+ return null;
131
+ }
132
+
133
+
134
+ return (
135
+ <div
136
+ className={`fixed inset-0 z-[9999] flex flex-col items-center justify-center transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] ${
137
+ isVisible
138
+ ? "opacity-100 backdrop-blur-xl bg-white/60 dark:bg-black/60"
139
+ : "opacity-0 backdrop-blur-none pointer-events-none"
140
+ }`}
141
+ >
142
+ <div
143
+ className={`transform transition-all duration-500 ${
144
+ isVisible ? "scale-100 translate-y-0" : "scale-95 translate-y-4"
145
+ }`}
146
+ >
147
+ <div className="flex flex-col items-center">
148
+ <div className="mb-6 p-6 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl ring-1 ring-black/5 dark:ring-white/10 relative overflow-hidden">
149
+ <div className="absolute inset-0 bg-gradient-to-tr from-indigo-500/10 via-purple-500/10 to-pink-500/10 opacity-50" />
150
+ <div className="relative z-10">
151
+ <UILoader />
152
+ </div>
153
+ </div>
154
+ <div className="flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-zinc-800/80 rounded-full border border-zinc-200 dark:border-zinc-700 shadow-sm backdrop-blur-md">
155
+ <RefreshCwIcon className="text-indigo-500 animate-spin shrink-0" />
156
+ <span className="text-sm font-medium text-zinc-600 dark:text-zinc-300 font-mono tracking-tight">
157
+ {message}
158
+ </span>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ class DevRuntimeErrorBoundary extends React.PureComponent<
167
+ {
168
+ children: React.ReactNode;
169
+ resetKey: number;
170
+ onRuntimeError: (error: unknown) => void;
171
+ },
172
+ { hasError: boolean }
173
+ > {
174
+ static getDerivedStateFromError() {
175
+ return { hasError: true };
176
+ }
177
+
178
+ componentDidCatch(error: unknown) {
179
+ this.props.onRuntimeError(error);
180
+ }
181
+
182
+ componentDidUpdate(prevProps: Readonly<{ resetKey: number }>) {
183
+ if (prevProps.resetKey !== this.props.resetKey && this.state.hasError) {
184
+ this.setState({ hasError: false });
185
+ }
186
+ }
187
+
188
+ state = { hasError: false };
189
+
190
+ render() {
191
+ if (this.state.hasError) return null;
192
+ return this.props.children;
193
+ }
194
+ }
195
+
196
+ export function DevPreviewShield({ children }: { children: React.ReactNode }) {
197
+ const [overlay, setOverlay] = useState<OverlayKind>("none");
198
+ const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null);
199
+ const [debugMode, setDebugMode] = useState(false);
200
+ const [toastVisible, setToastVisible] = useState(false);
201
+ const [resetKey, setResetKey] = useState(0);
202
+
203
+ const overlayRef = useRef<OverlayKind>("none");
204
+ const debugModeRef = useRef(false);
205
+ const visibleSinceRef = useRef<number | null>(null);
206
+
207
+ const showTimerRef = useRef<number | null>(null);
208
+ const hideTimerRef = useRef<number | null>(null);
209
+ const toastTimerRef = useRef<number | null>(null);
210
+
211
+ useEffect(() => {
212
+ overlayRef.current = overlay;
213
+ }, [overlay]);
214
+
215
+ useEffect(() => {
216
+ debugModeRef.current = debugMode;
217
+ }, [debugMode]);
218
+
219
+ const setPreviewShieldAttr = useCallback((on: boolean) => {
220
+ if (on) {
221
+ document.documentElement.dataset.previewShield = "on";
222
+ return;
223
+ }
224
+ delete document.documentElement.dataset.previewShield;
225
+ }, []);
226
+
227
+ const shouldRenderOverlay = overlay !== "none" && !debugMode;
228
+
229
+ useEffect(() => {
230
+ setPreviewShieldAttr(shouldRenderOverlay);
231
+ return () => setPreviewShieldAttr(false);
232
+ }, [setPreviewShieldAttr, shouldRenderOverlay]);
233
+
234
+ useEffect(() => {
235
+ if (!shouldRenderOverlay) return;
236
+ if (overlayRef.current === "none") return;
237
+ if (visibleSinceRef.current == null) visibleSinceRef.current = Date.now();
238
+ }, [shouldRenderOverlay]);
239
+
240
+ const clearTimer = useCallback((ref: React.MutableRefObject<number | null>) => {
241
+ if (ref.current == null) return;
242
+ window.clearTimeout(ref.current);
243
+ ref.current = null;
244
+ }, []);
245
+
246
+ const showToast = useCallback(() => {
247
+ clearTimer(toastTimerRef);
248
+ setToastVisible(true);
249
+ toastTimerRef.current = window.setTimeout(() => {
250
+ toastTimerRef.current = null;
251
+ setToastVisible(false);
252
+ }, TOAST_VISIBLE_MS);
253
+ }, [clearTimer]);
254
+
255
+ const showBuildingOverlay = useCallback(() => {
256
+ setErrorInfo(null);
257
+ setOverlay("building");
258
+ visibleSinceRef.current = Date.now();
259
+ }, []);
260
+
261
+ const enterBuilding = useCallback(() => {
262
+ clearTimer(hideTimerRef);
263
+ clearTimer(toastTimerRef);
264
+
265
+ if (overlayRef.current !== "none") {
266
+ showBuildingOverlay();
267
+ return;
268
+ }
269
+
270
+ if (showTimerRef.current != null) return;
271
+ showTimerRef.current = window.setTimeout(() => {
272
+ showTimerRef.current = null;
273
+ showBuildingOverlay();
274
+ }, SHOW_DELAY_MS);
275
+ }, [clearTimer, showBuildingOverlay]);
276
+
277
+ const enterError = useCallback(
278
+ (message: string, source: ErrorSource) => {
279
+ clearTimer(showTimerRef);
280
+ clearTimer(hideTimerRef);
281
+
282
+ setOverlay("error");
283
+ setErrorInfo({ source, message: formatErrorSnippet(message) });
284
+ if (visibleSinceRef.current == null) visibleSinceRef.current = Date.now();
285
+ },
286
+ [clearTimer]
287
+ );
288
+
289
+ const hideOverlayWithMinVisible = useCallback(() => {
290
+ clearTimer(showTimerRef);
291
+
292
+ const since = visibleSinceRef.current;
293
+ const elapsed = since == null ? MIN_VISIBLE_MS : Date.now() - since;
294
+ const remaining = Math.max(0, MIN_VISIBLE_MS - elapsed);
295
+
296
+ const doHide = () => {
297
+ hideTimerRef.current = null;
298
+ setOverlay("none");
299
+ setErrorInfo(null);
300
+ visibleSinceRef.current = null;
301
+ };
302
+
303
+ if (remaining === 0) {
304
+ doHide();
305
+ return;
306
+ }
307
+ clearTimer(hideTimerRef);
308
+ hideTimerRef.current = window.setTimeout(doHide, remaining);
309
+ }, [clearTimer]);
310
+
311
+ const handleBuildOk = useCallback(() => {
312
+ const hadPendingShow = showTimerRef.current != null;
313
+
314
+ clearTimer(showTimerRef);
315
+ clearTimer(hideTimerRef);
316
+
317
+ setDebugMode(false);
318
+ setResetKey((k) => k + 1);
319
+
320
+ if (hadPendingShow && !debugModeRef.current) {
321
+ showToast();
322
+ }
323
+
324
+ if (overlayRef.current === "none") {
325
+ setErrorInfo(null);
326
+ return;
327
+ }
328
+
329
+ hideOverlayWithMinVisible();
330
+ }, [clearTimer, hideOverlayWithMinVisible, showToast]);
331
+
332
+ const handleBuildMessage = useCallback(
333
+ (msg: unknown) => {
334
+ if (!msg || typeof msg !== "object") return;
335
+ const type = (msg as { type?: unknown }).type;
336
+ if (type === "building") {
337
+ enterBuilding();
338
+ return;
339
+ }
340
+
341
+ if (type === "built" || type === "sync") {
342
+ const errors = (msg as { errors?: unknown }).errors;
343
+ if (Array.isArray(errors) && errors.length > 0) {
344
+ const first = errors[0];
345
+ enterError(
346
+ typeof first === "string" ? first : toErrorMessage(first),
347
+ "compile"
348
+ );
349
+ return;
350
+ }
351
+ handleBuildOk();
352
+ return;
353
+ }
354
+
355
+ if (type === "serverError") {
356
+ const errorJSON = (msg as { errorJSON?: unknown }).errorJSON;
357
+ if (typeof errorJSON === "string" && errorJSON) {
358
+ try {
359
+ const parsed = JSON.parse(errorJSON) as { message?: unknown };
360
+ enterError(toErrorMessage(parsed), "server");
361
+ return;
362
+ } catch {
363
+ enterError(errorJSON, "server");
364
+ return;
365
+ }
366
+ }
367
+ enterError("服务器发生错误。", "server");
368
+ }
369
+ },
370
+ [enterBuilding, enterError, handleBuildOk]
371
+ );
372
+
373
+ const onRuntimeError = useCallback(
374
+ (error: unknown) => {
375
+ enterError(toErrorMessage(error), "runtime");
376
+ },
377
+ [enterError]
378
+ );
379
+
380
+ const shortcutLabel = useMemo(() => {
381
+ const isMac =
382
+ typeof navigator !== "undefined" &&
383
+ /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
384
+ return isMac ? "⌘⇧D" : "Ctrl⇧D";
385
+ }, []);
386
+
387
+ useEffect(() => {
388
+ const onKeyDown = (e: KeyboardEvent) => {
389
+ const key = e.key.toLowerCase();
390
+ if (!e.shiftKey || key !== "d") return;
391
+ if (!(e.ctrlKey || e.metaKey)) return;
392
+ e.preventDefault();
393
+ setDebugMode((v) => !v);
394
+ };
395
+ window.addEventListener("keydown", onKeyDown);
396
+ return () => window.removeEventListener("keydown", onKeyDown);
397
+ }, []);
398
+
399
+ useEffect(() => {
400
+ const onError = (e: ErrorEvent) => {
401
+ if (e.error) {
402
+ enterError(toErrorMessage(e.error), "runtime");
403
+ } else if (e.message) {
404
+ enterError(e.message, "runtime");
405
+ } else {
406
+ enterError("发生运行时错误。", "runtime");
407
+ }
408
+ };
409
+ const onRejection = (e: PromiseRejectionEvent) => {
410
+ enterError(toErrorMessage(e.reason), "runtime");
411
+ };
412
+ window.addEventListener("error", onError);
413
+ window.addEventListener("unhandledrejection", onRejection);
414
+ return () => {
415
+ window.removeEventListener("error", onError);
416
+ window.removeEventListener("unhandledrejection", onRejection);
417
+ };
418
+ }, [enterError]);
419
+
420
+ useEffect(() => {
421
+ let alive = true;
422
+ let ws: WebSocket | null = null;
423
+ let retryCount = 0;
424
+ let retryTimer: number | null = null;
425
+
426
+ const connect = () => {
427
+ if (!alive) return;
428
+ if (retryTimer != null) {
429
+ window.clearTimeout(retryTimer);
430
+ retryTimer = null;
431
+ }
432
+
433
+ const url = getHmrWebSocketUrl();
434
+ try {
435
+ ws = new WebSocket(url);
436
+ ws.binaryType = "arraybuffer";
437
+ } catch {
438
+ return;
439
+ }
440
+
441
+ ws.onmessage = (event) => {
442
+ if (!alive) return;
443
+
444
+ if (event.data instanceof ArrayBuffer) {
445
+ try {
446
+ const t = new DataView(event.data).getUint8(0);
447
+ if (t === 1) {
448
+ enterError("检测到编译错误。", "compile");
449
+ } else {
450
+ enterError("检测到错误。", "unknown");
451
+ }
452
+ } catch {
453
+ enterError("检测到错误。", "unknown");
454
+ }
455
+ return;
456
+ }
457
+
458
+ if (typeof event.data !== "string") return;
459
+ try {
460
+ const msg = JSON.parse(event.data) as unknown;
461
+ handleBuildMessage(msg);
462
+ } catch {
463
+ // ignore non-JSON messages
464
+ }
465
+ };
466
+
467
+ ws.onopen = () => {
468
+ retryCount = 0;
469
+ };
470
+
471
+ ws.onclose = () => {
472
+ if (!alive) return;
473
+ retryCount += 1;
474
+ const backoffMs = Math.min(5000, 250 * retryCount);
475
+ retryTimer = window.setTimeout(connect, backoffMs);
476
+ };
477
+
478
+ ws.onerror = () => {
479
+ // let onclose handle reconnection
480
+ };
481
+ };
482
+
483
+ connect();
484
+
485
+ return () => {
486
+ alive = false;
487
+ if (retryTimer != null) window.clearTimeout(retryTimer);
488
+ if (ws) ws.close();
489
+ clearTimer(showTimerRef);
490
+ clearTimer(hideTimerRef);
491
+ clearTimer(toastTimerRef);
492
+ setPreviewShieldAttr(false);
493
+ };
494
+ }, [
495
+ clearTimer,
496
+ enterError,
497
+ handleBuildMessage,
498
+ setPreviewShieldAttr,
499
+ enterBuilding,
500
+ ]);
501
+
502
+ const title = overlay === "building" ? "正在更新预览…" : "预览暂时不可用";
503
+ const description =
504
+ overlay === "building"
505
+ ? "请稍候,改动即将生效。"
506
+ : "检测到错误,修复后将自动恢复。";
507
+
508
+ return (
509
+ <>
510
+ <DevRuntimeErrorBoundary resetKey={resetKey} onRuntimeError={onRuntimeError}>
511
+ {children}
512
+ </DevRuntimeErrorBoundary>
513
+
514
+ {!debugMode && toastVisible ? (
515
+ <div className="fixed left-1/2 top-6 z-[2147483646] -translate-x-1/2">
516
+ <div className="flex items-center gap-2 rounded-full border border-black/10 bg-white/80 px-4 py-2 text-xs font-medium text-black/70 shadow-lg backdrop-blur-md dark:border-white/10 dark:bg-zinc-950/60 dark:text-white/80">
517
+ <svg
518
+ viewBox="0 0 20 20"
519
+ className="h-4 w-4 text-emerald-600 dark:text-emerald-400"
520
+ fill="none"
521
+ aria-hidden="true"
522
+ >
523
+ <path
524
+ d="M16.25 5.75 8.25 13.75 3.75 9.25"
525
+ stroke="currentColor"
526
+ strokeWidth="2"
527
+ strokeLinecap="round"
528
+ strokeLinejoin="round"
529
+ />
530
+ </svg>
531
+ <span>已更新</span>
532
+ </div>
533
+ </div>
534
+ ) : null}
535
+
536
+ <HMRStatusOverlay
537
+ isVisible={overlay === "building"}
538
+ message="正在更新预览…"
539
+ />
540
+
541
+ {shouldRenderOverlay && overlay === "error" ? (
542
+ <div
543
+ className="fixed inset-0 z-[2147483646] flex items-center justify-center bg-white/70 px-6 py-16 backdrop-blur-md dark:bg-black/40"
544
+ role="alertdialog"
545
+ aria-modal="true"
546
+ >
547
+ <div className="w-full max-w-2xl">
548
+ <div className="mx-auto w-full max-w-xl rounded-3xl border border-black/10 bg-white/80 shadow-[0_24px_80px_-40px_rgba(0,0,0,0.45)] backdrop-blur-xl dark:border-white/10 dark:bg-zinc-950/60">
549
+ <div className="flex items-center justify-between px-5 py-4">
550
+ <div className="flex items-center gap-1.5">
551
+ <span className="h-2.5 w-2.5 rounded-full bg-red-400/90" />
552
+ <span className="h-2.5 w-2.5 rounded-full bg-amber-400/90" />
553
+ <span className="h-2.5 w-2.5 rounded-full bg-emerald-400/90" />
554
+ </div>
555
+ <div className="flex items-center gap-2 rounded-full border border-black/10 bg-black/[0.03] px-3 py-1 text-xs font-medium text-black/60 shadow-sm dark:border-white/10 dark:bg-white/5 dark:text-white/70">
556
+ <span className="h-1.5 w-1.5 rounded-full bg-rose-400" />
557
+ <span>Error</span>
558
+ </div>
559
+ <div className="w-10" />
560
+ </div>
561
+
562
+ <div className="px-6 pb-8 pt-2">
563
+ <div className="rounded-2xl border border-black/5 bg-white/60 p-6 shadow-sm dark:border-white/10 dark:bg-white/5">
564
+ <div className="flex items-center justify-between">
565
+ <div className="h-3 w-28 rounded-full bg-black/10 dark:bg-white/10" />
566
+ <div className="h-3 w-10 rounded-full bg-black/10 dark:bg-white/10" />
567
+ </div>
568
+ {errorInfo?.message ? (
569
+ <div className="mt-6">
570
+ <div className="mb-3 text-xs font-medium text-black/60 dark:text-white/70">
571
+ {errorInfo.source === "compile"
572
+ ? "编译错误"
573
+ : errorInfo.source === "server"
574
+ ? "服务端错误"
575
+ : errorInfo.source === "runtime"
576
+ ? "运行时错误"
577
+ : "错误"}
578
+ </div>
579
+ <pre className="w-full max-h-44 overflow-auto rounded-2xl border border-black/5 bg-white/70 p-4 text-left text-sm leading-6 text-black/80 shadow-inner dark:border-white/10 dark:bg-zinc-950/40 dark:text-white/85">
580
+ <code>{errorInfo.message}</code>
581
+ </pre>
582
+ </div>
583
+ ) : null}
584
+ </div>
585
+ </div>
586
+ </div>
587
+
588
+ <div className="mt-10 text-center">
589
+ <h2 className="text-2xl font-semibold tracking-tight text-black/90 dark:text-white">
590
+ {title}
591
+ </h2>
592
+ <p className="mt-3 text-sm leading-6 text-black/60 dark:text-white/70">
593
+ {description}
594
+ </p>
595
+
596
+ <div className="mt-8 flex w-full flex-col items-center gap-3 sm:flex-row sm:justify-center">
597
+ <button
598
+ type="button"
599
+ className="inline-flex h-11 w-full items-center justify-center rounded-full bg-black px-5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-black/90 sm:w-auto dark:bg-white dark:text-black dark:hover:bg-white/90"
600
+ onClick={() => window.location.reload()}
601
+ >
602
+ 刷新预览
603
+ </button>
604
+ <button
605
+ type="button"
606
+ className="inline-flex h-11 w-full items-center justify-center rounded-full border border-black/10 bg-white/70 px-5 text-sm font-medium text-black/80 shadow-sm transition-colors hover:bg-white sm:w-auto dark:border-white/10 dark:bg-zinc-950/40 dark:text-white/80 dark:hover:bg-zinc-950/60"
607
+ onClick={() => setDebugMode(true)}
608
+ >
609
+ 查看错误详情
610
+ </button>
611
+ </div>
612
+
613
+ <p className="mt-6 text-xs text-black/45 dark:text-white/55">
614
+ 快捷键切换遮罩 / Next 面板:{shortcutLabel}
615
+ </p>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ ) : null}
620
+
621
+ {debugMode && overlay !== "none" ? (
622
+ <div className="fixed bottom-4 right-4 z-[2147483647]">
623
+ <button
624
+ type="button"
625
+ className="inline-flex items-center gap-2 rounded-full border border-black/10 bg-white/80 px-4 py-2 text-xs font-medium text-black/70 shadow-lg backdrop-blur-md transition-colors hover:bg-white dark:border-white/10 dark:bg-zinc-950/60 dark:text-white/80 dark:hover:bg-zinc-950/70"
626
+ onClick={() => setDebugMode(false)}
627
+ title={`返回预览遮罩(${shortcutLabel})`}
628
+ >
629
+ 返回预览遮罩
630
+ <span className="rounded-full border border-black/10 bg-black/[0.03] px-2 py-0.5 text-[10px] text-black/55 dark:border-white/10 dark:bg-white/5 dark:text-white/70">
631
+ {shortcutLabel}
632
+ </span>
633
+ </button>
634
+ </div>
635
+ ) : null}
636
+ </>
637
+ );
638
+ }
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from "next/server";
2
+ import { api } from "@shared/routes";
3
+ import { storage } from "@/server/storage";
4
+
5
+ export const runtime = "nodejs";
6
+ export const dynamic = "force-dynamic";
7
+
8
+ export async function GET() {
9
+ try {
10
+ const message = await storage.getMessage();
11
+ const body = { message };
12
+
13
+ // Keep server/client contract in sync.
14
+ api.greeting.get.responses[200].parse(body);
15
+
16
+ return NextResponse.json(body);
17
+ } catch (error) {
18
+ console.error("GET /api/greeting failed:", error);
19
+
20
+ const message =
21
+ error instanceof Error
22
+ ? error.message
23
+ : "Internal Server Error";
24
+
25
+ return NextResponse.json({ message }, { status: 500 });
26
+ }
27
+ }