fix(ui): move theme checkmark when theme changes in Appearance menu

The checkmark in the Appearance theme menu was rendered server-side
(if user.Theme == opt.Name), so it never moved after switching themes
in the browser.

- Always render the check for every theme option, hidden by default,
  using a new themeCheckClass(name, current) helper that returns the
  hidden class unless the option is the active theme.
- Give each theme button a data-theme attribute and add
  updateThemeIndicators() to web/src/theme.ts, which reads the applied
  theme from the body class (theme-<name>) and toggles the hidden class
  on each check accordingly.
- Call updateThemeIndicators() from changeTheme (before the async save
  and on failure), initializeTheme, and loadUserTheme so the menu stays
  in sync with the applied theme.
- Add unit test for themeCheckClass (templates/utils_test.go) and
  regenerate templ output.
This commit is contained in:
2026-08-10 13:43:32 -04:00
parent ab465d8e0e
commit 8d65e03555
5 changed files with 203 additions and 129 deletions
+20
View File
@@ -41,6 +41,7 @@ const loadTheme = (): void => {
// Change: Remove DOM element lookup, accept theme as parameter
const changeTheme = async (theme: string): Promise<void> => {
applyTheme(theme);
updateThemeIndicators();
// Save to server if logged in
const token = localStorage.getItem(TOKEN_STORAGE_KEY);
@@ -78,6 +79,7 @@ const loadUserTheme = async (): Promise<void> => {
const data = await response.json();
if (data.theme) {
applyTheme(data.theme as string);
updateThemeIndicators();
}
}
} catch {
@@ -85,10 +87,26 @@ const loadUserTheme = async (): Promise<void> => {
}
};
// Update the active-theme checkmark in the Appearance menu to follow the
// theme currently applied to <body>. Each theme button carries its theme name
// in data-theme and a .theme-check element that is shown for the active theme
// and hidden for the rest.
const updateThemeIndicators = (): void => {
const match = document.body.className.match(/theme-([\w-]+)/);
const current = match ? match[1] : DEFAULT_THEME;
document.querySelectorAll<HTMLElement>(".theme-btn").forEach((btn) => {
const check = btn.querySelector(".theme-check");
if (!check) return;
const theme = btn.getAttribute("data-theme");
check.classList.toggle("hidden", theme !== current);
});
};
// Initialize theme system
const initializeTheme = (): void => {
loadTheme();
loadUserTheme();
updateThemeIndicators();
// Set theme select value to current theme
const themeSelect = document.getElementById(
@@ -210,6 +228,7 @@ Alpine.data("theme", () => ({
loadTheme,
loadUserTheme,
loadWoodPaneling,
updateThemeIndicators,
updateWoodPanelingIndicators,
}));
@@ -221,6 +240,7 @@ export {
loadTheme,
loadUserTheme,
loadWoodPaneling,
updateThemeIndicators,
updateWoodPanelingIndicators,
};
export type { ThemeType };