release: v0.2.0 — TypeScript 源码重构
### Changed - 全部源码从 JavaScript 迁移到 TypeScript (strict mode) - core.js (1244行) 拆分为 toast.ts + api.ts + templates.ts - 删除手动维护的 types/index.d.ts,类型从源码自动生成 - 构建工具链: Babel → ts-jest, 新增 @rollup/plugin-typescript - 新增 rollup-plugin-dts 生成合并 .d.ts ### Added - tsconfig.json (strict: true) - src/types.ts 核心类型模块 (39 个导出类型) - .eslintrc.json (@typescript-eslint) - .gitea/workflows/ci.yml (Node 18/20/22/24 矩阵) - CHANGELOG.md ### Aligned with metona-starter - package.json: type:module, engines≥16, prepublishOnly - rollup.config.js: dts plugin, port 3001 - jest.config.cjs, build.sh, serve.sh, .gitignore (.npmrc) ### Removed - babel.config.js, types/ 目录, 所有 src/*.js
This commit is contained in:
+407
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.2.0
|
||||
*/
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析主题
|
||||
*/
|
||||
export const resolveTheme = (theme: string): string => {
|
||||
if (theme === 'auto') {
|
||||
if (currentTheme && currentTheme !== 'auto') {
|
||||
return currentTheme;
|
||||
}
|
||||
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<string, ThemeConfig>)[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<string, ThemeConfig>)[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有主题
|
||||
*/
|
||||
export const getAllThemes = (): Record<string, ThemeConfig> => {
|
||||
return { ...THEMES } as Record<string, ThemeConfig>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题名称列表
|
||||
*/
|
||||
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 };
|
||||
Reference in New Issue
Block a user