blumenjs 0.2.5 → 0.2.6

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.
@@ -838,11 +838,32 @@ function getTemplateFiles(projectName, template) {
838
838
  // Complex files — copied from the framework source
839
839
  ["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
840
840
  ["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
841
+ ["app/shared/ErrorBoundary.tsx", readProjectFile("app/shared/ErrorBoundary.tsx")],
842
+ ["app/shared/BlumenHead.tsx", readProjectFile("app/shared/BlumenHead.tsx")],
843
+ ["app/shared/prefetchCache.ts", readProjectFile("app/shared/prefetchCache.ts")],
844
+ ["app/shared/DefaultLoading.tsx", readProjectFile("app/shared/DefaultLoading.tsx")],
845
+ ["app/shared/serverAction.ts", readProjectFile("app/shared/serverAction.ts")],
846
+ ["app/shared/types.ts", readProjectFile("app/shared/types.ts")],
847
+ ["app/shared/useServerAction.ts", readProjectFile("app/shared/useServerAction.ts")],
848
+ ["app/shared/api.ts", readProjectFile("app/shared/api.ts")],
849
+ ["app/shared/useWebSocket.ts", readProjectFile("app/shared/useWebSocket.ts")],
850
+ ["app/shared/i18n.ts", readProjectFile("app/shared/i18n.ts")],
851
+ ["app/shared/blumenConfig.ts", readProjectFile("app/shared/blumenConfig.ts")],
852
+ ["app/shared/BlumenImage.tsx", readProjectFile("app/shared/BlumenImage.tsx")],
841
853
  ["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
842
854
  ["node-ssr/server.ts", readProjectFile("node-ssr/server.ts")],
843
855
  ["go-server/main.go", readProjectFile("go-server/main.go")],
856
+ ["go-server/image.go", readProjectFile("go-server/image.go")],
857
+ ["go-server/cache.go", readProjectFile("go-server/cache.go")],
858
+ ["go-server/websocket.go", readProjectFile("go-server/websocket.go")],
859
+ ["go-server/middleware.go", readProjectFile("go-server/middleware.go")],
860
+ ["go-server/ssg.go", readProjectFile("go-server/ssg.go")],
861
+ ["go-server/redirects.go", readProjectFile("go-server/redirects.go")],
862
+ ["go-server/actions.go", readProjectFile("go-server/actions.go")],
844
863
  ["scripts/generate-routes.ts", readProjectFile("scripts/generate-routes.ts")],
845
864
  ["scripts/generate-api-routes.ts", readProjectFile("scripts/generate-api-routes.ts")],
865
+ ["go.mod", readProjectFile("go.mod")],
866
+ ["go.sum", readProjectFile("go.sum")],
846
867
  // Placeholder
847
868
  ["static/js/.gitkeep", ""],
848
869
  // Docker support (production deployment)
@@ -522,11 +522,32 @@ function getTemplateFiles(projectName, template) {
522
522
  // Complex files — copied from the framework source
523
523
  ["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
524
524
  ["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
525
+ ["app/shared/ErrorBoundary.tsx", readProjectFile("app/shared/ErrorBoundary.tsx")],
526
+ ["app/shared/BlumenHead.tsx", readProjectFile("app/shared/BlumenHead.tsx")],
527
+ ["app/shared/prefetchCache.ts", readProjectFile("app/shared/prefetchCache.ts")],
528
+ ["app/shared/DefaultLoading.tsx", readProjectFile("app/shared/DefaultLoading.tsx")],
529
+ ["app/shared/serverAction.ts", readProjectFile("app/shared/serverAction.ts")],
530
+ ["app/shared/types.ts", readProjectFile("app/shared/types.ts")],
531
+ ["app/shared/useServerAction.ts", readProjectFile("app/shared/useServerAction.ts")],
532
+ ["app/shared/api.ts", readProjectFile("app/shared/api.ts")],
533
+ ["app/shared/useWebSocket.ts", readProjectFile("app/shared/useWebSocket.ts")],
534
+ ["app/shared/i18n.ts", readProjectFile("app/shared/i18n.ts")],
535
+ ["app/shared/blumenConfig.ts", readProjectFile("app/shared/blumenConfig.ts")],
536
+ ["app/shared/BlumenImage.tsx", readProjectFile("app/shared/BlumenImage.tsx")],
525
537
  ["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
526
538
  ["node-ssr/server.ts", readProjectFile("node-ssr/server.ts")],
527
539
  ["go-server/main.go", readProjectFile("go-server/main.go")],
540
+ ["go-server/image.go", readProjectFile("go-server/image.go")],
541
+ ["go-server/cache.go", readProjectFile("go-server/cache.go")],
542
+ ["go-server/websocket.go", readProjectFile("go-server/websocket.go")],
543
+ ["go-server/middleware.go", readProjectFile("go-server/middleware.go")],
544
+ ["go-server/ssg.go", readProjectFile("go-server/ssg.go")],
545
+ ["go-server/redirects.go", readProjectFile("go-server/redirects.go")],
546
+ ["go-server/actions.go", readProjectFile("go-server/actions.go")],
528
547
  ["scripts/generate-routes.ts", readProjectFile("scripts/generate-routes.ts")],
529
548
  ["scripts/generate-api-routes.ts", readProjectFile("scripts/generate-api-routes.ts")],
549
+ ["go.mod", readProjectFile("go.mod")],
550
+ ["go.sum", readProjectFile("go.sum")],
530
551
  // Placeholder
531
552
  ["static/js/.gitkeep", ""],
532
553
  // Docker support (production deployment)
@@ -0,0 +1,157 @@
1
+ /**
2
+ * BlumenHead — Client-side <head> tag manager.
3
+ *
4
+ * Updates document.title and manages <meta> tags on the client.
5
+ * Used internally by the router for SPA navigations, and available
6
+ * to developers as an escape hatch for imperative head control.
7
+ *
8
+ * On the server, this component renders nothing — the Document
9
+ * component handles <head> rendering during SSR.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * import { BlumenHead } from '../shared/BlumenHead';
14
+ *
15
+ * export default function MyPage() {
16
+ * return (
17
+ * <>
18
+ * <BlumenHead
19
+ * title="My Page | My App"
20
+ * description="A great page"
21
+ * />
22
+ * <div>Page content</div>
23
+ * </>
24
+ * );
25
+ * }
26
+ * ```
27
+ */
28
+
29
+ import React, { useEffect } from "react";
30
+ import type { BlumenMetadata } from "./types";
31
+
32
+ // Marker attribute to identify tags managed by BlumenHead
33
+ const BLUMEN_HEAD_ATTR = "data-blumen-head";
34
+
35
+ /**
36
+ * Apply metadata to the document <head>.
37
+ * Can be called from React effects or from the router.
38
+ */
39
+ export function applyMetadata(metadata: BlumenMetadata): void {
40
+ if (typeof document === "undefined") return;
41
+
42
+ // Update title
43
+ if (metadata.title) {
44
+ document.title = metadata.title;
45
+ }
46
+
47
+ // Remove all previously managed tags
48
+ const existing = document.querySelectorAll(`[${BLUMEN_HEAD_ATTR}]`);
49
+ existing.forEach((el) => el.remove());
50
+
51
+ const head = document.head;
52
+
53
+ // Helper to add a meta tag
54
+ const addMeta = (name: string, content: string) => {
55
+ const tag = document.createElement("meta");
56
+ tag.setAttribute("name", name);
57
+ tag.setAttribute("content", content);
58
+ tag.setAttribute(BLUMEN_HEAD_ATTR, "true");
59
+ head.appendChild(tag);
60
+ };
61
+
62
+ // Helper to add an OG/Twitter meta tag (uses property instead of name)
63
+ const addProperty = (property: string, content: string) => {
64
+ const tag = document.createElement("meta");
65
+ tag.setAttribute("property", property);
66
+ tag.setAttribute("content", content);
67
+ tag.setAttribute(BLUMEN_HEAD_ATTR, "true");
68
+ head.appendChild(tag);
69
+ };
70
+
71
+ // Description
72
+ if (metadata.description) {
73
+ addMeta("description", metadata.description);
74
+ }
75
+
76
+ // Keywords
77
+ if (metadata.keywords?.length) {
78
+ addMeta("keywords", metadata.keywords.join(", "));
79
+ }
80
+
81
+ // Robots
82
+ if (metadata.robots) {
83
+ addMeta("robots", metadata.robots);
84
+ }
85
+
86
+ // Canonical
87
+ if (metadata.canonical) {
88
+ const link = document.createElement("link");
89
+ link.setAttribute("rel", "canonical");
90
+ link.setAttribute("href", metadata.canonical);
91
+ link.setAttribute(BLUMEN_HEAD_ATTR, "true");
92
+ head.appendChild(link);
93
+ }
94
+
95
+ // Open Graph
96
+ if (metadata.openGraph) {
97
+ const og = metadata.openGraph;
98
+ if (og.title) addProperty("og:title", og.title);
99
+ if (og.description) addProperty("og:description", og.description);
100
+ if (og.image) addProperty("og:image", og.image);
101
+ if (og.url) addProperty("og:url", og.url);
102
+ if (og.type) addProperty("og:type", og.type);
103
+ if (og.siteName) addProperty("og:site_name", og.siteName);
104
+ }
105
+
106
+ // Twitter Card
107
+ if (metadata.twitter) {
108
+ const tw = metadata.twitter;
109
+ if (tw.card) addMeta("twitter:card", tw.card);
110
+ if (tw.title) addMeta("twitter:title", tw.title);
111
+ if (tw.description) addMeta("twitter:description", tw.description);
112
+ if (tw.image) addMeta("twitter:image", tw.image);
113
+ if (tw.creator) addMeta("twitter:creator", tw.creator);
114
+ if (tw.site) addMeta("twitter:site", tw.site);
115
+ }
116
+
117
+ // Custom meta tags
118
+ if (metadata.other) {
119
+ for (const [name, content] of Object.entries(metadata.other)) {
120
+ addMeta(name, content);
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * BlumenHead component — declarative head management.
127
+ *
128
+ * Accepts the same shape as BlumenMetadata. Updates <head> on mount
129
+ * and cleans up on unmount. SSR-safe (renders nothing on the server).
130
+ */
131
+ export function BlumenHead(props: BlumenMetadata) {
132
+ useEffect(() => {
133
+ applyMetadata(props);
134
+
135
+ // Cleanup: remove managed tags when component unmounts
136
+ return () => {
137
+ if (typeof document === "undefined") return;
138
+ const managed = document.querySelectorAll(`[${BLUMEN_HEAD_ATTR}]`);
139
+ managed.forEach((el) => el.remove());
140
+ };
141
+ }, [
142
+ props.title,
143
+ props.description,
144
+ props.canonical,
145
+ props.robots,
146
+ props.keywords?.join(","),
147
+ props.openGraph?.title,
148
+ props.openGraph?.description,
149
+ props.openGraph?.image,
150
+ props.twitter?.card,
151
+ props.twitter?.title,
152
+ props.twitter?.image,
153
+ ]);
154
+
155
+ // Render nothing — head mutations are side effects
156
+ return null;
157
+ }
@@ -0,0 +1,209 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+
3
+ /**
4
+ * BlumenImage - Optimized image component for BlumenJS.
5
+ *
6
+ * Features:
7
+ * - Automatic responsive srcset generation
8
+ * - Lazy loading by default (disable with `priority`)
9
+ * - WebP/AVIF format negotiation via the Go image endpoint
10
+ * - Blur placeholder (LQIP) fade-in effect
11
+ * - On-the-fly server-side resizing and caching
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { BlumenImage } from '../../shared/BlumenImage';
16
+ *
17
+ * <BlumenImage
18
+ * src="/static/hero.jpg"
19
+ * alt="Hero banner"
20
+ * width={1200}
21
+ * height={600}
22
+ * quality={80}
23
+ * priority // Above-the-fold: skip lazy loading
24
+ * />
25
+ * ```
26
+ */
27
+
28
+ // Default responsive breakpoints (matches common device widths)
29
+ const DEFAULT_WIDTHS = [640, 750, 828, 1080, 1200, 1920, 2048];
30
+
31
+ interface BlumenImageProps {
32
+ /** Source path relative to the project root (e.g. "/static/hero.jpg") */
33
+ src: string;
34
+ /** Alt text for accessibility */
35
+ alt: string;
36
+ /** Display width in pixels */
37
+ width: number;
38
+ /** Display height in pixels */
39
+ height: number;
40
+ /** Image quality (1-100, default: 80) */
41
+ quality?: number;
42
+ /** If true, disables lazy loading and loads immediately (use for above-the-fold images) */
43
+ priority?: boolean;
44
+ /** Object-fit CSS property (default: "cover") */
45
+ fit?: "cover" | "contain" | "fill" | "none";
46
+ /** Additional CSS class name */
47
+ className?: string;
48
+ /** Custom responsive widths for srcset generation */
49
+ sizes?: string;
50
+ /** Inline style overrides */
51
+ style?: React.CSSProperties;
52
+ /** Border radius */
53
+ borderRadius?: string | number;
54
+ }
55
+
56
+ /**
57
+ * Build the optimized image URL using the Go /_blumen/image endpoint.
58
+ */
59
+ function buildImageUrl(
60
+ src: string,
61
+ width: number,
62
+ quality: number,
63
+ ): string {
64
+ // If src is already an external URL, return as-is
65
+ if (src.startsWith("http://") || src.startsWith("https://")) {
66
+ return src;
67
+ }
68
+ return `/_blumen/image?src=${encodeURIComponent(src)}&w=${width}&q=${quality}`;
69
+ }
70
+
71
+ /**
72
+ * Generate a tiny blur data URI for the placeholder.
73
+ * Uses the Go endpoint with a very small width.
74
+ */
75
+ function buildBlurUrl(src: string): string {
76
+ if (src.startsWith("http://") || src.startsWith("https://")) {
77
+ return src;
78
+ }
79
+ return `/_blumen/image?src=${encodeURIComponent(src)}&w=20&q=30&blur=1`;
80
+ }
81
+
82
+ export function BlumenImage({
83
+ src,
84
+ alt,
85
+ width,
86
+ height,
87
+ quality = 80,
88
+ priority = false,
89
+ fit = "cover",
90
+ className = "",
91
+ sizes,
92
+ style,
93
+ borderRadius,
94
+ }: BlumenImageProps) {
95
+ const [loaded, setLoaded] = useState(false);
96
+ const [error, setError] = useState(false);
97
+ const imgRef = useRef<HTMLImageElement>(null);
98
+
99
+ // Generate the primary optimized URL
100
+ const optimizedSrc = buildImageUrl(src, width, quality);
101
+
102
+ // Generate srcset for responsive images
103
+ const srcsetWidths = DEFAULT_WIDTHS.filter((w) => w <= width * 2);
104
+ if (!srcsetWidths.includes(width)) {
105
+ srcsetWidths.push(width);
106
+ srcsetWidths.sort((a, b) => a - b);
107
+ }
108
+
109
+ const srcset = srcsetWidths
110
+ .map((w) => `${buildImageUrl(src, w, quality)} ${w}w`)
111
+ .join(", ");
112
+
113
+ // Default sizes attribute if not provided
114
+ const defaultSizes =
115
+ sizes || `(max-width: ${width}px) 100vw, ${width}px`;
116
+
117
+ // Blur placeholder URL
118
+ const blurSrc = buildBlurUrl(src);
119
+
120
+ // Check if image is already loaded (SSR or cached)
121
+ useEffect(() => {
122
+ if (imgRef.current?.complete) {
123
+ setLoaded(true);
124
+ }
125
+ }, []);
126
+
127
+ const containerStyle: React.CSSProperties = {
128
+ position: "relative",
129
+ width: `${width}px`,
130
+ maxWidth: "100%",
131
+ aspectRatio: `${width} / ${height}`,
132
+ overflow: "hidden",
133
+ borderRadius: borderRadius || undefined,
134
+ ...style,
135
+ };
136
+
137
+ const imgStyle: React.CSSProperties = {
138
+ display: "block",
139
+ width: "100%",
140
+ height: "100%",
141
+ objectFit: fit,
142
+ opacity: loaded ? 1 : 0,
143
+ transition: "opacity 0.4s ease-in-out",
144
+ };
145
+
146
+ const placeholderStyle: React.CSSProperties = {
147
+ position: "absolute",
148
+ inset: 0,
149
+ width: "100%",
150
+ height: "100%",
151
+ objectFit: fit,
152
+ filter: "blur(20px)",
153
+ transform: "scale(1.1)",
154
+ opacity: loaded ? 0 : 1,
155
+ transition: "opacity 0.4s ease-in-out",
156
+ pointerEvents: "none",
157
+ };
158
+
159
+ if (error) {
160
+ // Fallback: render a standard <img> without optimization
161
+ return (
162
+ <img
163
+ src={src}
164
+ alt={alt}
165
+ width={width}
166
+ height={height}
167
+ className={className}
168
+ style={{ objectFit: fit, maxWidth: "100%", ...style }}
169
+ loading={priority ? "eager" : "lazy"}
170
+ />
171
+ );
172
+ }
173
+
174
+ return (
175
+ <div
176
+ className={`blumen-image ${className}`}
177
+ style={containerStyle}
178
+ >
179
+ {/* Blur placeholder */}
180
+ {!priority && (
181
+ <img
182
+ src={blurSrc}
183
+ alt=""
184
+ aria-hidden="true"
185
+ style={placeholderStyle}
186
+ />
187
+ )}
188
+
189
+ {/* Main optimized image */}
190
+ <img
191
+ ref={imgRef}
192
+ src={optimizedSrc}
193
+ srcSet={srcset}
194
+ sizes={defaultSizes}
195
+ alt={alt}
196
+ width={width}
197
+ height={height}
198
+ loading={priority ? "eager" : "lazy"}
199
+ decoding={priority ? "sync" : "async"}
200
+ fetchPriority={priority ? "high" : "auto"}
201
+ onLoad={() => setLoaded(true)}
202
+ onError={() => setError(true)}
203
+ style={imgStyle}
204
+ />
205
+ </div>
206
+ );
207
+ }
208
+
209
+ export default BlumenImage;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Blumen Default Loading Component
3
+ *
4
+ * A beautiful shimmer/skeleton loading state shown automatically
5
+ * during SPA navigation when the target page has getServerProps
6
+ * and a loading.tsx file.
7
+ *
8
+ * Developers can create their own loading.tsx files per directory
9
+ * for custom loading states, or use this default.
10
+ */
11
+
12
+ import React from "react";
13
+
14
+ const shimmerStyle: React.CSSProperties = {
15
+ background: "linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0) 100%)",
16
+ backgroundSize: "200% 100%",
17
+ animation: "blumen-shimmer 1.5s ease-in-out infinite",
18
+ };
19
+
20
+ const skeletonLine = (width: string, height: string = "14px", mb: string = "12px"): React.CSSProperties => ({
21
+ width,
22
+ height,
23
+ borderRadius: "6px",
24
+ backgroundColor: "rgba(255,255,255,0.06)",
25
+ marginBottom: mb,
26
+ position: "relative" as const,
27
+ overflow: "hidden" as const,
28
+ });
29
+
30
+ export default function DefaultLoading() {
31
+ return (
32
+ <div style={{ padding: "40px 24px", maxWidth: "680px", margin: "0 auto" }}>
33
+ <style>{`
34
+ @keyframes blumen-shimmer {
35
+ 0% { background-position: 200% 0; }
36
+ 100% { background-position: -200% 0; }
37
+ }
38
+ `}</style>
39
+ {/* Title skeleton */}
40
+ <div style={skeletonLine("60%", "28px", "24px")}>
41
+ <div style={{ ...shimmerStyle, position: "absolute", inset: 0 }} />
42
+ </div>
43
+ {/* Subtitle skeleton */}
44
+ <div style={skeletonLine("80%", "16px", "32px")}>
45
+ <div style={{ ...shimmerStyle, position: "absolute", inset: 0 }} />
46
+ </div>
47
+ {/* Content skeletons */}
48
+ <div style={skeletonLine("100%")}>
49
+ <div style={{ ...shimmerStyle, position: "absolute", inset: 0 }} />
50
+ </div>
51
+ <div style={skeletonLine("92%")}>
52
+ <div style={{ ...shimmerStyle, position: "absolute", inset: 0 }} />
53
+ </div>
54
+ <div style={skeletonLine("85%")}>
55
+ <div style={{ ...shimmerStyle, position: "absolute", inset: 0 }} />
56
+ </div>
57
+ <div style={skeletonLine("40%", "14px", "32px")}>
58
+ <div style={{ ...shimmerStyle, position: "absolute", inset: 0 }} />
59
+ </div>
60
+ {/* Card skeletons */}
61
+ <div style={{ display: "flex", gap: "16px", marginTop: "8px" }}>
62
+ {[1, 2, 3].map((i) => (
63
+ <div key={i} style={{
64
+ flex: 1,
65
+ height: "120px",
66
+ borderRadius: "12px",
67
+ backgroundColor: "rgba(255,255,255,0.04)",
68
+ border: "1px solid rgba(255,255,255,0.06)",
69
+ position: "relative",
70
+ overflow: "hidden",
71
+ }}>
72
+ <div style={{ ...shimmerStyle, position: "absolute", inset: 0 }} />
73
+ </div>
74
+ ))}
75
+ </div>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,174 @@
1
+ import React from "react";
2
+
3
+ /**
4
+ * Blumen Error Boundary
5
+ *
6
+ * Wraps the client hydration root to catch React rendering errors
7
+ * and display a user-friendly fallback instead of a white screen.
8
+ *
9
+ * In development, shows the error message and stack trace.
10
+ * In production, shows a clean recovery UI.
11
+ */
12
+
13
+ interface ErrorBoundaryProps {
14
+ children: React.ReactNode;
15
+ }
16
+
17
+ interface ErrorBoundaryState {
18
+ hasError: boolean;
19
+ error: Error | null;
20
+ }
21
+
22
+ export class BlumenErrorBoundary extends React.Component<
23
+ ErrorBoundaryProps,
24
+ ErrorBoundaryState
25
+ > {
26
+ constructor(props: ErrorBoundaryProps) {
27
+ super(props);
28
+ this.state = { hasError: false, error: null };
29
+ }
30
+
31
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
32
+ return { hasError: true, error };
33
+ }
34
+
35
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
36
+ console.error("[Blumen] Client-side rendering error:", error);
37
+ console.error("[Blumen] Component stack:", errorInfo.componentStack);
38
+ }
39
+
40
+ render() {
41
+ if (this.state.hasError) {
42
+ const isDev =
43
+ typeof window !== "undefined" &&
44
+ window.location.hostname === "localhost";
45
+ return (
46
+ <div
47
+ style={{
48
+ minHeight: "100vh",
49
+ display: "flex",
50
+ alignItems: "center",
51
+ justifyContent: "center",
52
+ background: isDev ? "#0d1117" : "#f9fafb",
53
+ fontFamily:
54
+ "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
55
+ padding: "2rem",
56
+ }}
57
+ >
58
+ <div
59
+ style={{
60
+ maxWidth: 560,
61
+ width: "100%",
62
+ textAlign: isDev ? "left" : "center",
63
+ }}
64
+ >
65
+ {isDev ? (
66
+ <>
67
+ <div
68
+ style={{
69
+ color: "#ff6b6b",
70
+ fontSize: "0.75rem",
71
+ fontWeight: 600,
72
+ textTransform: "uppercase",
73
+ letterSpacing: "0.05em",
74
+ marginBottom: 8,
75
+ }}
76
+ >
77
+ ⚠ Client Rendering Error
78
+ </div>
79
+ <div
80
+ style={{
81
+ color: "#e2e8f0",
82
+ fontSize: "1.1rem",
83
+ fontWeight: 600,
84
+ marginBottom: 16,
85
+ }}
86
+ >
87
+ {this.state.error?.message || "An unexpected error occurred"}
88
+ </div>
89
+ <pre
90
+ style={{
91
+ background: "#161b22",
92
+ border: "1px solid rgba(255,255,255,0.08)",
93
+ borderRadius: 8,
94
+ padding: 16,
95
+ fontSize: "0.75rem",
96
+ color: "#8b949e",
97
+ overflow: "auto",
98
+ maxHeight: 300,
99
+ lineHeight: 1.6,
100
+ }}
101
+ >
102
+ {this.state.error?.stack}
103
+ </pre>
104
+ <button
105
+ onClick={() => window.location.reload()}
106
+ style={{
107
+ marginTop: 16,
108
+ padding: "8px 20px",
109
+ background: "#7c3aed",
110
+ color: "#fff",
111
+ border: "none",
112
+ borderRadius: 6,
113
+ fontSize: "0.85rem",
114
+ fontWeight: 600,
115
+ cursor: "pointer",
116
+ }}
117
+ >
118
+ Reload Page
119
+ </button>
120
+ </>
121
+ ) : (
122
+ <>
123
+ <div
124
+ style={{
125
+ fontSize: "3rem",
126
+ marginBottom: 16,
127
+ }}
128
+ >
129
+
130
+ </div>
131
+ <h1
132
+ style={{
133
+ fontSize: "1.5rem",
134
+ fontWeight: 700,
135
+ color: "#1a202c",
136
+ marginBottom: 8,
137
+ }}
138
+ >
139
+ Something went wrong
140
+ </h1>
141
+ <p
142
+ style={{
143
+ color: "#6b7280",
144
+ fontSize: "0.95rem",
145
+ marginBottom: 24,
146
+ }}
147
+ >
148
+ An unexpected error occurred. Please try refreshing the page.
149
+ </p>
150
+ <button
151
+ onClick={() => window.location.reload()}
152
+ style={{
153
+ padding: "10px 24px",
154
+ background: "#7c3aed",
155
+ color: "#fff",
156
+ border: "none",
157
+ borderRadius: 8,
158
+ fontSize: "0.9rem",
159
+ fontWeight: 600,
160
+ cursor: "pointer",
161
+ }}
162
+ >
163
+ Refresh Page
164
+ </button>
165
+ </>
166
+ )}
167
+ </div>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ return this.props.children;
173
+ }
174
+ }