refactor: extract theme logic into shared core module

This commit is contained in:
2026-04-15 17:06:37 -04:00
parent e1a7815b76
commit 0d940e3997
3 changed files with 75 additions and 67 deletions
+71
View File
@@ -0,0 +1,71 @@
'use client';
import { useState, useEffect } from 'react';
import { Sun01Icon, Moon02Icon, SunCloud02Icon, MoonCloudIcon } from '@zen/core/shared/icons';
// Script à injecter dans <head> 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 };
}
+2 -66
View File
@@ -1,74 +1,10 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useTheme, getThemeIcon } from '@zen/core/themes';
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 };
}
export default function ThemeToggle() { export default function ThemeToggle() {
const { theme, toggle, systemIsDark } = useTheme(); const { theme, toggle, systemIsDark } = useTheme();
const Icon = theme === 'auto' ? getAutoIcon(systemIsDark) : THEME_ICONS[theme]; const Icon = getThemeIcon(theme, systemIsDark);
return ( return (
<button <button
+2 -1
View File
@@ -22,6 +22,7 @@ export default defineConfig([
'src/core/email/templates/index.js', 'src/core/email/templates/index.js',
'src/core/storage/index.js', 'src/core/storage/index.js',
'src/core/toast/index.js', 'src/core/toast/index.js',
'src/core/themes/index.js',
'src/features/provider/index.js', 'src/features/provider/index.js',
'src/shared/components/index.js', 'src/shared/components/index.js',
'src/shared/Icons.js', 'src/shared/Icons.js',
@@ -35,7 +36,7 @@ export default defineConfig([
splitting: false, splitting: false,
sourcemap: false, sourcemap: false,
clean: true, clean: true,
external: ['react', 'react-dom', 'next', 'pg', 'dotenv', 'dotenv/config', 'resend', '@react-email/components', 'node-cron', 'readline', 'crypto', 'url', 'fs', 'path', 'net', 'dns', 'tls', '@zen/core/api', '@zen/core/cron', '@zen/core/database', '@zen/core/email', '@zen/core/email/templates', '@zen/core/storage', '@zen/core/toast', '@zen/core/features/auth', '@zen/core/features/auth/actions', '@zen/core/features/auth/components', '@zen/core/shared/components', '@zen/core/shared/icons', '@zen/core/shared/logger', '@zen/core/shared/config', '@zen/core/shared/rate-limit', '@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner'], external: ['react', 'react-dom', 'next', 'pg', 'dotenv', 'dotenv/config', 'resend', '@react-email/components', 'node-cron', 'readline', 'crypto', 'url', 'fs', 'path', 'net', 'dns', 'tls', '@zen/core/api', '@zen/core/cron', '@zen/core/database', '@zen/core/email', '@zen/core/email/templates', '@zen/core/storage', '@zen/core/toast', '@zen/core/features/auth', '@zen/core/features/auth/actions', '@zen/core/features/auth/components', '@zen/core/shared/components', '@zen/core/shared/icons', '@zen/core/shared/logger', '@zen/core/shared/config', '@zen/core/shared/rate-limit', '@zen/core/themes', '@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner'],
noExternal: [], noExternal: [],
bundle: true, bundle: true,
banner: { banner: {