feat(core): syntax highlight editor overlay - Transparent highlight-layer behind textarea with regex-based token coloring - Supports heading, bold, italic, strikethrough, code, link, image, list, quote, hr - Scroll-synced with textarea, config.syntaxHighlight toggle feat(core): preview link click interception - Clicks on preview links intercepted, emitted as 'linkClick' event - Configurable via onLinkClick(uri, text, editor) callback - Default: opens in new tab, anchor links pass through feat(parser): Mermaid diagram rendering - ```mermaid code blocks render as <div class="me-mermaid"><pre class="mermaid"> feat(plugins): fileSystem plugin (IndexedDB persistence) - saveToFile(id) / loadFromFile(id) / deleteFile(id) / listFiles() - Uses IndexedDB 'metona-editor-fs' database feat(config): maxLength (char limit), syntaxHighlight toggle style: syntax highlight theme colors, Mermaid container chore: bump version 0.1.13 → 0.1.14 across all files
740 lines
20 KiB
JavaScript
740 lines
20 KiB
JavaScript
/**
|
||
* MetonaEditor Themes — 主题系统(增强版)
|
||
* @module themes
|
||
* @version 0.1.14
|
||
* @description 全局+实例级主题管理、CSS 变量隔离、外部主题跟随、主题继承
|
||
*
|
||
* v0.1.5 增强:
|
||
* - 修复 resolveTheme('auto') 始终解析为系统主题
|
||
* - 实例级 setTheme() / getTheme(),多实例独立主题
|
||
* - 实例级 themeChange 事件通知
|
||
* - 外部主题跟随:syncWithElement() / adoptFromParent() / watch()
|
||
* - 主题继承:registerTheme(name, { extends: 'light', ... })
|
||
* - 导出 CSS 变量:exportCSSVars() / getCSSVariable()
|
||
* - 主题悬停/聚焦状态变量支持
|
||
* - 应用主题到指定元素(不污染 :root)
|
||
*/
|
||
|
||
import { THEMES } from './constants.js';
|
||
import { prefersDark } from './utils.js';
|
||
|
||
// ============ 模块级状态(全局默认,向后兼容) ============
|
||
|
||
let globalCurrentTheme = 'auto';
|
||
let globalThemeListeners = new Set();
|
||
let systemThemeMediaQuery = null;
|
||
let systemThemeListener = null;
|
||
|
||
// ============ 系统主题 ============
|
||
|
||
/**
|
||
* 获取系统主题偏好
|
||
* @returns {'light'|'dark'}
|
||
*/
|
||
const getSystemTheme = () => {
|
||
if (typeof window === 'undefined') return 'light';
|
||
return prefersDark() ? 'dark' : 'light';
|
||
};
|
||
|
||
/**
|
||
* 监听系统主题变化(内部回调版)
|
||
* @param {(theme: 'light'|'dark') => void} callback
|
||
* @returns {() => void} 取消监听函数
|
||
*/
|
||
const _watchSystem = (callback) => {
|
||
if (typeof window === 'undefined' || !window.matchMedia) return () => {};
|
||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||
const handler = (e) => callback(e.matches ? 'dark' : 'light');
|
||
mql.addEventListener('change', handler);
|
||
return () => mql.removeEventListener('change', handler);
|
||
};
|
||
|
||
/**
|
||
* 监听系统主题变化(全局单例模式,向后兼容)
|
||
* 自动在 auto 模式下跟随系统主题切换
|
||
*/
|
||
const watchSystemTheme = () => {
|
||
if (typeof window === 'undefined' || !window.matchMedia) return;
|
||
|
||
// 清理旧监听
|
||
if (_sysWatchUnsub) { _sysWatchUnsub(); _sysWatchUnsub = null; }
|
||
|
||
_sysWatchUnsub = _watchSystem((sysTheme) => {
|
||
if (globalCurrentTheme === 'auto') {
|
||
applyTheme('auto');
|
||
}
|
||
});
|
||
};
|
||
|
||
// ============ 主题解析 ============
|
||
|
||
/**
|
||
* 解析主题名为实际主题值
|
||
* 修复:auto 始终解析为系统当前主题,不再被上次手动设置污染
|
||
* @param {string} theme - 主题名
|
||
* @returns {string} 解析后的主题名
|
||
*/
|
||
const resolveTheme = (theme) => {
|
||
if (!theme || theme === 'auto') {
|
||
return getSystemTheme();
|
||
}
|
||
return theme;
|
||
};
|
||
|
||
/**
|
||
* 获取主题配置对象
|
||
* @param {string} theme - 主题名
|
||
* @returns {Object} 主题配置
|
||
*/
|
||
const getThemeConfig = (theme) => {
|
||
const resolved = resolveTheme(theme);
|
||
return THEMES[resolved] || THEMES.light;
|
||
};
|
||
|
||
// ============ CSS 变量操作 ============
|
||
|
||
/**
|
||
* 将主题配置写入 CSS 变量
|
||
* @param {Object} config - 主题配置对象
|
||
* @param {HTMLElement} [target] - 目标元素(默认 documentElement)
|
||
*/
|
||
const setThemeVariables = (config, target) => {
|
||
if (typeof document === 'undefined' || !config || typeof config !== 'object') return;
|
||
|
||
const root = target || document.documentElement;
|
||
const vars = {
|
||
'--md-bg': config.bg,
|
||
'--md-text': config.text,
|
||
'--md-border': config.border,
|
||
'--md-shadow': config.shadow,
|
||
'--md-hover-shadow': config.hoverShadow,
|
||
'--md-toolbar-bg': config.toolbarBg || config.bg,
|
||
'--md-textarea-bg': config.textareaBg || config.bg,
|
||
'--md-preview-bg': config.previewBg || config.bg,
|
||
'--md-code-bg': config.codeBg || 'rgba(127,127,127,0.1)',
|
||
'--md-code-text': config.codeText || config.text,
|
||
'--md-accent': config.accent || '#3b82f6',
|
||
'--md-muted': config.muted || '#9ca3af',
|
||
};
|
||
|
||
Object.entries(vars).forEach(([k, v]) => {
|
||
if (v !== undefined && v !== null) {
|
||
root.style.setProperty(k, String(v));
|
||
}
|
||
});
|
||
|
||
// 兼容旧字段
|
||
if (config.progressBg) root.style.setProperty('--md-progress-bg', config.progressBg);
|
||
if (config.closeHoverBg) root.style.setProperty('--md-close-hover-bg', config.closeHoverBg);
|
||
};
|
||
|
||
/**
|
||
* 导出当前 CSS 变量值(从指定元素读取)
|
||
* @param {HTMLElement} [el] - 读取的元素(默认 documentElement)
|
||
* @returns {Object} CSS 变量键值对
|
||
*/
|
||
const exportCSSVars = (el) => {
|
||
const root = el || document.documentElement;
|
||
if (typeof document === 'undefined') return {};
|
||
const style = getComputedStyle(root);
|
||
const varNames = [
|
||
'--md-bg', '--md-text', '--md-border', '--md-shadow',
|
||
'--md-hover-shadow', '--md-toolbar-bg', '--md-textarea-bg',
|
||
'--md-preview-bg', '--md-code-bg', '--md-code-text',
|
||
'--md-accent', '--md-muted',
|
||
];
|
||
const result = {};
|
||
varNames.forEach((name) => {
|
||
const val = style.getPropertyValue(name).trim();
|
||
if (val) result[name] = val;
|
||
});
|
||
return result;
|
||
};
|
||
|
||
/**
|
||
* 获取单个 CSS 变量值
|
||
* @param {string} name - 变量名(自动补全 --md- 前缀)
|
||
* @param {HTMLElement} [el] - 读取的元素
|
||
* @returns {string} 变量值
|
||
*/
|
||
const getCSSVariable = (name, el) => {
|
||
const root = el || document.documentElement;
|
||
if (typeof document === 'undefined') return '';
|
||
const fullName = name.startsWith('--') ? name : `--md-${name}`;
|
||
return getComputedStyle(root).getPropertyValue(fullName).trim();
|
||
};
|
||
|
||
// ============ 主题切换与应用 ============
|
||
|
||
/**
|
||
* 应用主题到全局(向后兼容)
|
||
* @param {string} theme - 主题名
|
||
*/
|
||
const applyTheme = (theme) => {
|
||
globalCurrentTheme = 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', 'md-theme-warm');
|
||
document.documentElement.classList.add(`md-theme-${resolved}`);
|
||
|
||
const config = getThemeConfig(theme);
|
||
setThemeVariables(config);
|
||
}
|
||
|
||
notifyGlobalListeners(theme, resolved);
|
||
};
|
||
|
||
/**
|
||
* 应用主题到指定元素(不改变全局状态)
|
||
* @param {string} theme - 主题名
|
||
* @param {HTMLElement} target - 目标 DOM 元素
|
||
*/
|
||
const applyThemeToElement = (theme, target) => {
|
||
if (!target || typeof document === 'undefined') return;
|
||
const resolved = resolveTheme(theme);
|
||
const config = getThemeConfig(theme);
|
||
|
||
target.setAttribute('data-md-theme', resolved);
|
||
target.classList.remove('md-theme-light', 'md-theme-dark', 'md-theme-auto', 'md-theme-warm');
|
||
if (resolved !== 'auto') {
|
||
target.classList.add(`md-theme-${resolved}`);
|
||
}
|
||
|
||
setThemeVariables(config, target);
|
||
};
|
||
|
||
/**
|
||
* 切换主题并持久化
|
||
*/
|
||
const switchTheme = (theme) => {
|
||
applyTheme(theme);
|
||
saveTheme(theme);
|
||
};
|
||
|
||
/**
|
||
* 在 light/dark 间切换
|
||
*/
|
||
const toggleTheme = () => {
|
||
const resolved = getResolvedTheme();
|
||
switchTheme(resolved === 'dark' ? 'light' : 'dark');
|
||
};
|
||
|
||
/**
|
||
* 重置为自动主题
|
||
*/
|
||
const resetToAuto = () => {
|
||
switchTheme('auto');
|
||
};
|
||
|
||
/**
|
||
* 获取全局当前主题名
|
||
*/
|
||
const getCurrentTheme = () => globalCurrentTheme;
|
||
|
||
/**
|
||
* 获取解析后的全局主题
|
||
*/
|
||
const getResolvedTheme = () => resolveTheme(globalCurrentTheme);
|
||
|
||
// ============ 主题持久化 ============
|
||
|
||
const saveTheme = (theme) => {
|
||
if (typeof localStorage !== 'undefined') {
|
||
try { localStorage.setItem('metona-editor-theme', theme); } catch (_) {}
|
||
}
|
||
};
|
||
|
||
const loadTheme = () => {
|
||
if (typeof localStorage !== 'undefined') {
|
||
try { return localStorage.getItem('metona-editor-theme') || 'auto'; } catch (_) {}
|
||
}
|
||
return 'auto';
|
||
};
|
||
|
||
// ============ 全局监听 ============
|
||
|
||
const notifyGlobalListeners = (theme, resolved) => {
|
||
globalThemeListeners.forEach((fn) => {
|
||
try { fn(theme, resolved); } catch (e) { console.error('Theme listener error:', e); }
|
||
});
|
||
};
|
||
|
||
const addThemeListener = (fn) => {
|
||
globalThemeListeners.add(fn);
|
||
return () => globalThemeListeners.delete(fn);
|
||
};
|
||
|
||
const removeThemeListener = (fn) => {
|
||
globalThemeListeners.delete(fn);
|
||
};
|
||
|
||
const clearThemeListeners = () => {
|
||
globalThemeListeners.clear();
|
||
};
|
||
|
||
// ============ 系统主题监听 ============
|
||
|
||
let _sysWatchUnsub = null;
|
||
|
||
const startSystemWatch = () => {
|
||
watchSystemTheme(); // 使用兼容的全局单例
|
||
};
|
||
|
||
const stopSystemWatch = () => {
|
||
if (_sysWatchUnsub) { _sysWatchUnsub(); _sysWatchUnsub = null; }
|
||
};
|
||
|
||
// 保留旧 API 兼容
|
||
const unwatchSystemTheme = stopSystemWatch;
|
||
|
||
// ============ 初始化 ============
|
||
|
||
const initTheme = () => {
|
||
const savedTheme = loadTheme();
|
||
applyTheme(savedTheme);
|
||
startSystemWatch();
|
||
};
|
||
|
||
// ============ 外部主题跟随(v0.1.5 新增) ============
|
||
|
||
/**
|
||
* 跟随外部元素的主题
|
||
* 支持三种检测策略:
|
||
* 1. data-md-theme 属性(推荐,显式声明)
|
||
* 2. CSS 类名(如 theme-dark / theme-light)
|
||
* 3. 自定义回调函数
|
||
*
|
||
* @param {Object} options
|
||
* @param {HTMLElement} [options.element] - 观察的元素(默认 documentElement)
|
||
* @param {string} [options.attr='data-theme'] - 检测的属性名
|
||
* @param {string[]} [options.classMap] - 类名映射 { 'theme-dark': 'dark', 'theme-light': 'light' }
|
||
* @param {Function} [options.callback] - 自定义回调 (element) => themeName
|
||
* @param {Function} onThemeChange - 主题变化回调 (resolvedTheme) => void
|
||
* @returns {() => void} 取消跟随函数
|
||
*/
|
||
const followExternalTheme = (options, onThemeChange) => {
|
||
if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') {
|
||
return () => {};
|
||
}
|
||
|
||
const {
|
||
element = document.documentElement,
|
||
attr = 'data-theme',
|
||
classMap = null,
|
||
callback = null,
|
||
} = options || {};
|
||
|
||
const detect = () => {
|
||
if (typeof callback === 'function') {
|
||
return callback(element);
|
||
}
|
||
// 策略1:属性检测
|
||
const attrVal = element.getAttribute(attr);
|
||
if (attrVal) return attrVal;
|
||
// 策略2:classMap 检测
|
||
if (classMap) {
|
||
for (const [cls, theme] of Object.entries(classMap)) {
|
||
if (element.classList.contains(cls)) return theme;
|
||
}
|
||
}
|
||
// 策略3:data-md-theme 回退
|
||
const mdTheme = element.getAttribute('data-md-theme');
|
||
if (mdTheme) return mdTheme;
|
||
return null;
|
||
};
|
||
|
||
let lastTheme = detect();
|
||
if (lastTheme) onThemeChange(lastTheme);
|
||
|
||
const observer = new MutationObserver(() => {
|
||
const current = detect();
|
||
if (current && current !== lastTheme) {
|
||
lastTheme = current;
|
||
onThemeChange(current);
|
||
}
|
||
});
|
||
|
||
observer.observe(element, {
|
||
attributes: true,
|
||
attributeFilter: [attr, 'class', 'data-md-theme'],
|
||
});
|
||
|
||
return () => observer.disconnect();
|
||
};
|
||
|
||
/**
|
||
* 从父容器继承主题(读取 CSS 变量 + data-md-theme)
|
||
* 适用场景:编辑器嵌入已有主题体系的应用时自动适配
|
||
*
|
||
* @param {HTMLElement} container - 编辑器容器元素
|
||
* @param {(theme: string) => void} onThemeDetected - 检测到主题的回调
|
||
* @returns {() => void} 取消跟随函数
|
||
*/
|
||
const adoptFromParent = (container, onThemeDetected) => {
|
||
if (typeof document === 'undefined' || !container) return () => {};
|
||
|
||
const detect = () => {
|
||
// 向上遍历 DOM 树寻找 data-md-theme 属性
|
||
let el = container.parentElement;
|
||
while (el) {
|
||
const t = el.getAttribute('data-md-theme');
|
||
if (t) return t;
|
||
// 也检测常见类名
|
||
if (el.classList.contains('theme-dark') || el.classList.contains('dark')) return 'dark';
|
||
if (el.classList.contains('theme-light') || el.classList.contains('light')) return 'light';
|
||
el = el.parentElement;
|
||
}
|
||
// 回退:读取父元素 computed style 中的 --md-bg 判断明暗
|
||
if (container.parentElement) {
|
||
const bg = getComputedStyle(container.parentElement).getPropertyValue('--md-bg').trim()
|
||
|| getComputedStyle(container.parentElement).backgroundColor;
|
||
if (bg) {
|
||
// 简单亮度判断
|
||
const rgb = bg.match(/\d+/g);
|
||
if (rgb && rgb.length >= 3) {
|
||
const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000;
|
||
return brightness < 128 ? 'dark' : 'light';
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const detected = detect();
|
||
if (detected) onThemeDetected(detected);
|
||
|
||
// 监听父元素的属性变化
|
||
const observer = new MutationObserver(() => {
|
||
const d = detect();
|
||
if (d) onThemeDetected(d);
|
||
});
|
||
|
||
let target = container.parentElement;
|
||
while (target) {
|
||
observer.observe(target, { attributes: true, attributeFilter: ['class', 'data-md-theme', 'data-theme'] });
|
||
target = target.parentElement;
|
||
// 只观察向上3层,避免性能问题
|
||
if (target === document.documentElement) break;
|
||
}
|
||
|
||
return () => observer.disconnect();
|
||
};
|
||
|
||
/**
|
||
* 创建一个主题观察者,监听外部主题源变化
|
||
* @param {Function|string} source - 回调函数返回主题名,或 CSS 选择器
|
||
* @param {(theme: string) => void} onChange - 主题变化回调
|
||
* @returns {() => void} 取消观察
|
||
*/
|
||
const watch = (source, onChange) => {
|
||
if (typeof source === 'function') {
|
||
const tick = () => {
|
||
try {
|
||
const theme = source();
|
||
if (theme) onChange(theme);
|
||
} catch (_) {}
|
||
};
|
||
tick();
|
||
const id = setInterval(tick, 500);
|
||
return () => clearInterval(id);
|
||
}
|
||
|
||
if (typeof source === 'string' && typeof document !== 'undefined') {
|
||
const el = document.querySelector(source);
|
||
if (!el) return () => {};
|
||
return followExternalTheme({ element: el, attr: 'data-theme' }, onChange);
|
||
}
|
||
|
||
return () => {};
|
||
};
|
||
|
||
// ============ 自定义主题注册(v0.1.5: 支持继承) ============
|
||
|
||
/**
|
||
* 注册自定义主题
|
||
* @param {string} name - 主题名
|
||
* @param {Object} config - 主题配置
|
||
* @param {string} [config.extends] - 继承的基础主题名(如 'light')
|
||
*/
|
||
const registerTheme = (name, config = {}) => {
|
||
const baseName = config.extends || 'light';
|
||
const base = THEMES[baseName] || 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 || base.toolbarBg || base.bg,
|
||
textareaBg: config.textareaBg || base.textareaBg || base.bg,
|
||
previewBg: config.previewBg || base.previewBg || base.bg,
|
||
codeBg: config.codeBg || base.codeBg,
|
||
codeText: config.codeText || base.codeText,
|
||
accent: config.accent || base.accent,
|
||
muted: config.muted || base.muted,
|
||
progressBg: config.progressBg || base.progressBg,
|
||
closeHoverBg: config.closeHoverBg || base.closeHoverBg,
|
||
};
|
||
};
|
||
|
||
const unregisterTheme = (name) => {
|
||
if (name === 'light' || name === 'dark' || name === 'auto' || name === 'warm') {
|
||
console.warn('Cannot unregister built-in theme:', name);
|
||
return;
|
||
}
|
||
delete THEMES[name];
|
||
};
|
||
|
||
const getAllThemes = () => ({ ...THEMES });
|
||
const getThemeNames = () => Object.keys(THEMES);
|
||
const hasTheme = (name) => name in THEMES;
|
||
|
||
// ============ 实例级主题管理(v0.1.5 新增) ============
|
||
|
||
/**
|
||
* 为编辑器实例创建隔离的主题上下文
|
||
* 每个上下文独立跟踪主题,变更时自动同步 CSS 变量到实例 DOM
|
||
*
|
||
* @param {Object} editor - 编辑器实例(需有 el / config 属性)
|
||
* @returns {Object} 实例主题 API
|
||
*/
|
||
const createInstanceTheme = (editor) => {
|
||
let instanceTheme = editor.config?.theme || globalCurrentTheme || 'auto';
|
||
|
||
/** 应用主题到当前实例的 DOM */
|
||
const apply = (theme) => {
|
||
instanceTheme = theme;
|
||
const resolved = resolveTheme(theme);
|
||
const config = getThemeConfig(theme);
|
||
|
||
if (editor.el) {
|
||
// 更新 wrapper 类名
|
||
['me-theme-light', 'me-theme-dark', 'me-theme-warm'].forEach((c) => {
|
||
editor.el.classList.remove(c);
|
||
});
|
||
if (resolved !== 'auto') {
|
||
editor.el.classList.add(`me-theme-${resolved}`);
|
||
}
|
||
editor.el.setAttribute('data-md-theme', resolved);
|
||
|
||
// 设置实例级 CSS 变量(scope 到 wrapper)
|
||
setThemeVariables(config, editor.el);
|
||
}
|
||
|
||
// 触发编辑器实例事件
|
||
if (typeof editor._emit === 'function') {
|
||
editor._emit('themeChange', { theme, resolved, config });
|
||
}
|
||
|
||
// 强制刷新预览(主题可能影响渲染)
|
||
if (typeof editor.refresh === 'function') {
|
||
editor.refresh();
|
||
}
|
||
|
||
return instanceTheme;
|
||
};
|
||
|
||
/** 获取实例当前主题名 */
|
||
const get = () => instanceTheme;
|
||
|
||
/** 设置实例主题 */
|
||
const set = (theme) => apply(theme);
|
||
|
||
/** 切换 light/dark */
|
||
const toggle = () => {
|
||
const resolved = resolveTheme(instanceTheme);
|
||
return apply(resolved === 'dark' ? 'light' : 'dark');
|
||
};
|
||
|
||
/** 获取解析后的主题名 */
|
||
const getResolved = () => resolveTheme(instanceTheme);
|
||
|
||
/** 获取实例主题配置 */
|
||
const getConfig = () => getThemeConfig(instanceTheme);
|
||
|
||
/** 获取实例 CSS 变量导出 */
|
||
const getVars = () => {
|
||
if (editor.el) return exportCSSVars(editor.el);
|
||
return {};
|
||
};
|
||
|
||
/** 外部主题跟随:同步到外部元素 */
|
||
const syncWithElement = (element, opts = {}) => {
|
||
return followExternalTheme(
|
||
{ element, attr: opts.attr || 'data-theme', classMap: opts.classMap, callback: opts.callback },
|
||
(detected) => apply(detected),
|
||
);
|
||
};
|
||
|
||
/** 从父容器继承主题 */
|
||
const adopt = () => {
|
||
if (editor.container) {
|
||
return adoptFromParent(editor.container, (detected) => apply(detected));
|
||
}
|
||
return () => {};
|
||
};
|
||
|
||
/** 监听外部主题变化 */
|
||
const watchExternal = (source) => {
|
||
return watch(source, (theme) => apply(theme));
|
||
};
|
||
|
||
// 初始应用
|
||
apply(instanceTheme);
|
||
|
||
return {
|
||
apply,
|
||
get,
|
||
set,
|
||
toggle,
|
||
getResolved,
|
||
getConfig,
|
||
getVars,
|
||
syncWithElement,
|
||
adopt,
|
||
watch: watchExternal,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 创建独立主题管理器(工厂函数,向后兼容)
|
||
* @returns {Object} 主题管理器
|
||
*/
|
||
const createThemeManager = () => ({
|
||
getSystemTheme,
|
||
resolveTheme,
|
||
getThemeConfig,
|
||
applyTheme,
|
||
getCurrentTheme,
|
||
getResolvedTheme,
|
||
switchTheme,
|
||
toggleTheme,
|
||
resetToAuto,
|
||
initTheme,
|
||
watchSystemTheme,
|
||
unwatchSystemTheme,
|
||
addThemeListener,
|
||
removeThemeListener,
|
||
clearThemeListeners,
|
||
registerTheme,
|
||
unregisterTheme,
|
||
getAllThemes,
|
||
getThemeNames,
|
||
hasTheme,
|
||
saveTheme,
|
||
loadTheme,
|
||
setThemeVariables,
|
||
exportCSSVars,
|
||
getCSSVariable,
|
||
applyThemeToElement,
|
||
followExternalTheme,
|
||
adoptFromParent,
|
||
watch,
|
||
createInstanceTheme,
|
||
});
|
||
|
||
// ============ 预设主题 ============
|
||
|
||
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 },
|
||
};
|
||
|
||
// 初始化:确保 THEMES 中有预设
|
||
Object.entries(presetThemes).forEach(([name, theme]) => {
|
||
if (name !== 'auto' && theme.config !== 'auto') {
|
||
THEMES[name] = theme.config;
|
||
}
|
||
});
|
||
|
||
// ============ 工具代理层 ============
|
||
|
||
const themeUtils = {
|
||
// 系统
|
||
getSystemTheme,
|
||
resolveTheme,
|
||
getThemeConfig,
|
||
watchSystemTheme,
|
||
unwatchSystemTheme,
|
||
// 全局主题
|
||
applyTheme,
|
||
getCurrentTheme,
|
||
getResolvedTheme,
|
||
switchTheme,
|
||
toggleTheme,
|
||
resetToAuto,
|
||
initTheme,
|
||
// 持久化
|
||
saveTheme,
|
||
loadTheme,
|
||
// 监听
|
||
addThemeListener,
|
||
removeThemeListener,
|
||
clearThemeListeners,
|
||
// 注册
|
||
registerTheme,
|
||
unregisterTheme,
|
||
getAllThemes,
|
||
getThemeNames,
|
||
hasTheme,
|
||
// CSS 变量
|
||
setThemeVariables,
|
||
exportCSSVars,
|
||
getCSSVariable,
|
||
applyThemeToElement,
|
||
// 外部跟随(v0.1.5)
|
||
followExternalTheme,
|
||
adoptFromParent,
|
||
watch,
|
||
// 实例主题(v0.1.5)
|
||
createInstanceTheme,
|
||
};
|
||
|
||
// ============ 具名导出 ============
|
||
|
||
export {
|
||
themeUtils,
|
||
themeUtils as default,
|
||
getSystemTheme,
|
||
resolveTheme,
|
||
getThemeConfig,
|
||
applyTheme,
|
||
getCurrentTheme,
|
||
getResolvedTheme,
|
||
switchTheme,
|
||
toggleTheme,
|
||
resetToAuto,
|
||
saveTheme,
|
||
loadTheme,
|
||
initTheme,
|
||
watchSystemTheme,
|
||
unwatchSystemTheme,
|
||
addThemeListener,
|
||
removeThemeListener,
|
||
clearThemeListeners,
|
||
registerTheme,
|
||
unregisterTheme,
|
||
getAllThemes,
|
||
getThemeNames,
|
||
hasTheme,
|
||
setThemeVariables,
|
||
exportCSSVars,
|
||
getCSSVariable,
|
||
applyThemeToElement,
|
||
followExternalTheme,
|
||
adoptFromParent,
|
||
watch,
|
||
createInstanceTheme,
|
||
createThemeManager,
|
||
presetThemes,
|
||
};
|
||
|
||
// 向后兼容别名
|
||
const getTheme = resolveTheme;
|
||
export { getTheme };
|