@alepha/react 0.13.6 → 0.13.8
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 +105 -103
- package/dist/auth/index.js +5 -5
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.browser.js +407 -142
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +144 -116
- package/dist/core/index.js +409 -145
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +24 -2
- 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 +18 -18
- package/dist/head/index.js +5 -1
- package/dist/head/index.js.map +1 -1
- package/dist/i18n/index.d.ts +25 -25
- package/dist/i18n/index.js +4 -3
- package/dist/i18n/index.js.map +1 -1
- package/dist/websocket/index.d.ts +1 -1
- package/package.json +22 -23
- 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/AlephaProvider.tsx +41 -0
- package/src/core/contexts/RouterLayerContext.ts +2 -0
- package/src/core/hooks/useAction.ts +4 -1
- package/src/core/index.shared.ts +1 -0
- 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
|
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { AlephaContext } from "./AlephaContext.ts";
|
|
4
|
+
|
|
5
|
+
export interface AlephaProviderProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
onError: (error: Error) => ReactNode;
|
|
8
|
+
onLoading: () => ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AlephaProvider component to initialize and provide Alepha instance to the app.
|
|
13
|
+
* This isn't recommended for apps using alepha/react/router, as Router will handle this for you.
|
|
14
|
+
*/
|
|
15
|
+
export const AlephaProvider = (props: AlephaProviderProps) => {
|
|
16
|
+
const alepha = useMemo(() => Alepha.create(), []);
|
|
17
|
+
|
|
18
|
+
const [started, setStarted] = useState(false);
|
|
19
|
+
const [error, setError] = useState<Error | undefined>();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
alepha
|
|
23
|
+
.start()
|
|
24
|
+
.then(() => setStarted(true))
|
|
25
|
+
.catch((err) => setError(err));
|
|
26
|
+
}, [alepha]);
|
|
27
|
+
|
|
28
|
+
if (error) {
|
|
29
|
+
return props.onError(error);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!started) {
|
|
33
|
+
return props.onLoading();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<AlephaContext.Provider value={alepha}>
|
|
38
|
+
{props.children}
|
|
39
|
+
</AlephaContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -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
|
package/src/core/index.shared.ts
CHANGED