Skip to content

Commit 6037127

Browse files
committed
feat: enhance error overlay with close button and improved styling
1 parent 2dfe0ea commit 6037127

3 files changed

Lines changed: 359 additions & 20 deletions

File tree

client-src/overlay.js

Lines changed: 167 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,86 @@
11
import ansiHTML from "ansi-html-community";
22
import { encode as encodeHtmlEntity } from "html-entities";
33

4+
// The backdrop dims the page and centers the error card.
45
const clientOverlay = document.createElement("div");
56
clientOverlay.id = "webpack-dev-middleware-hot-overlay";
67

8+
// The card is the visible panel that holds the problem messages.
9+
const overlayCard = document.createElement("div");
10+
clientOverlay.append(overlayCard);
11+
12+
// A close (×) button pinned to the top-right corner of the card.
13+
const closeButton = document.createElement("button");
14+
closeButton.type = "button";
15+
closeButton.textContent = "×";
16+
closeButton.setAttribute("aria-label", "Close");
17+
closeButton.style.position = "absolute";
18+
closeButton.style.top = "8px";
19+
closeButton.style.right = "12px";
20+
closeButton.style.border = "none";
21+
closeButton.style.background = "transparent";
22+
closeButton.style.color = "#999999";
23+
closeButton.style.fontSize = "22px";
24+
closeButton.style.lineHeight = "1";
25+
closeButton.style.cursor = "pointer";
26+
closeButton.style.padding = "0";
27+
closeButton.addEventListener("click", () => {
28+
clear();
29+
});
30+
31+
// Dismiss the overlay when clicking the backdrop (but not the card itself).
32+
clientOverlay.addEventListener("click", (event) => {
33+
if (event.target === clientOverlay) {
34+
clear();
35+
}
36+
});
37+
38+
// Dismiss the overlay when pressing Escape.
39+
document.addEventListener("keydown", (event) => {
40+
if (event.key === "Escape") {
41+
clear();
42+
}
43+
});
44+
745
/** @type {Record<string, string | number>} */
8-
const styles = {
9-
background: "rgba(0,0,0,0.85)",
10-
color: "#e8e8e8",
11-
lineHeight: "1.6",
12-
whiteSpace: "pre",
13-
fontFamily: "Menlo, Consolas, monospace",
14-
fontSize: "13px",
46+
const backdropStyles = {
1547
position: "fixed",
16-
zIndex: 9999,
17-
padding: "10px",
48+
top: 0,
1849
left: 0,
1950
right: 0,
20-
top: 0,
2151
bottom: 0,
52+
zIndex: 9999,
53+
// webpack "Outer Space" (#2B3A42), translucent.
54+
background: "rgba(43,58,66,0.72)",
55+
display: "flex",
56+
alignItems: "center",
57+
justifyContent: "center",
58+
padding: "32px",
59+
boxSizing: "border-box",
2260
overflow: "auto",
23-
dir: "ltr",
61+
};
62+
63+
/** @type {Record<string, string | number>} */
64+
const styles = {
65+
// Dark panel; the top accent bar color is set per problem type in showProblems.
66+
position: "relative",
67+
background: "#101619",
68+
color: "#f2f2f2",
69+
lineHeight: "1.6",
70+
whiteSpace: "pre-wrap",
71+
fontFamily: "Menlo, Consolas, 'Courier New', monospace",
72+
fontSize: "14px",
73+
width: "100%",
74+
maxWidth: "960px",
75+
maxHeight: "90vh",
76+
margin: "auto",
77+
padding: "28px 32px",
78+
boxSizing: "border-box",
79+
borderRadius: "8px",
80+
borderTop: "3px solid #ff3348",
81+
boxShadow: "0 8px 40px rgba(0,0,0,0.5)",
82+
overflow: "auto",
83+
direction: "ltr",
2484
textAlign: "left",
2585
};
2686

@@ -40,35 +100,118 @@ const colors = {
40100

41101
/**
42102
* @param {"errors" | "warnings"} type problem type
43-
* @returns {string} HTML span with a colored badge
103+
* @returns {string | string[]} hex color (without `#`) for the given type
44104
*/
45-
function problemType(type) {
105+
function problemColor(type) {
46106
/** @type {Record<string, string | string[]>} */
47107
const problemColors = {
48108
errors: colors.red,
49109
warnings: colors.yellow,
50110
};
51-
const color = problemColors[type] || colors.red;
111+
return problemColors[type] || colors.red;
112+
}
113+
114+
/**
115+
* @param {"errors" | "warnings"} type problem type
116+
* @returns {string} HTML span with a colored badge
117+
*/
118+
function problemType(type) {
119+
const color = problemColor(type);
52120
return (
53121
`<span style="background-color:#${color}; color:#000000; ` +
54122
'padding:3px 6px; border-radius: 4px;">' +
55123
`${type.slice(0, -1).toUpperCase()}</span>`
56124
);
57125
}
58126

127+
/**
128+
* Highlight the offending line of a code frame — the one webpack marks with a
129+
* leading `>` gutter — so it stands out from the surrounding context lines.
130+
* @param {string} html message HTML (already entity-encoded, so `>` is `&gt;`)
131+
* @returns {string} HTML with the error line wrapped in a colored span
132+
*/
133+
function highlightCodeFrame(html) {
134+
return html
135+
.split("\n")
136+
.map((line) =>
137+
/^\s*&gt;/.test(line)
138+
? '<span style="display:inline-block; width:100%; margin:6px 0; ' +
139+
'color:#ff6b6b; background-color:rgba(255,107,107,0.12);">' +
140+
`${line}</span>`
141+
: line,
142+
)
143+
.join("\n");
144+
}
145+
146+
/**
147+
* Highlight the file references webpack reports. The header reference (the one
148+
* with a `line:col` location, e.g. `./src/render.js 7:2`) is rendered as a file
149+
* chip; bare paths elsewhere are just underlined.
150+
* @param {string} html message HTML
151+
* @returns {string} HTML with file references styled
152+
*/
153+
function highlightFilePath(html) {
154+
return html.replace(
155+
/(\.{1,2}\/[\w./-]+\.\w+)(:\d+:\d+|\s\d+:\d+)?/g,
156+
(match, filePath, location) => {
157+
if (!location) {
158+
return (
159+
'<span style="color:#8dd6f9; text-decoration:underline; ' +
160+
`text-underline-offset:2px;">${match}</span>`
161+
);
162+
}
163+
164+
return `<span style="color:#8dd6f9;">${filePath}</span>${location}\n`;
165+
},
166+
);
167+
}
168+
169+
/**
170+
* Turn bare `http(s)` URLs in the message into clickable links.
171+
* @param {string} html message HTML
172+
* @returns {string} HTML with URLs wrapped in anchor tags
173+
*/
174+
function linkify(html) {
175+
return html.replace(/https?:\/\/[^\s<>"]+/g, (url) => {
176+
// Keep trailing punctuation (e.g. a sentence-ending dot) out of the href.
177+
const trailing = url.match(/[.,;:!?)\]}]+$/);
178+
const cut = trailing ? trailing[0] : "";
179+
const href = url.slice(0, url.length - cut.length);
180+
return (
181+
`<a href="${href}" target="_blank" rel="noopener noreferrer" ` +
182+
`style="color:#8dd6f9;">${href}</a>${cut}`
183+
);
184+
});
185+
}
186+
59187
/**
60188
* @param {"errors" | "warnings"} type problem type
61189
* @param {string[]} lines messages to render
62190
*/
63191
export function showProblems(type, lines) {
64-
clientOverlay.innerHTML = "";
192+
// Accent the top bar with the problem color (red for errors, yellow for warnings).
193+
overlayCard.style.borderTopColor = `#${problemColor(type)}`;
194+
overlayCard.innerHTML = "";
195+
overlayCard.append(closeButton);
65196
for (const line of lines) {
66-
const msg = ansiHTML(encodeHtmlEntity(line));
197+
const msg = linkify(
198+
highlightFilePath(highlightCodeFrame(ansiHTML(encodeHtmlEntity(line)))),
199+
);
67200
const div = document.createElement("div");
68-
div.style.marginBottom = "26px";
201+
div.style.marginBottom = "20px";
69202
div.innerHTML = `${problemType(type)} in ${msg}`;
70-
clientOverlay.append(div);
203+
overlayCard.append(div);
71204
}
205+
206+
const hint = document.createElement("div");
207+
hint.style.marginTop = "4px";
208+
hint.style.paddingTop = "16px";
209+
hint.style.borderTop = "1px solid #465e69";
210+
hint.style.color = "#999999";
211+
hint.style.fontSize = "13px";
212+
hint.textContent = "Click outside, press Esc, or fix the code to dismiss.";
213+
overlayCard.append(hint);
214+
72215
if (document.body) {
73216
document.body.append(clientOverlay);
74217
}
@@ -103,9 +246,14 @@ export default function configureOverlay(options) {
103246
}
104247
}
105248

249+
for (const key of Object.keys(backdropStyles)) {
250+
/** @type {EXPECTED_ANY} */
251+
(clientOverlay.style)[key] = backdropStyles[key];
252+
}
253+
106254
for (const key of Object.keys(styles)) {
107255
/** @type {EXPECTED_ANY} */
108-
(clientOverlay.style)[key] = styles[key];
256+
(overlayCard.style)[key] = styles[key];
109257
}
110258

111259
return {

cspell.config.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,13 @@
99
"CHANGELOG.md",
1010
"cspell.config.json"
1111
],
12-
"words": ["Consolas", "cspellcache", "darkgrey", "eslintcache", "esmodules"]
12+
"words": [
13+
"Consolas",
14+
"cspellcache",
15+
"darkgrey",
16+
"eslintcache",
17+
"esmodules",
18+
"noopener",
19+
"noreferrer"
20+
]
1321
}

0 commit comments

Comments
 (0)