Fixes light dark mode toggle closes #429 #434

Merged
carla merged 3 commits from bug/429_light_dark_mode into main 2026-02-23 16:10:24 +01:00
4 changed files with 66 additions and 25 deletions
Showing only changes of commit cbed65de66 - Show all commits

View file

@ -24,7 +24,7 @@
@plugin "../vendor/daisyui-theme" { @plugin "../vendor/daisyui-theme" {
name: "dark"; name: "dark";
default: false; default: false;
prefersdark: true; prefersdark: false;
color-scheme: "dark"; color-scheme: "dark";
--color-base-100: oklch(30.33% 0.016 252.42); --color-base-100: oklch(30.33% 0.016 252.42);
--color-base-200: oklch(25.26% 0.014 253.1); --color-base-200: oklch(25.26% 0.014 253.1);

View file

@ -15,20 +15,56 @@
</script> </script>
<script> <script>
(() => { (() => {
const setTheme = (theme) => { const mq = window.matchMedia("(prefers-color-scheme: dark)");
if (theme === "system") { const systemTheme = () => (mq.matches ? "dark" : "light");
localStorage.removeItem("phx:theme");
document.documentElement.removeAttribute("data-theme"); // Single source of truth:
} else { // - localStorage["phx:theme"] = "light" | "dark" (explicit override)
localStorage.setItem("phx:theme", theme); // - missing key => "system"
document.documentElement.setAttribute("data-theme", theme); const storedTheme = () => localStorage.getItem("phx:theme") || "system";
}
const effectiveTheme = (t) => (t === "system" ? systemTheme() : t);
const applyThemeNow = (t) => {
document.documentElement.setAttribute("data-theme", effectiveTheme(t));
}; };
if (!document.documentElement.hasAttribute("data-theme")) {
setTheme(localStorage.getItem("phx:theme") || "system"); const syncToggle = () => {
} const eff = effectiveTheme(storedTheme());
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system")); document.querySelectorAll("[data-theme-toggle]").forEach((el) => {
el.checked = eff === "dark";
});
};
const setTheme = (t) => {
if (t === "system") localStorage.removeItem("phx:theme");
else localStorage.setItem("phx:theme", t);
applyThemeNow(t);
syncToggle(); // if toggle exists already
};
// 1) Apply theme ASAP to match system on first paint
applyThemeNow(storedTheme());
// 2) Sync toggle once DOM is ready (fixes initial "light" toggle)
document.addEventListener("DOMContentLoaded", syncToggle);
// 3) If toggle appears later (LiveView render), sync immediately
const obs = new MutationObserver(() => {
if (document.querySelector("[data-theme-toggle]")) syncToggle();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener("phx:set-theme", ({ detail: { theme } }) => setTheme(theme)); window.addEventListener("phx:set-theme", ({ detail: { theme } }) => setTheme(theme));
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
mq.addEventListener("change", () => {
if (localStorage.getItem("phx:theme") === null) {
applyThemeNow("system");
syncToggle();
}
});
})(); })();
</script> </script>
</head> </head>

View file

@ -248,12 +248,17 @@ defmodule MvWeb.Layouts.Sidebar do
aria-label={gettext("Toggle dark mode")} aria-label={gettext("Toggle dark mode")}
> >
<.icon name="hero-sun" class="size-5" aria-hidden="true" /> <.icon name="hero-sun" class="size-5" aria-hidden="true" />
<input <div id="theme-toggle" phx-update="ignore">
type="checkbox" <input
value="dark" id="theme-toggle-input"
class="toggle toggle-sm theme-controller focus:outline-none" type="checkbox"
aria-label={gettext("Toggle dark mode")} class="toggle toggle-sm focus:outline-none"
/> data-theme-toggle
onchange="window.dispatchEvent(new CustomEvent('phx:set-theme',{detail:{theme:this.checked?'dark':'light'}}))"
aria-label={gettext("Toggle dark mode")}
/>
</div>
<.icon name="hero-moon" class="size-5" aria-hidden="true" /> <.icon name="hero-moon" class="size-5" aria-hidden="true" />
</label> </label>
""" """

View file

@ -132,7 +132,7 @@ defmodule MvWeb.Layouts.SidebarTest do
refute html =~ ~s(role="menuitem") refute html =~ ~s(role="menuitem")
# Footer section should not be rendered # Footer section should not be rendered
refute html =~ "theme-controller" refute html =~ "data-theme-toggle"
refute html =~ "locale-select" refute html =~ "locale-select"
end end
@ -253,8 +253,8 @@ defmodule MvWeb.Layouts.SidebarTest do
# Check for language selector form # Check for language selector form
assert html =~ ~s(action="/set_locale") assert html =~ ~s(action="/set_locale")
# Check for theme toggle # Check for theme toggle (using data attribute instead of class)
assert has_class?(html, "theme-controller") assert html =~ "data-theme-toggle"
# Check for user menu/avatar # Check for user menu/avatar
assert has_class?(html, "avatar") assert has_class?(html, "avatar")
@ -536,7 +536,7 @@ defmodule MvWeb.Layouts.SidebarTest do
assert html =~ ~s(role="group") assert html =~ ~s(role="group")
# Footer section # Footer section
assert html =~ "theme-controller" assert html =~ "data-theme-toggle"
assert html =~ ~s(action="/set_locale") assert html =~ ~s(action="/set_locale")
# Check that critical navigation exists (at least /members) # Check that critical navigation exists (at least /members)
@ -694,8 +694,8 @@ defmodule MvWeb.Layouts.SidebarTest do
test "renders theme toggle" do test "renders theme toggle" do
html = render_sidebar(authenticated_assigns()) html = render_sidebar(authenticated_assigns())
# Toggle is always visible # Toggle is always visible (using data attribute instead of class)
assert has_class?(html, "theme-controller") assert html =~ "data-theme-toggle"
assert html =~ "hero-sun" assert html =~ "hero-sun"
assert html =~ "hero-moon" assert html =~ "hero-moon"
end end