@@ -9,20 +9,60 @@ const SVG_NS = "http://www.w3.org/2000/svg";
99// Circumference of the progress ring (r = 6).
1010const RING_LENGTH = 2 * Math . PI * 6 ;
1111
12- /** @type {HTMLElement | null } */
13- let host = null ;
14- /** @type {HTMLElement | null } */
15- let label = null ;
16- /** @type {HTMLElement | null } */
17- let dot = null ;
18- /** @type {SVGSVGElement | null } */
19- let ring = null ;
20- /** @type {SVGCircleElement | null } */
21- let ringValue = null ;
22-
2312// eslint-disable-next-line jsdoc/reject-any-type
2413/** @typedef {any } EXPECTED_ANY */
2514
15+ /**
16+ * @typedef {object } IndicatorState
17+ * @property {HTMLElement | null } host badge host element
18+ * @property {HTMLElement | null } label label inside the badge
19+ * @property {HTMLElement | null } dot pulsing dot (indeterminate mode)
20+ * @property {SVGSVGElement | null } ring progress ring (determinate mode)
21+ * @property {SVGCircleElement | null } ringValue ring value circle
22+ * @property {Record<string, true> } building sources with a build in progress — the badge hides only when every source finished
23+ */
24+
25+ /** @returns {IndicatorState } fresh indicator state */
26+ function createIndicatorState ( ) {
27+ return {
28+ host : null ,
29+ label : null ,
30+ dot : null ,
31+ ring : null ,
32+ ringValue : null ,
33+ building : { } ,
34+ } ;
35+ }
36+
37+ // Shared through `window` so every bundled copy of this module drives one
38+ // single badge instead of stacking duplicates (same pattern as the overlay).
39+ const INDICATOR_STATE_KEY = "__webpack_dev_middleware_hot_indicator_state__" ;
40+
41+ /** @type {IndicatorState } */
42+ const state = ( ( ) => {
43+ if ( typeof window === "undefined" ) {
44+ return createIndicatorState ( ) ;
45+ }
46+
47+ const holder = /** @type {EXPECTED_ANY } */ ( window ) ;
48+
49+ if ( ! holder [ INDICATOR_STATE_KEY ] ) {
50+ holder [ INDICATOR_STATE_KEY ] = createIndicatorState ( ) ;
51+ } else {
52+ // Fill fields another package version may not have created, in place.
53+ const defaults = createIndicatorState ( ) ;
54+
55+ for ( const key of Object . keys ( defaults ) ) {
56+ if ( ! ( key in holder [ INDICATOR_STATE_KEY ] ) ) {
57+ holder [ INDICATOR_STATE_KEY ] [ key ] =
58+ defaults [ /** @type {keyof IndicatorState } */ ( key ) ] ;
59+ }
60+ }
61+ }
62+
63+ return holder [ INDICATOR_STATE_KEY ] ;
64+ } ) ( ) ;
65+
2666/**
2767 * @param {EXPECTED_ANY } element element
2868 * @param {Record<string, string | number> } style style map
@@ -37,25 +77,25 @@ function applyStyle(element, style) {
3777 * Create (or reuse) the indicator host element.
3878 */
3979function ensureIndicator ( ) {
40- if ( host && host . parentNode ) {
80+ if ( state . host && state . host . parentNode ) {
4181 return ;
4282 }
4383
4484 if ( ! document . body ) {
4585 return ;
4686 }
4787
48- host = document . createElement ( "div" ) ;
49- host . id = INDICATOR_ID ;
50- applyStyle ( host , {
88+ state . host = document . createElement ( "div" ) ;
89+ state . host . id = INDICATOR_ID ;
90+ applyStyle ( state . host , {
5191 position : "fixed" ,
5292 right : "16px" ,
5393 bottom : "16px" ,
5494 zIndex : 9999 ,
5595 pointerEvents : "none" ,
5696 } ) ;
5797
58- const root = host . attachShadow ( { mode : "open" } ) ;
98+ const root = state . host . attachShadow ( { mode : "open" } ) ;
5999
60100 const badge = document . createElement ( "div" ) ;
61101 applyStyle ( badge , {
@@ -72,26 +112,26 @@ function ensureIndicator() {
72112 } ) ;
73113
74114 // Indeterminate mode: a pulsing dot.
75- dot = document . createElement ( "span" ) ;
76- applyStyle ( dot , {
115+ state . dot = document . createElement ( "span" ) ;
116+ applyStyle ( state . dot , {
77117 width : "8px" ,
78118 height : "8px" ,
79119 borderRadius : "50%" ,
80120 background : theme . accent ,
81121 } ) ;
82122
83123 // Pulse through the Web Animations API — no <style> element involved.
84- if ( typeof dot . animate === "function" ) {
85- dot . animate ( [ { opacity : 1 } , { opacity : 0.2 } , { opacity : 1 } ] , {
124+ if ( typeof state . dot . animate === "function" ) {
125+ state . dot . animate ( [ { opacity : 1 } , { opacity : 0.2 } , { opacity : 1 } ] , {
86126 duration : 1000 ,
87127 iterations : Number . POSITIVE_INFINITY ,
88128 } ) ;
89129 }
90130
91131 // Determinate mode: a progress ring drawn with SVG presentation attributes.
92- ring = document . createElementNS ( SVG_NS , "svg" ) ;
93- ring . setAttribute ( "viewBox" , "0 0 16 16" ) ;
94- applyStyle ( ring , { width : "14px" , height : "14px" , display : "none" } ) ;
132+ state . ring = document . createElementNS ( SVG_NS , "svg" ) ;
133+ state . ring . setAttribute ( "viewBox" , "0 0 16 16" ) ;
134+ applyStyle ( state . ring , { width : "14px" , height : "14px" , display : "none" } ) ;
95135
96136 const track = document . createElementNS ( SVG_NS , "circle" ) ;
97137 track . setAttribute ( "cx" , "8" ) ;
@@ -101,72 +141,91 @@ function ensureIndicator() {
101141 track . setAttribute ( "stroke" , "rgba(255,255,255,0.25)" ) ;
102142 track . setAttribute ( "stroke-width" , "2.5" ) ;
103143
104- ringValue = document . createElementNS ( SVG_NS , "circle" ) ;
105- ringValue . setAttribute ( "cx" , "8" ) ;
106- ringValue . setAttribute ( "cy" , "8" ) ;
107- ringValue . setAttribute ( "r" , "6" ) ;
108- ringValue . setAttribute ( "fill" , "none" ) ;
109- ringValue . setAttribute ( "stroke" , theme . accent ) ;
110- ringValue . setAttribute ( "stroke-width" , "2.5" ) ;
111- ringValue . setAttribute ( "stroke-linecap" , "round" ) ;
112- ringValue . setAttribute ( "stroke-dasharray" , String ( RING_LENGTH ) ) ;
113- ringValue . setAttribute ( "stroke-dashoffset" , String ( RING_LENGTH ) ) ;
144+ state . ringValue = document . createElementNS ( SVG_NS , "circle" ) ;
145+ state . ringValue . setAttribute ( "cx" , "8" ) ;
146+ state . ringValue . setAttribute ( "cy" , "8" ) ;
147+ state . ringValue . setAttribute ( "r" , "6" ) ;
148+ state . ringValue . setAttribute ( "fill" , "none" ) ;
149+ state . ringValue . setAttribute ( "stroke" , theme . accent ) ;
150+ state . ringValue . setAttribute ( "stroke-width" , "2.5" ) ;
151+ state . ringValue . setAttribute ( "stroke-linecap" , "round" ) ;
152+ state . ringValue . setAttribute ( "stroke-dasharray" , String ( RING_LENGTH ) ) ;
153+ state . ringValue . setAttribute ( "stroke-dashoffset" , String ( RING_LENGTH ) ) ;
114154 // Start the ring at 12 o'clock.
115- ringValue . setAttribute ( "transform" , "rotate(-90 8 8)" ) ;
155+ state . ringValue . setAttribute ( "transform" , "rotate(-90 8 8)" ) ;
116156
117- ring . append ( track , ringValue ) ;
157+ state . ring . append ( track , state . ringValue ) ;
118158
119- label = document . createElement ( "span" ) ;
120- badge . append ( dot , ring , label ) ;
159+ state . label = document . createElement ( "span" ) ;
160+ badge . append ( state . dot , state . ring , state . label ) ;
121161 root . append ( badge ) ;
122- document . body . append ( host ) ;
162+ document . body . append ( state . host ) ;
123163}
124164
125165/**
126166 * Show the indicator (idempotent). With a percent the badge renders a
127167 * progress ring; without one it renders a pulsing dot.
128168 * @param {string= } text label text
129169 * @param {number= } percent compilation progress (0-100)
170+ * @param {string= } source who is building (e.g. a compilation name, or a
171+ * client sharing the badge) — the badge stays until every source finished
130172 */
131- export function show ( text , percent ) {
173+ export function show ( text , percent , source = "" ) {
174+ state . building [ source ] = true ;
132175 ensureIndicator ( ) ;
133176
134- if ( ! label ) {
177+ if ( ! state . label ) {
135178 return ;
136179 }
137180
138- label . textContent = text || "Rebuilding…" ;
181+ state . label . textContent = text || "Rebuilding…" ;
139182
140183 const determinate = typeof percent === "number" ;
141184
142- /** @type {HTMLElement } */ ( dot ) . style . display = determinate
185+ /** @type {HTMLElement } */ ( state . dot ) . style . display = determinate
143186 ? "none"
144187 : "inline-block" ;
145- /** @type {SVGSVGElement } */ ( ring ) . style . display = determinate
188+ /** @type {SVGSVGElement } */ ( state . ring ) . style . display = determinate
146189 ? "block"
147190 : "none" ;
148191
149192 if ( determinate ) {
150193 const clamped = Math . min ( 100 , Math . max ( 0 , percent ) ) ;
151194
152- /** @type {SVGCircleElement } */ ( ringValue ) . setAttribute (
195+ /** @type {SVGCircleElement } */ ( state . ringValue ) . setAttribute (
153196 "stroke-dashoffset" ,
154197 String ( RING_LENGTH * ( 1 - clamped / 100 ) ) ,
155198 ) ;
156199 }
157200}
158201
159202/**
160- * Remove the indicator from the page.
203+ * Mark one source's build as finished, or remove the indicator entirely.
204+ * @param {string= } source when given, only that source is dropped and the
205+ * badge stays while any other source is still building; without it the badge
206+ * is removed unconditionally
161207 */
162- export function hide ( ) {
163- if ( host && host . parentNode ) {
164- host . remove ( ) ;
208+ export function hide ( source ) {
209+ if ( source !== undefined ) {
210+ if ( ! Object . prototype . hasOwnProperty . call ( state . building , source ) ) {
211+ return ;
212+ }
213+
214+ delete state . building [ source ] ;
215+
216+ if ( Object . keys ( state . building ) . length > 0 ) {
217+ return ;
218+ }
219+ }
220+
221+ if ( state . host && state . host . parentNode ) {
222+ state . host . remove ( ) ;
165223 }
166224
167- host = null ;
168- label = null ;
169- dot = null ;
170- ring = null ;
171- ringValue = null ;
225+ state . host = null ;
226+ state . label = null ;
227+ state . dot = null ;
228+ state . ring = null ;
229+ state . ringValue = null ;
230+ state . building = { } ;
172231}
0 commit comments