From f0c90f2c3b5b4768693cc912b6ba363b0430a671 Mon Sep 17 00:00:00 2001 From: Kosztyk <36381705+Kosztyk@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:08:11 +0200 Subject: [PATCH 1/4] Add files via upload --- src/theme/theme.css | 73 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/src/theme/theme.css b/src/theme/theme.css index 34a0051b..fdecc824 100644 --- a/src/theme/theme.css +++ b/src/theme/theme.css @@ -1,6 +1,16 @@ +/* + ConvertX theme tokens + + Strategy: + - Default (no explicit override): follow OS preference via prefers-color-scheme. + - Manual override: set . + - JS sets/clears data-theme based on localStorage (see /public/theme.js). +*/ + :root { - /* Light mode */ + /* Light mode (default tokens) */ --contrast: oklch(100% 0 0); + /* Neutral colors - Gray */ --neutral-950: oklch(98.5% 0.002 247.839); --neutral-900: oklch(96.7% 0.003 264.542); @@ -13,6 +23,8 @@ --neutral-200: oklch(26.9% 0 0); --neutral-100: oklch(21% 0.034 264.665); --neutral-50: oklch(13% 0.028 261.692); + + /* Accent (lime) */ /* lime-700 */ --accent-600: oklch(53.2% 0.157 131.589); /* lime-600 */ @@ -21,11 +33,60 @@ --accent-400: oklch(76.8% 0.233 130.85); } +/* Manual override: dark */ +:root[data-theme="dark"] { + --contrast: oklch(0% 0 0); + + /* Neutral colors - Gray */ + --neutral-950: oklch(13% 0.028 261.692); + --neutral-900: oklch(21% 0.034 264.665); + --neutral-800: oklch(27.8% 0.033 256.848); + --neutral-700: oklch(37.3% 0.034 259.733); + --neutral-600: oklch(44.6% 0.03 256.802); + --neutral-500: oklch(55.1% 0.027 264.364); + --neutral-400: oklch(70.7% 0.022 261.325); + --neutral-300: oklch(87.2% 0.01 258.338); + --neutral-200: oklch(92.8% 0.006 264.531); + --neutral-100: oklch(96.7% 0.003 264.542); + --neutral-50: oklch(98.5% 0.002 247.839); + + /* Accent (lime) */ + /* lime-600 */ + --accent-600: oklch(64.8% 0.2 131.684); + /* lime-500 */ + --accent-500: oklch(76.8% 0.233 130.85); + /* lime-400 */ + --accent-400: oklch(84.1% 0.238 128.85); +} + +/* Manual override: light (kept explicit for completeness) */ +:root[data-theme="light"] { + --contrast: oklch(100% 0 0); + + /* Neutral colors - Gray */ + --neutral-950: oklch(98.5% 0.002 247.839); + --neutral-900: oklch(96.7% 0.003 264.542); + --neutral-800: oklch(92.8% 0.006 264.531); + --neutral-700: oklch(87.2% 0.01 258.338); + --neutral-600: oklch(70.7% 0.022 261.325); + --neutral-500: oklch(55.1% 0.027 264.364); + --neutral-400: oklch(44.6% 0.03 256.802); + --neutral-300: oklch(37.3% 0.034 259.733); + --neutral-200: oklch(26.9% 0 0); + --neutral-100: oklch(21% 0.034 264.665); + --neutral-50: oklch(13% 0.028 261.692); + + /* Accent (lime) */ + --accent-600: oklch(53.2% 0.157 131.589); + --accent-500: oklch(64.8% 0.2 131.684); + --accent-400: oklch(76.8% 0.233 130.85); +} + +/* Default behavior (no manual override): follow OS preference */ @media (prefers-color-scheme: dark) { - /* Dark mode */ - :root { + :root:not([data-theme]) { --contrast: oklch(0% 0 0); - /* Neutral colors - Gray */ + --neutral-950: oklch(13% 0.028 261.692); --neutral-900: oklch(21% 0.034 264.665); --neutral-800: oklch(27.8% 0.033 256.848); @@ -37,11 +98,9 @@ --neutral-200: oklch(92.8% 0.006 264.531); --neutral-100: oklch(96.7% 0.003 264.542); --neutral-50: oklch(98.5% 0.002 247.839); - /* lime-600 */ + --accent-600: oklch(64.8% 0.2 131.684); - /* lime-500 */ --accent-500: oklch(76.8% 0.233 130.85); - /* lime-400 */ --accent-400: oklch(84.1% 0.238 128.85); } } From ad4d365d7f0ae87b271525974957213590eca94e Mon Sep 17 00:00:00 2001 From: Kosztyk <36381705+Kosztyk@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:11:03 +0200 Subject: [PATCH 2/4] Add files via upload --- public/theme.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 public/theme.js diff --git a/public/theme.js b/public/theme.js new file mode 100644 index 00000000..8923457a --- /dev/null +++ b/public/theme.js @@ -0,0 +1,111 @@ +/* + * ConvertX Theme Toggle + * + * - Stores explicit user choice in localStorage under KEY. + * - If no explicit choice exists, the UI follows the OS preference + * (prefers-color-scheme) and does not set data-theme. + */ + +(() => { + const KEY = "convertx-theme"; + const root = document.documentElement; + + const mql = window.matchMedia?.("(prefers-color-scheme: dark)"); + + const getStoredTheme = () => { + try { + const v = localStorage.getItem(KEY); + return v === "dark" || v === "light" ? v : null; + } catch { + return null; + } + }; + + const getSystemTheme = () => (mql && mql.matches ? "dark" : "light"); + + const getEffectiveTheme = () => getStoredTheme() ?? getSystemTheme(); + + const setTheme = (theme, { persist } = { persist: true }) => { + if (theme !== "dark" && theme !== "light") return; + + root.setAttribute("data-theme", theme); + // Hint to the browser for built-in UI (form controls, scrollbars, etc.) + root.style.colorScheme = theme; + + if (persist) { + try { + localStorage.setItem(KEY, theme); + } catch { + // ignore + } + } + }; + + const clearThemeOverride = () => { + root.removeAttribute("data-theme"); + root.style.colorScheme = ""; + try { + localStorage.removeItem(KEY); + } catch { + // ignore + } + }; + + const syncUI = () => { + const checkbox = document.getElementById("cx-theme-switch"); + const label = document.getElementById("cx-theme-label"); + + if (!checkbox && !label) return; + + const theme = getEffectiveTheme(); + + if (checkbox) { + checkbox.checked = theme === "dark"; + checkbox.setAttribute( + "aria-checked", + checkbox.checked ? "true" : "false", + ); + } + + if (label) { + label.textContent = theme === "dark" ? "Dark" : "Light"; + } + }; + + // --- Initial state --- + // If the user chose a theme before, enforce it. + const stored = getStoredTheme(); + if (stored) { + setTheme(stored, { persist: false }); + } + + // Keep the toggle in sync once the DOM is available. + document.addEventListener("DOMContentLoaded", () => { + syncUI(); + + const checkbox = document.getElementById("cx-theme-switch"); + if (!checkbox) return; + + checkbox.addEventListener("change", () => { + // Explicit user choice always overrides system. + setTheme(checkbox.checked ? "dark" : "light", { persist: true }); + syncUI(); + }); + }); + + // If there's no explicit override, reflect OS changes in the UI. + if (mql) { + const onChange = () => { + if (getStoredTheme() == null) { + clearThemeOverride(); + syncUI(); + } + }; + // Safari uses addListener/removeListener + if (typeof mql.addEventListener === "function") { + mql.addEventListener("change", onChange); + } else if (typeof mql.addListener === "function") { + mql.addListener(onChange); + } + } +})(); From f71ebd5d984acaad31535ea6222e77b1dab7ec7e Mon Sep 17 00:00:00 2001 From: Kosztyk <36381705+Kosztyk@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:12:23 +0200 Subject: [PATCH 3/4] Add files via upload --- src/components/base.tsx | 26 ++++++++++++++++++++++++++ src/components/header.tsx | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/components/base.tsx b/src/components/base.tsx index c44d1f65..34587475 100644 --- a/src/components/base.tsx +++ b/src/components/base.tsx @@ -15,7 +15,33 @@ export const BaseHtml = ({ {title} + + {/* + Apply the persisted theme as early as possible to avoid a flash. + - If no preference is stored, tokens fall back to OS preference via CSS. + */} + + + + {/* Theme toggle behavior (syncs the header switch + persists choice). */} +