Skip to content

Commit 6ebdb7f

Browse files
authored
Merge pull request #112 from richorama/copilot/add-dark-mode-toggle
Add dark mode with system preference detection and localStorage persistence
2 parents ea065db + 155f1f8 commit 6ebdb7f

File tree

3 files changed

+213
-4
lines changed

3 files changed

+213
-4
lines changed

index.html

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,117 @@
1212
<title>Azure Speed Test 2.0</title>
1313
<link rel="icon" type="image/png" href="/favicon.png" />
1414
<style>
15+
:root {
16+
--bg-color: #ffffff;
17+
--text-color: #212529;
18+
--jumbotron-bg: #e9ecef;
19+
--table-bg: #ffffff;
20+
--table-hover-bg: #f8f9fa;
21+
--table-header-bg: #e9ecef;
22+
--table-border-color: #dee2e6;
23+
--progress-bg: #e9ecef;
24+
--badge-bg: #dc3545;
25+
--sparkline-color: #B8BABC;
26+
--row-gradient-color: #e9ecef;
27+
}
28+
29+
[data-theme="dark"] {
30+
--bg-color: #212529;
31+
--text-color: #f8f9fa;
32+
--jumbotron-bg: #343a40;
33+
--table-bg: #2b3035;
34+
--table-hover-bg: #343a40;
35+
--table-header-bg: #24282d;
36+
--table-border-color: #495057;
37+
--progress-bg: #495057;
38+
--badge-bg: #dc3545;
39+
--sparkline-color: #6c757d;
40+
--row-gradient-color: #343a40;
41+
}
42+
43+
@media (prefers-color-scheme: dark) {
44+
:root:not([data-theme="light"]) {
45+
--bg-color: #212529;
46+
--text-color: #f8f9fa;
47+
--jumbotron-bg: #343a40;
48+
--table-bg: #2b3035;
49+
--table-hover-bg: #343a40;
50+
--table-header-bg: #24282d;
51+
--table-border-color: #495057;
52+
--progress-bg: #495057;
53+
--badge-bg: #dc3545;
54+
--sparkline-color: #6c757d;
55+
--row-gradient-color: #343a40;
56+
}
57+
}
58+
59+
body {
60+
background-color: var(--bg-color);
61+
color: var(--text-color);
62+
transition: background-color 0.3s ease, color 0.3s ease;
63+
}
64+
65+
.jumbotron {
66+
background-color: var(--jumbotron-bg);
67+
color: var(--text-color);
68+
}
69+
70+
.table {
71+
background-color: var(--table-bg);
72+
color: var(--text-color);
73+
}
74+
75+
.table thead th {
76+
background-color: var(--table-header-bg);
77+
color: var(--text-color);
78+
border-color: var(--table-border-color);
79+
}
80+
81+
.table .thead-light th {
82+
background-color: var(--table-header-bg) !important;
83+
color: var(--text-color) !important;
84+
border-color: var(--table-border-color) !important;
85+
}
86+
87+
.table td, .table th {
88+
border-color: var(--table-border-color);
89+
}
90+
91+
.table-hover tbody tr:hover {
92+
background-color: var(--table-hover-bg);
93+
color: var(--text-color);
94+
}
95+
96+
.progress {
97+
background-color: var(--progress-bg);
98+
}
99+
100+
.theme-toggle {
101+
position: absolute;
102+
top: 20px;
103+
right: 20px;
104+
z-index: 1000;
105+
background: transparent;
106+
border: none;
107+
border-radius: 50%;
108+
width: 30px;
109+
height: 30px;
110+
display: flex;
111+
align-items: center;
112+
justify-content: center;
113+
cursor: pointer;
114+
transition: all 0.3s ease;
115+
padding: 0;
116+
color: var(--text-color);
117+
}
118+
119+
.theme-toggle svg {
120+
display: block;
121+
}
122+
123+
.theme-toggle:hover {
124+
transform: scale(1.1);
125+
}
15126

16127
.icon {
17128
width: 25px;
@@ -68,6 +179,20 @@ <h4>Initializing Azure Speed Test</h4>
68179

69180
<noscript>You need to enabled JavaScript for this web application to work.</noscript>
70181

182+
<script>
183+
// Initialize theme before content loads to prevent flash
184+
(function() {
185+
const savedTheme = localStorage.getItem('theme');
186+
if (savedTheme) {
187+
document.documentElement.setAttribute('data-theme', savedTheme);
188+
} else {
189+
// Check system preference
190+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
191+
document.documentElement.setAttribute('data-theme', 'dark');
192+
}
193+
}
194+
})();
195+
</script>
71196
<script>
72197
// Suppress network error messages in console for cleaner output during latency testing
73198
// Save original console.error for debugging purposes if needed

index.jsx

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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
2439
speedtest.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
*/
172253
const 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 | {' '}

index.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)