Skip to content

Commit d1d1eef

Browse files
committed
feat: add dark/light mode toggle with persistence
- Add theme toggle button in header (☽/☀ icons) - Create light theme with CSS variables override - Store preference in localStorage (key: webstatuspi-theme) - Respect prefers-color-scheme system preference on first visit - Disable CRT scanline effects in light mode for cleaner look - Update FEATURE_SUGGESTIONS.md to mark as implemented
1 parent 3812bf6 commit d1d1eef

4 files changed

Lines changed: 179 additions & 5 deletions

File tree

docs/FEATURE_SUGGESTIONS.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,16 @@ incidents:
176176

177177
---
178178

179-
### 5. Dark/Light Mode Toggle with Persistence (Priority: P3)
179+
### 5. Dark/Light Mode Toggle with Persistence (Priority: P3) ✅ IMPLEMENTED
180+
181+
> **Status**: Implemented
182+
> - Toggle button in header (☽/☀ icons)
183+
> - localStorage persistence with key `webstatuspi-theme`
184+
> - Respects `prefers-color-scheme` system preference
185+
> - CSS variables for all theme colors
186+
> - Light theme: clean, professional look with subtle shadows
187+
> - Dark theme: original CRT cyberpunk aesthetic
188+
180189

181190
**Value**: Improves user experience by allowing users to choose their preferred theme and persist the choice.
182191

@@ -202,7 +211,13 @@ incidents:
202211

203212
---
204213

205-
### 6. Data Export (CSV/JSON) (Priority: P3)
214+
### 6. Data Export (CSV/JSON) (Priority: P3) ✅ IMPLEMENTED
215+
216+
> **Status**: Implemented
217+
> - Endpoints: `GET /api/export/json`, `GET /api/export/csv`
218+
> - Query params: `?days=7` (default), `?url=SERVICE_NAME` (optional filter)
219+
> - Uses stdlib only (json, csv modules)
220+
> - Tests: `tests/test_api.py::TestExportEndpoints`
206221

207222
**Value**: Enables external analysis, reporting, and integration with other tools.
208223

@@ -334,7 +349,7 @@ maintenance:
334349

335350
### Phase 1 (High Impact, Low Complexity)
336351
1. **RSS Feed** - ✅ IMPLEMENTED
337-
2. **Dark/Light Mode Toggle** - Improves UX immediately
352+
2. **Dark/Light Mode Toggle** - u2705 IMPLEMENTED
338353
3. **System Aggregated Statistics** - Enhances dashboard value
339354

340355
### Phase 2 (High Impact, Medium Complexity)

webstatuspi/_dashboard/_css.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,4 +1769,118 @@
17691769
animation: none;
17701770
}
17711771
}
1772+
/* ============================================
1773+
LIGHT THEME
1774+
============================================ */
1775+
[data-theme="light"] {
1776+
/* === COLORS - Light Theme === */
1777+
--bg-dark: #f5f5f7;
1778+
--bg-panel: #ffffff;
1779+
--cyan: #0066cc;
1780+
--magenta: #cc00cc;
1781+
--yellow: #cc9900;
1782+
--orange: #cc5500;
1783+
--green: #00994d;
1784+
--red: #cc0033;
1785+
--text: #1a1a1a;
1786+
--text-dim: #666680;
1787+
--border: #d0d0d8;
1788+
--text-muted: #5a6670;
1789+
1790+
/* === RGB VALUES (for rgba()) - Light Theme === */
1791+
--cyan-rgb: 0, 102, 204;
1792+
--red-rgb: 204, 0, 51;
1793+
--green-rgb: 0, 153, 77;
1794+
--orange-rgb: 204, 85, 0;
1795+
--yellow-rgb: 204, 153, 0;
1796+
--text-dim-rgb: 102, 102, 128;
1797+
--text-rgb: 26, 26, 26;
1798+
1799+
/* === GLOW EFFECTS - Subtle for light theme === */
1800+
--glow-cyan: 0 0 4px var(--cyan);
1801+
--glow-cyan-subtle: 0 0 3px rgba(var(--cyan-rgb), 0.3);
1802+
--glow-red: 0 0 3px rgba(var(--red-rgb), 0.3);
1803+
--glow-green: 0 0 3px var(--green);
1804+
--glow-orange: 0 0 2px var(--orange);
1805+
--glow-yellow: 0 0 2px var(--yellow);
1806+
--glow-dim: none;
1807+
1808+
/* === BOX SHADOWS - Light theme === */
1809+
--shadow-cyan-subtle: 0 1px 3px rgba(0, 0, 0, 0.1);
1810+
--shadow-cyan-medium: 0 2px 8px rgba(0, 0, 0, 0.12);
1811+
--shadow-cyan-strong: 0 2px 12px rgba(0, 0, 0, 0.08);
1812+
--shadow-cyan-glow: 0 4px 20px rgba(0, 0, 0, 0.15);
1813+
--shadow-red-subtle: 0 1px 3px rgba(var(--red-rgb), 0.15);
1814+
--shadow-red-medium: 0 2px 8px rgba(var(--red-rgb), 0.2);
1815+
--shadow-green-glow: 0 0 4px var(--green);
1816+
--shadow-yellow-glow: 0 0 4px var(--yellow);
1817+
}
1818+
1819+
/* Disable CRT effects in light theme */
1820+
[data-theme="light"] body::before,
1821+
[data-theme="light"] .scanline-overlay {
1822+
display: none;
1823+
}
1824+
1825+
[data-theme="light"] body {
1826+
animation: none;
1827+
}
1828+
1829+
/* Card adjustments for light theme */
1830+
[data-theme="light"] .card::before {
1831+
background: linear-gradient(135deg, rgba(var(--cyan-rgb), 0.02) 0%, transparent 50%);
1832+
}
1833+
1834+
[data-theme="light"] .metric {
1835+
background: rgba(0, 0, 0, 0.03);
1836+
}
1837+
1838+
[data-theme="light"] .mini-stat {
1839+
background: rgba(0, 0, 0, 0.02);
1840+
}
1841+
1842+
/* ============================================
1843+
THEME TOGGLE BUTTON
1844+
============================================ */
1845+
.theme-toggle {
1846+
background: var(--bg-panel);
1847+
border: 1px solid var(--border);
1848+
color: var(--text-dim);
1849+
font-family: var(--font-mono);
1850+
font-size: 0.75rem;
1851+
padding: 0.4rem 0.75rem;
1852+
cursor: pointer;
1853+
transition: var(--transition-fast);
1854+
clip-path: var(--clip-corner-sm);
1855+
display: flex;
1856+
align-items: center;
1857+
gap: 0.4rem;
1858+
min-height: 36px;
1859+
}
1860+
1861+
.theme-toggle:hover {
1862+
border-color: var(--cyan);
1863+
color: var(--cyan);
1864+
box-shadow: 0 0 8px rgba(var(--cyan-rgb), 0.2);
1865+
}
1866+
1867+
.theme-toggle:focus-visible {
1868+
border-color: var(--cyan);
1869+
color: var(--cyan);
1870+
outline: 2px solid var(--cyan);
1871+
outline-offset: 2px;
1872+
}
1873+
1874+
.theme-toggle-icon {
1875+
font-size: 1rem;
1876+
line-height: 1;
1877+
}
1878+
1879+
/* Show appropriate icon based on theme */
1880+
.theme-toggle .icon-sun { display: none; }
1881+
.theme-toggle .icon-moon { display: inline; }
1882+
1883+
[data-theme="light"] .theme-toggle .icon-sun { display: inline; }
1884+
[data-theme="light"] .theme-toggle .icon-moon { display: none; }
1885+
17721886
"""

webstatuspi/_dashboard/_js_core.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,48 @@
882882
window.addEventListener('offline', showOfflineBanner);
883883
884884
// Check initial status
885-
if (!navigator.onLine) {
886-
showOfflineBanner();
885+
886+
// ============================================
887+
// Theme Toggle (Dark/Light Mode)
888+
// ============================================
889+
const THEME_KEY = 'webstatuspi-theme';
890+
const themeToggle = document.getElementById('themeToggle');
891+
892+
function getPreferredTheme() {
893+
const stored = localStorage.getItem(THEME_KEY);
894+
if (stored) return stored;
895+
// Respect system preference
896+
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
897+
}
898+
899+
function setTheme(theme) {
900+
document.documentElement.setAttribute('data-theme', theme);
901+
localStorage.setItem(THEME_KEY, theme);
902+
// Update button aria-label
903+
if (themeToggle) {
904+
const label = theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme';
905+
themeToggle.setAttribute('aria-label', label);
906+
}
907+
}
908+
909+
function toggleTheme() {
910+
const current = document.documentElement.getAttribute('data-theme') || 'dark';
911+
setTheme(current === 'dark' ? 'light' : 'dark');
912+
}
913+
914+
// Initialize theme on page load (before render to prevent flash)
915+
setTheme(getPreferredTheme());
916+
917+
// Theme toggle button
918+
if (themeToggle) {
919+
themeToggle.addEventListener('click', toggleTheme);
887920
}
921+
922+
// Listen for system theme changes
923+
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
924+
// Only auto-switch if user hasn't manually set a preference
925+
if (!localStorage.getItem(THEME_KEY)) {
926+
setTheme(e.matches ? 'light' : 'dark');
927+
}
928+
});
888929
"""

webstatuspi/_dashboard/dashboard.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ <h1><span class="logo-bracket">&lt;</span><span class="logo-text">WebStatusπ</s
3030
<span class="live-dot" id="liveDot" aria-hidden="true"></span>
3131
<span id="liveStatusText">// LIVE FEED [10 sec]</span>
3232
</div>
33+
<button class="theme-toggle" id="themeToggle" type="button" aria-label="Toggle dark/light theme" title="Toggle theme">
34+
<span class="theme-toggle-icon icon-moon" aria-hidden="true"></span>
35+
<span class="theme-toggle-icon icon-sun" aria-hidden="true"></span>
36+
</button>
3337
</header>
3438
<nav class="summary-bar" aria-label="Status summary">
3539
<div class="summary-counts" role="status" aria-live="polite" aria-atomic="true">

0 commit comments

Comments
 (0)