11import ansiHTML from "ansi-html-community" ;
22import { encode as encodeHtmlEntity } from "html-entities" ;
33
4+ // The backdrop dims the page and centers the error card.
45const clientOverlay = document . createElement ( "div" ) ;
56clientOverlay . 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 `>`)
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 * & g t ; / . 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 ( / h t t p s ? : \/ \/ [ ^ \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 */
63191export 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 {
0 commit comments