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 });