@alepha/react 0.13.6 → 0.13.7
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/auth/index.browser.js +5 -5
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/index.js +5 -5
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.browser.js +385 -141
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +105 -89
- package/dist/core/index.js +387 -144
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/form/index.d.ts +14 -6
- package/dist/form/index.js +32 -12
- package/dist/form/index.js.map +1 -1
- package/dist/head/index.d.ts +1 -1
- package/dist/head/index.js +5 -1
- package/dist/head/index.js.map +1 -1
- package/dist/i18n/index.d.ts +3 -3
- package/dist/i18n/index.js +4 -3
- package/dist/i18n/index.js.map +1 -1
- package/dist/websocket/index.d.ts +20 -20
- package/package.json +6 -5
- package/src/auth/hooks/useAuth.ts +1 -0
- package/src/auth/services/ReactAuth.ts +6 -4
- package/src/core/components/ErrorViewer.tsx +378 -130
- package/src/core/components/NestedView.tsx +16 -11
- package/src/core/contexts/RouterLayerContext.ts +2 -0
- package/src/core/hooks/useAction.ts +4 -1
- package/src/core/primitives/$page.ts +15 -2
- package/src/core/providers/ReactPageProvider.ts +6 -7
- package/src/core/providers/ReactServerProvider.ts +2 -6
- package/src/form/services/FormModel.ts +81 -26
- package/src/head/index.ts +2 -1
- package/src/i18n/providers/I18nProvider.ts +4 -2
|
@@ -1,163 +1,411 @@
|
|
|
1
1
|
import type { Alepha } from "alepha";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { type CSSProperties, useState } from "react";
|
|
3
3
|
|
|
4
4
|
interface ErrorViewerProps {
|
|
5
5
|
error: Error;
|
|
6
6
|
alepha: Alepha;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
interface StackFrame {
|
|
10
|
+
fn: string;
|
|
11
|
+
file: string;
|
|
12
|
+
line: string;
|
|
13
|
+
col: string;
|
|
14
|
+
raw: string;
|
|
15
|
+
}
|
|
10
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Error viewer component that displays error details in development mode
|
|
19
|
+
*/
|
|
11
20
|
const ErrorViewer = ({ error, alepha }: ErrorViewerProps) => {
|
|
12
21
|
const [expanded, setExpanded] = useState(false);
|
|
13
22
|
const isProduction = alepha.isProduction();
|
|
14
|
-
// const status = isHttpError(error) ? error.status : 500;
|
|
15
23
|
|
|
16
24
|
if (isProduction) {
|
|
17
25
|
return <ErrorViewerProduction />;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
28
|
+
const frames = parseStackTrace(error.stack);
|
|
29
|
+
const visibleFrames = expanded ? frames : frames.slice(0, 6);
|
|
30
|
+
const hiddenCount = frames.length - 6;
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
return (
|
|
33
|
+
<div style={styles.overlay}>
|
|
34
|
+
<div style={styles.container}>
|
|
35
|
+
<Header error={error} />
|
|
36
|
+
<StackTraceSection
|
|
37
|
+
frames={frames}
|
|
38
|
+
visibleFrames={visibleFrames}
|
|
39
|
+
expanded={expanded}
|
|
40
|
+
hiddenCount={hiddenCount}
|
|
41
|
+
onToggle={() => setExpanded(!expanded)}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default ErrorViewer;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse stack trace string into structured frames
|
|
52
|
+
*/
|
|
53
|
+
function parseStackTrace(stack?: string): StackFrame[] {
|
|
54
|
+
if (!stack) return [];
|
|
55
|
+
|
|
56
|
+
const lines = stack.split("\n").slice(1);
|
|
57
|
+
const frames: StackFrame[] = [];
|
|
58
|
+
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (!trimmed.startsWith("at ")) continue;
|
|
62
|
+
|
|
63
|
+
const frame = parseStackLine(trimmed);
|
|
64
|
+
if (frame) frames.push(frame);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return frames;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse a single stack trace line into a structured frame
|
|
72
|
+
*/
|
|
73
|
+
function parseStackLine(line: string): StackFrame | null {
|
|
74
|
+
const withFn = line.match(/^at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/);
|
|
75
|
+
if (withFn) {
|
|
76
|
+
return {
|
|
77
|
+
fn: withFn[1],
|
|
78
|
+
file: withFn[2],
|
|
79
|
+
line: withFn[3],
|
|
80
|
+
col: withFn[4],
|
|
81
|
+
raw: line,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const withoutFn = line.match(/^at\s+(.+):(\d+):(\d+)$/);
|
|
86
|
+
if (withoutFn) {
|
|
87
|
+
return {
|
|
88
|
+
fn: "<anonymous>",
|
|
89
|
+
file: withoutFn[1],
|
|
90
|
+
line: withoutFn[2],
|
|
91
|
+
col: withoutFn[3],
|
|
92
|
+
raw: line,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { fn: "", file: line.replace(/^at\s+/, ""), line: "", col: "", raw: line };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Copy text to clipboard
|
|
101
|
+
*/
|
|
102
|
+
function copyToClipboard(text: string): void {
|
|
103
|
+
navigator.clipboard.writeText(text).catch((err) => {
|
|
104
|
+
console.error("Clipboard error:", err);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Header section with error type and message
|
|
110
|
+
*/
|
|
111
|
+
function Header({ error }: { error: Error }) {
|
|
112
|
+
const [copied, setCopied] = useState(false);
|
|
29
113
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
color: "#7F1D1D",
|
|
35
|
-
border: "1px solid #FECACA",
|
|
36
|
-
borderRadius: "16px",
|
|
37
|
-
boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
|
|
38
|
-
fontFamily: "monospace",
|
|
39
|
-
maxWidth: "768px",
|
|
40
|
-
margin: "40px auto",
|
|
41
|
-
},
|
|
42
|
-
heading: {
|
|
43
|
-
fontSize: "20px",
|
|
44
|
-
fontWeight: "bold",
|
|
45
|
-
marginBottom: "10px",
|
|
46
|
-
},
|
|
47
|
-
name: {
|
|
48
|
-
fontSize: "16px",
|
|
49
|
-
fontWeight: 600,
|
|
50
|
-
},
|
|
51
|
-
message: {
|
|
52
|
-
fontSize: "14px",
|
|
53
|
-
marginBottom: "16px",
|
|
54
|
-
},
|
|
55
|
-
sectionHeader: {
|
|
56
|
-
display: "flex",
|
|
57
|
-
justifyContent: "space-between",
|
|
58
|
-
alignItems: "center",
|
|
59
|
-
fontSize: "12px",
|
|
60
|
-
marginBottom: "4px",
|
|
61
|
-
color: "#991B1B",
|
|
62
|
-
},
|
|
63
|
-
copyButton: {
|
|
64
|
-
fontSize: "12px",
|
|
65
|
-
color: "#DC2626",
|
|
66
|
-
background: "none",
|
|
67
|
-
border: "none",
|
|
68
|
-
cursor: "pointer",
|
|
69
|
-
textDecoration: "underline",
|
|
70
|
-
},
|
|
71
|
-
stackContainer: {
|
|
72
|
-
backgroundColor: "#FEE2E2",
|
|
73
|
-
padding: "12px",
|
|
74
|
-
borderRadius: "8px",
|
|
75
|
-
fontSize: "13px",
|
|
76
|
-
lineHeight: "1.4",
|
|
77
|
-
overflowX: "auto" as const,
|
|
78
|
-
whiteSpace: "pre-wrap" as const,
|
|
79
|
-
},
|
|
80
|
-
expandLine: {
|
|
81
|
-
color: "#F87171",
|
|
82
|
-
cursor: "pointer",
|
|
83
|
-
marginTop: "8px",
|
|
84
|
-
},
|
|
114
|
+
const handleCopy = () => {
|
|
115
|
+
copyToClipboard(error.stack || error.message);
|
|
116
|
+
setCopied(true);
|
|
117
|
+
setTimeout(() => setCopied(false), 2000);
|
|
85
118
|
};
|
|
86
119
|
|
|
87
120
|
return (
|
|
88
|
-
<div style={styles.
|
|
89
|
-
<div>
|
|
90
|
-
<div style={styles.
|
|
91
|
-
<
|
|
92
|
-
|
|
121
|
+
<div style={styles.header}>
|
|
122
|
+
<div style={styles.headerTop}>
|
|
123
|
+
<div style={styles.badge}>{error.name}</div>
|
|
124
|
+
<button type="button" onClick={handleCopy} style={styles.copyBtn}>
|
|
125
|
+
{copied ? "Copied" : "Copy Stack"}
|
|
126
|
+
</button>
|
|
93
127
|
</div>
|
|
128
|
+
<h1 style={styles.message}>{error.message}</h1>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
94
132
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Stack trace section with expandable frames
|
|
135
|
+
*/
|
|
136
|
+
function StackTraceSection({
|
|
137
|
+
frames,
|
|
138
|
+
visibleFrames,
|
|
139
|
+
expanded,
|
|
140
|
+
hiddenCount,
|
|
141
|
+
onToggle,
|
|
142
|
+
}: {
|
|
143
|
+
frames: StackFrame[];
|
|
144
|
+
visibleFrames: StackFrame[];
|
|
145
|
+
expanded: boolean;
|
|
146
|
+
hiddenCount: number;
|
|
147
|
+
onToggle: () => void;
|
|
148
|
+
}) {
|
|
149
|
+
if (frames.length === 0) return null;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div style={styles.stackSection}>
|
|
153
|
+
<div style={styles.stackHeader}>Call Stack</div>
|
|
154
|
+
<div style={styles.frameList}>
|
|
155
|
+
{visibleFrames.map((frame, i) => (
|
|
156
|
+
<StackFrameRow key={i} frame={frame} index={i} />
|
|
157
|
+
))}
|
|
158
|
+
{!expanded && hiddenCount > 0 && (
|
|
159
|
+
<button type="button" onClick={onToggle} style={styles.expandBtn}>
|
|
160
|
+
Show {hiddenCount} more frames
|
|
161
|
+
</button>
|
|
162
|
+
)}
|
|
163
|
+
{expanded && hiddenCount > 0 && (
|
|
164
|
+
<button type="button" onClick={onToggle} style={styles.expandBtn}>
|
|
165
|
+
Show less
|
|
166
|
+
</button>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
119
169
|
</div>
|
|
120
170
|
);
|
|
121
|
-
}
|
|
171
|
+
}
|
|
122
172
|
|
|
123
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Single stack frame row
|
|
175
|
+
*/
|
|
176
|
+
function StackFrameRow({ frame, index }: { frame: StackFrame; index: number }) {
|
|
177
|
+
const isFirst = index === 0;
|
|
178
|
+
const fileName = frame.file.split("/").pop() || frame.file;
|
|
179
|
+
const dirPath = frame.file.substring(0, frame.file.length - fileName.length);
|
|
124
180
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
},
|
|
153
|
-
};
|
|
181
|
+
return (
|
|
182
|
+
<div
|
|
183
|
+
style={{
|
|
184
|
+
...styles.frame,
|
|
185
|
+
...(isFirst ? styles.frameFirst : {}),
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<div style={styles.frameIndex}>{index + 1}</div>
|
|
189
|
+
<div style={styles.frameContent}>
|
|
190
|
+
{frame.fn && (
|
|
191
|
+
<div style={styles.fnName}>
|
|
192
|
+
{frame.fn}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
<div style={styles.filePath}>
|
|
196
|
+
<span style={styles.dirPath}>{dirPath}</span>
|
|
197
|
+
<span style={styles.fileName}>{fileName}</span>
|
|
198
|
+
{frame.line && (
|
|
199
|
+
<span style={styles.lineCol}>
|
|
200
|
+
:{frame.line}:{frame.col}
|
|
201
|
+
</span>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
154
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Production error view - minimal information
|
|
211
|
+
*/
|
|
212
|
+
function ErrorViewerProduction() {
|
|
155
213
|
return (
|
|
156
|
-
<div style={styles.
|
|
157
|
-
<div style={styles.
|
|
158
|
-
|
|
159
|
-
|
|
214
|
+
<div style={styles.overlay}>
|
|
215
|
+
<div style={styles.prodContainer}>
|
|
216
|
+
<div style={styles.prodIcon}>!</div>
|
|
217
|
+
<h1 style={styles.prodTitle}>Application Error</h1>
|
|
218
|
+
<p style={styles.prodMessage}>
|
|
219
|
+
An unexpected error occurred. Please try again later.
|
|
220
|
+
</p>
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
onClick={() => window.location.reload()}
|
|
224
|
+
style={styles.prodButton}
|
|
225
|
+
>
|
|
226
|
+
Reload Page
|
|
227
|
+
</button>
|
|
160
228
|
</div>
|
|
161
229
|
</div>
|
|
162
230
|
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const styles: Record<string, CSSProperties> = {
|
|
234
|
+
overlay: {
|
|
235
|
+
position: "fixed",
|
|
236
|
+
inset: 0,
|
|
237
|
+
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
238
|
+
display: "flex",
|
|
239
|
+
alignItems: "flex-start",
|
|
240
|
+
justifyContent: "center",
|
|
241
|
+
padding: "40px 20px",
|
|
242
|
+
overflow: "auto",
|
|
243
|
+
fontFamily:
|
|
244
|
+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
245
|
+
zIndex: 99999,
|
|
246
|
+
},
|
|
247
|
+
container: {
|
|
248
|
+
width: "100%",
|
|
249
|
+
maxWidth: "960px",
|
|
250
|
+
backgroundColor: "#1a1a1a",
|
|
251
|
+
borderRadius: "12px",
|
|
252
|
+
overflow: "hidden",
|
|
253
|
+
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
|
|
254
|
+
},
|
|
255
|
+
header: {
|
|
256
|
+
padding: "24px 28px",
|
|
257
|
+
borderBottom: "1px solid #333",
|
|
258
|
+
background: "linear-gradient(to bottom, #1f1f1f, #1a1a1a)",
|
|
259
|
+
},
|
|
260
|
+
headerTop: {
|
|
261
|
+
display: "flex",
|
|
262
|
+
alignItems: "center",
|
|
263
|
+
justifyContent: "space-between",
|
|
264
|
+
marginBottom: "16px",
|
|
265
|
+
},
|
|
266
|
+
badge: {
|
|
267
|
+
display: "inline-block",
|
|
268
|
+
padding: "6px 12px",
|
|
269
|
+
backgroundColor: "#dc2626",
|
|
270
|
+
color: "#fff",
|
|
271
|
+
fontSize: "12px",
|
|
272
|
+
fontWeight: 600,
|
|
273
|
+
borderRadius: "6px",
|
|
274
|
+
letterSpacing: "0.025em",
|
|
275
|
+
},
|
|
276
|
+
copyBtn: {
|
|
277
|
+
padding: "8px 16px",
|
|
278
|
+
backgroundColor: "transparent",
|
|
279
|
+
color: "#888",
|
|
280
|
+
fontSize: "13px",
|
|
281
|
+
fontWeight: 500,
|
|
282
|
+
border: "1px solid #444",
|
|
283
|
+
borderRadius: "6px",
|
|
284
|
+
cursor: "pointer",
|
|
285
|
+
transition: "all 0.15s",
|
|
286
|
+
},
|
|
287
|
+
message: {
|
|
288
|
+
margin: 0,
|
|
289
|
+
fontSize: "20px",
|
|
290
|
+
fontWeight: 500,
|
|
291
|
+
color: "#fff",
|
|
292
|
+
lineHeight: 1.5,
|
|
293
|
+
wordBreak: "break-word",
|
|
294
|
+
},
|
|
295
|
+
stackSection: {
|
|
296
|
+
padding: "0",
|
|
297
|
+
},
|
|
298
|
+
stackHeader: {
|
|
299
|
+
padding: "16px 28px",
|
|
300
|
+
fontSize: "11px",
|
|
301
|
+
fontWeight: 600,
|
|
302
|
+
color: "#666",
|
|
303
|
+
textTransform: "uppercase",
|
|
304
|
+
letterSpacing: "0.1em",
|
|
305
|
+
borderBottom: "1px solid #2a2a2a",
|
|
306
|
+
},
|
|
307
|
+
frameList: {
|
|
308
|
+
display: "flex",
|
|
309
|
+
flexDirection: "column",
|
|
310
|
+
},
|
|
311
|
+
frame: {
|
|
312
|
+
display: "flex",
|
|
313
|
+
alignItems: "flex-start",
|
|
314
|
+
padding: "14px 28px",
|
|
315
|
+
borderBottom: "1px solid #252525",
|
|
316
|
+
transition: "background-color 0.15s",
|
|
317
|
+
},
|
|
318
|
+
frameFirst: {
|
|
319
|
+
backgroundColor: "rgba(220, 38, 38, 0.1)",
|
|
320
|
+
},
|
|
321
|
+
frameIndex: {
|
|
322
|
+
width: "28px",
|
|
323
|
+
flexShrink: 0,
|
|
324
|
+
fontSize: "12px",
|
|
325
|
+
fontWeight: 500,
|
|
326
|
+
color: "#555",
|
|
327
|
+
fontFamily: "monospace",
|
|
328
|
+
},
|
|
329
|
+
frameContent: {
|
|
330
|
+
flex: 1,
|
|
331
|
+
minWidth: 0,
|
|
332
|
+
},
|
|
333
|
+
fnName: {
|
|
334
|
+
fontSize: "14px",
|
|
335
|
+
fontWeight: 500,
|
|
336
|
+
color: "#e5e5e5",
|
|
337
|
+
marginBottom: "4px",
|
|
338
|
+
fontFamily:
|
|
339
|
+
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
340
|
+
},
|
|
341
|
+
filePath: {
|
|
342
|
+
fontSize: "13px",
|
|
343
|
+
color: "#888",
|
|
344
|
+
fontFamily:
|
|
345
|
+
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
346
|
+
wordBreak: "break-all",
|
|
347
|
+
},
|
|
348
|
+
dirPath: {
|
|
349
|
+
color: "#555",
|
|
350
|
+
},
|
|
351
|
+
fileName: {
|
|
352
|
+
color: "#0ea5e9",
|
|
353
|
+
},
|
|
354
|
+
lineCol: {
|
|
355
|
+
color: "#eab308",
|
|
356
|
+
},
|
|
357
|
+
expandBtn: {
|
|
358
|
+
padding: "16px 28px",
|
|
359
|
+
backgroundColor: "transparent",
|
|
360
|
+
color: "#666",
|
|
361
|
+
fontSize: "13px",
|
|
362
|
+
fontWeight: 500,
|
|
363
|
+
border: "none",
|
|
364
|
+
borderTop: "1px solid #252525",
|
|
365
|
+
cursor: "pointer",
|
|
366
|
+
textAlign: "left",
|
|
367
|
+
transition: "all 0.15s",
|
|
368
|
+
},
|
|
369
|
+
prodContainer: {
|
|
370
|
+
textAlign: "center",
|
|
371
|
+
padding: "60px 40px",
|
|
372
|
+
backgroundColor: "#1a1a1a",
|
|
373
|
+
borderRadius: "12px",
|
|
374
|
+
maxWidth: "400px",
|
|
375
|
+
},
|
|
376
|
+
prodIcon: {
|
|
377
|
+
width: "64px",
|
|
378
|
+
height: "64px",
|
|
379
|
+
margin: "0 auto 24px",
|
|
380
|
+
backgroundColor: "#dc2626",
|
|
381
|
+
borderRadius: "50%",
|
|
382
|
+
display: "flex",
|
|
383
|
+
alignItems: "center",
|
|
384
|
+
justifyContent: "center",
|
|
385
|
+
fontSize: "32px",
|
|
386
|
+
fontWeight: 700,
|
|
387
|
+
color: "#fff",
|
|
388
|
+
},
|
|
389
|
+
prodTitle: {
|
|
390
|
+
margin: "0 0 12px",
|
|
391
|
+
fontSize: "24px",
|
|
392
|
+
fontWeight: 600,
|
|
393
|
+
color: "#fff",
|
|
394
|
+
},
|
|
395
|
+
prodMessage: {
|
|
396
|
+
margin: "0 0 28px",
|
|
397
|
+
fontSize: "15px",
|
|
398
|
+
color: "#888",
|
|
399
|
+
lineHeight: 1.6,
|
|
400
|
+
},
|
|
401
|
+
prodButton: {
|
|
402
|
+
padding: "12px 24px",
|
|
403
|
+
backgroundColor: "#fff",
|
|
404
|
+
color: "#000",
|
|
405
|
+
fontSize: "14px",
|
|
406
|
+
fontWeight: 600,
|
|
407
|
+
border: "none",
|
|
408
|
+
borderRadius: "8px",
|
|
409
|
+
cursor: "pointer",
|
|
410
|
+
},
|
|
163
411
|
};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { memo, type ReactNode, use, useRef, useState } from "react";
|
|
2
2
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
3
|
-
import type { PageAnimation } from "../primitives/$page.ts";
|
|
3
|
+
import type { ErrorHandler, PageAnimation } from "../primitives/$page.ts";
|
|
4
4
|
import { Redirection } from "../errors/Redirection.ts";
|
|
5
5
|
import { useEvents } from "../hooks/useEvents.ts";
|
|
6
6
|
import { useRouterState } from "../hooks/useRouterState.ts";
|
|
7
7
|
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
8
8
|
import ErrorBoundary from "./ErrorBoundary.tsx";
|
|
9
|
+
import ErrorViewer from "./ErrorViewer.tsx";
|
|
10
|
+
import { useAlepha } from "../hooks/useAlepha.ts";
|
|
9
11
|
|
|
10
12
|
export interface NestedViewProps {
|
|
11
13
|
children?: ReactNode;
|
|
@@ -34,8 +36,11 @@ export interface NestedViewProps {
|
|
|
34
36
|
* ```
|
|
35
37
|
*/
|
|
36
38
|
const NestedView = (props: NestedViewProps) => {
|
|
37
|
-
const
|
|
39
|
+
const routerLayer = use(RouterLayerContext);
|
|
40
|
+
const index = routerLayer?.index ?? 0;
|
|
41
|
+
const onError = routerLayer?.onError;
|
|
38
42
|
const state = useRouterState();
|
|
43
|
+
const alepha = useAlepha();
|
|
39
44
|
|
|
40
45
|
const [view, setView] = useState<ReactNode | undefined>(
|
|
41
46
|
state.layers[index]?.element,
|
|
@@ -148,16 +153,16 @@ const NestedView = (props: NestedViewProps) => {
|
|
|
148
153
|
);
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
const fallback = (error: Error) => {
|
|
157
|
+
const result = onError?.(error, state) ?? <ErrorViewer error={error} alepha={alepha}/>;
|
|
158
|
+
if (result instanceof Redirection) {
|
|
159
|
+
return "Redirection inside ErrorBoundary is not allowed.";
|
|
160
|
+
}
|
|
161
|
+
return result as ReactNode;
|
|
162
|
+
}
|
|
163
|
+
|
|
151
164
|
return (
|
|
152
|
-
<ErrorBoundary
|
|
153
|
-
fallback={(error) => {
|
|
154
|
-
const result = state.onError(error, state); // TODO: onError is not refreshed
|
|
155
|
-
if (result instanceof Redirection) {
|
|
156
|
-
return "Redirection inside ErrorBoundary is not allowed.";
|
|
157
|
-
}
|
|
158
|
-
return result as ReactNode;
|
|
159
|
-
}}
|
|
160
|
-
>
|
|
165
|
+
<ErrorBoundary fallback={fallback}>
|
|
161
166
|
{element}
|
|
162
167
|
</ErrorBoundary>
|
|
163
168
|
);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createContext } from "react";
|
|
2
|
+
import type { ErrorHandler } from "../primitives/$page.ts";
|
|
2
3
|
|
|
3
4
|
export interface RouterLayerContextValue {
|
|
4
5
|
index: number;
|
|
5
6
|
path: string;
|
|
7
|
+
onError: ErrorHandler;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export const RouterLayerContext = createContext<
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "react";
|
|
14
14
|
import { useAlepha } from "./useAlepha.ts";
|
|
15
15
|
import { useInject } from "./useInject.ts";
|
|
16
|
+
import type { Async } from "alepha";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Hook for handling async actions with automatic error handling and event emission.
|
|
@@ -361,7 +362,7 @@ export interface UseActionOptions<Args extends any[] = any[], Result = any> {
|
|
|
361
362
|
* The async action handler function.
|
|
362
363
|
* Receives the action arguments plus an ActionContext as the last parameter.
|
|
363
364
|
*/
|
|
364
|
-
handler: (...args: [...Args, ActionContext]) =>
|
|
365
|
+
handler: (...args: [...Args, ActionContext]) => Async<Result>;
|
|
365
366
|
|
|
366
367
|
/**
|
|
367
368
|
* Custom error handler. If provided, prevents default error re-throw.
|
|
@@ -378,6 +379,8 @@ export interface UseActionOptions<Args extends any[] = any[], Result = any> {
|
|
|
378
379
|
*/
|
|
379
380
|
id?: string;
|
|
380
381
|
|
|
382
|
+
name?: string;
|
|
383
|
+
|
|
381
384
|
/**
|
|
382
385
|
* Debounce delay in milliseconds. If specified, the action will only execute
|
|
383
386
|
* after the specified delay has passed since the last call. Useful for search inputs
|
|
@@ -150,10 +150,10 @@ export interface PagePrimitiveOptions<
|
|
|
150
150
|
* Load data before rendering the page.
|
|
151
151
|
*
|
|
152
152
|
* This function receives
|
|
153
|
-
* - the request context
|
|
153
|
+
* - the request context (params, query, etc.)
|
|
154
154
|
* - the parent props (if page has a parent)
|
|
155
155
|
*
|
|
156
|
-
* In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
|
|
156
|
+
* > In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
|
|
157
157
|
*
|
|
158
158
|
* Resolve can be stopped by throwing an error, which will be handled by the `errorHandler` function.
|
|
159
159
|
* It's common to throw a `NotFoundError` to display a 404 page.
|
|
@@ -162,6 +162,13 @@ export interface PagePrimitiveOptions<
|
|
|
162
162
|
*/
|
|
163
163
|
resolve?: (context: PageResolve<TConfig, TPropsParent>) => Async<TProps>;
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Default props to pass to the component when rendering the page.
|
|
167
|
+
*
|
|
168
|
+
* Resolved props from the `resolve` function will override these default props.
|
|
169
|
+
*/
|
|
170
|
+
props?: () => Partial<TProps>;
|
|
171
|
+
|
|
165
172
|
/**
|
|
166
173
|
* The component to render when the page is loaded.
|
|
167
174
|
*
|
|
@@ -189,6 +196,12 @@ export interface PagePrimitiveOptions<
|
|
|
189
196
|
*/
|
|
190
197
|
parent?: PagePrimitive<PageConfigSchema, TPropsParent, any>;
|
|
191
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Function to determine if the page can be accessed.
|
|
201
|
+
*
|
|
202
|
+
* If it returns false, the page will not be accessible and a 403 Forbidden error will be returned.
|
|
203
|
+
* This function can be used to implement permission-based access control.
|
|
204
|
+
*/
|
|
192
205
|
can?: () => boolean;
|
|
193
206
|
|
|
194
207
|
/**
|