Initial commit: MetonaEditor v0.1.0
This commit is contained in:
+388
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* MetonaEditor Themes - 主题管理
|
||||
* @module themes
|
||||
* @version 0.1.0
|
||||
* @description 主题系统、自定义主题和主题切换
|
||||
*/
|
||||
|
||||
import { THEMES } from './constants.js';
|
||||
import { prefersDark } from './utils.js';
|
||||
|
||||
let currentTheme = 'auto';
|
||||
let themeListeners = new Set();
|
||||
let systemThemeMediaQuery = null;
|
||||
let systemThemeListener = null;
|
||||
|
||||
/**
|
||||
* 获取系统主题
|
||||
*/
|
||||
export const getSystemTheme = () => {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
return prefersDark() ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题(别名)
|
||||
*/
|
||||
export const getTheme = (theme) => {
|
||||
return resolveTheme(theme);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析主题
|
||||
*/
|
||||
export const resolveTheme = (theme) => {
|
||||
if (theme === 'auto') {
|
||||
if (currentTheme && currentTheme !== 'auto') {
|
||||
return currentTheme;
|
||||
}
|
||||
return getSystemTheme();
|
||||
}
|
||||
return theme;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题配置
|
||||
*/
|
||||
export const getThemeConfig = (theme) => {
|
||||
const resolved = resolveTheme(theme);
|
||||
return THEMES[resolved] || THEMES.light;
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用主题
|
||||
*/
|
||||
export const applyTheme = (theme) => {
|
||||
currentTheme = theme;
|
||||
const resolved = resolveTheme(theme);
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-md-theme', resolved);
|
||||
document.documentElement.classList.remove('md-theme-light', 'md-theme-dark', 'md-theme-auto');
|
||||
document.documentElement.classList.add(`md-theme-${resolved}`);
|
||||
|
||||
const config = getThemeConfig(theme);
|
||||
setThemeVariables(config);
|
||||
}
|
||||
|
||||
notifyThemeListeners(theme, resolved);
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置主题CSS变量
|
||||
*/
|
||||
export const setThemeVariables = (config) => {
|
||||
if (typeof document === 'undefined' || !config || typeof config !== 'object') return;
|
||||
|
||||
const root = document.documentElement;
|
||||
// 基础变量(保留用于全局与组件回退)
|
||||
root.style.setProperty('--md-bg', config.bg);
|
||||
root.style.setProperty('--md-text', config.text);
|
||||
root.style.setProperty('--md-border', config.border);
|
||||
root.style.setProperty('--md-shadow', config.shadow);
|
||||
root.style.setProperty('--md-hover-shadow', config.hoverShadow);
|
||||
// 编辑器专属变量
|
||||
root.style.setProperty('--md-toolbar-bg', config.toolbarBg || config.bg);
|
||||
root.style.setProperty('--md-textarea-bg', config.textareaBg || config.bg);
|
||||
root.style.setProperty('--md-preview-bg', config.previewBg || config.bg);
|
||||
root.style.setProperty('--md-code-bg', config.codeBg || 'rgba(127,127,127,0.1)');
|
||||
root.style.setProperty('--md-code-text', config.codeText || config.text);
|
||||
root.style.setProperty('--md-accent', config.accent || '#3b82f6');
|
||||
root.style.setProperty('--md-muted', config.muted || '#9ca3af');
|
||||
// 兼容旧字段
|
||||
if (config.progressBg) root.style.setProperty('--md-progress-bg', config.progressBg);
|
||||
if (config.closeHoverBg) root.style.setProperty('--md-close-hover-bg', config.closeHoverBg);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前主题
|
||||
*/
|
||||
export const getCurrentTheme = () => {
|
||||
return currentTheme;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取解析后的主题
|
||||
*/
|
||||
export const getResolvedTheme = () => {
|
||||
return resolveTheme(currentTheme);
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
*/
|
||||
export const switchTheme = (theme) => {
|
||||
applyTheme(theme);
|
||||
saveTheme(theme);
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换亮色/暗色主题
|
||||
*/
|
||||
export const toggleTheme = () => {
|
||||
const resolved = getResolvedTheme();
|
||||
const newTheme = resolved === 'dark' ? 'light' : 'dark';
|
||||
switchTheme(newTheme);
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置为自动主题
|
||||
*/
|
||||
export const resetToAuto = () => {
|
||||
switchTheme('auto');
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存主题到本地存储
|
||||
*/
|
||||
export const saveTheme = (theme) => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem('metona-editor-theme', theme);
|
||||
} catch (e) {
|
||||
console.warn('Failed to save theme:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从本地存储加载主题
|
||||
*/
|
||||
export const loadTheme = () => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
return localStorage.getItem('metona-editor-theme') || 'auto';
|
||||
} catch (e) {
|
||||
console.warn('Failed to load theme:', e);
|
||||
}
|
||||
}
|
||||
return 'auto';
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化主题系统
|
||||
*/
|
||||
export const initTheme = () => {
|
||||
const savedTheme = loadTheme();
|
||||
applyTheme(savedTheme);
|
||||
watchSystemTheme();
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听系统主题变化
|
||||
*/
|
||||
export const watchSystemTheme = () => {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) return;
|
||||
|
||||
if (systemThemeMediaQuery && systemThemeListener) {
|
||||
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
||||
}
|
||||
|
||||
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
systemThemeListener = (e) => {
|
||||
if (currentTheme === 'auto') {
|
||||
applyTheme('auto');
|
||||
}
|
||||
};
|
||||
|
||||
systemThemeMediaQuery.addEventListener('change', systemThemeListener);
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止监听系统主题变化
|
||||
*/
|
||||
export const unwatchSystemTheme = () => {
|
||||
if (systemThemeMediaQuery && systemThemeListener) {
|
||||
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
||||
systemThemeMediaQuery = null;
|
||||
systemThemeListener = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加主题监听器
|
||||
*/
|
||||
export const addThemeListener = (listener) => {
|
||||
themeListeners.add(listener);
|
||||
|
||||
return () => {
|
||||
themeListeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除主题监听器
|
||||
*/
|
||||
export const removeThemeListener = (listener) => {
|
||||
themeListeners.delete(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* 通知主题监听器
|
||||
*/
|
||||
const notifyThemeListeners = (theme, resolved) => {
|
||||
themeListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(theme, resolved);
|
||||
} catch (e) {
|
||||
console.error('Theme listener error:', e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除所有主题监听器
|
||||
*/
|
||||
export const clearThemeListeners = () => {
|
||||
themeListeners.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册自定义主题
|
||||
*/
|
||||
export const registerTheme = (name, config) => {
|
||||
const base = THEMES.light;
|
||||
THEMES[name] = {
|
||||
bg: config.bg || base.bg,
|
||||
text: config.text || base.text,
|
||||
border: config.border || base.border,
|
||||
shadow: config.shadow || base.shadow,
|
||||
hoverShadow: config.hoverShadow || base.hoverShadow,
|
||||
toolbarBg: config.toolbarBg || config.bg || base.toolbarBg,
|
||||
textareaBg: config.textareaBg || config.bg || base.textareaBg,
|
||||
previewBg: config.previewBg || config.bg || base.previewBg,
|
||||
codeBg: config.codeBg || base.codeBg,
|
||||
codeText: config.codeText || base.codeText,
|
||||
accent: config.accent || base.accent,
|
||||
muted: config.muted || base.muted,
|
||||
// 兼容旧字段
|
||||
progressBg: config.progressBg,
|
||||
closeHoverBg: config.closeHoverBg,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 注销自定义主题
|
||||
*/
|
||||
export const unregisterTheme = (name) => {
|
||||
if (name === 'light' || name === 'dark' || name === 'auto') {
|
||||
console.warn('Cannot unregister built-in theme:', name);
|
||||
return;
|
||||
}
|
||||
|
||||
delete THEMES[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有主题
|
||||
*/
|
||||
export const getAllThemes = () => {
|
||||
return { ...THEMES };
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题名称列表
|
||||
*/
|
||||
export const getThemeNames = () => {
|
||||
return Object.keys(THEMES);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查主题是否存在
|
||||
*/
|
||||
export const hasTheme = (name) => {
|
||||
return name in THEMES;
|
||||
};
|
||||
|
||||
/**
|
||||
* 主题工具 — 代理层
|
||||
*/
|
||||
export const themeUtils = {
|
||||
getSystemTheme,
|
||||
resolveTheme,
|
||||
getThemeConfig,
|
||||
applyTheme,
|
||||
getCurrentTheme,
|
||||
getResolvedTheme,
|
||||
switchTheme,
|
||||
toggleTheme,
|
||||
resetToAuto,
|
||||
initTheme,
|
||||
watchSystemTheme,
|
||||
unwatchSystemTheme,
|
||||
addThemeListener,
|
||||
removeThemeListener,
|
||||
clearThemeListeners,
|
||||
registerTheme,
|
||||
unregisterTheme,
|
||||
getAllThemes,
|
||||
getThemeNames,
|
||||
hasTheme,
|
||||
saveTheme,
|
||||
loadTheme,
|
||||
};
|
||||
|
||||
/**
|
||||
* 预设主题
|
||||
*/
|
||||
export const presetThemes = {
|
||||
light: {
|
||||
name: '浅色',
|
||||
description: '明亮清晰的主题',
|
||||
config: THEMES.light,
|
||||
},
|
||||
dark: {
|
||||
name: '深色',
|
||||
description: '护眼舒适的暗色主题',
|
||||
config: THEMES.dark,
|
||||
},
|
||||
auto: {
|
||||
name: '自动',
|
||||
description: '跟随系统主题设置',
|
||||
config: 'auto',
|
||||
},
|
||||
warm: {
|
||||
name: '暖色',
|
||||
description: '温馨舒适的暖色主题',
|
||||
config: THEMES.warm,
|
||||
},
|
||||
};
|
||||
|
||||
// 注册预设主题
|
||||
Object.entries(presetThemes).forEach(([name, theme]) => {
|
||||
if (name !== 'auto' && theme.config !== 'auto') {
|
||||
THEMES[name] = theme.config;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建主题管理器
|
||||
*/
|
||||
export const createThemeManager = () => {
|
||||
return {
|
||||
getSystemTheme,
|
||||
resolveTheme,
|
||||
getThemeConfig,
|
||||
applyTheme,
|
||||
getCurrentTheme,
|
||||
getResolvedTheme,
|
||||
switchTheme,
|
||||
toggleTheme,
|
||||
resetToAuto,
|
||||
initTheme,
|
||||
watchSystemTheme,
|
||||
unwatchSystemTheme,
|
||||
addThemeListener,
|
||||
removeThemeListener,
|
||||
clearThemeListeners,
|
||||
registerTheme,
|
||||
unregisterTheme,
|
||||
getAllThemes,
|
||||
getThemeNames,
|
||||
hasTheme,
|
||||
saveTheme,
|
||||
loadTheme,
|
||||
};
|
||||
};
|
||||
|
||||
export { themeUtils as default };
|
||||
Reference in New Issue
Block a user