revine 1.1.3 → 1.2.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.
@@ -0,0 +1,269 @@
1
+ import { useEffect, useRef, useState, type ImgHTMLAttributes } from "react";
2
+
3
+ type ObjectFit = "contain" | "cover" | "fill" | "none" | "scale-down";
4
+
5
+ export interface ImageProps extends Omit<
6
+ ImgHTMLAttributes<HTMLImageElement>,
7
+ "src" | "width" | "height" | "placeholder"
8
+ > {
9
+ /** Image source URL or import (e.g. import logo from './logo.png') */
10
+ src: string;
11
+ /** Alt text — required for accessibility */
12
+ alt: string;
13
+ /** Intrinsic width in px. Required unless fill={true} */
14
+ width?: number;
15
+ /** Intrinsic height in px. Required unless fill={true} */
16
+ height?: number;
17
+ /**
18
+ * Stretch the image to fill its parent container (parent must be position:relative).
19
+ * When true, width/height are not required.
20
+ */
21
+ fill?: boolean;
22
+ /** CSS object-fit when fill is true. Defaults to "cover" */
23
+ objectFit?: ObjectFit;
24
+ /** CSS object-position when fill is true. Defaults to "center" */
25
+ objectPosition?: string;
26
+ /**
27
+ * Eagerly load the image and skip lazy loading.
28
+ * Use for above-the-fold images (hero, LCP element).
29
+ */
30
+ priority?: boolean;
31
+ /**
32
+ * Show a blurred low-quality placeholder while the image loads.
33
+ * Pass a base64 data URL or a solid color string like "#e5e7eb".
34
+ * Defaults to a subtle gray shimmer if not provided.
35
+ */
36
+ placeholder?: string;
37
+ /** Called when the image fails to load */
38
+ onError?: () => void;
39
+ /** Custom fallback element shown on error */
40
+ fallback?: React.ReactNode;
41
+ className?: string;
42
+ style?: React.CSSProperties;
43
+ quality?: never; // reserved for future CDN support — not implemented yet
44
+ }
45
+
46
+ const shimmerBase64 =
47
+ "data:image/svg+xml;base64," +
48
+ btoa(`<svg xmlns='http://www.w3.org/2000/svg' width='400' height='300'>
49
+ <defs>
50
+ <linearGradient id='g' x1='0%' y1='0%' x2='100%' y2='0%'>
51
+ <stop offset='0%' stop-color='#e8e8e8'/>
52
+ <stop offset='50%' stop-color='#f0f0f0'/>
53
+ <stop offset='100%' stop-color='#e8e8e8'/>
54
+ </linearGradient>
55
+ </defs>
56
+ <rect width='400' height='300' fill='url(#g)'/>
57
+ </svg>`);
58
+
59
+ export function Image({
60
+ src,
61
+ alt,
62
+ width,
63
+ height,
64
+ fill = false,
65
+ objectFit = "cover",
66
+ objectPosition = "center",
67
+ priority = false,
68
+ placeholder,
69
+ onError,
70
+ fallback,
71
+ className,
72
+ style,
73
+ ...rest
74
+ }: ImageProps) {
75
+ const [loaded, setLoaded] = useState(false);
76
+ const [errored, setErrored] = useState(false);
77
+ const imgRef = useRef<HTMLImageElement>(null);
78
+
79
+ // If image is already cached (e.g. browser back-nav), mark loaded immediately
80
+ useEffect(() => {
81
+ if (imgRef.current?.complete && imgRef.current.naturalWidth > 0) {
82
+ setLoaded(true);
83
+ }
84
+ }, []);
85
+
86
+ const handleError = () => {
87
+ setErrored(true);
88
+ onError?.();
89
+ };
90
+
91
+ const placeholderSrc = placeholder ?? shimmerBase64;
92
+
93
+ // ── Fill mode: stretch to parent container
94
+ if (fill) {
95
+ return (
96
+ <span
97
+ style={{
98
+ position: "absolute",
99
+ inset: 0,
100
+ display: "block",
101
+ overflow: "hidden",
102
+ }}
103
+ >
104
+ {/* Placeholder layer */}
105
+ {!loaded && !errored && (
106
+ <span
107
+ aria-hidden="true"
108
+ style={{
109
+ position: "absolute",
110
+ inset: 0,
111
+ backgroundImage: placeholder
112
+ ? `url(${placeholderSrc})`
113
+ : undefined,
114
+ backgroundColor: placeholder ? undefined : "#e8e8e8",
115
+ backgroundSize: "cover",
116
+ backgroundPosition: "center",
117
+ filter: placeholder ? "blur(8px)" : undefined,
118
+ transform: "scale(1.05)",
119
+ }}
120
+ />
121
+ )}
122
+ {!errored ? (
123
+ <img
124
+ ref={imgRef}
125
+ src={src}
126
+ alt={alt}
127
+ loading={priority ? "eager" : "lazy"}
128
+ decoding={priority ? "sync" : "async"}
129
+ fetchPriority={priority ? "high" : "auto"}
130
+ onLoad={() => setLoaded(true)}
131
+ onError={handleError}
132
+ className={className}
133
+ style={{
134
+ position: "absolute",
135
+ inset: 0,
136
+ width: "100%",
137
+ height: "100%",
138
+ objectFit,
139
+ objectPosition,
140
+ opacity: loaded ? 1 : 0,
141
+ transition: "opacity 300ms ease",
142
+ ...style,
143
+ }}
144
+ {...rest}
145
+ />
146
+ ) : fallback ? (
147
+ <>{fallback}</>
148
+ ) : (
149
+ <DefaultFallback fill />
150
+ )}
151
+ </span>
152
+ );
153
+ }
154
+
155
+ // ── Fixed size mode
156
+ if (!width || !height) {
157
+ console.warn(
158
+ "[Revine <Image>] `width` and `height` are required when `fill` is not set. " +
159
+ `Missing on: ${src}`,
160
+ );
161
+ }
162
+
163
+ return (
164
+ <span
165
+ style={{
166
+ display: "inline-block",
167
+ position: "relative",
168
+ width: width ? `${width}px` : undefined,
169
+ height: height ? `${height}px` : undefined,
170
+ overflow: "hidden",
171
+ flexShrink: 0,
172
+ }}
173
+ >
174
+ {/* Placeholder layer */}
175
+ {!loaded && !errored && (
176
+ <span
177
+ aria-hidden="true"
178
+ style={{
179
+ position: "absolute",
180
+ inset: 0,
181
+ backgroundImage: placeholder ? `url(${placeholderSrc})` : undefined,
182
+ backgroundColor: placeholder ? undefined : "#e8e8e8",
183
+ backgroundSize: "cover",
184
+ backgroundPosition: "center",
185
+ filter: placeholder ? "blur(8px)" : undefined,
186
+ transform: "scale(1.05)",
187
+ }}
188
+ />
189
+ )}
190
+ {!errored ? (
191
+ <img
192
+ ref={imgRef}
193
+ src={src}
194
+ alt={alt}
195
+ width={width}
196
+ height={height}
197
+ loading={priority ? "eager" : "lazy"}
198
+ decoding={priority ? "sync" : "async"}
199
+ fetchPriority={priority ? "high" : "auto"}
200
+ onLoad={() => setLoaded(true)}
201
+ onError={handleError}
202
+ className={className}
203
+ style={{
204
+ display: "block",
205
+ width: "100%",
206
+ height: "100%",
207
+ objectFit: "cover",
208
+ opacity: loaded ? 1 : 0,
209
+ transition: "opacity 300ms ease",
210
+ ...style,
211
+ }}
212
+ {...rest}
213
+ />
214
+ ) : fallback ? (
215
+ <>{fallback}</>
216
+ ) : (
217
+ <DefaultFallback width={width} height={height} />
218
+ )}
219
+ </span>
220
+ );
221
+ }
222
+
223
+ function DefaultFallback({
224
+ width,
225
+ height,
226
+ fill,
227
+ }: {
228
+ width?: number;
229
+ height?: number;
230
+ fill?: boolean;
231
+ }) {
232
+ return (
233
+ <span
234
+ role="img"
235
+ aria-label="Image failed to load"
236
+ style={{
237
+ position: fill ? "absolute" : "relative",
238
+ inset: fill ? 0 : undefined,
239
+ display: "flex",
240
+ alignItems: "center",
241
+ justifyContent: "center",
242
+ width: fill ? "100%" : width ? `${width}px` : "100%",
243
+ height: fill ? "100%" : height ? `${height}px` : "100%",
244
+ background: "#f3f4f6",
245
+ color: "#9ca3af",
246
+ fontSize: "12px",
247
+ fontFamily: "system-ui, sans-serif",
248
+ gap: "6px",
249
+ flexDirection: "column",
250
+ }}
251
+ >
252
+ <svg
253
+ width="24"
254
+ height="24"
255
+ viewBox="0 0 24 24"
256
+ fill="none"
257
+ stroke="currentColor"
258
+ strokeWidth="1.5"
259
+ strokeLinecap="round"
260
+ strokeLinejoin="round"
261
+ >
262
+ <rect x="3" y="3" width="18" height="18" rx="2" />
263
+ <circle cx="8.5" cy="8.5" r="1.5" />
264
+ <polyline points="21 15 16 10 5 21" />
265
+ </svg>
266
+ <span>Failed to load</span>
267
+ </span>
268
+ );
269
+ }
@@ -1,5 +1,350 @@
1
1
  const VIRTUAL_ROUTING_ID = "\0revine:routing";
2
2
 
3
+ const errorBoundaryComponent = `
4
+ function RevineErrorDialog() {
5
+ const error = useRouteError();
6
+ const [expanded, setExpanded] = React.useState(false);
7
+ const [copied, setCopied] = React.useState(false);
8
+
9
+ const message = error?.message || String(error) || "An unexpected error occurred.";
10
+ const stack = error?.stack || "";
11
+ const stackLines = stack
12
+ .split("\\n")
13
+ .filter((l) => l.trim().startsWith("at "))
14
+ .slice(0, 8)
15
+ .join("\\n");
16
+
17
+ const handleCopy = () => {
18
+ const text = message + (stackLines ? "\\n\\n" + stackLines : "");
19
+ navigator.clipboard.writeText(text).then(() => {
20
+ setCopied(true);
21
+ setTimeout(() => setCopied(false), 2000);
22
+ });
23
+ };
24
+
25
+ return React.createElement(
26
+ "div",
27
+ { style: overlayStyle },
28
+ React.createElement(
29
+ "div",
30
+ { style: dialogStyle },
31
+
32
+ React.createElement(
33
+ "div",
34
+ { style: topBarStyle },
35
+ React.createElement(
36
+ "div",
37
+ { style: brandStyle },
38
+ React.createElement(
39
+ "svg",
40
+ { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none",
41
+ stroke: "#a78bfa", strokeWidth: "2.2", strokeLinecap: "round",
42
+ strokeLinejoin: "round", style: { flexShrink: 0 } },
43
+ React.createElement("polygon", { points: "13 2 3 14 12 14 11 22 21 10 12 10 13 2" })
44
+ ),
45
+ React.createElement("span", { style: brandNameStyle }, "Revine")
46
+ ),
47
+ React.createElement("span", { style: badgeStyle }, "Runtime Error")
48
+ ),
49
+
50
+ React.createElement("div", { style: dividerStyle }),
51
+
52
+ React.createElement(
53
+ "div",
54
+ { style: headerStyle },
55
+ React.createElement(
56
+ "div",
57
+ { style: iconWrapStyle },
58
+ React.createElement(
59
+ "svg",
60
+ { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none",
61
+ stroke: "#f87171", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
62
+ React.createElement("circle", { cx: "12", cy: "12", r: "10" }),
63
+ React.createElement("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
64
+ React.createElement("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
65
+ )
66
+ ),
67
+ React.createElement("span", { style: titleStyle }, "Application Error")
68
+ ),
69
+
70
+ React.createElement(
71
+ "div",
72
+ { style: messagePanelStyle },
73
+ React.createElement("p", { style: messageStyle }, message),
74
+ React.createElement(
75
+ "button",
76
+ { onClick: handleCopy, style: copyBtnStyle, title: "Copy error" },
77
+ copied
78
+ ? React.createElement(
79
+ "svg",
80
+ { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
81
+ stroke: "#4ade80", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
82
+ React.createElement("polyline", { points: "20 6 9 17 4 12" })
83
+ )
84
+ : React.createElement(
85
+ "svg",
86
+ { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
87
+ stroke: "#888", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
88
+ React.createElement("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
89
+ React.createElement("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
90
+ )
91
+ )
92
+ ),
93
+
94
+ stackLines.length > 0 &&
95
+ React.createElement(
96
+ "div",
97
+ { style: stackSectionStyle },
98
+ React.createElement(
99
+ "button",
100
+ { onClick: () => setExpanded((v) => !v), style: toggleBtnStyle },
101
+ React.createElement(
102
+ "svg",
103
+ { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none",
104
+ stroke: "#666", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round",
105
+ style: { transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
106
+ transition: "transform 200ms ease", flexShrink: 0 } },
107
+ React.createElement("polyline", { points: "9 18 15 12 9 6" })
108
+ ),
109
+ React.createElement("span", null, "Stack trace")
110
+ ),
111
+ expanded &&
112
+ React.createElement("pre", { style: stackStyle }, stackLines)
113
+ ),
114
+
115
+ React.createElement(
116
+ "div",
117
+ { style: actionsStyle },
118
+ React.createElement(
119
+ "button",
120
+ { onClick: () => window.location.reload(), style: primaryBtnStyle },
121
+ React.createElement(
122
+ "svg",
123
+ { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
124
+ stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
125
+ React.createElement("polyline", { points: "23 4 23 10 17 10" }),
126
+ React.createElement("path", { d: "M20.49 15a9 9 0 1 1-2.12-9.36L23 10" })
127
+ ),
128
+ "Reload page"
129
+ ),
130
+ React.createElement(
131
+ "button",
132
+ { onClick: () => (window.location.href = "/"), style: secondaryBtnStyle },
133
+ React.createElement(
134
+ "svg",
135
+ { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none",
136
+ stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
137
+ React.createElement("path", { d: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }),
138
+ React.createElement("polyline", { points: "9 22 9 12 15 12 15 22" })
139
+ ),
140
+ "Go to home"
141
+ )
142
+ )
143
+ )
144
+ );
145
+ }
146
+
147
+ const overlayStyle = {
148
+ position: "fixed", inset: 0,
149
+ background: "rgba(0,0,0,0.72)",
150
+ backdropFilter: "blur(6px)",
151
+ display: "flex", alignItems: "center", justifyContent: "center",
152
+ zIndex: 9999,
153
+ };
154
+ const dialogStyle = {
155
+ background: "#141414",
156
+ border: "1px solid #2a2a2a",
157
+ borderRadius: "14px",
158
+ padding: "0",
159
+ maxWidth: "580px", width: "92%",
160
+ boxShadow: "0 32px 80px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.04) inset",
161
+ color: "#e5e5e5",
162
+ overflow: "hidden",
163
+ fontFamily: "system-ui, -apple-system, sans-serif",
164
+ };
165
+ const topBarStyle = {
166
+ display: "flex", alignItems: "center", justifyContent: "space-between",
167
+ padding: "12px 18px", background: "#0e0e0e",
168
+ };
169
+ const brandStyle = { display: "flex", alignItems: "center", gap: "7px" };
170
+ const brandNameStyle = {
171
+ fontSize: "13px", fontWeight: 700,
172
+ color: "#c4b5fd", letterSpacing: "0.04em",
173
+ fontFamily: "system-ui, sans-serif",
174
+ };
175
+ const badgeStyle = {
176
+ fontSize: "11px", fontWeight: 600, color: "#f87171",
177
+ background: "rgba(248,113,113,0.1)",
178
+ border: "1px solid rgba(248,113,113,0.2)",
179
+ borderRadius: "999px", padding: "2px 10px", letterSpacing: "0.03em",
180
+ };
181
+ const dividerStyle = { height: "1px", background: "#1f1f1f" };
182
+ const headerStyle = {
183
+ display: "flex", alignItems: "center", gap: "10px",
184
+ padding: "20px 22px 0 22px",
185
+ };
186
+ const iconWrapStyle = {
187
+ width: "28px", height: "28px", borderRadius: "8px",
188
+ background: "rgba(248,113,113,0.1)",
189
+ border: "1px solid rgba(248,113,113,0.15)",
190
+ display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
191
+ };
192
+ const titleStyle = { fontSize: "15px", fontWeight: 650, color: "#fff", letterSpacing: "-0.01em" };
193
+ const messagePanelStyle = {
194
+ position: "relative", margin: "14px 22px 0 22px",
195
+ background: "rgba(248,113,113,0.05)",
196
+ border: "1px solid rgba(248,113,113,0.12)",
197
+ borderRadius: "8px", padding: "12px 40px 12px 14px",
198
+ };
199
+ const messageStyle = {
200
+ fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
201
+ fontSize: "12.5px", color: "#fca5a5",
202
+ margin: 0, lineHeight: 1.65, wordBreak: "break-word",
203
+ };
204
+ const copyBtnStyle = {
205
+ position: "absolute", top: "10px", right: "10px",
206
+ background: "rgba(255,255,255,0.05)", border: "1px solid #2e2e2e",
207
+ borderRadius: "6px", width: "28px", height: "28px",
208
+ display: "flex", alignItems: "center", justifyContent: "center",
209
+ cursor: "pointer", transition: "background 150ms ease", flexShrink: 0,
210
+ };
211
+ const stackSectionStyle = { margin: "14px 22px 0 22px" };
212
+ const toggleBtnStyle = {
213
+ background: "none", border: "none", cursor: "pointer",
214
+ color: "#666", fontSize: "12px", padding: "4px 0",
215
+ display: "flex", alignItems: "center", gap: "6px",
216
+ letterSpacing: "0.02em", transition: "color 150ms ease",
217
+ };
218
+ const stackStyle = {
219
+ background: "#0a0a0a", border: "1px solid #222", borderRadius: "8px",
220
+ padding: "14px 16px", fontSize: "11px", color: "#888",
221
+ overflowX: "auto", lineHeight: 1.8, marginTop: "8px", marginBottom: 0,
222
+ whiteSpace: "pre-wrap", wordBreak: "break-all",
223
+ fontFamily: "ui-monospace, 'Cascadia Code', monospace",
224
+ };
225
+ const actionsStyle = {
226
+ display: "flex", gap: "10px", padding: "18px 22px 22px 22px", marginTop: "16px",
227
+ };
228
+ const primaryBtnStyle = {
229
+ flex: 1, padding: "10px 0", borderRadius: "8px", border: "none",
230
+ background: "linear-gradient(135deg, #7c3aed, #6d28d9)",
231
+ color: "#fff", fontWeight: 600, fontSize: "13px", cursor: "pointer",
232
+ display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
233
+ letterSpacing: "0.01em", boxShadow: "0 2px 12px rgba(124,58,237,0.35)",
234
+ fontFamily: "system-ui, sans-serif",
235
+ };
236
+ const secondaryBtnStyle = {
237
+ flex: 1, padding: "10px 0", borderRadius: "8px",
238
+ border: "1px solid #2e2e2e", background: "rgba(255,255,255,0.03)",
239
+ color: "#999", fontSize: "13px", cursor: "pointer",
240
+ display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
241
+ letterSpacing: "0.01em", fontFamily: "system-ui, sans-serif",
242
+ };
243
+ `;
244
+
245
+ // ── Shared overlay HTML builder (used in both the inline script and module error handler)
246
+ // Written as a plain JS string so it can be embedded inside the injected <script> tag.
247
+ const overlayScriptContent = `
248
+ (function () {
249
+ function showOverlay(title, message, detail) {
250
+ if (document.getElementById('__revine_error_overlay__')) return;
251
+
252
+ var overlay = document.createElement('div');
253
+ overlay.id = '__revine_error_overlay__';
254
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.72);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif';
255
+
256
+ var inner = document.createElement('div');
257
+ inner.style.cssText = 'background:#141414;border:1px solid #2a2a2a;border-radius:14px;max-width:580px;width:92%;overflow:hidden;box-shadow:0 32px 80px rgba(0,0,0,0.7)';
258
+
259
+ // Top bar
260
+ var topBar = document.createElement('div');
261
+ topBar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:12px 18px;background:#0e0e0e';
262
+ topBar.innerHTML = '<div style="display:flex;align-items:center;gap:7px"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#a78bfa" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg><span style="font-size:13px;font-weight:700;color:#c4b5fd;letter-spacing:0.04em">Revine</span></div><span style="font-size:11px;font-weight:600;color:#f87171;background:rgba(248,113,113,0.1);border:1px solid rgba(248,113,113,0.2);border-radius:999px;padding:2px 10px">' + title + '</span>';
263
+
264
+ // Divider
265
+ var divider = document.createElement('div');
266
+ divider.style.cssText = 'height:1px;background:#1f1f1f';
267
+
268
+ // Body
269
+ var body = document.createElement('div');
270
+ body.style.cssText = 'padding:20px 22px 0';
271
+
272
+ var header = document.createElement('div');
273
+ header.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:14px';
274
+ header.innerHTML = '<div style="width:28px;height:28px;border-radius:8px;flex-shrink:0;background:rgba(248,113,113,0.1);border:1px solid rgba(248,113,113,0.15);display:flex;align-items:center;justify-content:center"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f87171" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div><span style="font-size:15px;font-weight:600;color:#fff">' + message + '</span>';
275
+
276
+ // Message panel with copy button
277
+ var msgPanel = document.createElement('div');
278
+ msgPanel.style.cssText = 'position:relative;background:rgba(248,113,113,0.05);border:1px solid rgba(248,113,113,0.12);border-radius:8px;padding:12px 44px 12px 14px;margin-bottom:14px';
279
+
280
+ var pre = document.createElement('pre');
281
+ pre.id = '__revine_err_detail__';
282
+ pre.style.cssText = 'font-family:ui-monospace,monospace;font-size:12px;color:#fca5a5;margin:0;line-height:1.65;white-space:pre-wrap;word-break:break-all';
283
+ pre.textContent = detail;
284
+
285
+ var copyBtn = document.createElement('button');
286
+ copyBtn.textContent = '⎘';
287
+ copyBtn.style.cssText = 'position:absolute;top:10px;right:10px;background:rgba(255,255,255,0.05);border:1px solid #2e2e2e;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#888;font-size:13px';
288
+ copyBtn.onclick = function() {
289
+ navigator.clipboard.writeText(pre.textContent || '').then(function() {
290
+ copyBtn.textContent = '✓';
291
+ setTimeout(function() { copyBtn.textContent = '⎘'; }, 2000);
292
+ });
293
+ };
294
+
295
+ msgPanel.appendChild(pre);
296
+ msgPanel.appendChild(copyBtn);
297
+ body.appendChild(header);
298
+ body.appendChild(msgPanel);
299
+
300
+ // Actions
301
+ var actions = document.createElement('div');
302
+ actions.style.cssText = 'display:flex;gap:10px;padding:4px 22px 22px';
303
+
304
+ var reloadBtn = document.createElement('button');
305
+ reloadBtn.textContent = 'Reload page';
306
+ reloadBtn.style.cssText = 'flex:1;padding:10px 0;border-radius:8px;border:none;background:linear-gradient(135deg,#7c3aed,#6d28d9);color:#fff;font-weight:600;font-size:13px;cursor:pointer;box-shadow:0 2px 12px rgba(124,58,237,0.35)';
307
+ reloadBtn.onclick = function() { window.location.reload(); };
308
+
309
+ var dismissBtn = document.createElement('button');
310
+ dismissBtn.textContent = 'Dismiss';
311
+ dismissBtn.style.cssText = 'flex:1;padding:10px 0;border-radius:8px;border:1px solid #2e2e2e;background:rgba(255,255,255,0.03);color:#999;font-size:13px;cursor:pointer';
312
+ dismissBtn.onclick = function() { overlay.remove(); };
313
+
314
+ actions.appendChild(reloadBtn);
315
+ actions.appendChild(dismissBtn);
316
+
317
+ inner.appendChild(topBar);
318
+ inner.appendChild(divider);
319
+ inner.appendChild(body);
320
+ inner.appendChild(actions);
321
+ overlay.appendChild(inner);
322
+ document.body.appendChild(overlay);
323
+ }
324
+
325
+ // Expose globally so the module onerror attribute can call it
326
+ window.__revineShowOverlay = showOverlay;
327
+
328
+ // Catches runtime JS errors and async rejections
329
+ window.addEventListener('error', function(e) {
330
+ // Ignore errors that already have an overlay
331
+ if (document.getElementById('__revine_error_overlay__')) return;
332
+ var msg = e.message || 'Unknown error';
333
+ var src = e.filename ? e.filename.replace(location.origin, '') : '';
334
+ var detail = src ? msg + '\\n\\nSource: ' + src + (e.lineno ? ':' + e.lineno : '') : msg;
335
+ showOverlay('Module Error', 'Failed to load module', detail);
336
+ });
337
+
338
+ window.addEventListener('unhandledrejection', function(e) {
339
+ if (document.getElementById('__revine_error_overlay__')) return;
340
+ var reason = e.reason;
341
+ var msg = (reason && reason.message) ? reason.message : String(reason || 'Unhandled Promise rejection');
342
+ var detail = (reason && reason.stack) ? reason.stack : msg;
343
+ showOverlay('Unhandled Rejection', 'Promise rejected', detail);
344
+ });
345
+ })();
346
+ `;
347
+
3
348
  export function revinePlugin(): any {
4
349
  return {
5
350
  name: "revine",
@@ -11,11 +356,35 @@ export function revinePlugin(): any {
11
356
  }
12
357
  },
13
358
 
359
+ transformIndexHtml(html: string) {
360
+ // 1. Inject the overlay listener script into <head>
361
+ let result = html.replace(
362
+ "</head>",
363
+ `<script>${overlayScriptContent}</script></head>`,
364
+ );
365
+
366
+ // 2. Find the main module entry <script> tag and attach an onerror handler.
367
+ // This is the ONLY way to catch ES module link-time errors like
368
+ // "does not provide an export named 'X'" — window.onerror does NOT fire for these.
369
+ result = result.replace(
370
+ /(<script\s[^>]*type=["']module["'][^>]*src=["'][^"']+["'][^>]*)(\/?>)/g,
371
+ (_match: string, opening: string, closing: string) =>
372
+ opening +
373
+ ` onerror="window.__revineShowOverlay && window.__revineShowOverlay('Module Error','Failed to load application',event.type+': A module failed to load. Check all imports are valid and run \\'npm run build\\' in the Revine package.')"` +
374
+ closing,
375
+ );
376
+
377
+ return result;
378
+ },
379
+
14
380
  load(id: string) {
15
381
  if (id === VIRTUAL_ROUTING_ID) {
16
382
  return `
17
- import { createBrowserRouter } from "react-router-dom";
383
+ import { createBrowserRouter, useRouteError } from "react-router-dom";
18
384
  import { lazy, Suspense, createElement } from "react";
385
+ import React from "react";
386
+
387
+ ${errorBoundaryComponent}
19
388
 
20
389
  const notFoundModules = import.meta.glob("/src/NotFound.tsx", { eager: true });
21
390
  const NotFoundComponent = Object.values(notFoundModules)[0]?.default;
@@ -84,7 +453,7 @@ const routes = pageEntries.map(([filePath, component]) => {
84
453
 
85
454
  const fallback = Loading
86
455
  ? createElement(Loading)
87
- : createElement("div", null, "Loading\\u2026");
456
+ : createElement("div", null, "Loading\u2026");
88
457
 
89
458
  const pageElement = createElement(
90
459
  Suspense,
@@ -95,6 +464,7 @@ const routes = pageEntries.map(([filePath, component]) => {
95
464
  return {
96
465
  path: routePath,
97
466
  element: layouts.length > 0 ? wrapWithLayouts(pageElement, layouts) : pageElement,
467
+ errorElement: createElement(RevineErrorDialog),
98
468
  };
99
469
  });
100
470
 
@@ -103,6 +473,7 @@ routes.push({
103
473
  element: NotFoundComponent
104
474
  ? createElement(NotFoundComponent)
105
475
  : createElement("div", null, "404 - Page Not Found"),
476
+ errorElement: createElement(RevineErrorDialog),
106
477
  });
107
478
 
108
479
  export const router = createBrowserRouter(routes);