|
9 | 9 | // It is intentionally dependency-free (no app imports) so it can live in the |
10 | 10 | // shared `components` library and be reused by any Chemical webview app. |
11 | 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 | | -} |
| 12 | +// IMPORTANT: visibility is expressed as a JSX conditional used DIRECTLY as a |
| 13 | +// child expression (`{open && errors.length > 0 ? <div>…</div> : null}`). This |
| 14 | +// framework runs component bodies once and only re-renders reactive (derived) |
| 15 | +// JSX nodes, so a control-flow `if(!open) return null` would never update when |
| 16 | +// `open` flips. The styling uses inline `style={{...}}` objects (not `#css` |
| 17 | +// helpers) because `#css` needs a server-only `page` pointer that is absent |
| 18 | +// during client hydration. |
139 | 19 |
|
140 | 20 | public #universal ErrorOverlay(props) { |
141 | 21 | var title = props.title || "Application Error" |
@@ -204,48 +84,74 @@ public #universal ErrorOverlay(props) { |
204 | 84 | } |
205 | 85 | }, []) |
206 | 86 |
|
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 | | - |
| 87 | + var dismiss = () => { open = false } |
216 | 88 | var copyReport = () => { |
| 89 | + var cur = (errors.length > 0) ? errors[selected] : null |
| 90 | + var report = (cur ? (cur.message || "") : "") + "\n\n" + (cur ? (cur.stack || "") : "") + "\n\n[source: " + (cur ? (cur.source || "") : "") + " | time: " + (cur ? (cur.time || "") : "") + "]" |
217 | 91 | try { |
218 | 92 | if(navigator.clipboard && navigator.clipboard.writeText) { |
219 | | - navigator.clipboard.writeText(reportText) |
| 93 | + navigator.clipboard.writeText(report) |
220 | 94 | } |
221 | 95 | } catch (e) { } |
222 | 96 | copied = true |
223 | 97 | setTimeout(() => { copied = false }, 2000) |
224 | 98 | } |
225 | | - var dismiss = () => { open = false } |
226 | 99 |
|
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> |
| 100 | + return <div style={{ display: "contents" }}> |
| 101 | + {open && errors.length > 0 ? <div style={{ |
| 102 | + position: "fixed", |
| 103 | + inset: "0", |
| 104 | + zIndex: "1000", |
| 105 | + display: "flex", |
| 106 | + alignItems: "flex-start", |
| 107 | + justifyContent: "center", |
| 108 | + padding: "2rem 1rem", |
| 109 | + background: "rgba(0,0,0,0.55)", |
| 110 | + overflow: "auto" |
| 111 | + }} onClick={dismiss}> |
| 112 | + <div style={{ |
| 113 | + width: "100%", |
| 114 | + maxWidth: "46rem", |
| 115 | + borderRadius: "12px", |
| 116 | + border: "1px solid #e11d48", |
| 117 | + background: "#ffffff", |
| 118 | + color: "#111111", |
| 119 | + boxShadow: "0 10px 30px rgba(0,0,0,0.35)", |
| 120 | + display: "flex", |
| 121 | + flexDirection: "column", |
| 122 | + maxHeight: "80vh", |
| 123 | + overflow: "hidden", |
| 124 | + marginTop: "2rem" |
| 125 | + }} onClick={(e) => { e.stopPropagation() }}> |
| 126 | + <div style={{ |
| 127 | + display: "flex", |
| 128 | + alignItems: "center", |
| 129 | + justifyContent: "space-between", |
| 130 | + gap: "0.75rem", |
| 131 | + padding: "0.875rem 1rem", |
| 132 | + borderBottom: "1px solid #e5e7eb", |
| 133 | + background: "rgba(225,29,72,0.08)" |
| 134 | + }}> |
| 135 | + <h3 style={{ fontWeight: 600, fontSize: "0.95rem", margin: 0 }}>{title}</h3> |
| 136 | + <button type="button" style={{ border: "none", background: "transparent", fontSize: "1.1rem", cursor: "pointer", color: "#111111" }} onClick={dismiss}>{"×"}</button> |
| 137 | + </div> |
| 138 | + <div style={{ padding: "1rem", overflow: "auto", display: "flex", flexDirection: "column", gap: "0.75rem" }}> |
| 139 | + {errors.length > 1 ? <div style={{ display: "flex", flexWrap: "wrap", gap: "0.4rem" }}> |
| 140 | + {errors.map((er, i) => ( |
| 141 | + <span style={{ fontSize: "0.72rem", padding: "0.15rem 0.5rem", borderRadius: "999px", border: "1px solid #e5e7eb", background: "#f3f4f6", cursor: "pointer" }} onClick={() => { selected = i }}> |
| 142 | + {("#" + (i + 1) + " " + (er.source || "err"))} |
| 143 | + </span> |
| 144 | + ))} |
| 145 | + </div> : null} |
| 146 | + <p style={{ fontWeight: 600, fontSize: "0.9rem", color: "#e11d48", margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>{errors[selected].message}</p> |
| 147 | + <pre style={{ margin: 0, padding: "0.75rem", borderRadius: "8px", background: "#f3f4f6", border: "1px solid #e5e7eb", fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", fontSize: "0.8rem", lineHeight: "1.35rem", whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: "40vh", overflow: "auto" }}>{errors[selected].stack}</pre> |
| 148 | + </div> |
| 149 | + <div style={{ display: "flex", alignItems: "center", justifyContent: "flex-end", gap: "0.5rem", padding: "0.75rem 1rem", borderTop: "1px solid #e5e7eb" }}> |
| 150 | + <span style={{ fontSize: "0.72rem", color: "#6b7280" }}>{"source: " + (errors[selected].source || "") + " @ " + (errors[selected].time || "")}</span> |
| 151 | + <button type="button" style={{ border: "1px solid #e5e7eb", background: "#ffffff", borderRadius: "8px", padding: "0.35rem 0.7rem", cursor: "pointer" }} onClick={copyReport}>{copied ? "Copied!" : "Copy report"}</button> |
| 152 | + <button type="button" style={{ border: "1px solid #e11d48", background: "#e11d48", color: "#ffffff", borderRadius: "8px", padding: "0.35rem 0.7rem", cursor: "pointer" }} onClick={dismiss}>{"Dismiss"}</button> |
| 153 | + </div> |
248 | 154 | </div> |
249 | | - </div> |
| 155 | + </div> : null} |
250 | 156 | </div> |
251 | 157 | } |
0 commit comments