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 wrapping a checkbox (new toggle-switch + // design) or a (legacy button design). Try the checkbox path + // first, then fall back to the old button scan. let toggled = false; - for (const b of allButtons) { - const text = await b.textContent(); - if (text.includes('\u2600') || text.includes('\ud83c\udf19') || text.includes('\ud83c\udf11') || text.includes('\ud83c\udf15')) { - await b.click(); - toggled = true; - break; + + // New toggle-switch: click the label or directly set the checkbox + const toggleLabel = await page.$('#darkModeToggle'); + if (toggleLabel) { + await toggleLabel.click(); + toggled = true; + } else { + // Legacy fallback: scan buttons for sun/moon emoji + const allButtons = await page.$$('button'); + for (const b of allButtons) { + const text = await b.textContent(); + if (text.includes('\u2600') || text.includes('\ud83c\udf19') || text.includes('\ud83c\udf11') || text.includes('\ud83c\udf15')) { + await b.click(); + toggled = true; + break; + } } } + assert(toggled, 'Could not find dark mode toggle button'); await page.waitForFunction( (before) => document.documentElement.getAttribute('data-theme') !== before,