Fixes light dark mode toggle closes #429 #434
5 changed files with 67 additions and 26 deletions
|
|
@ -84,7 +84,7 @@ steps:
|
||||||
# Fetch dependencies
|
# Fetch dependencies
|
||||||
- mix deps.get
|
- mix deps.get
|
||||||
# Run fast tests (excludes slow/performance and UI tests)
|
# Run fast tests (excludes slow/performance and UI tests)
|
||||||
- mix test --exclude slow --exclude ui
|
- mix test --exclude slow --exclude ui --max-cases 2
|
||||||
|
|
||||||
- name: rebuild-cache
|
- name: rebuild-cache
|
||||||
image: drillster/drone-volume-cache
|
image: drillster/drone-volume-cache
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue