@@ -20,6 +20,21 @@ let progressState = {
2020 isVisible : true
2121}
2222
23+ // Theme management
24+ const getTheme = ( ) => {
25+ const saved = localStorage . getItem ( 'theme' )
26+ if ( saved ) return saved
27+ if ( window . matchMedia && window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ) {
28+ return 'dark'
29+ }
30+ return 'light'
31+ }
32+
33+ const setTheme = ( theme ) => {
34+ localStorage . setItem ( 'theme' , theme )
35+ document . documentElement . setAttribute ( 'data-theme' , theme )
36+ }
37+
2338// Record history
2439speedtest . on ( history . record )
2540
@@ -36,6 +51,9 @@ speedtest.on(() => {
3651 }
3752 lastRenderTime = now
3853
54+ // Clear CSS cache before re-render to pick up theme changes
55+ cssVariablesCache = null
56+
3957 const scrollPosition = window . scrollY
4058 render ( < Table history = { history . read ( ) } blockList = { globalBlockList } /> )
4159
@@ -95,6 +113,50 @@ function render(jsx) {
95113 }
96114}
97115
116+ /**
117+ * Theme toggle button component
118+ * @returns {React.Element } Theme toggle button
119+ */
120+ const ThemeToggle = ( ) => {
121+ const [ theme , setThemeState ] = React . useState ( getTheme ( ) )
122+
123+ const handleToggle = ( ) => {
124+ const current = getTheme ( )
125+ const next = current === 'dark' ? 'light' : 'dark'
126+ setTheme ( next )
127+ setThemeState ( next )
128+ // Clear CSS cache to pick up new theme colors
129+ cssVariablesCache = null
130+ }
131+
132+ return (
133+ < button
134+ className = "theme-toggle"
135+ onClick = { handleToggle }
136+ title = { `Switch to ${ theme === 'dark' ? 'light' : 'dark' } mode` }
137+ aria-label = { `Switch to ${ theme === 'dark' ? 'light' : 'dark' } mode` }
138+ >
139+ { theme === 'dark' ? (
140+ < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "1" opacity = "0.6" >
141+ < circle cx = "12" cy = "12" r = "5" />
142+ < line x1 = "12" y1 = "1" x2 = "12" y2 = "3" />
143+ < line x1 = "12" y1 = "21" x2 = "12" y2 = "23" />
144+ < line x1 = "4.22" y1 = "4.22" x2 = "5.64" y2 = "5.64" />
145+ < line x1 = "18.36" y1 = "18.36" x2 = "19.78" y2 = "19.78" />
146+ < line x1 = "1" y1 = "12" x2 = "3" y2 = "12" />
147+ < line x1 = "21" y1 = "12" x2 = "23" y2 = "12" />
148+ < line x1 = "4.22" y1 = "19.78" x2 = "5.64" y2 = "18.36" />
149+ < line x1 = "18.36" y1 = "5.64" x2 = "19.78" y2 = "4.22" />
150+ </ svg >
151+ ) : (
152+ < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "1" opacity = "0.6" >
153+ < path d = "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
154+ </ svg >
155+ ) }
156+ </ button >
157+ )
158+ }
159+
98160/**
99161 * Progress indicator component for warm-up phase
100162 * @param {Object } props - Component props
@@ -105,7 +167,9 @@ const ProgressIndicator = ({ progress }) => {
105167 const { completed, total, percentage, phase } = progress
106168
107169 return (
108- < div className = "text-center mt-5" >
170+ < div >
171+ < ThemeToggle />
172+ < div className = "text-center mt-5" >
109173 < div className = "mb-4" >
110174 < div className = "spinner-border text-primary mb-3" role = "status" >
111175 < span className = "sr-only" > Loading...</ span >
@@ -143,6 +207,7 @@ const ProgressIndicator = ({ progress }) => {
143207 </ small >
144208 </ div >
145209 ) }
210+ </ div >
146211 </ div >
147212 )
148213}
@@ -164,15 +229,33 @@ const renderFlag = (item) => {
164229 )
165230}
166231
232+ /**
233+ * Get CSS variable values (cached per render cycle)
234+ */
235+ let cssVariablesCache = null
236+ const getCSSVariables = ( ) => {
237+ if ( ! cssVariablesCache ) {
238+ const styles = getComputedStyle ( document . documentElement )
239+ cssVariablesCache = {
240+ gradientColor : styles . getPropertyValue ( '--row-gradient-color' ) . trim ( ) ,
241+ bgColor : styles . getPropertyValue ( '--table-bg' ) . trim ( ) ,
242+ sparklineColor : styles . getPropertyValue ( '--sparkline-color' ) . trim ( )
243+ }
244+ }
245+ return cssVariablesCache
246+ }
247+
167248/**
168249 * Render a data row for active locations
169250 * @param {Object } item - Location data with latency information
170251 * @returns {React.Element } Table row element
171252 */
172253const renderRow = ( item ) => {
173254 const percentage = Math . min ( Math . round ( item . percent || 0 ) , 100 )
255+ const { gradientColor, bgColor, sparklineColor } = getCSSVariables ( )
256+
174257 const rowStyle = {
175- backgroundImage : `linear-gradient(to right, #e9ecef ${ percentage } %, #ffffff ${ percentage } %)`
258+ backgroundImage : `linear-gradient(to right, ${ gradientColor } ${ percentage } %, ${ bgColor } ${ percentage } %)`
176259 }
177260
178261 return (
@@ -194,7 +277,7 @@ const renderRow = (item) => {
194277 margin = { 2 }
195278 >
196279 < SparklinesLine
197- color = "#B8BABC"
280+ color = { sparklineColor }
198281 style = { { strokeWidth : 2 } }
199282 />
200283 </ Sparklines >
@@ -255,6 +338,7 @@ const Table = ({ history = [], blockList = [] }) => {
255338
256339 return (
257340 < div >
341+ < ThemeToggle />
258342 < div className = "mb-3" >
259343 < small className = "text-muted" >
260344 Testing { history . length + blockList . length } Azure regions | { ' ' }
0 commit comments