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/4] 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/4] 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 { From 1ec79365e1fc549438943540212cd94583d4b859 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:57:19 +0100 Subject: [PATCH 3/4] Update useTheme.mjs Co-Authored-By: Aviv Keller --- src/generators/web/ui/hooks/useTheme.mjs | 118 ++++++----------------- 1 file changed, 31 insertions(+), 87 deletions(-) diff --git a/src/generators/web/ui/hooks/useTheme.mjs b/src/generators/web/ui/hooks/useTheme.mjs index da6a1c25..a8e8ad5b 100644 --- a/src/generators/web/ui/hooks/useTheme.mjs +++ b/src/generators/web/ui/hooks/useTheme.mjs @@ -1,112 +1,56 @@ import { useState, useEffect, useCallback } from 'react'; -const THEME_STORAGE_KEY = 'theme'; -const THEME_PREFERENCES = new Set(['system', 'light', 'dark']); - -/** - * Sets up theme toggle button and system preference listener - */ +/** @returns {'dark'|'light'} The current OS-level color scheme. */ 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. + * Applies a theme to the document root. + * Resolves 'system' to the actual OS preference before applying. + * @param {'system'|'light'|'dark'} pref - The theme preference. */ -const getStoredThemePreference = () => { - try { - const storedTheme = localStorage.getItem(THEME_STORAGE_KEY); - return THEME_PREFERENCES.has(storedTheme) ? storedTheme : null; - } catch { - return null; - } +const applyTheme = pref => { + const theme = pref === 'system' ? getSystemTheme() : pref; + document.documentElement.setAttribute('data-theme', theme); + document.documentElement.style.colorScheme = theme; }; /** - * 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. + * Applies the system theme to the document root. */ -const setStoredThemePreference = themePreference => { - try { - localStorage.setItem(THEME_STORAGE_KEY, themePreference); - } catch { - // Ignore storage failures and keep non-persistent in-memory preference. - } -}; - -/** - * Applies a theme preference to the document. - * - * 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 applyThemePreference = themePreference => { - const resolvedTheme = - themePreference === 'system' ? getSystemTheme() : themePreference; - - document.documentElement.setAttribute('data-theme', resolvedTheme); - document.documentElement.style.colorScheme = resolvedTheme; -}; +const applySystemTheme = () => applyTheme('system'); /** - * A React hook for managing the application's theme preference. + * React hook for managing theme preference. + * Persists the choice to localStorage and listens for OS theme changes + * when set to 'system'. + * @returns {['system'|'light'|'dark', (next: 'system'|'light'|'dark') => void]} */ export const useTheme = () => { - const [themePreference, setThemePreferenceState] = useState('system'); + // Read stored preference once on mount; default to 'system'. + const [pref, setPref] = useState(() => { + return localStorage.getItem('theme') || 'system'; + }); + // Apply theme on every preference change, and if 'system', + // also listen for OS-level color scheme changes. useEffect(() => { - // Use persisted preference if available, otherwise default to system. - const initialPreference = getStoredThemePreference() || 'system'; + applyTheme(pref); - applyThemePreference(initialPreference); - setThemePreferenceState(initialPreference); - }, []); - - /** - * Keep the resolved document theme in sync with system changes - * whenever the preference is set to 'system'. - */ - useEffect(() => { - if (themePreference !== 'system') { + if (pref !== '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; - } + const mql = matchMedia('(prefers-color-scheme: dark)'); + mql.addEventListener('change', applySystemTheme); + return () => mql.removeEventListener('change', applySystemTheme); + }, [pref]); - setThemePreferenceState(nextPreference); - setStoredThemePreference(nextPreference); - applyThemePreference(nextPreference); + /** Updates the preference in both React state and localStorage. */ + const setTheme = useCallback(next => { + setPref(next); + localStorage.setItem('theme', next); }, []); - return [themePreference, setThemePreference]; + return [pref, setTheme]; }; From 68bdbd390c9cd848a661eaad56c7e244d1627371 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:11:25 +0100 Subject: [PATCH 4/4] Update useTheme.mjs Co-Authored-By: Aviv Keller --- src/generators/web/ui/hooks/useTheme.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/generators/web/ui/hooks/useTheme.mjs b/src/generators/web/ui/hooks/useTheme.mjs index a8e8ad5b..df0ce734 100644 --- a/src/generators/web/ui/hooks/useTheme.mjs +++ b/src/generators/web/ui/hooks/useTheme.mjs @@ -29,6 +29,10 @@ const applySystemTheme = () => applyTheme('system'); export const useTheme = () => { // Read stored preference once on mount; default to 'system'. const [pref, setPref] = useState(() => { + if (typeof window === 'undefined') { + return 'system'; + } + return localStorage.getItem('theme') || 'system'; }); @@ -49,7 +53,9 @@ export const useTheme = () => { /** Updates the preference in both React state and localStorage. */ const setTheme = useCallback(next => { setPref(next); - localStorage.setItem('theme', next); + if (typeof window !== 'undefined') { + localStorage.setItem('theme', next); + } }, []); return [pref, setTheme];