revine 1.1.4 → 1.2.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.
- package/dist/client.d.ts +6 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -1
- package/dist/components/Image.d.ts +47 -0
- package/dist/components/Image.d.ts.map +1 -0
- package/dist/components/Image.js +106 -0
- package/dist/runtime/bundler/revinePlugin.d.ts.map +1 -1
- package/dist/runtime/bundler/revinePlugin.js +144 -64
- package/package.json +1 -1
- package/src/client.ts +6 -4
- package/src/components/Image.tsx +275 -0
- package/src/runtime/bundler/revinePlugin.ts +155 -64
- package/template/src/root.tsx +7 -2
package/dist/client.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export { Outlet, RouterProvider, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
2
|
-
export
|
|
1
|
+
export { Outlet, RouterProvider, useLocation, useNavigate, useParams, useSearchParams, } from "react-router-dom";
|
|
2
|
+
export { Image } from "./components/Image.js";
|
|
3
|
+
export type { ImageProps } from "./components/Image.js";
|
|
3
4
|
export { Link } from "./components/Link.js";
|
|
5
|
+
export type { LinkProps } from "./components/Link.js";
|
|
4
6
|
export { NavLink } from "./components/NavLink.js";
|
|
7
|
+
export type { NavLinkProps } from "./components/NavLink.js";
|
|
5
8
|
export { defineConfig } from "./runtime/defineConfig.js";
|
|
6
|
-
export type { LayoutProps } from "./runtime/types.js";
|
|
7
|
-
export type { LinkProps } from "./components/Link.js";
|
|
8
9
|
export { env, envAll } from "./runtime/env.js";
|
|
10
|
+
export type { LayoutProps } from "./runtime/types.js";
|
|
9
11
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,cAAc,EACd,WAAW,EACX,WAAW,EACX,SAAS,EACT,eAAe,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,cAAc,EACd,WAAW,EACX,WAAW,EACX,SAAS,EACT,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { Outlet, RouterProvider, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
1
|
+
export { Outlet, RouterProvider, useLocation, useNavigate, useParams, useSearchParams, } from "react-router-dom";
|
|
2
|
+
export { Image } from "./components/Image.js";
|
|
2
3
|
export { Link } from "./components/Link.js";
|
|
3
4
|
export { NavLink } from "./components/NavLink.js";
|
|
4
5
|
export { defineConfig } from "./runtime/defineConfig.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ImgHTMLAttributes } from "react";
|
|
2
|
+
type ObjectFit = "contain" | "cover" | "fill" | "none" | "scale-down";
|
|
3
|
+
declare module "react" {
|
|
4
|
+
interface ImgHTMLAttributes<T> {
|
|
5
|
+
fetchpriority?: "high" | "low" | "auto";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height" | "placeholder"> {
|
|
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;
|
|
44
|
+
}
|
|
45
|
+
export declare function Image({ src, alt, width, height, fill, objectFit, objectPosition, priority, placeholder, onError, fallback, className, style, ...rest }: ImageProps): import("react/jsx-runtime").JSX.Element;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=Image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../../src/components/Image.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE5E,KAAK,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAC;AAEtE,OAAO,QAAQ,OAAO,CAAC;IACrB,UAAU,iBAAiB,CAAC,CAAC;QAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;KACzC;CACF;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI,CACtC,iBAAiB,CAAC,gBAAgB,CAAC,EACnC,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,CAC3C;IACC,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4DAA4D;IAC5D,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB;AAeD,wBAAgB,KAAK,CAAC,EACpB,GAAG,EACH,GAAG,EACH,KAAK,EACL,MAAM,EACN,IAAY,EACZ,SAAmB,EACnB,cAAyB,EACzB,QAAgB,EAChB,WAAW,EACX,OAAO,EACP,QAAQ,EACR,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,UAAU,2CAmJZ"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
const shimmerBase64 = "data:image/svg+xml;base64," +
|
|
4
|
+
btoa(`<svg xmlns='http://www.w3.org/2000/svg' width='400' height='300'>
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id='g' x1='0%' y1='0%' x2='100%' y2='0%'>
|
|
7
|
+
<stop offset='0%' stop-color='#e8e8e8'/>
|
|
8
|
+
<stop offset='50%' stop-color='#f0f0f0'/>
|
|
9
|
+
<stop offset='100%' stop-color='#e8e8e8'/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect width='400' height='300' fill='url(#g)'/>
|
|
13
|
+
</svg>`);
|
|
14
|
+
export function Image({ src, alt, width, height, fill = false, objectFit = "cover", objectPosition = "center", priority = false, placeholder, onError, fallback, className, style, ...rest }) {
|
|
15
|
+
const [loaded, setLoaded] = useState(false);
|
|
16
|
+
const [errored, setErrored] = useState(false);
|
|
17
|
+
const imgRef = useRef(null);
|
|
18
|
+
// If image is already cached (e.g. browser back-nav), mark loaded immediately
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (imgRef.current?.complete && imgRef.current.naturalWidth > 0) {
|
|
21
|
+
setLoaded(true);
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
const handleError = () => {
|
|
25
|
+
setErrored(true);
|
|
26
|
+
onError?.();
|
|
27
|
+
};
|
|
28
|
+
const placeholderSrc = placeholder ?? shimmerBase64;
|
|
29
|
+
// ── Fill mode: stretch to parent container
|
|
30
|
+
if (fill) {
|
|
31
|
+
return (_jsxs("span", { style: {
|
|
32
|
+
position: "absolute",
|
|
33
|
+
inset: 0,
|
|
34
|
+
display: "block",
|
|
35
|
+
overflow: "hidden",
|
|
36
|
+
}, children: [!loaded && !errored && (_jsx("span", { "aria-hidden": "true", style: {
|
|
37
|
+
position: "absolute",
|
|
38
|
+
inset: 0,
|
|
39
|
+
backgroundImage: placeholder
|
|
40
|
+
? `url(${placeholderSrc})`
|
|
41
|
+
: undefined,
|
|
42
|
+
backgroundColor: placeholder ? undefined : "#e8e8e8",
|
|
43
|
+
backgroundSize: "cover",
|
|
44
|
+
backgroundPosition: "center",
|
|
45
|
+
filter: placeholder ? "blur(8px)" : undefined,
|
|
46
|
+
transform: "scale(1.05)",
|
|
47
|
+
} })), !errored ? (_jsx("img", { ref: imgRef, src: src, alt: alt, loading: priority ? "eager" : "lazy", decoding: priority ? "sync" : "async", fetchpriority: priority ? "high" : "auto", onLoad: () => setLoaded(true), onError: handleError, className: className, style: {
|
|
48
|
+
position: "absolute",
|
|
49
|
+
inset: 0,
|
|
50
|
+
width: "100%",
|
|
51
|
+
height: "100%",
|
|
52
|
+
objectFit,
|
|
53
|
+
objectPosition,
|
|
54
|
+
opacity: loaded ? 1 : 0,
|
|
55
|
+
transition: "opacity 300ms ease",
|
|
56
|
+
...style,
|
|
57
|
+
}, ...rest })) : fallback ? (_jsx(_Fragment, { children: fallback })) : (_jsx(DefaultFallback, { fill: true }))] }));
|
|
58
|
+
}
|
|
59
|
+
// ── Fixed size mode
|
|
60
|
+
if (!width || !height) {
|
|
61
|
+
console.warn("[Revine <Image>] `width` and `height` are required when `fill` is not set. " +
|
|
62
|
+
`Missing on: ${src}`);
|
|
63
|
+
}
|
|
64
|
+
return (_jsxs("span", { style: {
|
|
65
|
+
display: "inline-block",
|
|
66
|
+
position: "relative",
|
|
67
|
+
width: width ? `${width}px` : undefined,
|
|
68
|
+
height: height ? `${height}px` : undefined,
|
|
69
|
+
overflow: "hidden",
|
|
70
|
+
flexShrink: 0,
|
|
71
|
+
}, children: [!loaded && !errored && (_jsx("span", { "aria-hidden": "true", style: {
|
|
72
|
+
position: "absolute",
|
|
73
|
+
inset: 0,
|
|
74
|
+
backgroundImage: placeholder ? `url(${placeholderSrc})` : undefined,
|
|
75
|
+
backgroundColor: placeholder ? undefined : "#e8e8e8",
|
|
76
|
+
backgroundSize: "cover",
|
|
77
|
+
backgroundPosition: "center",
|
|
78
|
+
filter: placeholder ? "blur(8px)" : undefined,
|
|
79
|
+
transform: "scale(1.05)",
|
|
80
|
+
} })), !errored ? (_jsx("img", { ref: imgRef, src: src, alt: alt, width: width, height: height, loading: priority ? "eager" : "lazy", decoding: priority ? "sync" : "async", fetchpriority: priority ? "high" : "auto", onLoad: () => setLoaded(true), onError: handleError, className: className, style: {
|
|
81
|
+
display: "block",
|
|
82
|
+
width: "100%",
|
|
83
|
+
height: "100%",
|
|
84
|
+
objectFit: "cover",
|
|
85
|
+
opacity: loaded ? 1 : 0,
|
|
86
|
+
transition: "opacity 300ms ease",
|
|
87
|
+
...style,
|
|
88
|
+
}, ...rest })) : fallback ? (_jsx(_Fragment, { children: fallback })) : (_jsx(DefaultFallback, { width: width, height: height }))] }));
|
|
89
|
+
}
|
|
90
|
+
function DefaultFallback({ width, height, fill, }) {
|
|
91
|
+
return (_jsxs("span", { role: "img", "aria-label": "Image failed to load", style: {
|
|
92
|
+
position: fill ? "absolute" : "relative",
|
|
93
|
+
inset: fill ? 0 : undefined,
|
|
94
|
+
display: "flex",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
justifyContent: "center",
|
|
97
|
+
width: fill ? "100%" : width ? `${width}px` : "100%",
|
|
98
|
+
height: fill ? "100%" : height ? `${height}px` : "100%",
|
|
99
|
+
background: "#f3f4f6",
|
|
100
|
+
color: "#9ca3af",
|
|
101
|
+
fontSize: "12px",
|
|
102
|
+
fontFamily: "system-ui, sans-serif",
|
|
103
|
+
gap: "6px",
|
|
104
|
+
flexDirection: "column",
|
|
105
|
+
}, children: [_jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }), _jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }), _jsx("polyline", { points: "21 15 16 10 5 21" })] }), _jsx("span", { children: "Failed to load" })] }));
|
|
106
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"revinePlugin.d.ts","sourceRoot":"","sources":["../../../src/runtime/bundler/revinePlugin.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"revinePlugin.d.ts","sourceRoot":"","sources":["../../../src/runtime/bundler/revinePlugin.ts"],"names":[],"mappings":"AA2VA,wBAAgB,YAAY,IAAI,GAAG,CA6IlC"}
|
|
@@ -28,7 +28,6 @@ function RevineErrorDialog() {
|
|
|
28
28
|
"div",
|
|
29
29
|
{ style: dialogStyle },
|
|
30
30
|
|
|
31
|
-
// ── Top bar: Revine brand + badge
|
|
32
31
|
React.createElement(
|
|
33
32
|
"div",
|
|
34
33
|
{ style: topBarStyle },
|
|
@@ -47,10 +46,8 @@ function RevineErrorDialog() {
|
|
|
47
46
|
React.createElement("span", { style: badgeStyle }, "Runtime Error")
|
|
48
47
|
),
|
|
49
48
|
|
|
50
|
-
// ── Divider
|
|
51
49
|
React.createElement("div", { style: dividerStyle }),
|
|
52
50
|
|
|
53
|
-
// ── Error icon + title
|
|
54
51
|
React.createElement(
|
|
55
52
|
"div",
|
|
56
53
|
{ style: headerStyle },
|
|
@@ -69,7 +66,6 @@ function RevineErrorDialog() {
|
|
|
69
66
|
React.createElement("span", { style: titleStyle }, "Application Error")
|
|
70
67
|
),
|
|
71
68
|
|
|
72
|
-
// ── Error message + copy button
|
|
73
69
|
React.createElement(
|
|
74
70
|
"div",
|
|
75
71
|
{ style: messagePanelStyle },
|
|
@@ -94,7 +90,6 @@ function RevineErrorDialog() {
|
|
|
94
90
|
)
|
|
95
91
|
),
|
|
96
92
|
|
|
97
|
-
// ── Stack trace toggle + content
|
|
98
93
|
stackLines.length > 0 &&
|
|
99
94
|
React.createElement(
|
|
100
95
|
"div",
|
|
@@ -116,7 +111,6 @@ function RevineErrorDialog() {
|
|
|
116
111
|
React.createElement("pre", { style: stackStyle }, stackLines)
|
|
117
112
|
),
|
|
118
113
|
|
|
119
|
-
// ── Actions
|
|
120
114
|
React.createElement(
|
|
121
115
|
"div",
|
|
122
116
|
{ style: actionsStyle },
|
|
@@ -169,28 +163,21 @@ const dialogStyle = {
|
|
|
169
163
|
};
|
|
170
164
|
const topBarStyle = {
|
|
171
165
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
172
|
-
padding: "12px 18px",
|
|
173
|
-
background: "#0e0e0e",
|
|
174
|
-
};
|
|
175
|
-
const brandStyle = {
|
|
176
|
-
display: "flex", alignItems: "center", gap: "7px",
|
|
166
|
+
padding: "12px 18px", background: "#0e0e0e",
|
|
177
167
|
};
|
|
168
|
+
const brandStyle = { display: "flex", alignItems: "center", gap: "7px" };
|
|
178
169
|
const brandNameStyle = {
|
|
179
170
|
fontSize: "13px", fontWeight: 700,
|
|
180
171
|
color: "#c4b5fd", letterSpacing: "0.04em",
|
|
181
172
|
fontFamily: "system-ui, sans-serif",
|
|
182
173
|
};
|
|
183
174
|
const badgeStyle = {
|
|
184
|
-
fontSize: "11px", fontWeight: 600,
|
|
185
|
-
color: "#f87171",
|
|
175
|
+
fontSize: "11px", fontWeight: 600, color: "#f87171",
|
|
186
176
|
background: "rgba(248,113,113,0.1)",
|
|
187
177
|
border: "1px solid rgba(248,113,113,0.2)",
|
|
188
|
-
borderRadius: "999px", padding: "2px 10px",
|
|
189
|
-
letterSpacing: "0.03em",
|
|
190
|
-
};
|
|
191
|
-
const dividerStyle = {
|
|
192
|
-
height: "1px", background: "#1f1f1f",
|
|
178
|
+
borderRadius: "999px", padding: "2px 10px", letterSpacing: "0.03em",
|
|
193
179
|
};
|
|
180
|
+
const dividerStyle = { height: "1px", background: "#1f1f1f" };
|
|
194
181
|
const headerStyle = {
|
|
195
182
|
display: "flex", alignItems: "center", gap: "10px",
|
|
196
183
|
padding: "20px 22px 0 22px",
|
|
@@ -199,20 +186,14 @@ const iconWrapStyle = {
|
|
|
199
186
|
width: "28px", height: "28px", borderRadius: "8px",
|
|
200
187
|
background: "rgba(248,113,113,0.1)",
|
|
201
188
|
border: "1px solid rgba(248,113,113,0.15)",
|
|
202
|
-
display: "flex", alignItems: "center", justifyContent: "center",
|
|
203
|
-
flexShrink: 0,
|
|
204
|
-
};
|
|
205
|
-
const titleStyle = {
|
|
206
|
-
fontSize: "15px", fontWeight: 650, color: "#fff",
|
|
207
|
-
letterSpacing: "-0.01em",
|
|
189
|
+
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
208
190
|
};
|
|
191
|
+
const titleStyle = { fontSize: "15px", fontWeight: 650, color: "#fff", letterSpacing: "-0.01em" };
|
|
209
192
|
const messagePanelStyle = {
|
|
210
|
-
position: "relative",
|
|
211
|
-
margin: "14px 22px 0 22px",
|
|
193
|
+
position: "relative", margin: "14px 22px 0 22px",
|
|
212
194
|
background: "rgba(248,113,113,0.05)",
|
|
213
195
|
border: "1px solid rgba(248,113,113,0.12)",
|
|
214
|
-
borderRadius: "8px",
|
|
215
|
-
padding: "12px 40px 12px 14px",
|
|
196
|
+
borderRadius: "8px", padding: "12px 40px 12px 14px",
|
|
216
197
|
};
|
|
217
198
|
const messageStyle = {
|
|
218
199
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
|
@@ -221,63 +202,146 @@ const messageStyle = {
|
|
|
221
202
|
};
|
|
222
203
|
const copyBtnStyle = {
|
|
223
204
|
position: "absolute", top: "10px", right: "10px",
|
|
224
|
-
background: "rgba(255,255,255,0.05)",
|
|
225
|
-
|
|
226
|
-
borderRadius: "6px",
|
|
227
|
-
width: "28px", height: "28px",
|
|
205
|
+
background: "rgba(255,255,255,0.05)", border: "1px solid #2e2e2e",
|
|
206
|
+
borderRadius: "6px", width: "28px", height: "28px",
|
|
228
207
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
229
|
-
cursor: "pointer", transition: "background 150ms ease",
|
|
230
|
-
flexShrink: 0,
|
|
231
|
-
};
|
|
232
|
-
const stackSectionStyle = {
|
|
233
|
-
margin: "14px 22px 0 22px",
|
|
208
|
+
cursor: "pointer", transition: "background 150ms ease", flexShrink: 0,
|
|
234
209
|
};
|
|
210
|
+
const stackSectionStyle = { margin: "14px 22px 0 22px" };
|
|
235
211
|
const toggleBtnStyle = {
|
|
236
212
|
background: "none", border: "none", cursor: "pointer",
|
|
237
|
-
color: "#666", fontSize: "12px",
|
|
238
|
-
padding: "4px 0",
|
|
213
|
+
color: "#666", fontSize: "12px", padding: "4px 0",
|
|
239
214
|
display: "flex", alignItems: "center", gap: "6px",
|
|
240
|
-
letterSpacing: "0.02em",
|
|
241
|
-
transition: "color 150ms ease",
|
|
215
|
+
letterSpacing: "0.02em", transition: "color 150ms ease",
|
|
242
216
|
};
|
|
243
217
|
const stackStyle = {
|
|
244
|
-
background: "#0a0a0a",
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
padding: "14px 16px",
|
|
248
|
-
fontSize: "11px", color: "#888",
|
|
249
|
-
overflowX: "auto", lineHeight: 1.8,
|
|
250
|
-
marginTop: "8px", marginBottom: 0,
|
|
218
|
+
background: "#0a0a0a", border: "1px solid #222", borderRadius: "8px",
|
|
219
|
+
padding: "14px 16px", fontSize: "11px", color: "#888",
|
|
220
|
+
overflowX: "auto", lineHeight: 1.8, marginTop: "8px", marginBottom: 0,
|
|
251
221
|
whiteSpace: "pre-wrap", wordBreak: "break-all",
|
|
252
222
|
fontFamily: "ui-monospace, 'Cascadia Code', monospace",
|
|
253
223
|
};
|
|
254
224
|
const actionsStyle = {
|
|
255
|
-
display: "flex", gap: "10px",
|
|
256
|
-
padding: "18px 22px 22px 22px",
|
|
257
|
-
marginTop: "16px",
|
|
225
|
+
display: "flex", gap: "10px", padding: "18px 22px 22px 22px", marginTop: "16px",
|
|
258
226
|
};
|
|
259
227
|
const primaryBtnStyle = {
|
|
260
|
-
flex: 1, padding: "10px 0",
|
|
261
|
-
borderRadius: "8px", border: "none",
|
|
228
|
+
flex: 1, padding: "10px 0", borderRadius: "8px", border: "none",
|
|
262
229
|
background: "linear-gradient(135deg, #7c3aed, #6d28d9)",
|
|
263
|
-
color: "#fff", fontWeight: 600,
|
|
264
|
-
fontSize: "13px", cursor: "pointer",
|
|
230
|
+
color: "#fff", fontWeight: 600, fontSize: "13px", cursor: "pointer",
|
|
265
231
|
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
266
|
-
letterSpacing: "0.01em",
|
|
267
|
-
boxShadow: "0 2px 12px rgba(124,58,237,0.35)",
|
|
232
|
+
letterSpacing: "0.01em", boxShadow: "0 2px 12px rgba(124,58,237,0.35)",
|
|
268
233
|
fontFamily: "system-ui, sans-serif",
|
|
269
234
|
};
|
|
270
235
|
const secondaryBtnStyle = {
|
|
271
|
-
flex: 1, padding: "10px 0",
|
|
272
|
-
|
|
273
|
-
border: "1px solid #2e2e2e",
|
|
274
|
-
background: "rgba(255,255,255,0.03)",
|
|
236
|
+
flex: 1, padding: "10px 0", borderRadius: "8px",
|
|
237
|
+
border: "1px solid #2e2e2e", background: "rgba(255,255,255,0.03)",
|
|
275
238
|
color: "#999", fontSize: "13px", cursor: "pointer",
|
|
276
239
|
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
277
|
-
letterSpacing: "0.01em",
|
|
278
|
-
fontFamily: "system-ui, sans-serif",
|
|
240
|
+
letterSpacing: "0.01em", fontFamily: "system-ui, sans-serif",
|
|
279
241
|
};
|
|
280
242
|
`;
|
|
243
|
+
// ── Shared overlay HTML builder (used in both the inline script and module error handler)
|
|
244
|
+
// Written as a plain JS string so it can be embedded inside the injected <script> tag.
|
|
245
|
+
const overlayScriptContent = `
|
|
246
|
+
(function () {
|
|
247
|
+
function showOverlay(title, message, detail) {
|
|
248
|
+
if (document.getElementById('__revine_error_overlay__')) return;
|
|
249
|
+
|
|
250
|
+
var overlay = document.createElement('div');
|
|
251
|
+
overlay.id = '__revine_error_overlay__';
|
|
252
|
+
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';
|
|
253
|
+
|
|
254
|
+
var inner = document.createElement('div');
|
|
255
|
+
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)';
|
|
256
|
+
|
|
257
|
+
// Top bar
|
|
258
|
+
var topBar = document.createElement('div');
|
|
259
|
+
topBar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:12px 18px;background:#0e0e0e';
|
|
260
|
+
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>';
|
|
261
|
+
|
|
262
|
+
// Divider
|
|
263
|
+
var divider = document.createElement('div');
|
|
264
|
+
divider.style.cssText = 'height:1px;background:#1f1f1f';
|
|
265
|
+
|
|
266
|
+
// Body
|
|
267
|
+
var body = document.createElement('div');
|
|
268
|
+
body.style.cssText = 'padding:20px 22px 0';
|
|
269
|
+
|
|
270
|
+
var header = document.createElement('div');
|
|
271
|
+
header.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:14px';
|
|
272
|
+
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>';
|
|
273
|
+
|
|
274
|
+
// Message panel with copy button
|
|
275
|
+
var msgPanel = document.createElement('div');
|
|
276
|
+
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';
|
|
277
|
+
|
|
278
|
+
var pre = document.createElement('pre');
|
|
279
|
+
pre.id = '__revine_err_detail__';
|
|
280
|
+
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';
|
|
281
|
+
pre.textContent = detail;
|
|
282
|
+
|
|
283
|
+
var copyBtn = document.createElement('button');
|
|
284
|
+
copyBtn.textContent = '⎘';
|
|
285
|
+
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';
|
|
286
|
+
copyBtn.onclick = function() {
|
|
287
|
+
navigator.clipboard.writeText(pre.textContent || '').then(function() {
|
|
288
|
+
copyBtn.textContent = '✓';
|
|
289
|
+
setTimeout(function() { copyBtn.textContent = '⎘'; }, 2000);
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
msgPanel.appendChild(pre);
|
|
294
|
+
msgPanel.appendChild(copyBtn);
|
|
295
|
+
body.appendChild(header);
|
|
296
|
+
body.appendChild(msgPanel);
|
|
297
|
+
|
|
298
|
+
// Actions
|
|
299
|
+
var actions = document.createElement('div');
|
|
300
|
+
actions.style.cssText = 'display:flex;gap:10px;padding:4px 22px 22px';
|
|
301
|
+
|
|
302
|
+
var reloadBtn = document.createElement('button');
|
|
303
|
+
reloadBtn.textContent = 'Reload page';
|
|
304
|
+
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)';
|
|
305
|
+
reloadBtn.onclick = function() { window.location.reload(); };
|
|
306
|
+
|
|
307
|
+
var dismissBtn = document.createElement('button');
|
|
308
|
+
dismissBtn.textContent = 'Dismiss';
|
|
309
|
+
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';
|
|
310
|
+
dismissBtn.onclick = function() { overlay.remove(); };
|
|
311
|
+
|
|
312
|
+
actions.appendChild(reloadBtn);
|
|
313
|
+
actions.appendChild(dismissBtn);
|
|
314
|
+
|
|
315
|
+
inner.appendChild(topBar);
|
|
316
|
+
inner.appendChild(divider);
|
|
317
|
+
inner.appendChild(body);
|
|
318
|
+
inner.appendChild(actions);
|
|
319
|
+
overlay.appendChild(inner);
|
|
320
|
+
document.body.appendChild(overlay);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Expose globally so the module onerror attribute can call it
|
|
324
|
+
window.__revineShowOverlay = showOverlay;
|
|
325
|
+
|
|
326
|
+
// Catches runtime JS errors and async rejections
|
|
327
|
+
window.addEventListener('error', function(e) {
|
|
328
|
+
// Ignore errors that already have an overlay
|
|
329
|
+
if (document.getElementById('__revine_error_overlay__')) return;
|
|
330
|
+
var msg = e.message || 'Unknown error';
|
|
331
|
+
var src = e.filename ? e.filename.replace(location.origin, '') : '';
|
|
332
|
+
var detail = src ? msg + '\\n\\nSource: ' + src + (e.lineno ? ':' + e.lineno : '') : msg;
|
|
333
|
+
showOverlay('Module Error', 'Failed to load module', detail);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
window.addEventListener('unhandledrejection', function(e) {
|
|
337
|
+
if (document.getElementById('__revine_error_overlay__')) return;
|
|
338
|
+
var reason = e.reason;
|
|
339
|
+
var msg = (reason && reason.message) ? reason.message : String(reason || 'Unhandled Promise rejection');
|
|
340
|
+
var detail = (reason && reason.stack) ? reason.stack : msg;
|
|
341
|
+
showOverlay('Unhandled Rejection', 'Promise rejected', detail);
|
|
342
|
+
});
|
|
343
|
+
})();
|
|
344
|
+
`;
|
|
281
345
|
export function revinePlugin() {
|
|
282
346
|
return {
|
|
283
347
|
name: "revine",
|
|
@@ -287,6 +351,17 @@ export function revinePlugin() {
|
|
|
287
351
|
return VIRTUAL_ROUTING_ID;
|
|
288
352
|
}
|
|
289
353
|
},
|
|
354
|
+
transformIndexHtml(html) {
|
|
355
|
+
// 1. Inject the overlay listener script into <head>
|
|
356
|
+
let result = html.replace("</head>", `<script>${overlayScriptContent}</script></head>`);
|
|
357
|
+
// 2. Find the main module entry <script> tag and attach an onerror handler.
|
|
358
|
+
// This is the ONLY way to catch ES module link-time errors like
|
|
359
|
+
// "does not provide an export named 'X'" — window.onerror does NOT fire for these.
|
|
360
|
+
result = result.replace(/(<script\s[^>]*type=["']module["'][^>]*src=["'][^"']+["'][^>]*)(\/?>)/g, (_match, opening, closing) => opening +
|
|
361
|
+
` 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.')"` +
|
|
362
|
+
closing);
|
|
363
|
+
return result;
|
|
364
|
+
},
|
|
290
365
|
load(id) {
|
|
291
366
|
if (id === VIRTUAL_ROUTING_ID) {
|
|
292
367
|
return `
|
|
@@ -386,7 +461,12 @@ routes.push({
|
|
|
386
461
|
errorElement: createElement(RevineErrorDialog),
|
|
387
462
|
});
|
|
388
463
|
|
|
389
|
-
export const router = createBrowserRouter(routes
|
|
464
|
+
export const router = createBrowserRouter(routes, {
|
|
465
|
+
future: {
|
|
466
|
+
v7_startTransition: true,
|
|
467
|
+
v7_relativeSplatPath: true,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
390
470
|
`;
|
|
391
471
|
}
|
|
392
472
|
},
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -4,12 +4,14 @@ export {
|
|
|
4
4
|
useLocation,
|
|
5
5
|
useNavigate,
|
|
6
6
|
useParams,
|
|
7
|
-
useSearchParams
|
|
7
|
+
useSearchParams,
|
|
8
8
|
} from "react-router-dom";
|
|
9
|
-
export
|
|
9
|
+
export { Image } from "./components/Image.js";
|
|
10
|
+
export type { ImageProps } from "./components/Image.js";
|
|
10
11
|
export { Link } from "./components/Link.js";
|
|
12
|
+
export type { LinkProps } from "./components/Link.js";
|
|
11
13
|
export { NavLink } from "./components/NavLink.js";
|
|
14
|
+
export type { NavLinkProps } from "./components/NavLink.js";
|
|
12
15
|
export { defineConfig } from "./runtime/defineConfig.js";
|
|
13
|
-
export type { LayoutProps } from "./runtime/types.js";
|
|
14
|
-
export type { LinkProps } from "./components/Link.js";
|
|
15
16
|
export { env, envAll } from "./runtime/env.js";
|
|
17
|
+
export type { LayoutProps } from "./runtime/types.js";
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ImgHTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
type ObjectFit = "contain" | "cover" | "fill" | "none" | "scale-down";
|
|
4
|
+
|
|
5
|
+
declare module "react" {
|
|
6
|
+
interface ImgHTMLAttributes<T> {
|
|
7
|
+
fetchpriority?: "high" | "low" | "auto";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ImageProps extends Omit<
|
|
12
|
+
ImgHTMLAttributes<HTMLImageElement>,
|
|
13
|
+
"src" | "width" | "height" | "placeholder"
|
|
14
|
+
> {
|
|
15
|
+
/** Image source URL or import (e.g. import logo from './logo.png') */
|
|
16
|
+
src: string;
|
|
17
|
+
/** Alt text — required for accessibility */
|
|
18
|
+
alt: string;
|
|
19
|
+
/** Intrinsic width in px. Required unless fill={true} */
|
|
20
|
+
width?: number;
|
|
21
|
+
/** Intrinsic height in px. Required unless fill={true} */
|
|
22
|
+
height?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Stretch the image to fill its parent container (parent must be position:relative).
|
|
25
|
+
* When true, width/height are not required.
|
|
26
|
+
*/
|
|
27
|
+
fill?: boolean;
|
|
28
|
+
/** CSS object-fit when fill is true. Defaults to "cover" */
|
|
29
|
+
objectFit?: ObjectFit;
|
|
30
|
+
/** CSS object-position when fill is true. Defaults to "center" */
|
|
31
|
+
objectPosition?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Eagerly load the image and skip lazy loading.
|
|
34
|
+
* Use for above-the-fold images (hero, LCP element).
|
|
35
|
+
*/
|
|
36
|
+
priority?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Show a blurred low-quality placeholder while the image loads.
|
|
39
|
+
* Pass a base64 data URL or a solid color string like "#e5e7eb".
|
|
40
|
+
* Defaults to a subtle gray shimmer if not provided.
|
|
41
|
+
*/
|
|
42
|
+
placeholder?: string;
|
|
43
|
+
/** Called when the image fails to load */
|
|
44
|
+
onError?: () => void;
|
|
45
|
+
/** Custom fallback element shown on error */
|
|
46
|
+
fallback?: React.ReactNode;
|
|
47
|
+
className?: string;
|
|
48
|
+
style?: React.CSSProperties;
|
|
49
|
+
quality?: never; // reserved for future CDN support — not implemented yet
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const shimmerBase64 =
|
|
53
|
+
"data:image/svg+xml;base64," +
|
|
54
|
+
btoa(`<svg xmlns='http://www.w3.org/2000/svg' width='400' height='300'>
|
|
55
|
+
<defs>
|
|
56
|
+
<linearGradient id='g' x1='0%' y1='0%' x2='100%' y2='0%'>
|
|
57
|
+
<stop offset='0%' stop-color='#e8e8e8'/>
|
|
58
|
+
<stop offset='50%' stop-color='#f0f0f0'/>
|
|
59
|
+
<stop offset='100%' stop-color='#e8e8e8'/>
|
|
60
|
+
</linearGradient>
|
|
61
|
+
</defs>
|
|
62
|
+
<rect width='400' height='300' fill='url(#g)'/>
|
|
63
|
+
</svg>`);
|
|
64
|
+
|
|
65
|
+
export function Image({
|
|
66
|
+
src,
|
|
67
|
+
alt,
|
|
68
|
+
width,
|
|
69
|
+
height,
|
|
70
|
+
fill = false,
|
|
71
|
+
objectFit = "cover",
|
|
72
|
+
objectPosition = "center",
|
|
73
|
+
priority = false,
|
|
74
|
+
placeholder,
|
|
75
|
+
onError,
|
|
76
|
+
fallback,
|
|
77
|
+
className,
|
|
78
|
+
style,
|
|
79
|
+
...rest
|
|
80
|
+
}: ImageProps) {
|
|
81
|
+
const [loaded, setLoaded] = useState(false);
|
|
82
|
+
const [errored, setErrored] = useState(false);
|
|
83
|
+
const imgRef = useRef<HTMLImageElement>(null);
|
|
84
|
+
|
|
85
|
+
// If image is already cached (e.g. browser back-nav), mark loaded immediately
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (imgRef.current?.complete && imgRef.current.naturalWidth > 0) {
|
|
88
|
+
setLoaded(true);
|
|
89
|
+
}
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const handleError = () => {
|
|
93
|
+
setErrored(true);
|
|
94
|
+
onError?.();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const placeholderSrc = placeholder ?? shimmerBase64;
|
|
98
|
+
|
|
99
|
+
// ── Fill mode: stretch to parent container
|
|
100
|
+
if (fill) {
|
|
101
|
+
return (
|
|
102
|
+
<span
|
|
103
|
+
style={{
|
|
104
|
+
position: "absolute",
|
|
105
|
+
inset: 0,
|
|
106
|
+
display: "block",
|
|
107
|
+
overflow: "hidden",
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{/* Placeholder layer */}
|
|
111
|
+
{!loaded && !errored && (
|
|
112
|
+
<span
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
style={{
|
|
115
|
+
position: "absolute",
|
|
116
|
+
inset: 0,
|
|
117
|
+
backgroundImage: placeholder
|
|
118
|
+
? `url(${placeholderSrc})`
|
|
119
|
+
: undefined,
|
|
120
|
+
backgroundColor: placeholder ? undefined : "#e8e8e8",
|
|
121
|
+
backgroundSize: "cover",
|
|
122
|
+
backgroundPosition: "center",
|
|
123
|
+
filter: placeholder ? "blur(8px)" : undefined,
|
|
124
|
+
transform: "scale(1.05)",
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
{!errored ? (
|
|
129
|
+
<img
|
|
130
|
+
ref={imgRef}
|
|
131
|
+
src={src}
|
|
132
|
+
alt={alt}
|
|
133
|
+
loading={priority ? "eager" : "lazy"}
|
|
134
|
+
decoding={priority ? "sync" : "async"}
|
|
135
|
+
fetchpriority={priority ? "high" : "auto"}
|
|
136
|
+
onLoad={() => setLoaded(true)}
|
|
137
|
+
onError={handleError}
|
|
138
|
+
className={className}
|
|
139
|
+
style={{
|
|
140
|
+
position: "absolute",
|
|
141
|
+
inset: 0,
|
|
142
|
+
width: "100%",
|
|
143
|
+
height: "100%",
|
|
144
|
+
objectFit,
|
|
145
|
+
objectPosition,
|
|
146
|
+
opacity: loaded ? 1 : 0,
|
|
147
|
+
transition: "opacity 300ms ease",
|
|
148
|
+
...style,
|
|
149
|
+
}}
|
|
150
|
+
{...rest}
|
|
151
|
+
/>
|
|
152
|
+
) : fallback ? (
|
|
153
|
+
<>{fallback}</>
|
|
154
|
+
) : (
|
|
155
|
+
<DefaultFallback fill />
|
|
156
|
+
)}
|
|
157
|
+
</span>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Fixed size mode
|
|
162
|
+
if (!width || !height) {
|
|
163
|
+
console.warn(
|
|
164
|
+
"[Revine <Image>] `width` and `height` are required when `fill` is not set. " +
|
|
165
|
+
`Missing on: ${src}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<span
|
|
171
|
+
style={{
|
|
172
|
+
display: "inline-block",
|
|
173
|
+
position: "relative",
|
|
174
|
+
width: width ? `${width}px` : undefined,
|
|
175
|
+
height: height ? `${height}px` : undefined,
|
|
176
|
+
overflow: "hidden",
|
|
177
|
+
flexShrink: 0,
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
{/* Placeholder layer */}
|
|
181
|
+
{!loaded && !errored && (
|
|
182
|
+
<span
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
style={{
|
|
185
|
+
position: "absolute",
|
|
186
|
+
inset: 0,
|
|
187
|
+
backgroundImage: placeholder ? `url(${placeholderSrc})` : undefined,
|
|
188
|
+
backgroundColor: placeholder ? undefined : "#e8e8e8",
|
|
189
|
+
backgroundSize: "cover",
|
|
190
|
+
backgroundPosition: "center",
|
|
191
|
+
filter: placeholder ? "blur(8px)" : undefined,
|
|
192
|
+
transform: "scale(1.05)",
|
|
193
|
+
}}
|
|
194
|
+
/>
|
|
195
|
+
)}
|
|
196
|
+
{!errored ? (
|
|
197
|
+
<img
|
|
198
|
+
ref={imgRef}
|
|
199
|
+
src={src}
|
|
200
|
+
alt={alt}
|
|
201
|
+
width={width}
|
|
202
|
+
height={height}
|
|
203
|
+
loading={priority ? "eager" : "lazy"}
|
|
204
|
+
decoding={priority ? "sync" : "async"}
|
|
205
|
+
fetchpriority={priority ? "high" : "auto"}
|
|
206
|
+
onLoad={() => setLoaded(true)}
|
|
207
|
+
onError={handleError}
|
|
208
|
+
className={className}
|
|
209
|
+
style={{
|
|
210
|
+
display: "block",
|
|
211
|
+
width: "100%",
|
|
212
|
+
height: "100%",
|
|
213
|
+
objectFit: "cover",
|
|
214
|
+
opacity: loaded ? 1 : 0,
|
|
215
|
+
transition: "opacity 300ms ease",
|
|
216
|
+
...style,
|
|
217
|
+
}}
|
|
218
|
+
{...rest}
|
|
219
|
+
/>
|
|
220
|
+
) : fallback ? (
|
|
221
|
+
<>{fallback}</>
|
|
222
|
+
) : (
|
|
223
|
+
<DefaultFallback width={width} height={height} />
|
|
224
|
+
)}
|
|
225
|
+
</span>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function DefaultFallback({
|
|
230
|
+
width,
|
|
231
|
+
height,
|
|
232
|
+
fill,
|
|
233
|
+
}: {
|
|
234
|
+
width?: number;
|
|
235
|
+
height?: number;
|
|
236
|
+
fill?: boolean;
|
|
237
|
+
}) {
|
|
238
|
+
return (
|
|
239
|
+
<span
|
|
240
|
+
role="img"
|
|
241
|
+
aria-label="Image failed to load"
|
|
242
|
+
style={{
|
|
243
|
+
position: fill ? "absolute" : "relative",
|
|
244
|
+
inset: fill ? 0 : undefined,
|
|
245
|
+
display: "flex",
|
|
246
|
+
alignItems: "center",
|
|
247
|
+
justifyContent: "center",
|
|
248
|
+
width: fill ? "100%" : width ? `${width}px` : "100%",
|
|
249
|
+
height: fill ? "100%" : height ? `${height}px` : "100%",
|
|
250
|
+
background: "#f3f4f6",
|
|
251
|
+
color: "#9ca3af",
|
|
252
|
+
fontSize: "12px",
|
|
253
|
+
fontFamily: "system-ui, sans-serif",
|
|
254
|
+
gap: "6px",
|
|
255
|
+
flexDirection: "column",
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<svg
|
|
259
|
+
width="24"
|
|
260
|
+
height="24"
|
|
261
|
+
viewBox="0 0 24 24"
|
|
262
|
+
fill="none"
|
|
263
|
+
stroke="currentColor"
|
|
264
|
+
strokeWidth="1.5"
|
|
265
|
+
strokeLinecap="round"
|
|
266
|
+
strokeLinejoin="round"
|
|
267
|
+
>
|
|
268
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
269
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
270
|
+
<polyline points="21 15 16 10 5 21" />
|
|
271
|
+
</svg>
|
|
272
|
+
<span>Failed to load</span>
|
|
273
|
+
</span>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
@@ -29,7 +29,6 @@ function RevineErrorDialog() {
|
|
|
29
29
|
"div",
|
|
30
30
|
{ style: dialogStyle },
|
|
31
31
|
|
|
32
|
-
// ── Top bar: Revine brand + badge
|
|
33
32
|
React.createElement(
|
|
34
33
|
"div",
|
|
35
34
|
{ style: topBarStyle },
|
|
@@ -48,10 +47,8 @@ function RevineErrorDialog() {
|
|
|
48
47
|
React.createElement("span", { style: badgeStyle }, "Runtime Error")
|
|
49
48
|
),
|
|
50
49
|
|
|
51
|
-
// ── Divider
|
|
52
50
|
React.createElement("div", { style: dividerStyle }),
|
|
53
51
|
|
|
54
|
-
// ── Error icon + title
|
|
55
52
|
React.createElement(
|
|
56
53
|
"div",
|
|
57
54
|
{ style: headerStyle },
|
|
@@ -70,7 +67,6 @@ function RevineErrorDialog() {
|
|
|
70
67
|
React.createElement("span", { style: titleStyle }, "Application Error")
|
|
71
68
|
),
|
|
72
69
|
|
|
73
|
-
// ── Error message + copy button
|
|
74
70
|
React.createElement(
|
|
75
71
|
"div",
|
|
76
72
|
{ style: messagePanelStyle },
|
|
@@ -95,7 +91,6 @@ function RevineErrorDialog() {
|
|
|
95
91
|
)
|
|
96
92
|
),
|
|
97
93
|
|
|
98
|
-
// ── Stack trace toggle + content
|
|
99
94
|
stackLines.length > 0 &&
|
|
100
95
|
React.createElement(
|
|
101
96
|
"div",
|
|
@@ -117,7 +112,6 @@ function RevineErrorDialog() {
|
|
|
117
112
|
React.createElement("pre", { style: stackStyle }, stackLines)
|
|
118
113
|
),
|
|
119
114
|
|
|
120
|
-
// ── Actions
|
|
121
115
|
React.createElement(
|
|
122
116
|
"div",
|
|
123
117
|
{ style: actionsStyle },
|
|
@@ -170,28 +164,21 @@ const dialogStyle = {
|
|
|
170
164
|
};
|
|
171
165
|
const topBarStyle = {
|
|
172
166
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
173
|
-
padding: "12px 18px",
|
|
174
|
-
background: "#0e0e0e",
|
|
175
|
-
};
|
|
176
|
-
const brandStyle = {
|
|
177
|
-
display: "flex", alignItems: "center", gap: "7px",
|
|
167
|
+
padding: "12px 18px", background: "#0e0e0e",
|
|
178
168
|
};
|
|
169
|
+
const brandStyle = { display: "flex", alignItems: "center", gap: "7px" };
|
|
179
170
|
const brandNameStyle = {
|
|
180
171
|
fontSize: "13px", fontWeight: 700,
|
|
181
172
|
color: "#c4b5fd", letterSpacing: "0.04em",
|
|
182
173
|
fontFamily: "system-ui, sans-serif",
|
|
183
174
|
};
|
|
184
175
|
const badgeStyle = {
|
|
185
|
-
fontSize: "11px", fontWeight: 600,
|
|
186
|
-
color: "#f87171",
|
|
176
|
+
fontSize: "11px", fontWeight: 600, color: "#f87171",
|
|
187
177
|
background: "rgba(248,113,113,0.1)",
|
|
188
178
|
border: "1px solid rgba(248,113,113,0.2)",
|
|
189
|
-
borderRadius: "999px", padding: "2px 10px",
|
|
190
|
-
letterSpacing: "0.03em",
|
|
191
|
-
};
|
|
192
|
-
const dividerStyle = {
|
|
193
|
-
height: "1px", background: "#1f1f1f",
|
|
179
|
+
borderRadius: "999px", padding: "2px 10px", letterSpacing: "0.03em",
|
|
194
180
|
};
|
|
181
|
+
const dividerStyle = { height: "1px", background: "#1f1f1f" };
|
|
195
182
|
const headerStyle = {
|
|
196
183
|
display: "flex", alignItems: "center", gap: "10px",
|
|
197
184
|
padding: "20px 22px 0 22px",
|
|
@@ -200,20 +187,14 @@ const iconWrapStyle = {
|
|
|
200
187
|
width: "28px", height: "28px", borderRadius: "8px",
|
|
201
188
|
background: "rgba(248,113,113,0.1)",
|
|
202
189
|
border: "1px solid rgba(248,113,113,0.15)",
|
|
203
|
-
display: "flex", alignItems: "center", justifyContent: "center",
|
|
204
|
-
flexShrink: 0,
|
|
205
|
-
};
|
|
206
|
-
const titleStyle = {
|
|
207
|
-
fontSize: "15px", fontWeight: 650, color: "#fff",
|
|
208
|
-
letterSpacing: "-0.01em",
|
|
190
|
+
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
209
191
|
};
|
|
192
|
+
const titleStyle = { fontSize: "15px", fontWeight: 650, color: "#fff", letterSpacing: "-0.01em" };
|
|
210
193
|
const messagePanelStyle = {
|
|
211
|
-
position: "relative",
|
|
212
|
-
margin: "14px 22px 0 22px",
|
|
194
|
+
position: "relative", margin: "14px 22px 0 22px",
|
|
213
195
|
background: "rgba(248,113,113,0.05)",
|
|
214
196
|
border: "1px solid rgba(248,113,113,0.12)",
|
|
215
|
-
borderRadius: "8px",
|
|
216
|
-
padding: "12px 40px 12px 14px",
|
|
197
|
+
borderRadius: "8px", padding: "12px 40px 12px 14px",
|
|
217
198
|
};
|
|
218
199
|
const messageStyle = {
|
|
219
200
|
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace",
|
|
@@ -222,64 +203,148 @@ const messageStyle = {
|
|
|
222
203
|
};
|
|
223
204
|
const copyBtnStyle = {
|
|
224
205
|
position: "absolute", top: "10px", right: "10px",
|
|
225
|
-
background: "rgba(255,255,255,0.05)",
|
|
226
|
-
|
|
227
|
-
borderRadius: "6px",
|
|
228
|
-
width: "28px", height: "28px",
|
|
206
|
+
background: "rgba(255,255,255,0.05)", border: "1px solid #2e2e2e",
|
|
207
|
+
borderRadius: "6px", width: "28px", height: "28px",
|
|
229
208
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
230
|
-
cursor: "pointer", transition: "background 150ms ease",
|
|
231
|
-
flexShrink: 0,
|
|
232
|
-
};
|
|
233
|
-
const stackSectionStyle = {
|
|
234
|
-
margin: "14px 22px 0 22px",
|
|
209
|
+
cursor: "pointer", transition: "background 150ms ease", flexShrink: 0,
|
|
235
210
|
};
|
|
211
|
+
const stackSectionStyle = { margin: "14px 22px 0 22px" };
|
|
236
212
|
const toggleBtnStyle = {
|
|
237
213
|
background: "none", border: "none", cursor: "pointer",
|
|
238
|
-
color: "#666", fontSize: "12px",
|
|
239
|
-
padding: "4px 0",
|
|
214
|
+
color: "#666", fontSize: "12px", padding: "4px 0",
|
|
240
215
|
display: "flex", alignItems: "center", gap: "6px",
|
|
241
|
-
letterSpacing: "0.02em",
|
|
242
|
-
transition: "color 150ms ease",
|
|
216
|
+
letterSpacing: "0.02em", transition: "color 150ms ease",
|
|
243
217
|
};
|
|
244
218
|
const stackStyle = {
|
|
245
|
-
background: "#0a0a0a",
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
padding: "14px 16px",
|
|
249
|
-
fontSize: "11px", color: "#888",
|
|
250
|
-
overflowX: "auto", lineHeight: 1.8,
|
|
251
|
-
marginTop: "8px", marginBottom: 0,
|
|
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,
|
|
252
222
|
whiteSpace: "pre-wrap", wordBreak: "break-all",
|
|
253
223
|
fontFamily: "ui-monospace, 'Cascadia Code', monospace",
|
|
254
224
|
};
|
|
255
225
|
const actionsStyle = {
|
|
256
|
-
display: "flex", gap: "10px",
|
|
257
|
-
padding: "18px 22px 22px 22px",
|
|
258
|
-
marginTop: "16px",
|
|
226
|
+
display: "flex", gap: "10px", padding: "18px 22px 22px 22px", marginTop: "16px",
|
|
259
227
|
};
|
|
260
228
|
const primaryBtnStyle = {
|
|
261
|
-
flex: 1, padding: "10px 0",
|
|
262
|
-
borderRadius: "8px", border: "none",
|
|
229
|
+
flex: 1, padding: "10px 0", borderRadius: "8px", border: "none",
|
|
263
230
|
background: "linear-gradient(135deg, #7c3aed, #6d28d9)",
|
|
264
|
-
color: "#fff", fontWeight: 600,
|
|
265
|
-
fontSize: "13px", cursor: "pointer",
|
|
231
|
+
color: "#fff", fontWeight: 600, fontSize: "13px", cursor: "pointer",
|
|
266
232
|
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
267
|
-
letterSpacing: "0.01em",
|
|
268
|
-
boxShadow: "0 2px 12px rgba(124,58,237,0.35)",
|
|
233
|
+
letterSpacing: "0.01em", boxShadow: "0 2px 12px rgba(124,58,237,0.35)",
|
|
269
234
|
fontFamily: "system-ui, sans-serif",
|
|
270
235
|
};
|
|
271
236
|
const secondaryBtnStyle = {
|
|
272
|
-
flex: 1, padding: "10px 0",
|
|
273
|
-
|
|
274
|
-
border: "1px solid #2e2e2e",
|
|
275
|
-
background: "rgba(255,255,255,0.03)",
|
|
237
|
+
flex: 1, padding: "10px 0", borderRadius: "8px",
|
|
238
|
+
border: "1px solid #2e2e2e", background: "rgba(255,255,255,0.03)",
|
|
276
239
|
color: "#999", fontSize: "13px", cursor: "pointer",
|
|
277
240
|
display: "flex", alignItems: "center", justifyContent: "center", gap: "7px",
|
|
278
|
-
letterSpacing: "0.01em",
|
|
279
|
-
fontFamily: "system-ui, sans-serif",
|
|
241
|
+
letterSpacing: "0.01em", fontFamily: "system-ui, sans-serif",
|
|
280
242
|
};
|
|
281
243
|
`;
|
|
282
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
|
+
|
|
283
348
|
export function revinePlugin(): any {
|
|
284
349
|
return {
|
|
285
350
|
name: "revine",
|
|
@@ -291,6 +356,27 @@ export function revinePlugin(): any {
|
|
|
291
356
|
}
|
|
292
357
|
},
|
|
293
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
|
+
|
|
294
380
|
load(id: string) {
|
|
295
381
|
if (id === VIRTUAL_ROUTING_ID) {
|
|
296
382
|
return `
|
|
@@ -390,7 +476,12 @@ routes.push({
|
|
|
390
476
|
errorElement: createElement(RevineErrorDialog),
|
|
391
477
|
});
|
|
392
478
|
|
|
393
|
-
export const router = createBrowserRouter(routes
|
|
479
|
+
export const router = createBrowserRouter(routes, {
|
|
480
|
+
future: {
|
|
481
|
+
v7_startTransition: true,
|
|
482
|
+
v7_relativeSplatPath: true,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
394
485
|
`;
|
|
395
486
|
}
|
|
396
487
|
},
|
package/template/src/root.tsx
CHANGED