From d5d156e65a22e10c4579f720fb57febaee4ec94e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 20:13:59 +0000 Subject: [PATCH 1/2] Initial plan From ee06705a810dd25da99160ce071ce8a2e92ace2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 20:46:42 +0000 Subject: [PATCH 2/2] feat: add dynamic theme hue slider in settings --- index.html | 1 + mods/dynamic_theme.js | 98 +++++++++++++++++++++++++++++++++++++++++ tests/main-menu.spec.js | 17 +++++++ 3 files changed, 116 insertions(+) create mode 100644 mods/dynamic_theme.js diff --git a/index.html b/index.html index 69bc57a..406e7fd 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,7 @@ + diff --git a/mods/dynamic_theme.js b/mods/dynamic_theme.js new file mode 100644 index 0000000..014dcc7 --- /dev/null +++ b/mods/dynamic_theme.js @@ -0,0 +1,98 @@ +(() => { + const THEME_HUE_KEY = "cwctrackThemeHue"; + const THEME_HUE_VAR = "--cwctrack-theme-hue"; + const THEME_STYLE_ID = "cwctrack-dynamic-theme-style"; + const THEME_SETTING_CLASS = "cwctrack-theme-setting"; + const DEFAULT_HUE = 0; + const MIN_HUE = 0; + const MAX_HUE = 360; + + const clampHue = value => { + if (!Number.isFinite(value)) return DEFAULT_HUE; + return Math.min(MAX_HUE, Math.max(MIN_HUE, Math.round(value))); + }; + + const readStoredHue = () => { + try { + return clampHue(Number(localStorage.getItem(THEME_HUE_KEY))); + } catch (_error) { + return DEFAULT_HUE; + } + }; + + const writeStoredHue = hue => { + try { + localStorage.setItem(THEME_HUE_KEY, String(hue)); + } catch (_error) {} + }; + + const applyHue = hue => { + document.documentElement.style.setProperty(THEME_HUE_VAR, `${hue}deg`); + }; + + const ensureThemeStyle = () => { + if (document.getElementById(THEME_STYLE_ID)) return; + + const style = document.createElement("style"); + style.id = THEME_STYLE_ID; + style.textContent = ` + :root { ${THEME_HUE_VAR}: 0deg; } + #screen, #ui, #transition-layer { filter: hue-rotate(var(${THEME_HUE_VAR})); } + .${THEME_SETTING_CLASS} > input[type="range"] { width: 220px; } + `; + document.head.append(style); + }; + + const createSetting = container => { + if (!container || container.querySelector(`.${THEME_SETTING_CLASS}`)) return; + + const setting = document.createElement("div"); + setting.className = `setting ${THEME_SETTING_CLASS}`; + + const label = document.createElement("p"); + label.textContent = "Theme hue"; + + const slider = document.createElement("input"); + slider.type = "range"; + slider.min = String(MIN_HUE); + slider.max = String(MAX_HUE); + slider.step = "1"; + slider.value = String(readStoredHue()); + + slider.addEventListener("input", () => { + const hue = clampHue(Number(slider.value)); + applyHue(hue); + writeStoredHue(hue); + }); + + setting.append(label, slider); + container.append(setting); + }; + + const run = () => { + ensureThemeStyle(); + applyHue(readStoredHue()); + + let checkQueued = false; + const checkForSettings = () => { + checkQueued = false; + createSetting(document.querySelector(".settings-menu-ui > .container")); + }; + const queueSettingsCheck = () => { + if (checkQueued) return; + checkQueued = true; + setTimeout(checkForSettings, 100); + }; + + const root = document.getElementById("ui") || document.body; + const observer = new MutationObserver(queueSettingsCheck); + observer.observe(root, { childList: true, subtree: true }); + queueSettingsCheck(); + }; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", run, { once: true }); + } else { + run(); + } +})(); diff --git a/tests/main-menu.spec.js b/tests/main-menu.spec.js index 7b7ff18..0e59c2d 100644 --- a/tests/main-menu.spec.js +++ b/tests/main-menu.spec.js @@ -20,6 +20,23 @@ test('test settings entry button', async ({ page }) => { await expect(page.locator('.settings-menu-ui')).toBeVisible({ timeout: 20_000 }); }); +test('dynamic theme hue slider updates css variable', async ({ page }) => { + await page.getByRole('button', { name: 'Settings' }).click(); + const slider = page.locator('.cwctrack-theme-setting input[type="range"]'); + await expect(slider).toBeVisible({ timeout: 20_000 }); + + await slider.evaluate((element, value) => { + element.value = value; + element.dispatchEvent(new Event('input', { bubbles: true })); + }, '120'); + + await expect.poll(async () => { + return await page.evaluate(() => { + return document.documentElement.style.getPropertyValue('--cwctrack-theme-hue').trim(); + }); + }).toBe('120deg'); +}); + test('test multiplayer entry button', async ({ page }) => { await page.getByRole('button', { name: 'Multiplayer' }).click(); await expect(page.locator('.multiplayer-ui > .join')).toBeVisible({ timeout: 20_000 });