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 CHANGED
@@ -1,9 +1,11 @@
1
- export { Outlet, RouterProvider, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
2
- export type { NavLinkProps } from "./components/NavLink.js";
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
@@ -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,EAChB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC"}
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":"AA0RA,wBAAgB,YAAY,IAAI,GAAG,CAmHlC"}
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
- border: "1px solid #2e2e2e",
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
- border: "1px solid #222",
246
- borderRadius: "8px",
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
- borderRadius: "8px",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "revine",
3
- "version": "1.1.4",
3
+ "version": "1.2.1",
4
4
  "description": "A react framework, but better.",
5
5
  "license": "MIT",
6
6
  "author": "Rachit Bharadwaj",
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 type { NavLinkProps } from "./components/NavLink.js";
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
- border: "1px solid #2e2e2e",
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
- border: "1px solid #222",
247
- borderRadius: "8px",
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
- borderRadius: "8px",
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
  },
@@ -9,6 +9,11 @@ const root = createRoot(container);
9
9
 
10
10
  root.render(
11
11
  <React.StrictMode>
12
- <RouterProvider router={router} />
12
+ <RouterProvider
13
+ router={router}
14
+ future={{
15
+ v7_startTransition: true,
16
+ }}
17
+ />
13
18
  </React.StrictMode>,
14
- );
19
+ );