From 0d940e3997697bf52e3a66669a31ab6c4ac43137 Mon Sep 17 00:00:00 2001 From: Hyko Date: Wed, 15 Apr 2026 17:06:37 -0400 Subject: [PATCH] refactor: extract theme logic into shared core module --- src/core/themes/index.js | 71 ++++++++++++++++++++ src/features/admin/components/ThemeToggle.js | 68 +------------------ tsup.config.js | 3 +- 3 files changed, 75 insertions(+), 67 deletions(-) create mode 100644 src/core/themes/index.js diff --git a/src/core/themes/index.js b/src/core/themes/index.js new file mode 100644 index 0000000..f28b0d9 --- /dev/null +++ b/src/core/themes/index.js @@ -0,0 +1,71 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Sun01Icon, Moon02Icon, SunCloud02Icon, MoonCloudIcon } from '@zen/core/shared/icons'; + +// Script à injecter dans pour appliquer le thème avant le premier rendu (anti-FOUC). +export const THEME_INIT_SCRIPT = `(function(){try{var t=localStorage.getItem('theme'),d=window.matchMedia('(prefers-color-scheme: dark)').matches;if(t==='dark'||(!t&&d))document.documentElement.classList.add('dark');}catch(e){}})();`; + +const THEME_ICONS = { + light: Sun01Icon, + dark: Moon02Icon, +}; + +export function getStoredTheme() { + const stored = localStorage.getItem('theme'); + if (stored === 'light' || stored === 'dark') return stored; + return 'auto'; +} + +export function applyTheme(theme) { + if (theme === 'dark') { + document.documentElement.classList.add('dark'); + localStorage.setItem('theme', 'dark'); + } else if (theme === 'light') { + document.documentElement.classList.remove('dark'); + localStorage.setItem('theme', 'light'); + } else { + localStorage.removeItem('theme'); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + document.documentElement.classList.toggle('dark', prefersDark); + } +} + +export function getThemeIcon(theme, systemIsDark) { + if (theme === 'auto') return systemIsDark ? MoonCloudIcon : SunCloud02Icon; + return THEME_ICONS[theme]; +} + +function getNextTheme(current) { + const systemIsDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + if (current === 'auto') return systemIsDark ? 'light' : 'dark'; + if (current === 'dark') return systemIsDark ? 'auto' : 'light'; + return systemIsDark ? 'dark' : 'auto'; +} + +export function useTheme() { + const [theme, setTheme] = useState('auto'); + const [systemIsDark, setSystemIsDark] = useState(false); + + useEffect(() => { + setTheme(getStoredTheme()); + setSystemIsDark(window.matchMedia('(prefers-color-scheme: dark)').matches); + + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + function onSystemChange(e) { + setSystemIsDark(e.matches); + if (localStorage.getItem('theme')) return; + document.documentElement.classList.toggle('dark', e.matches); + } + mq.addEventListener('change', onSystemChange); + return () => mq.removeEventListener('change', onSystemChange); + }, []); + + function toggle() { + const next = getNextTheme(theme); + setTheme(next); + applyTheme(next); + } + + return { theme, toggle, systemIsDark }; +} diff --git a/src/features/admin/components/ThemeToggle.js b/src/features/admin/components/ThemeToggle.js index b790e22..7b1b60d 100644 --- a/src/features/admin/components/ThemeToggle.js +++ b/src/features/admin/components/ThemeToggle.js @@ -1,74 +1,10 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { Sun01Icon, Moon02Icon, SunCloud02Icon, MoonCloudIcon } from '@zen/core/shared/icons'; - -function getNextTheme(current) { - const systemIsDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - if (current === 'auto') return systemIsDark ? 'light' : 'dark'; - if (current === 'dark') return systemIsDark ? 'auto' : 'light'; - return systemIsDark ? 'dark' : 'auto'; -} - -function getAutoIcon(systemIsDark) { - return systemIsDark ? MoonCloudIcon : SunCloud02Icon; -} - -const THEME_ICONS = { - light: Sun01Icon, - dark: Moon02Icon, -}; - -function getStoredTheme() { - const stored = localStorage.getItem('theme'); - if (stored === 'light' || stored === 'dark') return stored; - return 'auto'; -} - -function applyTheme(theme) { - if (theme === 'dark') { - document.documentElement.classList.add('dark'); - localStorage.setItem('theme', 'dark'); - } else if (theme === 'light') { - document.documentElement.classList.remove('dark'); - localStorage.setItem('theme', 'light'); - } else { - localStorage.removeItem('theme'); - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - document.documentElement.classList.toggle('dark', prefersDark); - } -} - -function useTheme() { - const [theme, setTheme] = useState('auto'); - const [systemIsDark, setSystemIsDark] = useState(false); - - useEffect(() => { - setTheme(getStoredTheme()); - setSystemIsDark(window.matchMedia('(prefers-color-scheme: dark)').matches); - - const mq = window.matchMedia('(prefers-color-scheme: dark)'); - function onSystemChange(e) { - setSystemIsDark(e.matches); - if (localStorage.getItem('theme')) return; - document.documentElement.classList.toggle('dark', e.matches); - } - mq.addEventListener('change', onSystemChange); - return () => mq.removeEventListener('change', onSystemChange); - }, []); - - function toggle() { - const next = getNextTheme(theme); - setTheme(next); - applyTheme(next); - } - - return { theme, toggle, systemIsDark }; -} +import { useTheme, getThemeIcon } from '@zen/core/themes'; export default function ThemeToggle() { const { theme, toggle, systemIsDark } = useTheme(); - const Icon = theme === 'auto' ? getAutoIcon(systemIsDark) : THEME_ICONS[theme]; + const Icon = getThemeIcon(theme, systemIsDark); return (