feat: move theme selector to user settings, remove from nav

Add an Appearance section to the Personal tab in user settings with
three clearly labelled options (Auto/Light/Dark). A ThemeSelector hook
reads localStorage to highlight the active choice and updates it when
selection changes. Remove the dropdown toggle from both the main nav
and admin nav.
This commit is contained in:
Graham McIntire 2026-03-12 15:11:35 -05:00
parent 2beadb0dc7
commit 7f11a890a8
No known key found for this signature in database
3 changed files with 83 additions and 4 deletions

View file

@ -1375,10 +1375,30 @@ const StatusTitle = {
}
}
const ThemeSelector = {
mounted() {
this.updateActive()
window.addEventListener("phx:set-theme", () => this.updateActive())
},
updateActive() {
const current = localStorage.getItem("phx:theme") || "system"
this.el.querySelectorAll<HTMLElement>("[data-phx-theme]").forEach((btn) => {
const isActive = btn.dataset.phxTheme === current
btn.setAttribute("aria-pressed", isActive ? "true" : "false")
btn.classList.toggle("ring-2", isActive)
btn.classList.toggle("ring-indigo-500", isActive)
btn.classList.toggle("bg-indigo-50", isActive)
btn.classList.toggle("dark:bg-indigo-900/20", isActive)
btn.classList.toggle("text-indigo-700", isActive)
btn.classList.toggle("dark:text-indigo-300", isActive)
})
},
}
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 5000,
params: { _csrf_token: csrfToken, timezone: userTimezone },
hooks: { ...colocatedHooks, SensorChart, CopyToClipboard, ScrollToTop, AutoDismissFlash, BetaBannerDismiss, NetworkMap, SitesMap, LeafletMap, DeviceListReorder, MikrotikPortSync, GlobalSearch, GlobalSearchTrigger, DynamicFavicon, StatusTitle },
hooks: { ...colocatedHooks, SensorChart, CopyToClipboard, ScrollToTop, AutoDismissFlash, BetaBannerDismiss, NetworkMap, SitesMap, LeafletMap, DeviceListReorder, MikrotikPortSync, GlobalSearch, GlobalSearchTrigger, DynamicFavicon, StatusTitle, ThemeSelector },
})
// Show progress bar on live navigation and form submits

View file

@ -444,8 +444,6 @@ defmodule ToweropsWeb.Layouts do
<.icon name="hero-question-mark-circle" class="h-5 w-5" />
</.link>
<.theme_toggle />
<!-- User menu -->
<button
type="button"
@ -977,7 +975,6 @@ defmodule ToweropsWeb.Layouts do
</div>
</div>
<div class="flex items-center gap-1 sm:gap-2">
<.theme_toggle />
<.link
navigate={~p"/orgs"}
class="text-xs sm:text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"

View file

@ -394,6 +394,68 @@
</.form>
</div>
<% end %>
<!-- Appearance Section -->
<%= if @active_tab == "personal" do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Appearance")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Choose your preferred interface theme.")}
</p>
</div>
<div class="md:col-span-2">
<div
id="theme-selector"
phx-hook="ThemeSelector"
phx-update="ignore"
class="flex gap-3"
>
<button
type="button"
aria-pressed="false"
data-phx-theme="system"
phx-click={JS.dispatch("phx:set-theme")}
class="flex flex-1 flex-col items-center gap-2 rounded-lg border border-gray-200 px-4 py-5 text-sm font-medium text-gray-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 dark:border-white/10 dark:text-gray-300 dark:hover:border-indigo-500 dark:hover:bg-indigo-900/20"
>
<.icon name="hero-computer-desktop" class="h-6 w-6" />
{t("Auto")}
<span class="text-xs font-normal text-gray-400 dark:text-gray-500">
{t("System default")}
</span>
</button>
<button
type="button"
aria-pressed="false"
data-phx-theme="light"
phx-click={JS.dispatch("phx:set-theme")}
class="flex flex-1 flex-col items-center gap-2 rounded-lg border border-gray-200 px-4 py-5 text-sm font-medium text-gray-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 dark:border-white/10 dark:text-gray-300 dark:hover:border-indigo-500 dark:hover:bg-indigo-900/20"
>
<.icon name="hero-sun" class="h-6 w-6" />
{t("Light")}
<span class="text-xs font-normal text-gray-400 dark:text-gray-500">
{t("Always light")}
</span>
</button>
<button
type="button"
aria-pressed="false"
data-phx-theme="dark"
phx-click={JS.dispatch("phx:set-theme")}
class="flex flex-1 flex-col items-center gap-2 rounded-lg border border-gray-200 px-4 py-5 text-sm font-medium text-gray-700 transition-colors hover:border-indigo-300 hover:bg-indigo-50 dark:border-white/10 dark:text-gray-300 dark:hover:border-indigo-500 dark:hover:bg-indigo-900/20"
>
<.icon name="hero-moon" class="h-6 w-6" />
{t("Dark")}
<span class="text-xs font-normal text-gray-400 dark:text-gray-500">
{t("Always dark")}
</span>
</button>
</div>
</div>
</div>
<% end %>
<!-- Account Tab -->
<%= if @active_tab == "account" do %>