Skip to content

Commit b12f96e

Browse files
committed
added error overlay
1 parent 6125cdc commit b12f96e

1 file changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// ErrorOverlay — a reusable global JavaScript error reporter.
2+
//
3+
// Drop `<ErrorOverlay />` anywhere in a `#universal` tree. On first client mount
4+
// it installs global handlers for `window.onerror`, the `error` event and
5+
// `unhandledrejection`, and shows a modal dialog with the full error message and
6+
// stack trace whenever an uncaught error occurs. A "Copy" button puts the whole
7+
// report on the clipboard so it can be pasted to a developer.
8+
//
9+
// It is intentionally dependency-free (no app imports) so it can live in the
10+
// shared `components` library and be reused by any Chemical webview app.
11+
//
12+
// Optional props:
13+
// title header text (default "Application Error")
14+
// max max number of captured errors kept (default 10)
15+
// variant accent color token (default "destructive")
16+
//
17+
// App code that swallows errors (e.g. a bridge `.catch`) can still surface them
18+
// by calling `window.__reportError(message, stack?)` — the overlay listens for
19+
// it and opens the dialog just like a real uncaught error.
20+
21+
func error_overlay_styles(page : &mut HtmlPage) : *char {
22+
return #css {
23+
position: fixed;
24+
inset: 0;
25+
z-index: 1000;
26+
display: flex;
27+
align-items: flex-start;
28+
justify-content: center;
29+
padding: 2rem 1rem;
30+
background: hsl(var(--background) / 0.6);
31+
backdrop-filter: blur(2px);
32+
overflow: auto;
33+
}
34+
}
35+
36+
func error_overlay_dialog_styles(page : &mut HtmlPage) : *char {
37+
return #css {
38+
width: 100%;
39+
max-width: 46rem;
40+
border-radius: var(--radius-lg);
41+
border: 1px solid hsl(var(--destructive) / 0.4);
42+
background: hsl(var(--card));
43+
color: hsl(var(--foreground));
44+
box-shadow: var(--shadow-lg);
45+
display: flex;
46+
flex-direction: column;
47+
max-height: 80vh;
48+
overflow: hidden;
49+
}
50+
}
51+
52+
func error_overlay_header_styles(page : &mut HtmlPage) : *char {
53+
return #css {
54+
display: flex;
55+
align-items: center;
56+
justify-content: space-between;
57+
gap: 0.75rem;
58+
padding: 0.875rem 1rem;
59+
border-bottom: 1px solid hsl(var(--border));
60+
background: hsl(var(--destructive) / 0.08);
61+
}
62+
}
63+
64+
func error_overlay_title_styles(page : &mut HtmlPage) : *char {
65+
return #css {
66+
font-weight: 600;
67+
font-size: 0.95rem;
68+
margin: 0;
69+
}
70+
}
71+
72+
func error_overlay_body_styles(page : &mut HtmlPage) : *char {
73+
return #css {
74+
padding: 1rem;
75+
overflow: auto;
76+
display: flex;
77+
flex-direction: column;
78+
gap: 0.75rem;
79+
}
80+
}
81+
82+
func error_overlay_msg_styles(page : &mut HtmlPage) : *char {
83+
return #css {
84+
font-weight: 600;
85+
font-size: 0.9rem;
86+
color: hsl(var(--destructive));
87+
margin: 0;
88+
white-space: pre-wrap;
89+
word-break: break-word;
90+
}
91+
}
92+
93+
func error_overlay_pre_styles(page : &mut HtmlPage) : *char {
94+
return #css {
95+
margin: 0;
96+
padding: 0.75rem;
97+
border-radius: var(--radius-md);
98+
background: hsl(var(--muted) / 0.5);
99+
border: 1px solid hsl(var(--border));
100+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
101+
font-size: 0.8rem;
102+
line-height: 1.35rem;
103+
white-space: pre-wrap;
104+
word-break: break-word;
105+
max-height: 40vh;
106+
overflow: auto;
107+
}
108+
}
109+
110+
func error_overlay_list_styles(page : &mut HtmlPage) : *char {
111+
return #css {
112+
display: flex;
113+
flex-wrap: wrap;
114+
gap: 0.4rem;
115+
}
116+
}
117+
118+
func error_overlay_chip_styles(page : &mut HtmlPage) : *char {
119+
return #css {
120+
font-size: 0.72rem;
121+
padding: 0.15rem 0.5rem;
122+
border-radius: 999px;
123+
border: 1px solid hsl(var(--border));
124+
background: hsl(var(--muted) / 0.4);
125+
cursor: pointer;
126+
}
127+
}
128+
129+
func error_overlay_footer_styles(page : &mut HtmlPage) : *char {
130+
return #css {
131+
display: flex;
132+
align-items: center;
133+
justify-content: flex-end;
134+
gap: 0.5rem;
135+
padding: 0.75rem 1rem;
136+
border-top: 1px solid hsl(var(--border));
137+
}
138+
}
139+
140+
public #universal ErrorOverlay(props) {
141+
var title = props.title || "Application Error"
142+
var max = props.max || 10
143+
state errors = []
144+
state open = false
145+
state selected = 0
146+
state copied = false
147+
148+
useEffect(() => {
149+
var pushError = (info) => {
150+
var next = errors.concat([info])
151+
if(next.length > max) { next = next.slice(next.length - max) }
152+
errors = next
153+
selected = next.length - 1
154+
open = true
155+
}
156+
var onError = (e) => {
157+
var err = (e && e.error) ? e.error : null
158+
var msg = (e && e.message) ? e.message : "Unknown error"
159+
var stack = (err && err.stack) ? err.stack : msg
160+
if(!err && e && (e.filename || e.lineno)) {
161+
stack = stack + "\n at " + (e.filename || "") + ":" + (e.lineno || 0) + ":" + (e.colno || 0)
162+
}
163+
pushError({
164+
time: new Date().toISOString(),
165+
message: msg,
166+
stack: stack,
167+
source: (e && e.filename) ? e.filename : "window.error",
168+
line: (e && e.lineno) ? e.lineno : 0,
169+
col: (e && e.colno) ? e.colno : 0
170+
})
171+
}
172+
var onRejection = (e) => {
173+
var reason = (e && e.reason) ? e.reason : "Unhandled promise rejection"
174+
var msg = (reason && reason.message) ? reason.message : ("" + reason)
175+
var stack = (reason && reason.stack) ? reason.stack : msg
176+
pushError({
177+
time: new Date().toISOString(),
178+
message: msg,
179+
stack: stack,
180+
source: "unhandledrejection",
181+
line: 0,
182+
col: 0
183+
})
184+
}
185+
if(window.addEventListener) {
186+
window.addEventListener("error", onError)
187+
window.addEventListener("unhandledrejection", onRejection)
188+
}
189+
window.__reportError = (msg, stack) => {
190+
pushError({
191+
time: new Date().toISOString(),
192+
message: (msg ? ("" + msg) : "Reported error"),
193+
stack: (stack ? ("" + stack) : ("" + msg)),
194+
source: "manual",
195+
line: 0,
196+
col: 0
197+
})
198+
}
199+
return () => {
200+
if(window.removeEventListener) {
201+
window.removeEventListener("error", onError)
202+
window.removeEventListener("unhandledrejection", onRejection)
203+
}
204+
}
205+
}, [])
206+
207+
if(!open) { return null }
208+
209+
var total = errors.length
210+
var idx = selected
211+
if(idx < 0) { idx = 0 }
212+
if(idx >= total) { idx = total - 1 }
213+
var current = errors[idx]
214+
var reportText = (current.message || "") + "\n\n" + (current.stack || "") + "\n\n[source: " + (current.source || "") + " | time: " + (current.time || "") + "]"
215+
216+
var copyReport = () => {
217+
try {
218+
if(navigator.clipboard && navigator.clipboard.writeText) {
219+
navigator.clipboard.writeText(reportText)
220+
}
221+
} catch (e) { }
222+
copied = true
223+
setTimeout(() => { copied = false }, 2000)
224+
}
225+
var dismiss = () => { open = false }
226+
227+
return <div class={error_overlay_styles(page)} onClick={dismiss}>
228+
<div class={error_overlay_dialog_styles(page)} onClick={(e) => { e.stopPropagation() }}>
229+
<div class={error_overlay_header_styles(page)}>
230+
<h3 class={error_overlay_title_styles(page)}>{title}</h3>
231+
<button type="button" class="cdm-btn" onClick={dismiss}>{"×"}</button>
232+
</div>
233+
<div class={error_overlay_body_styles(page)}>
234+
{total > 1 ? <div class={error_overlay_list_styles(page)}>
235+
{errors.map((er, i) => (
236+
<span class={error_overlay_chip_styles(page)} onClick={() => { selected = i }}>
237+
{("#" + (i + 1) + " " + (er.source || "err"))}
238+
</span>
239+
))}
240+
</div> : null}
241+
<p class={error_overlay_msg_styles(page)}>{current.message}</p>
242+
<pre class={error_overlay_pre_styles(page)}>{current.stack}</pre>
243+
</div>
244+
<div class={error_overlay_footer_styles(page)}>
245+
<span style={{ fontSize: "0.72rem", color: "hsl(var(--muted-foreground))" }}>{"source: " + (current.source || "") + " @ " + (current.time || "")}</span>
246+
<button type="button" class="cdm-btn" onClick={copyReport}>{copied ? "Copied!" : "Copy report"}</button>
247+
<button type="button" class="cdm-btn" data-variant="primary" onClick={dismiss}>{"Dismiss"}</button>
248+
</div>
249+
</div>
250+
</div>
251+
}

0 commit comments

Comments
 (0)