/** * MetonaToast Themes — 主题管理 * @module themes * @version 0.2.1 */ import { THEMES } from './constants.js'; import { prefersDark } from './utils.js'; import type { ThemeConfig, ThemePreview, ThemeUtils } from './types.js'; // 当前主题状态 let currentTheme = 'auto'; const themeListeners: Set<(theme: string, resolved: string) => void> = new Set(); let systemThemeMediaQuery: MediaQueryList | null = null; let systemThemeListener: ((e: MediaQueryListEvent) => void) | null = null; /** * 获取系统主题 */ export const getSystemTheme = (): 'light' | 'dark' => { if (typeof window === 'undefined') return 'light'; return prefersDark() ? 'dark' : 'light'; }; /** * 获取主题(别名) */ export const getTheme = (theme: string): string => { return resolveTheme(theme); }; /** * 解析主题 — 'auto' 始终按系统主题检测,不依赖当前手动选择的主题 */ export const resolveTheme = (theme: string): string => { if (theme === 'auto') { return getSystemTheme(); } return theme; }; /** * 获取主题配置 */ export const getThemeConfig = (theme: string): ThemeConfig => { const resolved = resolveTheme(theme); return (THEMES[resolved] as ThemeConfig) || (THEMES.light as ThemeConfig); }; /** * 应用主题 */ export const applyTheme = (theme: string): void => { currentTheme = theme; const resolved = resolveTheme(theme); if (typeof document !== 'undefined') { document.documentElement.setAttribute('data-met-theme', resolved); document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto'); document.documentElement.classList.add(`met-theme-${resolved}`); const config = getThemeConfig(theme); setThemeVariables(config); } notifyThemeListeners(theme, resolved); }; /** * 设置主题CSS变量 */ export const setThemeVariables = (config: ThemeConfig): void => { if (typeof document === 'undefined') return; const root = document.documentElement; root.style.setProperty('--met-bg', config.bg); root.style.setProperty('--met-text', config.text); root.style.setProperty('--met-border', config.border); root.style.setProperty('--met-shadow', config.shadow); root.style.setProperty('--met-hover-shadow', config.hoverShadow); root.style.setProperty('--met-progress-bg', config.progressBg); root.style.setProperty('--met-close-hover-bg', config.closeHoverBg); root.style.setProperty('--met-success', '#10b981'); root.style.setProperty('--met-error', '#ef4444'); root.style.setProperty('--met-warning', '#f59e0b'); root.style.setProperty('--met-info', '#3b82f6'); root.style.setProperty('--met-loading', '#6366f1'); }; /** * 清除主题CSS变量 */ export const clearThemeVariables = (): void => { if (typeof document === 'undefined') return; const root = document.documentElement; const variables = [ '--met-bg', '--met-text', '--met-border', '--met-shadow', '--met-hover-shadow', '--met-progress-bg', '--met-close-hover-bg', '--met-success', '--met-error', '--met-warning', '--met-info', '--met-loading', ]; variables.forEach((v) => { root.style.removeProperty(v); }); }; /** * 获取当前主题 */ export const getCurrentTheme = (): string => { return currentTheme; }; /** * 获取解析后的主题 */ export const getResolvedTheme = (): string => { return resolveTheme(currentTheme); }; /** * 切换主题 */ export const switchTheme = (theme: string): void => { applyTheme(theme); saveTheme(theme); }; /** * 切换亮色/暗色主题 */ export const toggleTheme = (): void => { const resolved = getResolvedTheme(); const newTheme = resolved === 'dark' ? 'light' : 'dark'; switchTheme(newTheme); }; /** * 重置为自动主题 */ export const resetToAuto = (): void => { switchTheme('auto'); }; /** * 保存主题到本地存储 */ export const saveTheme = (theme: string): void => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-theme', theme); } catch (e) { console.warn('Failed to save theme:', e); } } }; /** * 从本地存储加载主题 */ export const loadTheme = (): string => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-theme') || 'auto'; } catch (e) { console.warn('Failed to load theme:', e); } } return 'auto'; }; /** * 初始化主题系统 */ export const initTheme = (): void => { const savedTheme = loadTheme(); applyTheme(savedTheme); watchSystemTheme(); }; /** * 监听系统主题变化 */ export const watchSystemTheme = (): void => { if (typeof window === 'undefined' || !window.matchMedia) return; if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); } systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); systemThemeListener = () => { if (currentTheme === 'auto') { applyTheme('auto'); } }; systemThemeMediaQuery.addEventListener('change', systemThemeListener); }; /** * 停止监听系统主题变化 */ export const unwatchSystemTheme = (): void => { if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); systemThemeMediaQuery = null; systemThemeListener = null; } }; /** * 添加主题监听器 */ export const addThemeListener = (listener: (theme: string, resolved: string) => void): () => void => { themeListeners.add(listener); return () => { themeListeners.delete(listener); }; }; /** * 移除主题监听器 */ export const removeThemeListener = (listener: (theme: string, resolved: string) => void): void => { themeListeners.delete(listener); }; /** * 通知主题监听器 */ const notifyThemeListeners = (theme: string, resolved: string): void => { themeListeners.forEach((listener) => { try { listener(theme, resolved); } catch (e) { console.error('Theme listener error:', e); } }); }; /** * 清除所有主题监听器 */ export const clearThemeListeners = (): void => { themeListeners.clear(); }; /** * 注册自定义主题 */ export const registerTheme = (name: string, config: ThemeConfig): void => { (THEMES as Record)[name] = { bg: config.bg || (THEMES.light as ThemeConfig).bg, text: config.text || (THEMES.light as ThemeConfig).text, border: config.border || (THEMES.light as ThemeConfig).border, shadow: config.shadow || (THEMES.light as ThemeConfig).shadow, hoverShadow: config.hoverShadow || (THEMES.light as ThemeConfig).hoverShadow, progressBg: config.progressBg || (THEMES.light as ThemeConfig).progressBg, closeHoverBg: config.closeHoverBg || (THEMES.light as ThemeConfig).closeHoverBg, }; }; /** * 注销自定义主题 */ export const unregisterTheme = (name: string): void => { if (name === 'light' || name === 'dark' || name === 'auto') { console.warn('Cannot unregister built-in theme:', name); return; } delete (THEMES as Record)[name]; }; /** * 获取所有主题 */ export const getAllThemes = (): Record => { return { ...THEMES } as Record; }; /** * 获取主题名称列表 */ export const getThemeNames = (): string[] => { return Object.keys(THEMES); }; /** * 检查主题是否存在 */ export const hasTheme = (name: string): boolean => { return name in THEMES; }; /** * 获取主题预览 */ export const getThemePreview = (theme: string): ThemePreview => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return { name: theme, resolved, colors: { background: config.bg, text: config.text, border: config.border, }, isDark: resolved === 'dark', isLight: resolved === 'light', isAuto: theme === 'auto', }; }; /** * 生成主题CSS */ export const generateThemeCSS = (theme: string): string => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return ` .met-theme-${resolved} { --met-bg: ${config.bg}; --met-text: ${config.text}; --met-border: ${config.border}; --met-shadow: ${config.shadow}; --met-hover-shadow: ${config.hoverShadow}; --met-progress-bg: ${config.progressBg}; --met-close-hover-bg: ${config.closeHoverBg}; } `; }; /** * 应用主题CSS */ export const applyThemeCSS = (theme: string): void => { if (typeof document === 'undefined') return; const css = generateThemeCSS(theme); const styleId = 'metona-toast-theme-css'; const oldStyle = document.getElementById(styleId); if (oldStyle) { oldStyle.remove(); } const style = document.createElement('style'); style.id = styleId; style.textContent = css; document.head.appendChild(style); }; /** * 移除主题CSS */ export const removeThemeCSS = (): void => { if (typeof document === 'undefined') return; const styleId = 'metona-toast-theme-css'; const style = document.getElementById(styleId); if (style) { style.remove(); } }; /** * 主题工具 — 代理层 */ export const themeUtils: ThemeUtils = { getSystemTheme, resolveTheme, getThemeConfig, applyTheme, getCurrentTheme, getResolvedTheme, switchTheme, toggleTheme, resetToAuto, initTheme, watchSystemTheme, unwatchSystemTheme, addThemeListener, removeThemeListener, clearThemeListeners, registerTheme, unregisterTheme, getAllThemes, getThemeNames, hasTheme, getThemePreview, generateThemeCSS, applyThemeCSS, removeThemeCSS, saveTheme, loadTheme, }; export { themeUtils as default };