diff --git a/public/app.js b/public/app.js
index 151ec091..15fb060f 100644
--- a/public/app.js
+++ b/public/app.js
@@ -542,10 +542,11 @@ window.addEventListener('DOMContentLoaded', () => {
// --- Dark Mode ---
const darkToggle = document.getElementById('darkModeToggle');
+ const darkCheckbox = document.getElementById('darkModeCheckbox');
const savedTheme = localStorage.getItem('meshcore-theme');
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
- darkToggle.textContent = theme === 'dark' ? '🌙' : '☀️';
+ if (darkCheckbox) darkCheckbox.checked = theme === 'dark';
localStorage.setItem('meshcore-theme', theme);
// Re-apply user theme CSS vars for the correct mode (light/dark)
reapplyUserThemeVars(theme === 'dark');
@@ -593,10 +594,19 @@ window.addEventListener('DOMContentLoaded', () => {
} else {
applyTheme('light');
}
- darkToggle.addEventListener('click', () => {
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
- applyTheme(isDark ? 'light' : 'dark');
- });
+ if (darkCheckbox) {
+ darkCheckbox.addEventListener('change', () => {
+ applyTheme(darkCheckbox.checked ? 'dark' : 'light');
+ });
+ // Prevent click from bubbling to any legacy handler
+ darkToggle.addEventListener('click', (e) => { e.stopPropagation(); });
+ } else {
+ // Fallback for button-style toggle (upstream compatibility)
+ darkToggle.addEventListener('click', () => {
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
+ applyTheme(isDark ? 'light' : 'dark');
+ });
+ }
// --- Hamburger Menu ---
const hamburger = document.getElementById('hamburger');
diff --git a/public/index.html b/public/index.html
index 1187e0ce..a67704b8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -69,7 +69,14 @@
-
+
diff --git a/public/style.css b/public/style.css
index db9c5d72..77460254 100644
--- a/public/style.css
+++ b/public/style.css
@@ -173,6 +173,45 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible
min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
}
.nav-btn:hover { background: var(--nav-bg2); color: var(--nav-text); }
+
+/* === Theme Toggle Switch === */
+.theme-toggle {
+ display: inline-flex; align-items: center; cursor: pointer;
+ padding: 0; margin: 0; border: none; background: none;
+ min-width: 44px; min-height: 44px; justify-content: center;
+}
+.theme-toggle input[type="checkbox"] {
+ position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
+}
+.theme-toggle-track {
+ position: relative; width: 46px; height: 24px;
+ background: var(--border); border-radius: 12px;
+ transition: background 0.2s ease; display: flex; align-items: center;
+ border: 1px solid var(--border);
+}
+.theme-toggle input:checked ~ .theme-toggle-track {
+ background: var(--accent);
+}
+.theme-toggle-thumb {
+ position: absolute; left: 3px; width: 18px; height: 18px;
+ background: var(--nav-text); border-radius: 50%;
+ box-shadow: 0 1px 3px var(--shadow, rgba(0,0,0,0.3));
+ transition: transform 0.2s ease;
+ z-index: 1;
+}
+.theme-toggle input:checked ~ .theme-toggle-track .theme-toggle-thumb {
+ transform: translateX(22px);
+}
+.theme-toggle-icon {
+ position: absolute; font-size: 10px; line-height: 1;
+ top: 50%; transform: translateY(-50%);
+ pointer-events: none; user-select: none;
+ transition: opacity 0.2s ease;
+}
+.theme-toggle-sun { right: 4px; opacity: 1; }
+.theme-toggle-moon { left: 4px; opacity: 0; }
+.theme-toggle input:checked ~ .theme-toggle-track .theme-toggle-sun { opacity: 0; }
+.theme-toggle input:checked ~ .theme-toggle-track .theme-toggle-moon { opacity: 1; }
/* === Nav Stats === */
.nav-stats {
display: flex; gap: 12px; align-items: center; font-size: 12px; color: var(--nav-text-muted);
diff --git a/test-e2e-playwright.js b/test-e2e-playwright.js
index b37a556f..87ad8a39 100644
--- a/test-e2e-playwright.js
+++ b/test-e2e-playwright.js
@@ -152,17 +152,30 @@ async function run() {
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('nav, .navbar, .nav, [class*="nav"]');
const themeBefore = await page.$eval('html', el => el.getAttribute('data-theme'));
- // Find toggle button
- const allButtons = await page.$$('button');
+
+ // The toggle may be a