From aa859bb90cd9a150d129596a31eb564f59f58d5b Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sun, 8 Mar 2026 00:06:19 +0100 Subject: [PATCH 1/2] refracto: theming --- src/generators/web/ui/components/NavBar.jsx | 6 +- src/generators/web/ui/hooks/useTheme.mjs | 118 +++++++++++++++----- 2 files changed, 91 insertions(+), 33 deletions(-) diff --git a/src/generators/web/ui/components/NavBar.jsx b/src/generators/web/ui/components/NavBar.jsx index 48fbe98c..222b8afe 100644 --- a/src/generators/web/ui/components/NavBar.jsx +++ b/src/generators/web/ui/components/NavBar.jsx @@ -13,7 +13,7 @@ import Logo from '#config/Logo'; * NavBar component that displays the headings, search, etc. */ export default () => { - const [theme, toggleTheme] = useTheme(); + const [themePreference, setThemePreference] = useTheme(); return ( { > + matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + +/** + * + */ +const getStoredThemePreference = () => { + try { + const storedTheme = localStorage.getItem(THEME_STORAGE_KEY); + return THEME_PREFERENCES.has(storedTheme) ? storedTheme : null; + } catch { + return null; + } +}; + +/** + * + */ +const setStoredThemePreference = themePreference => { + try { + localStorage.setItem(THEME_STORAGE_KEY, themePreference); + } catch { + // Ignore storage failures and keep non-persistent in-memory preference. + } +}; + /** - * Applies the given theme to the `` element's `data-theme` attribute - * and persists the theme preference in `localStorage`. + * Applies a theme preference to the document. * - * @param {string} theme - The theme to apply ('light' or 'dark'). + * The persisted preference can be 'system', but the applied document theme is + * always resolved to either 'light' or 'dark'. + * + * @param {'system'|'light'|'dark'} themePreference - Theme preference. */ -const applyTheme = theme => { - document.documentElement.setAttribute('data-theme', theme); - document.documentElement.style.colorScheme = theme; - localStorage.setItem('theme', theme); +const applyThemePreference = themePreference => { + const resolvedTheme = + themePreference === 'system' ? getSystemTheme() : themePreference; + + document.documentElement.setAttribute('data-theme', resolvedTheme); + document.documentElement.style.colorScheme = resolvedTheme; }; /** - * A React hook for managing the application's light/dark theme. + * A React hook for managing the application's theme preference. */ export const useTheme = () => { - const [theme, setTheme] = useState('light'); + const [themePreference, setThemePreferenceState] = useState('system'); useEffect(() => { - const initial = - // Try to get the theme from localStorage first. - localStorage.getItem('theme') || - // If not found, check the `data-theme` attribute on the document element - document.documentElement.getAttribute('data-theme') || - // As a final fallback, check the user's system preference for dark mode. - (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); - - applyTheme(initial); - setTheme(initial); + // Use persisted preference if available, otherwise default to system. + const initialPreference = getStoredThemePreference() || 'system'; + + applyThemePreference(initialPreference); + setThemePreferenceState(initialPreference); }, []); /** - * Callback function to toggle between 'light' and 'dark' themes. + * Keep the resolved document theme in sync with system changes + * whenever the preference is set to 'system'. */ - const toggleTheme = useCallback(() => { - setTheme(prev => { - // Determine the next theme based on the current theme. - const next = prev === 'light' ? 'dark' : 'light'; - // Apply the new theme. - applyTheme(next); - // Return the new theme to update the state. - return next; - }); + useEffect(() => { + if (themePreference !== 'system') { + return; + } + + const mediaQueryList = matchMedia('(prefers-color-scheme: dark)'); + /** + * + */ + const handleSystemThemeChange = () => applyThemePreference('system'); + + if ('addEventListener' in mediaQueryList) { + mediaQueryList.addEventListener('change', handleSystemThemeChange); + return () => { + mediaQueryList.removeEventListener('change', handleSystemThemeChange); + }; + } + + mediaQueryList.addListener(handleSystemThemeChange); + return () => { + mediaQueryList.removeListener(handleSystemThemeChange); + }; + }, [themePreference]); + + /** + * Updates the theme preference and applies it immediately. + */ + const setThemePreference = useCallback(nextPreference => { + if (!THEME_PREFERENCES.has(nextPreference)) { + return; + } + + setThemePreferenceState(nextPreference); + setStoredThemePreference(nextPreference); + applyThemePreference(nextPreference); }, []); - return [theme, toggleTheme]; + return [themePreference, setThemePreference]; }; From bc6e7e7d01f37684170eaa812231140704fb40ea Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sun, 8 Mar 2026 00:17:14 +0100 Subject: [PATCH 2/2] Update useTheme.mjs --- src/generators/web/ui/hooks/useTheme.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/generators/web/ui/hooks/useTheme.mjs b/src/generators/web/ui/hooks/useTheme.mjs index 930ca266..da6a1c25 100644 --- a/src/generators/web/ui/hooks/useTheme.mjs +++ b/src/generators/web/ui/hooks/useTheme.mjs @@ -4,13 +4,15 @@ const THEME_STORAGE_KEY = 'theme'; const THEME_PREFERENCES = new Set(['system', 'light', 'dark']); /** - * + * Sets up theme toggle button and system preference listener */ const getSystemTheme = () => matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; /** + * Retrieves the stored theme preference from local storage. * + * @returns {'system'|'light'|'dark'|null} The stored theme preference or null if not found. */ const getStoredThemePreference = () => { try { @@ -22,7 +24,10 @@ const getStoredThemePreference = () => { }; /** + * Stores the theme preference in local storage. + * If storage is unavailable, it fails silently, allowing the application to continue functioning with an in-memory preference. * + * @param {'system'|'light'|'dark'} themePreference - The theme preference to store. */ const setStoredThemePreference = themePreference => { try {