- 自定义动画:animations.register() 注册的动画动态注入 keyframes 真实生效(此前仅存 Map 实际 fallback slide) - max 超限:通过实例注册表真正 close() 最早的 toast(此前仅删内存记录,DOM 残留) - 钩子收敛:beforeClose/beforeUpdate 支持返回 false 拦截;接入 click/hover/dragStart/dragEnd/animationStart/animationEnd/progressStart/progressEnd/configChange/themeChange/localeChange/beforeInit/afterInit/beforeDestroy/afterDestroy;HOOK_NAMES 移除永不触发的声明 - destroy() 全量清理:hooks、实例注册表、全部 metona-toast-* 样式标签;动画注册表真正清空 - applyThemeVariables 重复注入修复;init() 支持插件对象并复活 _destroyed;AnimationConfig 类型放宽 - VERSION 统一到 constants.ts 唯一维护;删除失效 scripts;新增 .gitattributes 统一 LF - docs.html 补 v0.2.1 新 API 文档;CHANGELOG 更新;测试 309 用例全过
4413 lines
156 KiB
JavaScript
4413 lines
156 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
/**
|
|
* MetonaToast Utils — 工具函数
|
|
* @module utils
|
|
* @version 0.3.0
|
|
*/
|
|
/**
|
|
* 生成唯一ID
|
|
*/
|
|
const generateId = () => {
|
|
return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
|
};
|
|
/**
|
|
* HTML转义
|
|
* @param s - 输入字符串
|
|
*/
|
|
const escapeHTML = (s) => {
|
|
if (typeof document === 'undefined') {
|
|
return String(s == null ? '' : s)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
const div = document.createElement('div');
|
|
div.textContent = String(s == null ? '' : s);
|
|
return div.innerHTML;
|
|
};
|
|
/**
|
|
* 检测暗色模式偏好
|
|
*/
|
|
const prefersDark = () => {
|
|
return typeof window !== 'undefined' &&
|
|
typeof window.matchMedia === 'function' &&
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
};
|
|
|
|
/**
|
|
* MetonaToast Icons — 图标SVG定义
|
|
* @module icons
|
|
* @version 0.3.0
|
|
* @description 107 个内置 SVG 图标
|
|
*/
|
|
const ICONS = {
|
|
success: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>`,
|
|
error: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
</svg>`,
|
|
warning: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
|
<line x1="12" y1="9" x2="12" y2="13"/>
|
|
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
</svg>`,
|
|
info: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="12" y1="16" x2="12" y2="12"/>
|
|
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
|
</svg>`,
|
|
loading: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 12a9 9 0 1 1-6.219-8.56" class="met-spin"/>
|
|
</svg>`,
|
|
close: `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>`,
|
|
check: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>`,
|
|
x: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>`,
|
|
alert: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
|
<line x1="12" y1="9" x2="12" y2="13"/>
|
|
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
</svg>`,
|
|
question: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
|
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
</svg>`,
|
|
star: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
|
</svg>`,
|
|
heart: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
|
</svg>`,
|
|
bell: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
|
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
|
</svg>`,
|
|
mail: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
|
<polyline points="22,6 12,13 2,6"/>
|
|
</svg>`,
|
|
settings: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="3"/>
|
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
</svg>`,
|
|
user: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
|
<circle cx="12" cy="7" r="4"/>
|
|
</svg>`,
|
|
home: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
|
<polyline points="9 22 9 12 15 12 15 22"/>
|
|
</svg>`,
|
|
search: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="11" cy="11" r="8"/>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
</svg>`,
|
|
plus: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="12" y1="5" x2="12" y2="19"/>
|
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>`,
|
|
minus: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>`,
|
|
edit: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
</svg>`,
|
|
trash: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="3 6 5 6 21 6"/>
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
|
<line x1="14" y1="11" x2="14" y2="17"/>
|
|
</svg>`,
|
|
download: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="7 10 12 15 17 10"/>
|
|
<line x1="12" y1="15" x2="12" y2="3"/>
|
|
</svg>`,
|
|
upload: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="17 8 12 3 7 8"/>
|
|
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
</svg>`,
|
|
share: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="18" cy="5" r="3"/>
|
|
<circle cx="6" cy="12" r="3"/>
|
|
<circle cx="18" cy="19" r="3"/>
|
|
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
|
|
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
|
|
</svg>`,
|
|
link: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
</svg>`,
|
|
external: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
<polyline points="15 3 21 3 21 9"/>
|
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
</svg>`,
|
|
clock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<polyline points="12 6 12 12 16 14"/>
|
|
</svg>`,
|
|
calendar: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
|
<line x1="16" y1="2" x2="16" y2="6"/>
|
|
<line x1="8" y1="2" x2="8" y2="6"/>
|
|
<line x1="3" y1="10" x2="21" y2="10"/>
|
|
</svg>`,
|
|
map: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
|
|
<line x1="8" y1="2" x2="8" y2="18"/>
|
|
<line x1="16" y1="6" x2="16" y2="22"/>
|
|
</svg>`,
|
|
compass: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>
|
|
</svg>`,
|
|
globe: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="2" y1="12" x2="22" y2="12"/>
|
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
|
</svg>`,
|
|
wifi: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M5 12.55a11 11 0 0 1 14.08 0"/>
|
|
<path d="M1.42 9a16 16 0 0 1 21.16 0"/>
|
|
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
|
|
<line x1="12" y1="20" x2="12.01" y2="20"/>
|
|
</svg>`,
|
|
cloud: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
|
|
</svg>`,
|
|
sun: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="5"/>
|
|
<line x1="12" y1="1" x2="12" y2="3"/>
|
|
<line x1="12" y1="21" x2="12" y2="23"/>
|
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
|
<line x1="1" y1="12" x2="3" y2="12"/>
|
|
<line x1="21" y1="12" x2="23" y2="12"/>
|
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
|
</svg>`,
|
|
moon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
</svg>`,
|
|
zap: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
|
</svg>`,
|
|
activity: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
|
</svg>`,
|
|
cpu: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/>
|
|
<rect x="9" y="9" width="6" height="6"/>
|
|
<line x1="9" y1="1" x2="9" y2="4"/>
|
|
<line x1="15" y1="1" x2="15" y2="4"/>
|
|
<line x1="9" y1="20" x2="9" y2="23"/>
|
|
<line x1="15" y1="20" x2="15" y2="23"/>
|
|
<line x1="20" y1="9" x2="23" y2="9"/>
|
|
<line x1="20" y1="14" x2="23" y2="14"/>
|
|
<line x1="1" y1="9" x2="4" y2="9"/>
|
|
<line x1="1" y1="14" x2="4" y2="14"/>
|
|
</svg>`,
|
|
database: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
|
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
|
|
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
|
|
</svg>`,
|
|
server: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
|
|
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
|
|
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
|
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
|
</svg>`,
|
|
terminal: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="4 17 10 11 4 5"/>
|
|
<line x1="12" y1="19" x2="20" y2="19"/>
|
|
</svg>`,
|
|
code: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="16 18 22 12 16 6"/>
|
|
<polyline points="8 6 2 12 8 18"/>
|
|
</svg>`,
|
|
git: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="18" cy="18" r="3"/>
|
|
<circle cx="6" cy="6" r="3"/>
|
|
<path d="M13 6h3a2 2 0 0 1 2 2v7"/>
|
|
<line x1="6" y1="9" x2="6" y2="21"/>
|
|
</svg>`,
|
|
package: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/>
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
|
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
|
</svg>`,
|
|
layers: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
|
|
<polyline points="2 17 12 22 22 17"/>
|
|
<polyline points="2 12 12 17 22 12"/>
|
|
</svg>`,
|
|
grid: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="3" width="7" height="7"/>
|
|
<rect x="14" y="3" width="7" height="7"/>
|
|
<rect x="14" y="14" width="7" height="7"/>
|
|
<rect x="3" y="14" width="7" height="7"/>
|
|
</svg>`,
|
|
list: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="8" y1="6" x2="21" y2="6"/>
|
|
<line x1="8" y1="12" x2="21" y2="12"/>
|
|
<line x1="8" y1="18" x2="21" y2="18"/>
|
|
<line x1="3" y1="6" x2="3.01" y2="6"/>
|
|
<line x1="3" y1="12" x2="3.01" y2="12"/>
|
|
<line x1="3" y1="18" x2="3.01" y2="18"/>
|
|
</svg>`,
|
|
filter: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>
|
|
</svg>`,
|
|
sort: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="12" y1="5" x2="12" y2="19"/>
|
|
<polyline points="19 12 12 19 5 12"/>
|
|
</svg>`,
|
|
refresh: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="23 4 23 10 17 10"/>
|
|
<polyline points="1 20 1 14 7 14"/>
|
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
</svg>`,
|
|
sync: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="23 4 23 10 17 10"/>
|
|
<polyline points="1 20 1 14 7 14"/>
|
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
</svg>`,
|
|
power: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
|
|
<line x1="12" y1="2" x2="12" y2="12"/>
|
|
</svg>`,
|
|
battery: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="1" y="6" width="18" height="12" rx="2" ry="2"/>
|
|
<line x1="23" y1="13" x2="23" y2="11"/>
|
|
</svg>`,
|
|
bluetooth: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"/>
|
|
</svg>`,
|
|
volume: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
|
|
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>
|
|
</svg>`,
|
|
mic: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
<line x1="12" y1="19" x2="12" y2="23"/>
|
|
<line x1="8" y1="23" x2="16" y2="23"/>
|
|
</svg>`,
|
|
camera: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
|
<circle cx="12" cy="13" r="4"/>
|
|
</svg>`,
|
|
image: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
<polyline points="21 15 16 10 5 21"/>
|
|
</svg>`,
|
|
video: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="23 7 16 12 23 17 23 7"/>
|
|
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
|
</svg>`,
|
|
music: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M9 18V5l12-2v13"/>
|
|
<circle cx="6" cy="18" r="3"/>
|
|
<circle cx="18" cy="16" r="3"/>
|
|
</svg>`,
|
|
file: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
|
|
<polyline points="13 2 13 9 20 9"/>
|
|
</svg>`,
|
|
folder: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
|
</svg>`,
|
|
clipboard: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
|
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
|
</svg>`,
|
|
save: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
|
<polyline points="17 21 17 13 7 13 7 21"/>
|
|
<polyline points="7 3 7 8 15 8"/>
|
|
</svg>`,
|
|
print: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="6 9 6 2 18 2 18 9"/>
|
|
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/>
|
|
<rect x="6" y="14" width="12" height="8"/>
|
|
</svg>`,
|
|
eye: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
<circle cx="12" cy="12" r="3"/>
|
|
</svg>`,
|
|
eyeOff: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
|
|
<line x1="1" y1="1" x2="23" y2="23"/>
|
|
</svg>`,
|
|
lock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
</svg>`,
|
|
unlock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
<path d="M7 11V7a5 5 0 0 1 9.9-1"/>
|
|
</svg>`,
|
|
shield: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
</svg>`,
|
|
key: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
|
|
</svg>`,
|
|
flag: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
|
|
<line x1="4" y1="22" x2="4" y2="15"/>
|
|
</svg>`,
|
|
bookmark: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
|
|
</svg>`,
|
|
tag: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
|
|
<line x1="7" y1="7" x2="7.01" y2="7"/>
|
|
</svg>`,
|
|
gift: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="20 12 20 22 4 22 4 12"/>
|
|
<rect x="2" y="7" width="20" height="5"/>
|
|
<line x1="12" y1="22" x2="12" y2="7"/>
|
|
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/>
|
|
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>
|
|
</svg>`,
|
|
award: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="8" r="7"/>
|
|
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/>
|
|
</svg>`,
|
|
target: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<circle cx="12" cy="12" r="6"/>
|
|
<circle cx="12" cy="12" r="2"/>
|
|
</svg>`,
|
|
crosshair: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="22" y1="12" x2="18" y2="12"/>
|
|
<line x1="6" y1="12" x2="2" y2="12"/>
|
|
<line x1="12" y1="6" x2="12" y2="2"/>
|
|
<line x1="12" y1="22" x2="12" y2="18"/>
|
|
</svg>`,
|
|
move: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="5 9 2 12 5 15"/>
|
|
<polyline points="9 5 12 2 15 5"/>
|
|
<polyline points="15 19 12 22 9 19"/>
|
|
<polyline points="19 9 22 12 19 15"/>
|
|
<line x1="2" y1="12" x2="22" y2="12"/>
|
|
<line x1="12" y1="2" x2="12" y2="22"/>
|
|
</svg>`,
|
|
maximize: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
|
</svg>`,
|
|
minimize: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
|
|
</svg>`,
|
|
copy: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
</svg>`,
|
|
cut: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="6" cy="6" r="3"/>
|
|
<circle cx="6" cy="18" r="3"/>
|
|
<line x1="20" y1="4" x2="8.12" y2="15.88"/>
|
|
<line x1="14.47" y1="14.48" x2="20" y2="20"/>
|
|
<line x1="8.12" y1="8.12" x2="12" y2="12"/>
|
|
</svg>`,
|
|
paste: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
|
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
|
</svg>`,
|
|
rotateCw: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="23 4 23 10 17 10"/>
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
</svg>`,
|
|
rotateCcw: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="1 4 1 10 7 10"/>
|
|
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
|
|
</svg>`,
|
|
zoomIn: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="11" cy="11" r="8"/>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
<line x1="11" y1="8" x2="11" y2="14"/>
|
|
<line x1="8" y1="11" x2="14" y2="11"/>
|
|
</svg>`,
|
|
zoomOut: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="11" cy="11" r="8"/>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
<line x1="8" y1="11" x2="14" y2="11"/>
|
|
</svg>`,
|
|
crop: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"/>
|
|
<path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"/>
|
|
</svg>`,
|
|
sliders: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="4" y1="21" x2="4" y2="14"/>
|
|
<line x1="4" y1="10" x2="4" y2="3"/>
|
|
<line x1="12" y1="21" x2="12" y2="12"/>
|
|
<line x1="12" y1="8" x2="12" y2="3"/>
|
|
<line x1="20" y1="21" x2="20" y2="16"/>
|
|
<line x1="20" y1="12" x2="20" y2="3"/>
|
|
<line x1="1" y1="14" x2="7" y2="14"/>
|
|
<line x1="9" y1="8" x2="15" y2="8"/>
|
|
<line x1="17" y1="16" x2="23" y2="16"/>
|
|
</svg>`,
|
|
toggleLeft: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/>
|
|
<circle cx="8" cy="12" r="3"/>
|
|
</svg>`,
|
|
toggleRight: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/>
|
|
<circle cx="16" cy="12" r="3"/>
|
|
</svg>`,
|
|
checkCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
|
<polyline points="22 4 12 14.01 9 11.01"/>
|
|
</svg>`,
|
|
xCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
</svg>`,
|
|
alertCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
</svg>`,
|
|
infoCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="12" y1="16" x2="12" y2="12"/>
|
|
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
|
</svg>`,
|
|
helpCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
|
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
</svg>`,
|
|
alertTriangle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
|
<line x1="12" y1="9" x2="12" y2="13"/>
|
|
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
</svg>`,
|
|
checkSquare: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="9 11 12 14 22 4"/>
|
|
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
|
</svg>`,
|
|
square: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
</svg>`,
|
|
circle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
</svg>`,
|
|
triangle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
|
</svg>`,
|
|
hexagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
|
</svg>`,
|
|
octagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/>
|
|
</svg>`,
|
|
pentagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="12 2 22 8.5 19 20 5 20 2 8.5 12 2"/>
|
|
</svg>`,
|
|
diamond: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="4.5" y="4.5" width="15" height="15" rx="1" transform="rotate(45 12 12)"/>
|
|
</svg>`,
|
|
};
|
|
|
|
/**
|
|
* MetonaToast Locales — 国际化翻译数据
|
|
* @module locales
|
|
* @version 0.3.0
|
|
* @description 内置 zh-CN / en-US 完整翻译
|
|
*/
|
|
const LOCALES = {
|
|
'zh-CN': {
|
|
// === 操作 ===
|
|
close: '关闭',
|
|
loading: '加载中...',
|
|
success: '操作成功',
|
|
error: '操作失败',
|
|
warning: '警告',
|
|
info: '信息',
|
|
confirm: '确认',
|
|
cancel: '取消',
|
|
yes: '是',
|
|
no: '否',
|
|
ok: '确定',
|
|
retry: '重试',
|
|
undo: '撤销',
|
|
redo: '重做',
|
|
save: '保存',
|
|
delete: '删除',
|
|
edit: '编辑',
|
|
add: '添加',
|
|
remove: '移除',
|
|
search: '搜索',
|
|
filter: '筛选',
|
|
sort: '排序',
|
|
refresh: '刷新',
|
|
sync: '同步',
|
|
upload: '上传',
|
|
download: '下载',
|
|
share: '分享',
|
|
copy: '复制',
|
|
cut: '剪切',
|
|
paste: '粘贴',
|
|
print: '打印',
|
|
export: '导出',
|
|
import: '导入',
|
|
settings: '设置',
|
|
help: '帮助',
|
|
about: '关于',
|
|
version: '版本',
|
|
language: '语言',
|
|
theme: '主题',
|
|
dark: '暗色',
|
|
light: '亮色',
|
|
auto: '自动',
|
|
notifications: '通知',
|
|
enable: '启用',
|
|
disable: '禁用',
|
|
on: '开启',
|
|
off: '关闭',
|
|
max: '最大',
|
|
min: '最小',
|
|
expand: '展开',
|
|
collapse: '折叠',
|
|
fullscreen: '全屏',
|
|
exitFullscreen: '退出全屏',
|
|
zoomIn: '放大',
|
|
zoomOut: '缩小',
|
|
reset: '重置',
|
|
apply: '应用',
|
|
submit: '提交',
|
|
send: '发送',
|
|
receive: '接收',
|
|
connect: '连接',
|
|
disconnect: '断开',
|
|
start: '开始',
|
|
stop: '停止',
|
|
pause: '暂停',
|
|
resume: '继续',
|
|
next: '下一个',
|
|
previous: '上一个',
|
|
first: '第一个',
|
|
last: '最后一个',
|
|
back: '返回',
|
|
forward: '前进',
|
|
home: '首页',
|
|
menu: '菜单',
|
|
more: '更多',
|
|
less: '更少',
|
|
show: '显示',
|
|
hide: '隐藏',
|
|
visible: '可见',
|
|
hidden: '隐藏',
|
|
// === 状态 ===
|
|
enabled: '已启用',
|
|
disabled: '已禁用',
|
|
active: '活跃',
|
|
inactive: '非活跃',
|
|
online: '在线',
|
|
offline: '离线',
|
|
connected: '已连接',
|
|
disconnected: '已断开',
|
|
loaded: '已加载',
|
|
saving: '保存中',
|
|
saved: '已保存',
|
|
deleting: '删除中',
|
|
deleted: '已删除',
|
|
uploading: '上传中',
|
|
uploaded: '已上传',
|
|
downloading: '下载中',
|
|
downloaded: '已下载',
|
|
processing: '处理中',
|
|
processed: '已处理',
|
|
syncing: '同步中',
|
|
synced: '已同步',
|
|
sending: '发送中',
|
|
sent: '已发送',
|
|
receiving: '接收中',
|
|
received: '已接收',
|
|
connecting: '连接中',
|
|
// === 交互 ===
|
|
question: '问题',
|
|
feedback: '反馈',
|
|
report: '报告',
|
|
bug: '错误',
|
|
feature: '功能',
|
|
improvement: '改进',
|
|
suggestion: '建议',
|
|
request: '请求',
|
|
response: '响应',
|
|
status: '状态',
|
|
progress: '进度',
|
|
complete: '完成',
|
|
incomplete: '未完成',
|
|
pending: '待处理',
|
|
failed: '失败',
|
|
cancelled: '已取消',
|
|
timeout: '超时',
|
|
expired: '已过期',
|
|
invalid: '无效',
|
|
valid: '有效',
|
|
required: '必填',
|
|
optional: '可选',
|
|
allowed: '允许',
|
|
denied: '拒绝',
|
|
approved: '已批准',
|
|
rejected: '已拒绝',
|
|
accepted: '已接受',
|
|
declined: '已拒绝',
|
|
confirmed: '已确认',
|
|
unconfirmed: '未确认',
|
|
verified: '已验证',
|
|
unverified: '未验证',
|
|
authenticated: '已认证',
|
|
unauthenticated: '未认证',
|
|
authorized: '已授权',
|
|
unauthorized: '未授权',
|
|
// === 范围 / 角色 ===
|
|
public: '公开',
|
|
private: '私有',
|
|
protected: '受保护',
|
|
internal: '内部',
|
|
external: '外部',
|
|
local: '本地',
|
|
remote: '远程',
|
|
global: '全局',
|
|
system: '系统',
|
|
user: '用户',
|
|
admin: '管理员',
|
|
moderator: '版主',
|
|
guest: '访客',
|
|
member: '成员',
|
|
owner: '所有者',
|
|
creator: '创建者',
|
|
editor: '编辑者',
|
|
viewer: '查看者',
|
|
subscriber: '订阅者',
|
|
follower: '关注者',
|
|
following: '关注中',
|
|
friend: '朋友',
|
|
contact: '联系人',
|
|
// === 组织 ===
|
|
group: '群组',
|
|
team: '团队',
|
|
organization: '组织',
|
|
company: '公司',
|
|
department: '部门',
|
|
project: '项目',
|
|
task: '任务',
|
|
issue: '问题',
|
|
ticket: '工单',
|
|
message: '消息',
|
|
notification: '通知',
|
|
alert: '警报',
|
|
reminder: '提醒',
|
|
event: '事件',
|
|
activity: '活动',
|
|
log: '日志',
|
|
analytics: '分析',
|
|
statistics: '统计',
|
|
metrics: '指标',
|
|
performance: '性能',
|
|
security: '安全',
|
|
privacy: '隐私',
|
|
compliance: '合规',
|
|
policy: '政策',
|
|
terms: '条款',
|
|
conditions: '条件',
|
|
agreement: '协议',
|
|
contract: '合同',
|
|
license: '许可证',
|
|
copyright: '版权',
|
|
trademark: '商标',
|
|
patent: '专利',
|
|
tradeSecret: '商业秘密',
|
|
confidential: '机密',
|
|
sensitive: '敏感',
|
|
critical: '关键',
|
|
important: '重要',
|
|
urgent: '紧急',
|
|
high: '高',
|
|
medium: '中',
|
|
low: '低',
|
|
none: '无',
|
|
all: '全部',
|
|
any: '任何',
|
|
some: '一些',
|
|
each: '每个',
|
|
every: '每个',
|
|
both: '两者',
|
|
neither: '两者都不',
|
|
either: '任一',
|
|
other: '其他',
|
|
another: '另一个',
|
|
this: '这个',
|
|
that: '那个',
|
|
these: '这些',
|
|
those: '那些',
|
|
here: '这里',
|
|
there: '那里',
|
|
where: '哪里',
|
|
when: '何时',
|
|
why: '为什么',
|
|
how: '如何',
|
|
what: '什么',
|
|
who: '谁',
|
|
which: '哪个',
|
|
whose: '谁的',
|
|
whom: '谁',
|
|
},
|
|
'en-US': {
|
|
// === Actions ===
|
|
close: 'Close',
|
|
loading: 'Loading...',
|
|
success: 'Success',
|
|
error: 'Error',
|
|
warning: 'Warning',
|
|
info: 'Info',
|
|
confirm: 'Confirm',
|
|
cancel: 'Cancel',
|
|
yes: 'Yes',
|
|
no: 'No',
|
|
ok: 'OK',
|
|
retry: 'Retry',
|
|
undo: 'Undo',
|
|
redo: 'Redo',
|
|
save: 'Save',
|
|
delete: 'Delete',
|
|
edit: 'Edit',
|
|
add: 'Add',
|
|
remove: 'Remove',
|
|
search: 'Search',
|
|
filter: 'Filter',
|
|
sort: 'Sort',
|
|
refresh: 'Refresh',
|
|
sync: 'Sync',
|
|
upload: 'Upload',
|
|
download: 'Download',
|
|
share: 'Share',
|
|
copy: 'Copy',
|
|
cut: 'Cut',
|
|
paste: 'Paste',
|
|
print: 'Print',
|
|
export: 'Export',
|
|
import: 'Import',
|
|
settings: 'Settings',
|
|
help: 'Help',
|
|
about: 'About',
|
|
version: 'Version',
|
|
language: 'Language',
|
|
theme: 'Theme',
|
|
dark: 'Dark',
|
|
light: 'Light',
|
|
auto: 'Auto',
|
|
notifications: 'Notifications',
|
|
enable: 'Enable',
|
|
disable: 'Disable',
|
|
on: 'On',
|
|
off: 'Off',
|
|
max: 'Max',
|
|
min: 'Min',
|
|
expand: 'Expand',
|
|
collapse: 'Collapse',
|
|
fullscreen: 'Fullscreen',
|
|
exitFullscreen: 'Exit Fullscreen',
|
|
zoomIn: 'Zoom In',
|
|
zoomOut: 'Zoom Out',
|
|
reset: 'Reset',
|
|
apply: 'Apply',
|
|
submit: 'Submit',
|
|
send: 'Send',
|
|
receive: 'Receive',
|
|
connect: 'Connect',
|
|
disconnect: 'Disconnect',
|
|
start: 'Start',
|
|
stop: 'Stop',
|
|
pause: 'Pause',
|
|
resume: 'Resume',
|
|
next: 'Next',
|
|
previous: 'Previous',
|
|
first: 'First',
|
|
last: 'Last',
|
|
back: 'Back',
|
|
forward: 'Forward',
|
|
home: 'Home',
|
|
menu: 'Menu',
|
|
more: 'More',
|
|
less: 'Less',
|
|
show: 'Show',
|
|
hide: 'Hide',
|
|
visible: 'Visible',
|
|
hidden: 'Hidden',
|
|
// === Status ===
|
|
enabled: 'Enabled',
|
|
disabled: 'Disabled',
|
|
active: 'Active',
|
|
inactive: 'Inactive',
|
|
online: 'Online',
|
|
offline: 'Offline',
|
|
connected: 'Connected',
|
|
disconnected: 'Disconnected',
|
|
loaded: 'Loaded',
|
|
saving: 'Saving',
|
|
saved: 'Saved',
|
|
deleting: 'Deleting',
|
|
deleted: 'Deleted',
|
|
uploading: 'Uploading',
|
|
uploaded: 'Uploaded',
|
|
downloading: 'Downloading',
|
|
downloaded: 'Downloaded',
|
|
processing: 'Processing',
|
|
processed: 'Processed',
|
|
syncing: 'Syncing',
|
|
synced: 'Synced',
|
|
sending: 'Sending',
|
|
sent: 'Sent',
|
|
receiving: 'Receiving',
|
|
received: 'Received',
|
|
connecting: 'Connecting',
|
|
// === Interaction ===
|
|
question: 'Question',
|
|
feedback: 'Feedback',
|
|
report: 'Report',
|
|
bug: 'Bug',
|
|
feature: 'Feature',
|
|
improvement: 'Improvement',
|
|
suggestion: 'Suggestion',
|
|
request: 'Request',
|
|
response: 'Response',
|
|
status: 'Status',
|
|
progress: 'Progress',
|
|
complete: 'Complete',
|
|
incomplete: 'Incomplete',
|
|
pending: 'Pending',
|
|
failed: 'Failed',
|
|
cancelled: 'Cancelled',
|
|
timeout: 'Timeout',
|
|
expired: 'Expired',
|
|
invalid: 'Invalid',
|
|
valid: 'Valid',
|
|
required: 'Required',
|
|
optional: 'Optional',
|
|
allowed: 'Allowed',
|
|
denied: 'Denied',
|
|
approved: 'Approved',
|
|
rejected: 'Rejected',
|
|
accepted: 'Accepted',
|
|
declined: 'Declined',
|
|
confirmed: 'Confirmed',
|
|
unconfirmed: 'Unconfirmed',
|
|
verified: 'Verified',
|
|
unverified: 'Unverified',
|
|
authenticated: 'Authenticated',
|
|
unauthenticated: 'Unauthenticated',
|
|
authorized: 'Authorized',
|
|
unauthorized: 'Unauthorized',
|
|
// === Scope / Role ===
|
|
public: 'Public',
|
|
private: 'Private',
|
|
protected: 'Protected',
|
|
internal: 'Internal',
|
|
external: 'External',
|
|
local: 'Local',
|
|
remote: 'Remote',
|
|
global: 'Global',
|
|
system: 'System',
|
|
user: 'User',
|
|
admin: 'Admin',
|
|
moderator: 'Moderator',
|
|
guest: 'Guest',
|
|
member: 'Member',
|
|
owner: 'Owner',
|
|
creator: 'Creator',
|
|
editor: 'Editor',
|
|
viewer: 'Viewer',
|
|
subscriber: 'Subscriber',
|
|
follower: 'Follower',
|
|
following: 'Following',
|
|
friend: 'Friend',
|
|
contact: 'Contact',
|
|
// === Organization ===
|
|
group: 'Group',
|
|
team: 'Team',
|
|
organization: 'Organization',
|
|
company: 'Company',
|
|
department: 'Department',
|
|
project: 'Project',
|
|
task: 'Task',
|
|
issue: 'Issue',
|
|
ticket: 'Ticket',
|
|
message: 'Message',
|
|
notification: 'Notification',
|
|
alert: 'Alert',
|
|
reminder: 'Reminder',
|
|
event: 'Event',
|
|
activity: 'Activity',
|
|
log: 'Log',
|
|
analytics: 'Analytics',
|
|
statistics: 'Statistics',
|
|
metrics: 'Metrics',
|
|
performance: 'Performance',
|
|
security: 'Security',
|
|
privacy: 'Privacy',
|
|
compliance: 'Compliance',
|
|
policy: 'Policy',
|
|
terms: 'Terms',
|
|
conditions: 'Conditions',
|
|
agreement: 'Agreement',
|
|
contract: 'Contract',
|
|
license: 'License',
|
|
copyright: 'Copyright',
|
|
trademark: 'Trademark',
|
|
patent: 'Patent',
|
|
tradeSecret: 'Trade Secret',
|
|
confidential: 'Confidential',
|
|
sensitive: 'Sensitive',
|
|
critical: 'Critical',
|
|
important: 'Important',
|
|
urgent: 'Urgent',
|
|
high: 'High',
|
|
medium: 'Medium',
|
|
low: 'Low',
|
|
none: 'None',
|
|
all: 'All',
|
|
any: 'Any',
|
|
some: 'Some',
|
|
each: 'Each',
|
|
every: 'Every',
|
|
both: 'Both',
|
|
neither: 'Neither',
|
|
either: 'Either',
|
|
other: 'Other',
|
|
another: 'Another',
|
|
this: 'This',
|
|
that: 'That',
|
|
these: 'These',
|
|
those: 'Those',
|
|
here: 'Here',
|
|
there: 'There',
|
|
where: 'Where',
|
|
when: 'When',
|
|
why: 'Why',
|
|
how: 'How',
|
|
what: 'What',
|
|
who: 'Who',
|
|
which: 'Which',
|
|
whose: 'Whose',
|
|
whom: 'Whom',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* MetonaToast Constants — 常量定义
|
|
* @module constants
|
|
* @version 0.3.0
|
|
*/
|
|
/**
|
|
* 版本号 — 唯一来源,发布时只需修改此处
|
|
*/
|
|
const VERSION = '0.3.0';
|
|
/**
|
|
* 默认配置
|
|
*/
|
|
const DEFAULTS = Object.freeze({
|
|
position: 'top-right',
|
|
duration: 4000,
|
|
max: 6,
|
|
gap: 12,
|
|
offset: 24,
|
|
pauseOnHover: true,
|
|
closeOnClick: true,
|
|
draggable: true,
|
|
showProgress: true,
|
|
progressDirection: 'horizontal',
|
|
icon: true,
|
|
closeButton: true,
|
|
theme: 'auto',
|
|
animation: 'slide',
|
|
zIndex: 9999,
|
|
width: 360,
|
|
className: '',
|
|
style: {},
|
|
onShow: null,
|
|
onBeforeShow: null,
|
|
onClose: null,
|
|
onClick: null,
|
|
onUpdate: null,
|
|
resetTimerOnUpdate: false,
|
|
notifyWhenHidden: false,
|
|
onError: null,
|
|
locale: 'zh-CN',
|
|
plugins: [],
|
|
});
|
|
/**
|
|
* 类型颜色配置
|
|
*/
|
|
const TYPE_COLORS = {
|
|
success: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
error: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
warning: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
info: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
loading: { fg: '#6366f1', bg: 'rgba(99, 102, 241, 0.1)' },
|
|
default: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
check: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
x: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
alert: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
question: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
star: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
heart: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' },
|
|
bell: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
mail: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
settings: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
user: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
home: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
search: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
plus: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
minus: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
edit: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
trash: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
download: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
upload: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
share: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
link: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
external: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
clock: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
calendar: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
map: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
compass: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
globe: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
wifi: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
cloud: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
sun: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
moon: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
zap: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
activity: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
cpu: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
database: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
server: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
terminal: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
code: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
git: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
package: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
layers: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
grid: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
list: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
filter: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
sort: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
refresh: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
sync: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
power: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
battery: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
bluetooth: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
volume: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
mic: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
camera: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
image: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
video: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
music: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
file: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
folder: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
clipboard: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
save: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
print: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
eye: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
eyeOff: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
lock: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
unlock: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
shield: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
key: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
flag: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
bookmark: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
tag: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
gift: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' },
|
|
award: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
target: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
crosshair: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
move: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
maximize: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
minimize: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
copy: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
cut: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
paste: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
rotateCw: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
rotateCcw: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
zoomIn: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
zoomOut: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
crop: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
sliders: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
toggleLeft: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
toggleRight: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
checkCircle: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
xCircle: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
alertCircle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
infoCircle: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
helpCircle: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
alertTriangle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
checkSquare: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
|
square: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
circle: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
triangle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
hexagon: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
octagon: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
|
pentagon: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
|
|
diamond: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' },
|
|
};
|
|
/**
|
|
* 动画配置
|
|
*/
|
|
const ANIMATIONS = {
|
|
slide: {
|
|
enter: { transform: 'translateX(80px)', opacity: '0' },
|
|
leave: { transform: 'translateX(120%)', opacity: '0' },
|
|
duration: 400,
|
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
},
|
|
fade: {
|
|
enter: { opacity: '0', filter: 'blur(3px)' },
|
|
leave: { opacity: '0', filter: 'blur(3px)' },
|
|
duration: 500,
|
|
easing: 'ease',
|
|
},
|
|
scale: {
|
|
enter: { transform: 'scale(0.55)', opacity: '0' },
|
|
leave: { transform: 'scale(0.55)', opacity: '0' },
|
|
duration: 450,
|
|
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
},
|
|
bounce: {
|
|
enter: { transform: 'translateY(-80px)', opacity: '0' },
|
|
leave: { transform: 'translateY(20px)', opacity: '0' },
|
|
duration: 650,
|
|
easing: 'ease',
|
|
},
|
|
flip: {
|
|
enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: '0' },
|
|
leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: '0' },
|
|
duration: 500,
|
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
},
|
|
rotate: {
|
|
enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: '0' },
|
|
leave: { transform: 'rotate(25deg) scale(0.6)', opacity: '0' },
|
|
duration: 500,
|
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
},
|
|
zoom: {
|
|
enter: { transform: 'scale(0.1)', opacity: '0' },
|
|
leave: { transform: 'scale(0.1)', opacity: '0' },
|
|
duration: 500,
|
|
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
},
|
|
slideUp: {
|
|
enter: { transform: 'translateY(60px)', opacity: '0' },
|
|
leave: { transform: 'translateY(-120%)', opacity: '0' },
|
|
duration: 400,
|
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
},
|
|
slideDown: {
|
|
enter: { transform: 'translateY(-60px)', opacity: '0' },
|
|
leave: { transform: 'translateY(120%)', opacity: '0' },
|
|
duration: 400,
|
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
},
|
|
slideLeft: {
|
|
enter: { transform: 'translateX(-90px)', opacity: '0' },
|
|
leave: { transform: 'translateX(120%)', opacity: '0' },
|
|
duration: 400,
|
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
},
|
|
slideRight: {
|
|
enter: { transform: 'translateX(90px)', opacity: '0' },
|
|
leave: { transform: 'translateX(-120%)', opacity: '0' },
|
|
duration: 400,
|
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
},
|
|
};
|
|
/**
|
|
* 主题配置
|
|
*/
|
|
const THEMES = {
|
|
light: {
|
|
bg: 'rgba(255, 255, 255, 0.96)',
|
|
text: '#1f2937',
|
|
border: 'rgba(0, 0, 0, 0.06)',
|
|
shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08)',
|
|
hoverShadow: '0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10)',
|
|
progressBg: 'rgba(0, 0, 0, 0.06)',
|
|
closeHoverBg: 'rgba(0, 0, 0, 0.06)',
|
|
},
|
|
dark: {
|
|
bg: 'rgba(28, 32, 40, 0.94)',
|
|
text: '#e6e8eb',
|
|
border: 'rgba(255, 255, 255, 0.08)',
|
|
shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4)',
|
|
hoverShadow: '0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4)',
|
|
progressBg: 'rgba(255, 255, 255, 0.08)',
|
|
closeHoverBg: 'rgba(255, 255, 255, 0.08)',
|
|
},
|
|
auto: {
|
|
bg: 'rgba(255, 255, 255, 0.96)',
|
|
text: '#1f2937',
|
|
border: 'rgba(0, 0, 0, 0.06)',
|
|
shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08)',
|
|
hoverShadow: '0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10)',
|
|
progressBg: 'rgba(0, 0, 0, 0.06)',
|
|
closeHoverBg: 'rgba(0, 0, 0, 0.06)',
|
|
},
|
|
warm: {
|
|
bg: 'rgba(255, 251, 235, 0.96)',
|
|
text: '#78350f',
|
|
border: 'rgba(245, 158, 11, 0.2)',
|
|
shadow: '0 10px 36px -10px rgba(245, 158, 11, 0.18), 0 4px 14px -4px rgba(245, 158, 11, 0.08)',
|
|
hoverShadow: '0 14px 48px -10px rgba(245, 158, 11, 0.22), 0 6px 18px -4px rgba(245, 158, 11, 0.10)',
|
|
progressBg: 'rgba(245, 158, 11, 0.1)',
|
|
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
|
|
},
|
|
};
|
|
/**
|
|
* 动画类型
|
|
*/
|
|
const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
|
|
|
/**
|
|
* MetonaToast Styles — 样式管理
|
|
* @module styles
|
|
* @version 0.3.0
|
|
*/
|
|
// 样式缓存
|
|
let styleElement = null;
|
|
/**
|
|
* 生成CSS样式
|
|
*/
|
|
const generateCSS = () => {
|
|
return `
|
|
.met-container {
|
|
pointer-events: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
box-sizing: border-box;
|
|
padding: 24px;
|
|
max-width: 100vw;
|
|
position: fixed;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.met-container.top-left { top: 0; left: 0; align-items: flex-start; }
|
|
.met-container.top-center { top: 0; left: 0; right: 0; align-items: center; }
|
|
.met-container.top-right { top: 0; right: 0; align-items: flex-end; }
|
|
.met-container.bottom-left { bottom: 0; left: 0; align-items: flex-start; }
|
|
.met-container.bottom-center{ bottom: 0; left: 0; right: 0; align-items: center; }
|
|
.met-container.bottom-right { bottom: 0; right: 0; align-items: flex-end; }
|
|
|
|
.met-toast {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 14px 16px;
|
|
border-radius: 12px;
|
|
box-sizing: border-box;
|
|
backdrop-filter: blur(14px) saturate(180%);
|
|
-webkit-backdrop-filter: blur(14px) saturate(180%);
|
|
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
|
box-shadow 0.25s ease,
|
|
opacity 0.25s ease;
|
|
will-change: transform, opacity;
|
|
flex-shrink: 0;
|
|
pointer-events: auto;
|
|
min-width: 240px;
|
|
max-width: calc(100vw - 48px);
|
|
width: 360px;
|
|
border: 1px solid var(--met-border, rgba(0,0,0,0.06));
|
|
background: var(--met-bg, rgba(255,255,255,0.96));
|
|
color: var(--met-text, #1f2937);
|
|
box-shadow: var(--met-shadow, 0 10px 36px -10px rgba(0,0,0,0.18), 0 4px 14px -4px rgba(0,0,0,0.08));
|
|
transform: translateZ(0);
|
|
-webkit-transform: translateZ(0);
|
|
backface-visibility: hidden;
|
|
-webkit-backface-visibility: hidden;
|
|
perspective: 1000;
|
|
}
|
|
|
|
.met-toast:hover {
|
|
box-shadow: var(--met-hover-shadow, 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10)) !important;
|
|
}
|
|
|
|
.met-toast.met-clickable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.met-icon {
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.met-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
word-wrap: break-word;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.met-title {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
letter-spacing: 0.1px;
|
|
}
|
|
|
|
.met-message {
|
|
font-size: 13px;
|
|
opacity: 0.85;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.met-close {
|
|
flex-shrink: 0;
|
|
background: transparent;
|
|
border: 0;
|
|
padding: 4px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 6px;
|
|
transition: background 0.2s, color 0.2s;
|
|
line-height: 0;
|
|
color: inherit;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.met-close:hover {
|
|
opacity: 1;
|
|
background: var(--met-close-hover-bg, rgba(0,0,0,0.06));
|
|
}
|
|
|
|
.met-progress {
|
|
position: absolute;
|
|
left: 0; right: 0; bottom: 0;
|
|
height: 3px;
|
|
overflow: hidden;
|
|
background: var(--met-progress-bg, rgba(0,0,0,0.06));
|
|
border-radius: 0 0 12px 12px;
|
|
}
|
|
|
|
.met-progress-v {
|
|
position: absolute;
|
|
left: 0; top: 0; bottom: 0;
|
|
width: 3px;
|
|
overflow: hidden;
|
|
background: var(--met-progress-bg, rgba(0,0,0,0.06));
|
|
border-radius: 12px 0 0 12px;
|
|
}
|
|
|
|
.met-bar {
|
|
position: absolute;
|
|
left: 0; top: 0;
|
|
height: 100%; width: 100%;
|
|
transform-origin: left;
|
|
transform: scaleX(1);
|
|
}
|
|
|
|
.met-bar-v {
|
|
position: absolute;
|
|
left: 0; bottom: 0;
|
|
width: 100%; height: 100%;
|
|
transform-origin: bottom;
|
|
transform: scaleY(1);
|
|
}
|
|
|
|
.met-side {
|
|
position: absolute;
|
|
left: 0; top: 0; bottom: 0;
|
|
width: 4px;
|
|
border-radius: 12px 0 0 12px;
|
|
}
|
|
|
|
.met-spin {
|
|
animation: met-rot 1s linear infinite;
|
|
transform-origin: 50% 50%;
|
|
}
|
|
|
|
.met-toast.met-leaving {
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
@keyframes met-rot {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.met-anim-slide.met-toast,
|
|
.met-anim-fade.met-toast,
|
|
.met-anim-scale.met-toast,
|
|
.met-anim-bounce.met-toast,
|
|
.met-anim-flip.met-toast,
|
|
.met-anim-rotate.met-toast,
|
|
.met-anim-zoom.met-toast,
|
|
.met-anim-slideUp.met-toast,
|
|
.met-anim-slideDown.met-toast,
|
|
.met-anim-slideLeft.met-toast,
|
|
.met-anim-slideRight.met-toast {
|
|
opacity: 0;
|
|
}
|
|
|
|
@keyframes met-slide-in {
|
|
0% { transform: translateX(80px); opacity: 0; }
|
|
65% { transform: translateX(-6px); opacity: 1; }
|
|
100% { transform: translateX(0); opacity: 1; }
|
|
}
|
|
@keyframes met-fade-in {
|
|
0% { opacity: 0; filter: blur(3px); }
|
|
100% { opacity: 1; filter: blur(0); }
|
|
}
|
|
@keyframes met-scale-in {
|
|
0% { transform: scale(0.55); opacity: 0; }
|
|
65% { transform: scale(1.07); opacity: 1; }
|
|
100% { transform: scale(1); opacity: 1; }
|
|
}
|
|
@keyframes met-bounce-in {
|
|
0% { transform: translateY(-80px); opacity: 0; animation-timing-function: ease-in; }
|
|
45% { transform: translateY(14px); opacity: 1; animation-timing-function: ease-out; }
|
|
65% { transform: translateY(-12px); animation-timing-function: ease-in; }
|
|
82% { transform: translateY(4px); animation-timing-function: ease-out; }
|
|
100% { transform: translateY(0); opacity: 1; }
|
|
}
|
|
@keyframes met-flip-in {
|
|
0% { transform: perspective(500px) rotateX(-90deg); opacity: 0; }
|
|
60% { transform: perspective(500px) rotateX(8deg); opacity: 1; }
|
|
85% { transform: perspective(500px) rotateX(-2deg); }
|
|
100% { transform: perspective(500px) rotateX(0); opacity: 1; }
|
|
}
|
|
@keyframes met-rotate-in {
|
|
0% { transform: rotate(-25deg) scale(0.6); opacity: 0; }
|
|
60% { transform: rotate(4deg) scale(1.05); opacity: 1; }
|
|
85% { transform: rotate(-1deg) scale(0.98); }
|
|
100% { transform: rotate(0) scale(1); opacity: 1; }
|
|
}
|
|
@keyframes met-zoom-in {
|
|
0% { transform: scale(0.1); opacity: 0; }
|
|
50% { transform: scale(1.12); opacity: 1; }
|
|
75% { transform: scale(0.94); }
|
|
100% { transform: scale(1); opacity: 1; }
|
|
}
|
|
@keyframes met-slideUp-in {
|
|
0% { transform: translateY(60px); opacity: 0; }
|
|
70% { transform: translateY(-6px); opacity: 1; }
|
|
100% { transform: translateY(0); opacity: 1; }
|
|
}
|
|
@keyframes met-slideDown-in {
|
|
0% { transform: translateY(-60px); opacity: 0; }
|
|
70% { transform: translateY(6px); opacity: 1; }
|
|
100% { transform: translateY(0); opacity: 1; }
|
|
}
|
|
@keyframes met-slideLeft-in {
|
|
0% { transform: translateX(-90px); opacity: 0; }
|
|
65% { transform: translateX(6px); opacity: 1; }
|
|
100% { transform: translateX(0); opacity: 1; }
|
|
}
|
|
@keyframes met-slideRight-in {
|
|
0% { transform: translateX(90px); opacity: 0; }
|
|
65% { transform: translateX(-6px); opacity: 1; }
|
|
100% { transform: translateX(0); opacity: 1; }
|
|
}
|
|
|
|
.met-anim-slide.met-toast.met-show { animation: met-slide-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
|
.met-anim-fade.met-toast.met-show { animation: met-fade-in 0.50s ease forwards; }
|
|
.met-anim-scale.met-toast.met-show { animation: met-scale-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
|
.met-anim-bounce.met-toast.met-show { animation: met-bounce-in 0.65s ease forwards; }
|
|
.met-anim-flip.met-toast.met-show { animation: met-flip-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
|
.met-anim-rotate.met-toast.met-show { animation: met-rotate-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
|
.met-anim-zoom.met-toast.met-show { animation: met-zoom-in 0.50s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
|
.met-anim-slideUp.met-toast.met-show { animation: met-slideUp-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
|
.met-anim-slideDown.met-toast.met-show { animation: met-slideDown-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
|
.met-anim-slideLeft.met-toast.met-show { animation: met-slideLeft-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
|
.met-anim-slideRight.met-toast.met-show { animation: met-slideRight-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
|
|
|
@media (max-width: 480px) {
|
|
.met-container { padding: 12px !important; }
|
|
.met-toast { width: 100% !important; min-width: 0 !important; }
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.met-toast { font-size: 13px; }
|
|
.met-title { font-size: 13px; }
|
|
.met-message { font-size: 12px; }
|
|
}
|
|
|
|
@media (min-width: 1200px) {
|
|
.met-toast { width: 400px; }
|
|
}
|
|
|
|
@media (min-width: 1920px) {
|
|
.met-toast { width: 440px; }
|
|
}
|
|
|
|
@media (max-width: 320px) {
|
|
.met-toast { width: 100%; min-width: 0; padding: 12px 14px; }
|
|
.met-container { padding: 8px; }
|
|
}
|
|
|
|
@media (orientation: landscape) and (max-height: 500px) {
|
|
.met-container { padding: 12px 24px; }
|
|
.met-toast { min-height: 44px; padding: 10px 14px; }
|
|
}
|
|
|
|
@media (orientation: portrait) and (max-width: 500px) {
|
|
.met-container { padding: 12px; }
|
|
.met-toast { width: 100%; min-width: 0; }
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.met-toast { transition: opacity 0.15s !important; }
|
|
.met-toast.met-show { animation: none !important; opacity: 1 !important; }
|
|
.met-spin { animation-duration: 2.5s !important; }
|
|
}
|
|
|
|
@media (forced-colors: active) {
|
|
.met-toast { border: 2px solid ButtonText; }
|
|
.met-close { border: 1px solid ButtonText; }
|
|
}
|
|
|
|
@media (prefers-contrast: high) {
|
|
.met-toast { border-width: 3px; border-style: solid; }
|
|
.met-close { border: 2px solid currentColor; border-radius: 4px; }
|
|
}
|
|
|
|
.met-toast:focus-visible {
|
|
outline: 2px solid #3b82f6;
|
|
outline-offset: 2px;
|
|
}
|
|
.met-close:focus-visible {
|
|
outline: 2px solid #3b82f6;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
@media (hover: none) and (pointer: coarse) {
|
|
.met-toast:hover { box-shadow: inherit; }
|
|
.met-toast { min-height: 48px; }
|
|
.met-close { min-width: 48px; min-height: 48px; }
|
|
}
|
|
|
|
@media print {
|
|
.met-container { display: none !important; }
|
|
.met-toast { display: none !important; }
|
|
}
|
|
|
|
:fullscreen .met-container,
|
|
:picture-in-picture .met-container {
|
|
z-index: 2147483647;
|
|
}
|
|
|
|
@supports (padding: max(0px)) {
|
|
.met-container {
|
|
padding: max(24px, env(safe-area-inset-top))
|
|
max(24px, env(safe-area-inset-right))
|
|
max(24px, env(safe-area-inset-bottom))
|
|
max(24px, env(safe-area-inset-left));
|
|
}
|
|
}
|
|
|
|
.met-toast:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.met-toast.met-success { border-left: 4px solid #10b981; }
|
|
.met-toast.met-error { border-left: 4px solid #ef4444; }
|
|
.met-toast.met-warning { border-left: 4px solid #f59e0b; }
|
|
.met-toast.met-info { border-left: 4px solid #3b82f6; }
|
|
.met-toast.met-loading { border-left: 4px solid #6366f1; }
|
|
|
|
.met-bar, .met-bar-v {
|
|
transition: transform 0.1s linear;
|
|
}
|
|
|
|
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
.met-container::-webkit-scrollbar-track { background: transparent; }
|
|
.met-container::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
|
.met-container::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); }
|
|
|
|
.met-toast.met-theme-dark {
|
|
background: rgba(28, 32, 40, 0.94);
|
|
color: #e6e8eb;
|
|
border-color: rgba(255, 255, 255, 0.08);
|
|
box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4);
|
|
}
|
|
.met-toast.met-theme-dark:hover {
|
|
box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important;
|
|
}
|
|
.met-toast.met-theme-dark .met-close:hover {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
.met-toast.met-theme-dark .met-progress,
|
|
.met-toast.met-theme-dark .met-progress-v {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.met-toast.met-theme-light {
|
|
background: rgba(255, 255, 255, 0.96);
|
|
color: #1f2937;
|
|
border-color: rgba(0, 0, 0, 0.06);
|
|
box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08);
|
|
}
|
|
.met-toast.met-theme-light:hover {
|
|
box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important;
|
|
}
|
|
.met-toast.met-theme-light .met-close:hover {
|
|
background: rgba(0, 0, 0, 0.06);
|
|
}
|
|
.met-toast.met-theme-light .met-progress,
|
|
.met-toast.met-theme-light .met-progress-v {
|
|
background: rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.met-toast[data-theme] {
|
|
--met-bg: var(--met-theme-bg);
|
|
--met-text: var(--met-theme-text);
|
|
--met-border: var(--met-theme-border);
|
|
--met-shadow: var(--met-theme-shadow);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.met-theme-auto .met-toast {
|
|
background: rgba(28, 32, 40, 0.94);
|
|
color: #e6e8eb;
|
|
border-color: rgba(255, 255, 255, 0.08);
|
|
box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4);
|
|
}
|
|
.met-theme-auto .met-toast:hover {
|
|
box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important;
|
|
}
|
|
.met-theme-auto .met-close:hover { background: rgba(255, 255, 255, 0.08); }
|
|
.met-theme-auto .met-progress,
|
|
.met-theme-auto .met-progress-v { background: rgba(255, 255, 255, 0.08); }
|
|
}
|
|
|
|
@media (prefers-color-scheme: light) {
|
|
.met-theme-auto .met-toast {
|
|
background: rgba(255, 255, 255, 0.96);
|
|
color: #1f2937;
|
|
border-color: rgba(0, 0, 0, 0.06);
|
|
box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08);
|
|
}
|
|
.met-theme-auto .met-toast:hover {
|
|
box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important;
|
|
}
|
|
.met-theme-auto .met-close:hover { background: rgba(0, 0, 0, 0.06); }
|
|
.met-theme-auto .met-progress,
|
|
.met-theme-auto .met-progress-v { background: rgba(0, 0, 0, 0.06); }
|
|
}
|
|
`;
|
|
};
|
|
/**
|
|
* 注入样式
|
|
*/
|
|
const injectStyles = () => {
|
|
if (typeof document === 'undefined')
|
|
return;
|
|
if (styleElement && document.getElementById('metona-toast-styles')) {
|
|
return;
|
|
}
|
|
const css = generateCSS();
|
|
styleElement = document.createElement('style');
|
|
styleElement.id = 'metona-toast-styles';
|
|
styleElement.textContent = css.replace(/\s+/g, ' ');
|
|
document.head.appendChild(styleElement);
|
|
};
|
|
|
|
/**
|
|
* MetonaToast Themes — 主题管理
|
|
* @module themes
|
|
* @version 0.3.0
|
|
*/
|
|
// 当前主题状态
|
|
let currentTheme = 'auto';
|
|
const themeListeners = new Set();
|
|
let systemThemeMediaQuery = null;
|
|
let systemThemeListener = null;
|
|
/**
|
|
* 获取系统主题
|
|
*/
|
|
const getSystemTheme = () => {
|
|
if (typeof window === 'undefined')
|
|
return 'light';
|
|
return prefersDark() ? 'dark' : 'light';
|
|
};
|
|
/**
|
|
* 获取主题(别名)
|
|
*/
|
|
const getTheme = (theme) => {
|
|
return resolveTheme(theme);
|
|
};
|
|
/**
|
|
* 解析主题 — 'auto' 始终按系统主题检测,不依赖当前手动选择的主题
|
|
*/
|
|
const resolveTheme = (theme) => {
|
|
if (theme === 'auto') {
|
|
return getSystemTheme();
|
|
}
|
|
return theme;
|
|
};
|
|
/**
|
|
* 获取主题配置
|
|
*/
|
|
const getThemeConfig = (theme) => {
|
|
const resolved = resolveTheme(theme);
|
|
return THEMES[resolved] || THEMES.light;
|
|
};
|
|
/**
|
|
* 应用主题
|
|
*/
|
|
const applyTheme = (theme) => {
|
|
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变量
|
|
*/
|
|
const setThemeVariables = (config) => {
|
|
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');
|
|
};
|
|
/**
|
|
* 获取当前主题
|
|
*/
|
|
const getCurrentTheme = () => {
|
|
return currentTheme;
|
|
};
|
|
/**
|
|
* 获取解析后的主题
|
|
*/
|
|
const getResolvedTheme = () => {
|
|
return resolveTheme(currentTheme);
|
|
};
|
|
/**
|
|
* 切换主题
|
|
*/
|
|
const switchTheme = (theme) => {
|
|
applyTheme(theme);
|
|
saveTheme(theme);
|
|
};
|
|
/**
|
|
* 切换亮色/暗色主题
|
|
*/
|
|
const toggleTheme = () => {
|
|
const resolved = getResolvedTheme();
|
|
const newTheme = resolved === 'dark' ? 'light' : 'dark';
|
|
switchTheme(newTheme);
|
|
};
|
|
/**
|
|
* 重置为自动主题
|
|
*/
|
|
const resetToAuto = () => {
|
|
switchTheme('auto');
|
|
};
|
|
/**
|
|
* 保存主题到本地存储
|
|
*/
|
|
const saveTheme = (theme) => {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
localStorage.setItem('metona-toast-theme', theme);
|
|
}
|
|
catch (e) {
|
|
console.warn('Failed to save theme:', e);
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* 从本地存储加载主题
|
|
*/
|
|
const loadTheme = () => {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
return localStorage.getItem('metona-toast-theme') || 'auto';
|
|
}
|
|
catch (e) {
|
|
console.warn('Failed to load theme:', e);
|
|
}
|
|
}
|
|
return 'auto';
|
|
};
|
|
/**
|
|
* 初始化主题系统
|
|
*/
|
|
const initTheme = () => {
|
|
const savedTheme = loadTheme();
|
|
applyTheme(savedTheme);
|
|
watchSystemTheme();
|
|
};
|
|
/**
|
|
* 监听系统主题变化
|
|
*/
|
|
const watchSystemTheme = () => {
|
|
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);
|
|
};
|
|
/**
|
|
* 停止监听系统主题变化
|
|
*/
|
|
const unwatchSystemTheme = () => {
|
|
if (systemThemeMediaQuery && systemThemeListener) {
|
|
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
|
systemThemeMediaQuery = null;
|
|
systemThemeListener = null;
|
|
}
|
|
};
|
|
/**
|
|
* 添加主题监听器
|
|
*/
|
|
const addThemeListener = (listener) => {
|
|
themeListeners.add(listener);
|
|
return () => {
|
|
themeListeners.delete(listener);
|
|
};
|
|
};
|
|
/**
|
|
* 移除主题监听器
|
|
*/
|
|
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);
|
|
}
|
|
});
|
|
};
|
|
/**
|
|
* 清除所有主题监听器
|
|
*/
|
|
const clearThemeListeners = () => {
|
|
themeListeners.clear();
|
|
};
|
|
/**
|
|
* 注册自定义主题
|
|
*/
|
|
const registerTheme = (name, config) => {
|
|
THEMES[name] = {
|
|
bg: config.bg || THEMES.light.bg,
|
|
text: config.text || THEMES.light.text,
|
|
border: config.border || THEMES.light.border,
|
|
shadow: config.shadow || THEMES.light.shadow,
|
|
hoverShadow: config.hoverShadow || THEMES.light.hoverShadow,
|
|
progressBg: config.progressBg || THEMES.light.progressBg,
|
|
closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg,
|
|
};
|
|
};
|
|
/**
|
|
* 注销自定义主题
|
|
*/
|
|
const unregisterTheme = (name) => {
|
|
if (name === 'light' || name === 'dark' || name === 'auto') {
|
|
console.warn('Cannot unregister built-in theme:', name);
|
|
return;
|
|
}
|
|
delete THEMES[name];
|
|
};
|
|
/**
|
|
* 获取所有主题
|
|
*/
|
|
const getAllThemes = () => {
|
|
return { ...THEMES };
|
|
};
|
|
/**
|
|
* 获取主题名称列表
|
|
*/
|
|
const getThemeNames = () => {
|
|
return Object.keys(THEMES);
|
|
};
|
|
/**
|
|
* 检查主题是否存在
|
|
*/
|
|
const hasTheme = (name) => {
|
|
return name in THEMES;
|
|
};
|
|
/**
|
|
* 获取主题预览
|
|
*/
|
|
const getThemePreview = (theme) => {
|
|
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
|
|
*/
|
|
const generateThemeCSS = (theme) => {
|
|
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
|
|
*/
|
|
const applyThemeCSS = (theme) => {
|
|
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
|
|
*/
|
|
const removeThemeCSS = () => {
|
|
if (typeof document === 'undefined')
|
|
return;
|
|
const styleId = 'metona-toast-theme-css';
|
|
const style = document.getElementById(styleId);
|
|
if (style) {
|
|
style.remove();
|
|
}
|
|
};
|
|
/**
|
|
* 主题工具 — 代理层
|
|
*/
|
|
const 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,
|
|
};
|
|
|
|
/**
|
|
* MetonaToast i18n — 国际化管理
|
|
* @module i18n
|
|
* @version 0.3.0
|
|
*/
|
|
// 当前语言状态
|
|
let currentLocale = 'zh-CN';
|
|
const localeListeners = new Set();
|
|
let fallbackLocale = 'zh-CN';
|
|
/**
|
|
* 获取当前语言
|
|
*/
|
|
const getCurrentLocale = () => {
|
|
return currentLocale;
|
|
};
|
|
/**
|
|
* 设置当前语言
|
|
*/
|
|
const setCurrentLocale = (locale) => {
|
|
if (!LOCALES[locale]) {
|
|
console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`);
|
|
locale = fallbackLocale;
|
|
}
|
|
currentLocale = locale;
|
|
notifyLocaleListeners(locale);
|
|
saveLocale(locale);
|
|
};
|
|
/**
|
|
* 获取回退语言
|
|
*/
|
|
const getFallbackLocale = () => {
|
|
return fallbackLocale;
|
|
};
|
|
/**
|
|
* 设置回退语言
|
|
*/
|
|
const setFallbackLocale = (locale) => {
|
|
if (!LOCALES[locale]) {
|
|
console.warn(`Fallback locale "${locale}" not found`);
|
|
return;
|
|
}
|
|
fallbackLocale = locale;
|
|
};
|
|
/**
|
|
* 翻译函数
|
|
*/
|
|
const t = (key, params = {}) => {
|
|
const currentTranslation = getTranslation(currentLocale, key);
|
|
if (currentTranslation !== undefined) {
|
|
return interpolate(currentTranslation, params);
|
|
}
|
|
if (currentLocale !== fallbackLocale) {
|
|
const fallbackTranslation = getTranslation(fallbackLocale, key);
|
|
if (fallbackTranslation !== undefined) {
|
|
return interpolate(fallbackTranslation, params);
|
|
}
|
|
}
|
|
console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`);
|
|
return key;
|
|
};
|
|
/**
|
|
* 获取翻译
|
|
*/
|
|
const getTranslation = (locale, key) => {
|
|
const localeData = LOCALES[locale];
|
|
if (!localeData)
|
|
return undefined;
|
|
const keys = key.split('.');
|
|
let result = localeData;
|
|
for (const k of keys) {
|
|
if (result && typeof result === 'object' && k in result) {
|
|
result = result[k];
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
return typeof result === 'string' ? result : undefined;
|
|
};
|
|
/**
|
|
* 插值函数
|
|
*/
|
|
const interpolate = (str, params) => {
|
|
return str.replace(/\{(\w+)\}/g, (_match, key) => {
|
|
return params[key] !== undefined ? String(params[key]) : _match;
|
|
});
|
|
};
|
|
/**
|
|
* 检查翻译是否存在
|
|
*/
|
|
const hasTranslation = (key) => {
|
|
return getTranslation(currentLocale, key) !== undefined ||
|
|
getTranslation(fallbackLocale, key) !== undefined;
|
|
};
|
|
/**
|
|
* 获取所有翻译
|
|
*/
|
|
const getTranslations = (locale) => {
|
|
return LOCALES[locale] || {};
|
|
};
|
|
/**
|
|
* 添加翻译
|
|
*/
|
|
const addTranslations = (locale, translations) => {
|
|
if (!LOCALES[locale]) {
|
|
LOCALES[locale] = {};
|
|
}
|
|
deepMerge(LOCALES[locale], translations);
|
|
};
|
|
/**
|
|
* 深度合并对象
|
|
*/
|
|
const deepMerge = (target, source) => {
|
|
for (const key in source) {
|
|
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
|
|
deepMerge(target[key], source[key]);
|
|
}
|
|
else {
|
|
target[key] = source[key];
|
|
}
|
|
}
|
|
return target;
|
|
};
|
|
/**
|
|
* 移除翻译
|
|
*/
|
|
const removeTranslation = (locale, key) => {
|
|
const localeData = LOCALES[locale];
|
|
if (!localeData)
|
|
return;
|
|
const keys = key.split('.');
|
|
let current = localeData;
|
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
if (current[keys[i]] && typeof current[keys[i]] === 'object') {
|
|
current = current[keys[i]];
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
}
|
|
delete current[keys[keys.length - 1]];
|
|
};
|
|
/**
|
|
* 清除翻译
|
|
*/
|
|
const clearTranslations = (locale) => {
|
|
if (LOCALES[locale]) {
|
|
LOCALES[locale] = {};
|
|
}
|
|
};
|
|
/**
|
|
* 获取支持的语言列表
|
|
*/
|
|
const getSupportedLocales = () => {
|
|
return Object.keys(LOCALES);
|
|
};
|
|
/**
|
|
* 检查语言是否支持
|
|
*/
|
|
const isLocaleSupported = (locale) => {
|
|
return locale in LOCALES;
|
|
};
|
|
/**
|
|
* 获取语言名称
|
|
*/
|
|
const getLocaleName = (locale) => {
|
|
const names = {
|
|
'zh-CN': '简体中文',
|
|
'zh-TW': '繁體中文',
|
|
'en-US': 'English (US)',
|
|
'en-GB': 'English (UK)',
|
|
'ja': '日本語',
|
|
'ko': '한국어',
|
|
'fr': 'Français',
|
|
'de': 'Deutsch',
|
|
'es': 'Español',
|
|
'pt': 'Português',
|
|
'ru': 'Русский',
|
|
'ar': 'العربية',
|
|
'hi': 'हिन्दी',
|
|
'th': 'ไทย',
|
|
'vi': 'Tiếng Việt',
|
|
'id': 'Bahasa Indonesia',
|
|
'ms': 'Bahasa Melayu',
|
|
'tr': 'Türkçe',
|
|
'it': 'Italiano',
|
|
'nl': 'Nederlands',
|
|
'pl': 'Polski',
|
|
'uk': 'Українська',
|
|
'cs': 'Čeština',
|
|
'sv': 'Svenska',
|
|
'da': 'Dansk',
|
|
'fi': 'Suomi',
|
|
'nb': 'Norsk',
|
|
'el': 'Ελληνικά',
|
|
'he': 'עברית',
|
|
'hu': 'Magyar',
|
|
'ro': 'Română',
|
|
'bg': 'Български',
|
|
'hr': 'Hrvatski',
|
|
'sk': 'Slovenčina',
|
|
'sl': 'Slovenščina',
|
|
'et': 'Eesti',
|
|
'lv': 'Latviešu',
|
|
'lt': 'Lietuvių',
|
|
'ca': 'Català',
|
|
'gl': 'Galego',
|
|
'eu': 'Euskara',
|
|
'cy': 'Cymraeg',
|
|
'ga': 'Gaeilge',
|
|
'mt': 'Malti',
|
|
'is': 'Íslenska',
|
|
'mk': 'Македонски',
|
|
'sq': 'Shqip',
|
|
'sr': 'Српски',
|
|
'bs': 'Bosanski',
|
|
'me': 'Crnogorski',
|
|
'ka': 'ქართული',
|
|
'hy': 'Հայերեն',
|
|
'az': 'Azərbaycan',
|
|
'uz': "O'zbek",
|
|
'kk': 'Қазақ',
|
|
'ky': 'Кыргыз',
|
|
'tg': 'Тоҷикӣ',
|
|
'tk': 'Türkmen',
|
|
'mn': 'Монгол',
|
|
'ne': 'नेपाली',
|
|
'si': 'සිංහල',
|
|
'my': 'မြန်မာ',
|
|
'km': 'ខ្មែរ',
|
|
'lo': 'ລາວ',
|
|
'am': 'አማርኛ',
|
|
'sw': 'Kiswahili',
|
|
'yo': 'Yorùbá',
|
|
'ig': 'Igbo',
|
|
'ha': 'Hausa',
|
|
'zu': 'isiZulu',
|
|
'af': 'Afrikaans',
|
|
'xh': 'isiXhosa',
|
|
'st': 'Sesotho',
|
|
'tn': 'Setswana',
|
|
'ts': 'Xitsonga',
|
|
'ss': 'siSwati',
|
|
've': 'Tshivenda',
|
|
'nr': 'isiNdebele',
|
|
};
|
|
return names[locale] || locale;
|
|
};
|
|
/**
|
|
* 获取语言方向
|
|
*/
|
|
const getLocaleDirection = (locale) => {
|
|
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug'];
|
|
return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
|
|
};
|
|
/**
|
|
* 获取语言信息
|
|
*/
|
|
const getLocaleInfo = (locale) => {
|
|
return {
|
|
code: locale,
|
|
name: getLocaleName(locale),
|
|
direction: getLocaleDirection(locale),
|
|
isSupported: isLocaleSupported(locale),
|
|
isCurrent: locale === currentLocale,
|
|
isFallback: locale === fallbackLocale,
|
|
};
|
|
};
|
|
/**
|
|
* 获取所有语言信息
|
|
*/
|
|
const getAllLocaleInfo = () => {
|
|
return getSupportedLocales().map(getLocaleInfo);
|
|
};
|
|
/**
|
|
* 保存语言到本地存储
|
|
*/
|
|
const saveLocale = (locale) => {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
localStorage.setItem('metona-toast-locale', locale);
|
|
}
|
|
catch (e) {
|
|
console.warn('Failed to save locale:', e);
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* 从本地存储加载语言
|
|
*/
|
|
const loadLocale = () => {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
return localStorage.getItem('metona-toast-locale') || getDefaultLocale();
|
|
}
|
|
catch (e) {
|
|
console.warn('Failed to load locale:', e);
|
|
}
|
|
}
|
|
return getDefaultLocale();
|
|
};
|
|
/**
|
|
* 获取默认语言
|
|
*/
|
|
const getDefaultLocale = () => {
|
|
if (typeof navigator !== 'undefined') {
|
|
const browserLocale = navigator.language || navigator.userLanguage;
|
|
if (browserLocale && isLocaleSupported(browserLocale)) {
|
|
return browserLocale;
|
|
}
|
|
const shortLocale = browserLocale?.split('-')[0];
|
|
if (shortLocale && isLocaleSupported(shortLocale)) {
|
|
return shortLocale;
|
|
}
|
|
}
|
|
return fallbackLocale;
|
|
};
|
|
/**
|
|
* 初始化国际化系统
|
|
*/
|
|
const initI18n = () => {
|
|
const savedLocale = loadLocale();
|
|
setCurrentLocale(savedLocale);
|
|
};
|
|
/**
|
|
* 切换语言
|
|
*/
|
|
const switchLocale = (locale) => {
|
|
setCurrentLocale(locale);
|
|
};
|
|
/**
|
|
* 添加语言监听器
|
|
*/
|
|
const addLocaleListener = (listener) => {
|
|
localeListeners.add(listener);
|
|
return () => {
|
|
localeListeners.delete(listener);
|
|
};
|
|
};
|
|
/**
|
|
* 移除语言监听器
|
|
*/
|
|
const removeLocaleListener = (listener) => {
|
|
localeListeners.delete(listener);
|
|
};
|
|
/**
|
|
* 通知语言监听器
|
|
*/
|
|
const notifyLocaleListeners = (locale) => {
|
|
localeListeners.forEach((listener) => {
|
|
try {
|
|
listener(locale);
|
|
}
|
|
catch (e) {
|
|
console.error('Locale listener error:', e);
|
|
}
|
|
});
|
|
};
|
|
/**
|
|
* 清除所有语言监听器
|
|
*/
|
|
const clearLocaleListeners = () => {
|
|
localeListeners.clear();
|
|
};
|
|
/**
|
|
* 格式化数字
|
|
*/
|
|
const formatNumber = (number, options = {}) => {
|
|
try {
|
|
return new Intl.NumberFormat(currentLocale, options).format(number);
|
|
}
|
|
catch (e) {
|
|
return number.toString();
|
|
}
|
|
};
|
|
/**
|
|
* 格式化货币
|
|
*/
|
|
const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
|
try {
|
|
return new Intl.NumberFormat(currentLocale, {
|
|
style: 'currency',
|
|
currency,
|
|
...options,
|
|
}).format(amount);
|
|
}
|
|
catch (e) {
|
|
return amount.toString();
|
|
}
|
|
};
|
|
/**
|
|
* 格式化百分比
|
|
*/
|
|
const formatPercent = (value, options = {}) => {
|
|
try {
|
|
return new Intl.NumberFormat(currentLocale, {
|
|
style: 'percent',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 2,
|
|
...options,
|
|
}).format(value / 100);
|
|
}
|
|
catch (e) {
|
|
return `${value}%`;
|
|
}
|
|
};
|
|
/**
|
|
* 格式化日期
|
|
*/
|
|
const formatDate = (date, options = {}) => {
|
|
try {
|
|
const dateObj = date instanceof Date ? date : new Date(date);
|
|
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
|
}
|
|
catch (e) {
|
|
return String(date);
|
|
}
|
|
};
|
|
/**
|
|
* 格式化时间
|
|
*/
|
|
const formatTime = (date, options = {}) => {
|
|
return formatDate(date, {
|
|
hour: 'numeric',
|
|
minute: 'numeric',
|
|
second: 'numeric',
|
|
...options,
|
|
});
|
|
};
|
|
/**
|
|
* 格式化相对时间
|
|
*/
|
|
const formatRelativeTime = (date, options = {}) => {
|
|
try {
|
|
const dateObj = date instanceof Date ? date : new Date(date);
|
|
const now = new Date();
|
|
const diff = dateObj.getTime() - now.getTime();
|
|
const rtf = new Intl.RelativeTimeFormat(currentLocale, {
|
|
numeric: 'auto',
|
|
...options,
|
|
});
|
|
const units = [
|
|
{ unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
|
|
{ unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
|
|
{ unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 },
|
|
{ unit: 'day', ms: 24 * 60 * 60 * 1000 },
|
|
{ unit: 'hour', ms: 60 * 60 * 1000 },
|
|
{ unit: 'minute', ms: 60 * 1000 },
|
|
{ unit: 'second', ms: 1000 },
|
|
];
|
|
for (const { unit, ms } of units) {
|
|
if (Math.abs(diff) >= ms || unit === 'second') {
|
|
const value = Math.round(diff / ms);
|
|
return rtf.format(value, unit);
|
|
}
|
|
}
|
|
return String(date);
|
|
}
|
|
catch (e) {
|
|
return String(date);
|
|
}
|
|
};
|
|
/**
|
|
* 格式化列表 — 优先使用 Intl.ListFormat,降级到逗号拼接
|
|
*/
|
|
const formatList = (list, _options = {}) => {
|
|
const intl = Intl;
|
|
if (typeof intl.ListFormat === 'function') {
|
|
try {
|
|
return new intl.ListFormat(currentLocale, {
|
|
style: 'long',
|
|
type: 'conjunction',
|
|
}).format(list);
|
|
}
|
|
catch (_e) { /* fallback below */ }
|
|
}
|
|
return list.join(', ');
|
|
};
|
|
/**
|
|
* 格式化复数
|
|
*/
|
|
const formatPlural = (count, options = {}) => {
|
|
try {
|
|
return new Intl.PluralRules(currentLocale, options).select(count);
|
|
}
|
|
catch (e) {
|
|
return count === 1 ? 'one' : 'other';
|
|
}
|
|
};
|
|
/**
|
|
* 复数翻译
|
|
*/
|
|
const plural = (key, count, params = {}) => {
|
|
const pluralForm = formatPlural(count);
|
|
const pluralKey = `${key}.${pluralForm}`;
|
|
if (hasTranslation(pluralKey)) {
|
|
return t(pluralKey, { ...params, count });
|
|
}
|
|
if (hasTranslation(key)) {
|
|
return t(key, { ...params, count });
|
|
}
|
|
return key;
|
|
};
|
|
/**
|
|
* 国际化工具 — 代理层
|
|
*/
|
|
const i18nUtils = {
|
|
t,
|
|
plural,
|
|
getCurrentLocale,
|
|
setCurrentLocale,
|
|
switchLocale,
|
|
getFallbackLocale,
|
|
setFallbackLocale,
|
|
hasTranslation,
|
|
getTranslations,
|
|
addTranslations,
|
|
removeTranslation,
|
|
clearTranslations,
|
|
getSupportedLocales,
|
|
isLocaleSupported,
|
|
getLocaleName,
|
|
getLocaleDirection,
|
|
getLocaleInfo,
|
|
getAllLocaleInfo,
|
|
formatNumber,
|
|
formatCurrency,
|
|
formatPercent,
|
|
formatDate,
|
|
formatTime,
|
|
formatRelativeTime,
|
|
formatList,
|
|
formatPlural,
|
|
addLocaleListener,
|
|
removeLocaleListener,
|
|
clearLocaleListeners,
|
|
initI18n,
|
|
saveLocale,
|
|
loadLocale,
|
|
getDefaultLocale,
|
|
};
|
|
/**
|
|
* 预设语言包
|
|
*/
|
|
({
|
|
'zh-CN': {
|
|
name: '简体中文',
|
|
nativeName: '简体中文',
|
|
direction: 'ltr',
|
|
translations: LOCALES['zh-CN'],
|
|
},
|
|
'en-US': {
|
|
name: 'English (US)',
|
|
nativeName: 'English (US)',
|
|
direction: 'ltr',
|
|
translations: LOCALES['en-US'],
|
|
},
|
|
});
|
|
|
|
/**
|
|
* MetonaToast Animations — 动画管理
|
|
* @module animations
|
|
* @version 0.2.1
|
|
*/
|
|
// 动画缓存
|
|
const animationMap = new Map();
|
|
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
|
|
const BUILTIN_ANIMATIONS = new Set(ANIMATION_TYPES);
|
|
// 动态注入的自定义动画样式(惰性创建)
|
|
let animStyleElement = null;
|
|
const injectedAnimations = new Set();
|
|
/**
|
|
* 将 CSS 属性对象转换为内联 CSS 字符串
|
|
*/
|
|
const toCss = (props) => Object.entries(props)
|
|
.map(([key, value]) => `${key}:${value}`)
|
|
.join(';');
|
|
/**
|
|
* 为自定义动画生成入场 keyframes + 类规则
|
|
*/
|
|
const buildAnimationCSS = (name, config) => {
|
|
const enter = toCss(config.enter);
|
|
return `
|
|
@keyframes met-${name}-in {
|
|
from { ${enter}; }
|
|
}
|
|
.met-anim-${name}.met-toast { opacity: 0; }
|
|
.met-anim-${name}.met-toast.met-show {
|
|
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
|
|
}
|
|
`;
|
|
};
|
|
/**
|
|
* 确保自定义动画的 CSS 已注入文档(幂等,内置动画自动跳过)
|
|
*/
|
|
const ensureAnimationCSS = (name) => {
|
|
if (typeof document === 'undefined')
|
|
return;
|
|
if (BUILTIN_ANIMATIONS.has(name))
|
|
return;
|
|
const config = animationMap.get(name);
|
|
if (!config || injectedAnimations.has(name))
|
|
return;
|
|
if (!animStyleElement) {
|
|
animStyleElement = document.getElementById('metona-toast-anim-styles');
|
|
if (!animStyleElement) {
|
|
animStyleElement = document.createElement('style');
|
|
animStyleElement.id = 'metona-toast-anim-styles';
|
|
document.head.appendChild(animStyleElement);
|
|
}
|
|
}
|
|
animStyleElement.textContent += buildAnimationCSS(name, config);
|
|
injectedAnimations.add(name);
|
|
};
|
|
/**
|
|
* 检查动画是否存在(内置或自定义)
|
|
*/
|
|
const hasAnimation = (name) => animationMap.has(name);
|
|
// 注册默认动画
|
|
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
|
animationMap.set(name, {
|
|
name,
|
|
enter: config.enter,
|
|
leave: config.leave,
|
|
duration: config.duration,
|
|
easing: config.easing,
|
|
});
|
|
});
|
|
/**
|
|
* 动画工具函数
|
|
*/
|
|
const animationUtils = {
|
|
register(name, config) {
|
|
animationMap.set(name, {
|
|
name,
|
|
enter: config.enter || {},
|
|
leave: config.leave || {},
|
|
duration: config.duration || 300,
|
|
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
});
|
|
},
|
|
unregister(name) {
|
|
animationMap.delete(name);
|
|
},
|
|
get(name) {
|
|
return animationMap.get(name) || null;
|
|
},
|
|
getAnimationNames() {
|
|
return Array.from(animationMap.keys());
|
|
},
|
|
getActiveCount() {
|
|
return animationMap.size;
|
|
},
|
|
cancelAll() {
|
|
// CSS动画由浏览器原生管理,无需手动取消
|
|
},
|
|
reset() {
|
|
animationMap.clear();
|
|
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
|
animationMap.set(name, {
|
|
name,
|
|
enter: config.enter,
|
|
leave: config.leave,
|
|
duration: config.duration,
|
|
easing: config.easing,
|
|
});
|
|
});
|
|
},
|
|
destroy() {
|
|
animationMap.clear();
|
|
injectedAnimations.clear();
|
|
animStyleElement = null;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* MetonaToast Toast — Toast 类
|
|
* @module toast
|
|
* @version 0.3.0
|
|
*/
|
|
/**
|
|
* Toast 类 — 核心通知组件
|
|
*/
|
|
class Toast {
|
|
/**
|
|
* 设置 Toast 类级别的回调函数
|
|
*/
|
|
static setCallbacks(callbacks) {
|
|
if (callbacks.onError !== undefined)
|
|
Toast._onError = callbacks.onError;
|
|
if (callbacks.removeToast !== undefined)
|
|
Toast._removeToast = callbacks.removeToast;
|
|
}
|
|
static on(name, fn) {
|
|
if (!this._hooks.has(name))
|
|
this._hooks.set(name, []);
|
|
this._hooks.get(name).push(fn);
|
|
return () => this.off(name, fn);
|
|
}
|
|
static off(name, fn) {
|
|
const list = this._hooks.get(name);
|
|
if (list)
|
|
this._hooks.set(name, list.filter(f => f !== fn));
|
|
}
|
|
/**
|
|
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
|
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
|
|
*/
|
|
static trigger(name, toast = null) {
|
|
const list = this._hooks.get(name);
|
|
if (!list || list.length === 0)
|
|
return true;
|
|
let allow = true;
|
|
list.forEach(fn => {
|
|
try {
|
|
const result = fn(toast);
|
|
if (result === false)
|
|
allow = false;
|
|
}
|
|
catch (e) {
|
|
console.error('Hook error:', name, e);
|
|
if (Toast._onError) {
|
|
try {
|
|
Toast._onError({ hook: name, error: e, toast: toast });
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
}
|
|
}
|
|
});
|
|
return allow;
|
|
}
|
|
constructor(opts) {
|
|
this.el = null;
|
|
this.barEl = null;
|
|
this.rafId = null;
|
|
this.startedAt = 0;
|
|
this.paused = false;
|
|
this.closing = false;
|
|
this.group = null;
|
|
this._cleanups = [];
|
|
this.id = opts.id || generateId();
|
|
this.type = opts.type || 'default';
|
|
this.title = opts.title || '';
|
|
this.message = opts.message ?? opts.content ?? '';
|
|
this.html = opts.html || '';
|
|
this.iconHTML = opts.iconHTML || '';
|
|
this.config = { ...DEFAULTS, ...opts };
|
|
// 防止嵌套对象被多个 toast 实例共享引用
|
|
if (opts.style && typeof opts.style === 'object') {
|
|
this.config.style = { ...opts.style };
|
|
}
|
|
this.remaining = this.config.duration || 0;
|
|
this.group = opts.group || null;
|
|
}
|
|
_palette() {
|
|
const theme = getTheme(this.config.theme || 'auto');
|
|
const c = TYPE_COLORS[this.type] || TYPE_COLORS.default;
|
|
const tc = THEMES[theme] || THEMES.light;
|
|
const t = { bg: tc.bg, border: tc.border, shadow: tc.shadow };
|
|
return { theme, c, t };
|
|
}
|
|
create() {
|
|
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
return this;
|
|
// beforeShow 钩子 — 返回 false 可阻止显示
|
|
if (!Toast.trigger('beforeShow', this))
|
|
return this;
|
|
// onBeforeShow 配置回调 — 返回 false 可阻止显示
|
|
if (typeof this.config.onBeforeShow === 'function') {
|
|
try {
|
|
if (this.config.onBeforeShow(this) === false)
|
|
return this;
|
|
}
|
|
catch (e) {
|
|
console.error('onBeforeShow callback error:', e);
|
|
}
|
|
}
|
|
injectStyles();
|
|
ensureAnimationCSS(this.config.animation || 'slide');
|
|
const container = this._getContainer();
|
|
const { theme, c, t } = this._palette();
|
|
this._limitToasts(container);
|
|
const el = document.createElement('div');
|
|
el.className = this._buildClassName(theme);
|
|
el.setAttribute('role', (this.type === 'error' || this.type === 'warning') ? 'alert' : 'status');
|
|
el.setAttribute('aria-live', this.type === 'error' ? 'assertive' : 'polite');
|
|
el.dataset.id = this.id;
|
|
this._applyStyles(el, theme, t);
|
|
this._buildContent(el, c);
|
|
this.el = el;
|
|
// Chrome bug: column-reverse + appendChild 会导致重叠。改用 column + insertBefore
|
|
const pos = this.config.position || 'top-right';
|
|
if (pos.startsWith('top')) {
|
|
container.insertBefore(el, container.firstChild);
|
|
}
|
|
else {
|
|
container.appendChild(el);
|
|
}
|
|
Toast._registry.set(this.id, this);
|
|
this._bindEvents(el);
|
|
this._startTimer();
|
|
// 进入动画
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
el.classList.add('met-show');
|
|
});
|
|
});
|
|
if (typeof this.config.onShow === 'function') {
|
|
try {
|
|
this.config.onShow(this);
|
|
}
|
|
catch (e) {
|
|
console.error('onShow callback error:', e);
|
|
}
|
|
}
|
|
Toast.trigger('afterShow', this);
|
|
return this;
|
|
}
|
|
_getContainer() {
|
|
const rawPosition = this.config.position || 'top-right';
|
|
const zIndex = this.config.zIndex || 9999;
|
|
// RTL 语言时翻转 left/right
|
|
const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl';
|
|
const position = isRTL
|
|
? rawPosition.replace('left', '__TMP__').replace('right', 'left').replace('__TMP__', 'right')
|
|
: rawPosition;
|
|
// 从模块级缓存获取 (由 api.ts 管理)
|
|
const cached = _containerCache.get(document.body);
|
|
if (cached && cached.has(position)) {
|
|
return cached.get(position);
|
|
}
|
|
// 缓存未命中时创建新容器
|
|
if (!_containerCache.has(document.body)) {
|
|
_containerCache.set(document.body, new Map());
|
|
}
|
|
const el = document.createElement('div');
|
|
el.className = `met-container ${position}`;
|
|
el.setAttribute('aria-label', 'Notifications');
|
|
el.setAttribute('role', 'region');
|
|
const posStyles = {
|
|
'top-left': 'top:0;left:0;align-items:flex-start',
|
|
'top-center': 'top:0;left:0;right:0;align-items:center',
|
|
'top-right': 'top:0;right:0;align-items:flex-end',
|
|
'bottom-left': 'bottom:0;left:0;align-items:flex-start',
|
|
'bottom-center': 'bottom:0;left:0;right:0;align-items:center',
|
|
'bottom-right': 'bottom:0;right:0;align-items:flex-end',
|
|
};
|
|
el.style.cssText = `
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:${this.config.gap || 12}px;
|
|
box-sizing:border-box;
|
|
padding:${this.config.offset || 24}px;
|
|
max-width:100vw;
|
|
position:fixed;
|
|
z-index:${zIndex};
|
|
pointer-events:none;
|
|
${posStyles[position] || 'top:0;right:0;align-items:flex-end'}
|
|
`;
|
|
document.body.appendChild(el);
|
|
_containerCache.get(document.body).set(position, el);
|
|
return el;
|
|
}
|
|
_limitToasts(container) {
|
|
const max = this.config.max || 6;
|
|
const list = Array.from(container.querySelectorAll('.met-toast'));
|
|
while (list.length >= max) {
|
|
const el = list.shift();
|
|
const id = el?.dataset.id;
|
|
if (!id)
|
|
continue;
|
|
const existing = Toast._registry.get(id);
|
|
if (existing) {
|
|
existing.close(true);
|
|
}
|
|
else if (Toast._removeToast) {
|
|
Toast._removeToast(id);
|
|
}
|
|
}
|
|
}
|
|
_buildClassName(theme) {
|
|
const anim = hasAnimation(this.config.animation || '') ? this.config.animation : 'slide';
|
|
return [
|
|
'met-toast',
|
|
`met-${this.type}`,
|
|
`met-theme-${theme}`,
|
|
`met-anim-${anim}`,
|
|
(this.config.closeOnClick || this.config.draggable) ? 'met-clickable' : '',
|
|
this.config.className || '',
|
|
].filter(Boolean).join(' ');
|
|
}
|
|
_applyStyles(el, _theme, _t) {
|
|
const widthValue = typeof this.config.width === 'number'
|
|
? `${this.config.width}px`
|
|
: (typeof this.config.width === 'string' ? this.config.width : '360px');
|
|
const set = (p, v) => el.style.setProperty(p, v, 'important');
|
|
set('position', 'relative');
|
|
set('flex-shrink', '0');
|
|
set('min-width', '240px');
|
|
set('max-width', 'calc(100vw - 48px)');
|
|
set('width', widthValue);
|
|
// 用户自定义样式最后应用
|
|
if (this.config.style && Object.keys(this.config.style).length > 0) {
|
|
Object.entries(this.config.style).forEach(([k, v]) => { el.style[k] = v; });
|
|
}
|
|
}
|
|
_buildContent(el, c) {
|
|
// 自定义渲染函数 — 完全接管 DOM 构建
|
|
if (typeof this.config.render === 'function') {
|
|
el.innerHTML = this.config.render(this);
|
|
this.barEl = el.querySelector('.met-bar, .met-bar-v');
|
|
return;
|
|
}
|
|
const showIcon = this.config.icon !== false && (this.iconHTML || ICONS[this.type]);
|
|
const showClose = this.config.closeButton !== false;
|
|
const showProgress = this.config.showProgress !== false && (this.config.duration || 0) > 0;
|
|
const showSide = (!showIcon && this.type !== 'default');
|
|
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
|
|
const safeMessage = this.html
|
|
? `<div class="met-message">${this.html}</div>`
|
|
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
|
const iconHTML = showIcon
|
|
? `<div class="met-icon" style="color:${c.fg}">${this.iconHTML || ICONS[this.type]}</div>` : '';
|
|
const closeHTML = showClose
|
|
? `<button class="met-close" type="button" aria-label="${t('close')}">
|
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>`
|
|
: '';
|
|
const progressHTML = showProgress
|
|
? (this.config.progressDirection === 'vertical'
|
|
? `<div class="met-progress-v"><div class="met-bar-v" style="background:${c.fg}"></div></div>`
|
|
: `<div class="met-progress"><div class="met-bar" style="background:${c.fg}"></div></div>`)
|
|
: '';
|
|
const sideHTML = showSide
|
|
? `<div class="met-side" style="background:${c.fg}"></div>` : '';
|
|
el.innerHTML = `
|
|
${iconHTML}
|
|
${sideHTML}
|
|
<div class="met-content">
|
|
${safeTitle}
|
|
${safeMessage}
|
|
</div>
|
|
${closeHTML}
|
|
${progressHTML}
|
|
`;
|
|
this.barEl = el.querySelector('.met-bar, .met-bar-v');
|
|
}
|
|
_bindEvents(el) {
|
|
const eventHandler = (e) => {
|
|
const target = e.target;
|
|
if (target.closest('.met-close')) {
|
|
e.stopPropagation();
|
|
this.close();
|
|
return;
|
|
}
|
|
Toast.trigger('click', this);
|
|
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
|
if (typeof this.config.onClick === 'function') {
|
|
try {
|
|
this.config.onClick(this);
|
|
}
|
|
catch (err) {
|
|
console.error('onClick callback error:', err);
|
|
}
|
|
}
|
|
this.close();
|
|
return;
|
|
}
|
|
};
|
|
el.addEventListener('click', eventHandler);
|
|
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
|
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
|
const mouseEnter = () => {
|
|
Toast.trigger('hover', this);
|
|
this._pause();
|
|
};
|
|
const mouseLeave = () => {
|
|
Toast.trigger('hover', this);
|
|
this._resume();
|
|
};
|
|
el.addEventListener('mouseenter', mouseEnter);
|
|
el.addEventListener('mouseleave', mouseLeave);
|
|
this._cleanups.push(() => {
|
|
el.removeEventListener('mouseenter', mouseEnter);
|
|
el.removeEventListener('mouseleave', mouseLeave);
|
|
});
|
|
}
|
|
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
|
|
const animStart = () => Toast.trigger('animationStart', this);
|
|
const animEnd = () => Toast.trigger('animationEnd', this);
|
|
el.addEventListener('animationstart', animStart);
|
|
el.addEventListener('animationend', animEnd);
|
|
this._cleanups.push(() => {
|
|
el.removeEventListener('animationstart', animStart);
|
|
el.removeEventListener('animationend', animEnd);
|
|
});
|
|
if (this.config.draggable) {
|
|
this._bindDrag(el);
|
|
}
|
|
}
|
|
_bindDrag(el) {
|
|
let sx = 0, sy = 0, dx = 0, dy = 0, dragging = false;
|
|
const down = (e) => {
|
|
if (e.target.closest('.met-close'))
|
|
return;
|
|
dragging = true;
|
|
sx = e.clientX;
|
|
sy = e.clientY;
|
|
el.setPointerCapture(e.pointerId);
|
|
el.style.transition = 'none';
|
|
this._pause();
|
|
Toast.trigger('dragStart', this);
|
|
};
|
|
const move = (e) => {
|
|
if (!dragging)
|
|
return;
|
|
dx = e.clientX - sx;
|
|
dy = e.clientY - sy;
|
|
el.style.transform = `translate(${dx}px, ${dy}px) rotate(${dx * 0.1}deg)`;
|
|
el.style.opacity = String(Math.max(0, 1 - Math.abs(dx) / 200));
|
|
};
|
|
const up = (e) => {
|
|
if (!dragging)
|
|
return;
|
|
dragging = false;
|
|
el.releasePointerCapture(e.pointerId);
|
|
el.style.transition = '';
|
|
if (Math.abs(dx) > 120) {
|
|
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
|
el.style.opacity = '0';
|
|
setTimeout(() => this.close(true), 250);
|
|
}
|
|
else {
|
|
el.style.transform = '';
|
|
el.style.opacity = '';
|
|
this._resume();
|
|
}
|
|
dx = dy = 0;
|
|
Toast.trigger('dragEnd', this);
|
|
};
|
|
el.addEventListener('pointerdown', down);
|
|
el.addEventListener('pointermove', move);
|
|
el.addEventListener('pointerup', up);
|
|
el.addEventListener('pointercancel', up);
|
|
this._cleanups.push(() => {
|
|
el.removeEventListener('pointerdown', down);
|
|
el.removeEventListener('pointermove', move);
|
|
el.removeEventListener('pointerup', up);
|
|
el.removeEventListener('pointercancel', up);
|
|
});
|
|
}
|
|
_startTimer(resuming = false) {
|
|
if ((this.config.duration || 0) <= 0)
|
|
return;
|
|
if (!resuming) {
|
|
this.startedAt = Date.now();
|
|
this.remaining = this.config.duration || 0;
|
|
Toast.trigger('progressStart', this);
|
|
}
|
|
const tick = () => {
|
|
if (this.paused || this.closing)
|
|
return;
|
|
try {
|
|
const elapsed = Date.now() - this.startedAt;
|
|
this.remaining = Math.max(0, (this.config.duration || 0) - elapsed);
|
|
if (this.barEl) {
|
|
const ratio = this.remaining / (this.config.duration || 1);
|
|
const t = this.config.progressDirection === 'vertical'
|
|
? `scaleY(${ratio})` : `scaleX(${ratio})`;
|
|
this.barEl.style.transform = t;
|
|
}
|
|
if (this.remaining <= 0) {
|
|
Toast.trigger('progressEnd', this);
|
|
this.close();
|
|
return;
|
|
}
|
|
}
|
|
catch (e) {
|
|
console.error('Timer tick error:', e);
|
|
if (Toast._onError) {
|
|
try {
|
|
Toast._onError({ source: 'timer', error: e, toast: this });
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
}
|
|
}
|
|
this.rafId = requestAnimationFrame(tick);
|
|
};
|
|
this.rafId = requestAnimationFrame(tick);
|
|
}
|
|
_pause() {
|
|
if (this.paused || (this.config.duration || 0) <= 0)
|
|
return;
|
|
this.paused = true;
|
|
if (this.rafId !== null)
|
|
cancelAnimationFrame(this.rafId);
|
|
this.remaining = Math.max(0, (this.config.duration || 0) - (Date.now() - this.startedAt));
|
|
}
|
|
_resume() {
|
|
if (!this.paused)
|
|
return;
|
|
this.paused = false;
|
|
this.startedAt = Date.now() - ((this.config.duration || 0) - this.remaining);
|
|
this._startTimer(true);
|
|
}
|
|
update(partial) {
|
|
// beforeUpdate 钩子 — 返回 false 可阻止更新
|
|
if (!Toast.trigger('beforeUpdate', this))
|
|
return this;
|
|
const typeChanged = partial.type && partial.type !== this.type;
|
|
if (partial.type)
|
|
this.type = partial.type;
|
|
if (partial.title !== undefined)
|
|
this.title = partial.title;
|
|
if (partial.message !== undefined)
|
|
this.message = partial.message;
|
|
if (partial.html !== undefined)
|
|
this.html = partial.html;
|
|
if (!this.el) {
|
|
Toast.trigger('afterUpdate', this);
|
|
return this;
|
|
}
|
|
// 类型变更时同步更新 DOM 类名、边框颜色和进度条颜色
|
|
if (typeChanged) {
|
|
const typeClasses = ['met-success', 'met-error', 'met-warning', 'met-info', 'met-loading', 'met-default'];
|
|
typeClasses.forEach(c => this.el.classList.remove(c));
|
|
this.el.classList.add(`met-${this.type}`);
|
|
if (this.barEl) {
|
|
const c = TYPE_COLORS[this.type] || TYPE_COLORS.default;
|
|
this.barEl.style.background = c.fg;
|
|
}
|
|
const side = this.el.querySelector('.met-side');
|
|
if (side) {
|
|
const c = TYPE_COLORS[this.type] || TYPE_COLORS.default;
|
|
side.style.background = c.fg;
|
|
}
|
|
}
|
|
const content = this.el.querySelector('.met-content');
|
|
if (content) {
|
|
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
|
|
const safeMessage = this.html
|
|
? `<div class="met-message">${this.html}</div>`
|
|
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
|
content.innerHTML = `${safeTitle}${safeMessage}`;
|
|
}
|
|
// resetTimerOnUpdate: 更新内容后重置计时器
|
|
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
|
if (this.rafId !== null)
|
|
cancelAnimationFrame(this.rafId);
|
|
this.startedAt = Date.now();
|
|
this.remaining = this.config.duration || 0;
|
|
this._startTimer();
|
|
}
|
|
if (typeof this.config.onUpdate === 'function') {
|
|
try {
|
|
this.config.onUpdate(this);
|
|
}
|
|
catch (e) {
|
|
console.error('onUpdate callback error:', e);
|
|
}
|
|
}
|
|
Toast.trigger('afterUpdate', this);
|
|
return this;
|
|
}
|
|
/**
|
|
* 运行时移动 Toast 到新位置
|
|
*/
|
|
updatePosition(position) {
|
|
if (!position || !this.el)
|
|
return this;
|
|
const oldContainer = this.el.parentNode;
|
|
this.config.position = position;
|
|
const newContainer = this._getContainer();
|
|
if (oldContainer === newContainer)
|
|
return this;
|
|
// 从旧容器移除,加入新容器
|
|
if (oldContainer && this.el.parentNode === oldContainer) {
|
|
oldContainer.removeChild(this.el);
|
|
}
|
|
const pos = position;
|
|
if (pos.startsWith('top')) {
|
|
newContainer.insertBefore(this.el, newContainer.firstChild);
|
|
}
|
|
else {
|
|
newContainer.appendChild(this.el);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* 立即从 DOM 和内存中移除(不触发离场动画)
|
|
*/
|
|
remove() {
|
|
if (this.rafId !== null) {
|
|
cancelAnimationFrame(this.rafId);
|
|
this.rafId = null;
|
|
}
|
|
this._cleanups.forEach(fn => { try {
|
|
fn();
|
|
}
|
|
catch (_e) { /* noop */ } });
|
|
this._cleanups = [];
|
|
if (this.el && this.el.parentNode) {
|
|
this.el.parentNode.removeChild(this.el);
|
|
}
|
|
this.el = null;
|
|
Toast._registry.delete(this.id);
|
|
if (Toast._removeToast)
|
|
Toast._removeToast(this.id);
|
|
}
|
|
close(immediate = false) {
|
|
if (this.closing)
|
|
return;
|
|
// beforeClose 钩子 — 返回 false 可阻止关闭
|
|
if (!Toast.trigger('beforeClose', this))
|
|
return;
|
|
this.closing = true;
|
|
if (this.rafId !== null)
|
|
cancelAnimationFrame(this.rafId);
|
|
this._cleanups.forEach(fn => { try {
|
|
fn();
|
|
}
|
|
catch (_e) { /* noop */ } });
|
|
this._cleanups = [];
|
|
const el = this.el;
|
|
if (!el) {
|
|
if (Toast._removeToast)
|
|
Toast._removeToast(this.id);
|
|
return;
|
|
}
|
|
// 立即脱离文档流
|
|
const container = el.parentNode;
|
|
if (container && !immediate) {
|
|
const elRect = el.getBoundingClientRect();
|
|
const containerRect = container.getBoundingClientRect();
|
|
el.style.position = 'absolute';
|
|
el.style.top = (elRect.top - containerRect.top) + 'px';
|
|
el.style.left = (elRect.left - containerRect.left) + 'px';
|
|
el.style.width = elRect.width + 'px';
|
|
el.style.margin = '0';
|
|
}
|
|
el.classList.add('met-leaving');
|
|
el.classList.remove('met-show');
|
|
const pos = this.config.position || 'top-right';
|
|
const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl';
|
|
let transform = 'scale(.96)';
|
|
if (pos.includes('right'))
|
|
transform = isRTL ? 'translateX(-120%)' : 'translateX(120%)';
|
|
else if (pos.includes('left'))
|
|
transform = isRTL ? 'translateX(120%)' : 'translateX(-120%)';
|
|
else if (pos.startsWith('top'))
|
|
transform = 'translateY(-20px)';
|
|
else
|
|
transform = 'translateY(20px)';
|
|
el.style.transform = transform;
|
|
el.style.opacity = '0';
|
|
setTimeout(() => this._destroy(), immediate ? 0 : 300);
|
|
}
|
|
_destroy() {
|
|
if (this.el && this.el.parentNode) {
|
|
this.el.parentNode.removeChild(this.el);
|
|
}
|
|
this.el = null;
|
|
if (typeof this.config.onClose === 'function') {
|
|
try {
|
|
this.config.onClose(this);
|
|
}
|
|
catch (e) {
|
|
console.error('onClose callback error:', e);
|
|
}
|
|
}
|
|
Toast.trigger('afterClose', this);
|
|
Toast._registry.delete(this.id);
|
|
if (Toast._removeToast)
|
|
Toast._removeToast(this.id);
|
|
}
|
|
}
|
|
// 静态钩子系统
|
|
Toast._hooks = new Map();
|
|
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
|
|
Toast._registry = new Map();
|
|
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
|
Toast._onError = null;
|
|
Toast._removeToast = null;
|
|
/**
|
|
* 模块级共享容器缓存,所有 Toast 实例共用
|
|
*/
|
|
const _containerCache = new Map();
|
|
|
|
/**
|
|
* MetonaToast Templates — HTML 模板辅助函数
|
|
* @module templates
|
|
* @version 0.3.0
|
|
* @description confirm / prompt / progress / action 的 DOM 模板
|
|
*/
|
|
/**
|
|
* 按钮行模板 — confirm/prompt 共用
|
|
*/
|
|
const btnRow = (buttons) => `
|
|
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
|
${buttons}
|
|
</div>`;
|
|
/**
|
|
* 确认对话框模板
|
|
*/
|
|
const confirmHTML = (opts) => {
|
|
const confirmText = opts.confirmText || t('confirm');
|
|
const cancelText = opts.cancelText || t('cancel');
|
|
const confirmColor = opts.confirmColor || '#10b981';
|
|
const cancelColor = opts.cancelColor || '#6b7280';
|
|
return btnRow(`
|
|
<button class="met-confirm-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${confirmColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(confirmText)}</button>
|
|
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
|
|
`);
|
|
};
|
|
/**
|
|
* 输入对话框模板
|
|
*/
|
|
const promptHTML = (opts) => {
|
|
const submitText = opts.submitText || t('confirm');
|
|
const cancelText = opts.cancelText || t('cancel');
|
|
const submitColor = opts.submitColor || '#3b82f6';
|
|
const cancelColor = opts.cancelColor || '#6b7280';
|
|
return `
|
|
<input type="${opts.inputType || 'text'}"
|
|
class="met-input"
|
|
placeholder="${escapeHTML(opts.placeholder || '')}"
|
|
value="${escapeHTML(opts.defaultValue || '')}"
|
|
style="width:100%;padding:8px 12px;border:1px solid rgba(0,0,0,0.1);border-radius:6px;font-size:13px;margin-top:8px;background:transparent;color:inherit;box-sizing:border-box">
|
|
${btnRow(`
|
|
<button class="met-submit-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${submitColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(submitText)}</button>
|
|
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
|
|
`)}
|
|
`;
|
|
};
|
|
/**
|
|
* 进度条模板
|
|
*/
|
|
const progressHTML = (opts) => {
|
|
const color = opts.progressColor || '#3b82f6';
|
|
return `
|
|
<div class="met-progress-bar" style="width:100%;height:8px;background:rgba(0,0,0,0.1);border-radius:4px;margin-top:12px;overflow:hidden">
|
|
<div class="met-progress-fill" style="width:0%;height:100%;background:${color};border-radius:4px;transition:width 0.3s ease"></div>
|
|
</div>
|
|
<div class="met-progress-text" style="font-size:12px;opacity:0.7;margin-top:4px;text-align:right">0%</div>
|
|
`;
|
|
};
|
|
/**
|
|
* Action Toast 模板
|
|
*/
|
|
const actionHTML = (actions) => {
|
|
if (!Array.isArray(actions) || actions.length === 0)
|
|
return '';
|
|
const buttons = actions.map((a, i) => {
|
|
const bg = a.style?.background || a.color || '#6366f1';
|
|
const cls = `met-action-btn-${i}`;
|
|
return `<button class="${cls}" style="flex:1;padding:6px 12px;border:none;border-radius:6px;background:${bg};color:white;cursor:pointer;font-size:12px;font-weight:500">${escapeHTML(a.text || '')}</button>`;
|
|
}).join('');
|
|
return btnRow(buttons);
|
|
};
|
|
|
|
/**
|
|
* MetonaToast Plugins — 插件系统
|
|
* @module plugins
|
|
* @version 0.3.0
|
|
*/
|
|
/**
|
|
* 插件管理器
|
|
*/
|
|
class PluginManager {
|
|
constructor() {
|
|
this.plugins = new Map();
|
|
this.initialized = false;
|
|
}
|
|
register(name, plugin) {
|
|
if (this.plugins.has(name)) {
|
|
console.warn(`Plugin "${name}" is already registered`);
|
|
return this;
|
|
}
|
|
if (!plugin || typeof plugin !== 'object' || !plugin.name) {
|
|
console.error(`Invalid plugin "${name}": must be an object with a "name" property`);
|
|
return this;
|
|
}
|
|
const stored = { ...plugin, installed: false, enabled: true };
|
|
if (!stored.name)
|
|
stored.name = name;
|
|
this.plugins.set(name, stored);
|
|
if (stored.install) {
|
|
try {
|
|
stored.install(this);
|
|
stored.installed = true;
|
|
}
|
|
catch (e) {
|
|
console.error(`Failed to install plugin "${name}":`, e);
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
unregister(name) {
|
|
const plugin = this.plugins.get(name);
|
|
if (!plugin)
|
|
return this;
|
|
if (plugin.uninstall) {
|
|
try {
|
|
plugin.uninstall(this);
|
|
}
|
|
catch (e) {
|
|
console.error(`Failed to uninstall plugin "${name}":`, e);
|
|
}
|
|
}
|
|
this.plugins.delete(name);
|
|
return this;
|
|
}
|
|
get(name) { return this.plugins.get(name) || null; }
|
|
has(name) { return this.plugins.has(name); }
|
|
getAll() { return Array.from(this.plugins.values()); }
|
|
getNames() { return Array.from(this.plugins.keys()); }
|
|
enable(name) {
|
|
const p = this.plugins.get(name);
|
|
if (p)
|
|
p.enabled = true;
|
|
return this;
|
|
}
|
|
disable(name) {
|
|
const p = this.plugins.get(name);
|
|
if (p)
|
|
p.enabled = false;
|
|
return this;
|
|
}
|
|
isEnabled(name) {
|
|
const p = this.plugins.get(name);
|
|
return p ? !!p.enabled : false;
|
|
}
|
|
destroy() {
|
|
this.plugins.forEach((p) => {
|
|
if (p.destroy) {
|
|
try {
|
|
p.destroy(this);
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
}
|
|
});
|
|
this.plugins.clear();
|
|
}
|
|
}
|
|
/**
|
|
* 预设插件
|
|
*/
|
|
const presetPlugins = {
|
|
/**
|
|
* 键盘快捷键插件 — ESC 关闭所有 Toast
|
|
*/
|
|
keyboard: {
|
|
name: 'keyboard',
|
|
version: '1.0.0',
|
|
description: 'ESC 关闭所有 Toast',
|
|
_handler: null,
|
|
install() {
|
|
if (typeof document === 'undefined')
|
|
return;
|
|
const handler = (e) => {
|
|
if (e.key === 'Escape') {
|
|
document.querySelectorAll('.met-toast .met-close').forEach(btn => {
|
|
try {
|
|
btn.click();
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
});
|
|
}
|
|
};
|
|
this._handler = handler;
|
|
document.addEventListener('keydown', handler);
|
|
},
|
|
uninstall() {
|
|
const handler = this._handler;
|
|
if (handler && typeof document !== 'undefined') {
|
|
document.removeEventListener('keydown', handler);
|
|
this._handler = null;
|
|
}
|
|
},
|
|
},
|
|
/**
|
|
* 持久化插件 — 自动保存/加载配置到 localStorage
|
|
*/
|
|
persistence: {
|
|
name: 'persistence',
|
|
version: '1.0.0',
|
|
description: '自动持久化配置到 localStorage',
|
|
storageKey: 'metona-toast-config',
|
|
install() {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
const key = this.storageKey;
|
|
const saved = localStorage.getItem(key);
|
|
return saved ? JSON.parse(saved) : null;
|
|
}
|
|
catch (_e) {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
save(config) {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
const key = this.storageKey;
|
|
localStorage.setItem(key, JSON.stringify(config));
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
}
|
|
},
|
|
uninstall() {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
const key = this.storageKey;
|
|
localStorage.removeItem(key);
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
}
|
|
},
|
|
},
|
|
/**
|
|
* 无障碍插件 — 屏幕阅读器公告
|
|
*/
|
|
accessibility: {
|
|
name: 'accessibility',
|
|
version: '1.0.0',
|
|
description: '通过屏幕阅读器朗读 Toast 内容',
|
|
announce(toast) {
|
|
if (typeof document === 'undefined')
|
|
return;
|
|
const el = document.createElement('div');
|
|
el.setAttribute('aria-live', 'assertive');
|
|
el.setAttribute('aria-atomic', 'true');
|
|
el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
|
|
const typeName = t(toast.type) || toast.type;
|
|
el.textContent = `${typeName}: ${toast.title || ''} ${toast.message || ''}`;
|
|
document.body.appendChild(el);
|
|
setTimeout(() => { if (el.parentNode)
|
|
el.parentNode.removeChild(el); }, 3000);
|
|
},
|
|
},
|
|
};
|
|
/**
|
|
* 插件工具
|
|
*/
|
|
const pluginUtils = {
|
|
createManager() { return new PluginManager(); },
|
|
register(name, plugin) { return defaultPluginManager.register(name, plugin); },
|
|
unregister(name) { return defaultPluginManager.unregister(name); },
|
|
get(name) { return defaultPluginManager.get(name); },
|
|
has(name) { return defaultPluginManager.has(name); },
|
|
getAll() { return defaultPluginManager.getAll(); },
|
|
getNames() { return defaultPluginManager.getNames(); },
|
|
enable(name) { return defaultPluginManager.enable(name); },
|
|
disable(name) { return defaultPluginManager.disable(name); },
|
|
isEnabled(name) { return defaultPluginManager.isEnabled(name); },
|
|
getPreset(name) { return presetPlugins[name] || null; },
|
|
getAllPresets() { return { ...presetPlugins }; },
|
|
createPlugin(config) {
|
|
return {
|
|
name: config.name || 'custom',
|
|
version: config.version || '1.0.0',
|
|
description: config.description || '',
|
|
hooks: config.hooks || {},
|
|
install: config.install || undefined,
|
|
uninstall: config.uninstall || undefined,
|
|
...config,
|
|
};
|
|
},
|
|
validatePlugin(plugin) {
|
|
const errors = [];
|
|
if (!plugin || typeof plugin !== 'object')
|
|
errors.push('Plugin must be an object');
|
|
if (!plugin.name && !plugin.version)
|
|
errors.push('Plugin must have a name or version');
|
|
return { valid: errors.length === 0, errors };
|
|
},
|
|
};
|
|
const defaultPluginManager = new PluginManager();
|
|
|
|
/**
|
|
* MetonaToast API — meToast 核心 API 对象
|
|
* @module api
|
|
* @version 0.3.0
|
|
*/
|
|
/**
|
|
* 参数标准化工具
|
|
*/
|
|
const normalizeArgs = (args, defaultType = 'default') => {
|
|
const [first, second] = args;
|
|
if (typeof first === 'string') {
|
|
return {
|
|
...(second || {}),
|
|
type: second?.type || defaultType,
|
|
message: first,
|
|
};
|
|
}
|
|
if (first && typeof first === 'object') {
|
|
return {
|
|
...first,
|
|
type: first.type || defaultType,
|
|
};
|
|
}
|
|
return {
|
|
type: defaultType,
|
|
message: '',
|
|
};
|
|
};
|
|
/**
|
|
* meToast API 对象
|
|
*/
|
|
const meToast = {
|
|
_toasts: new Map(),
|
|
_config: { ...DEFAULTS },
|
|
version: VERSION,
|
|
configure(opts) {
|
|
if (!opts || typeof opts !== 'object')
|
|
return this;
|
|
Object.assign(this._config, opts);
|
|
if (opts.theme) {
|
|
applyTheme(opts.theme);
|
|
}
|
|
if (opts.locale) {
|
|
setCurrentLocale(opts.locale);
|
|
}
|
|
Toast.trigger('configChange');
|
|
return this;
|
|
},
|
|
_emit(opts) {
|
|
const merged = { ...this._config, ...opts };
|
|
if (merged.content && !merged.message)
|
|
merged.message = merged.content;
|
|
const t = new Toast(merged);
|
|
t.create();
|
|
this._toasts.set(t.id, t);
|
|
return t;
|
|
},
|
|
_remove(id) {
|
|
this._toasts.delete(id);
|
|
},
|
|
find(id) {
|
|
if (!id)
|
|
return undefined;
|
|
return this._toasts.get(id);
|
|
},
|
|
show(messageOrOpts, opts = {}) {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'default');
|
|
return this._emit(normalized);
|
|
},
|
|
success(messageOrOpts, opts = {}) {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'success');
|
|
return this._emit(normalized);
|
|
},
|
|
error(messageOrOpts, opts = {}) {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'error');
|
|
return this._emit(normalized);
|
|
},
|
|
warning(messageOrOpts, opts = {}) {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'warning');
|
|
return this._emit(normalized);
|
|
},
|
|
info(messageOrOpts, opts = {}) {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
|
return this._emit(normalized);
|
|
},
|
|
loading(messageOrOpts, opts = {}) {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'loading');
|
|
normalized.duration = 0;
|
|
normalized.closeButton = false;
|
|
normalized.showProgress = false;
|
|
const toast = this._emit(normalized);
|
|
return {
|
|
id: toast.id,
|
|
success: (msg, o = {}) => this._resolve(toast, 'success', msg || t('success'), o),
|
|
error: (msg, o = {}) => this._resolve(toast, 'error', msg || t('error'), o),
|
|
info: (msg, o = {}) => this._resolve(toast, 'info', msg || t('info'), o),
|
|
warning: (msg, o = {}) => this._resolve(toast, 'warning', msg || t('warning'), o),
|
|
update: (p) => { toast.update(p); return this; },
|
|
dismiss: () => toast.close(),
|
|
};
|
|
},
|
|
promise(promise, opts = {}) {
|
|
if (!promise || typeof promise.then !== 'function') {
|
|
console.error('MeToast.promise: first argument must be a Promise');
|
|
return Promise.reject(new Error('Invalid promise'));
|
|
}
|
|
const loadingMsg = opts.loading || t('loading');
|
|
const successMsg = opts.success || t('success');
|
|
const errorMsg = opts.error || t('error');
|
|
const ctrl = this.loading(loadingMsg);
|
|
return Promise.resolve(promise)
|
|
.then((data) => {
|
|
ctrl.success(successMsg);
|
|
return data;
|
|
})
|
|
.catch((err) => {
|
|
ctrl.error(errorMsg);
|
|
throw err;
|
|
});
|
|
},
|
|
_resolve(loadingToast, type, message, opts) {
|
|
const id = loadingToast.id;
|
|
const old = this._toasts.get(id);
|
|
if (!old)
|
|
return null;
|
|
const position = old.config.position;
|
|
old.close();
|
|
return this._emit({ ...opts, type, message, position });
|
|
},
|
|
confirm(message, opts = {}) {
|
|
if (typeof message !== 'string') {
|
|
console.error('MeToast.confirm: message must be a string');
|
|
return Promise.resolve(false);
|
|
}
|
|
return new Promise((resolve) => {
|
|
let resolved = false;
|
|
const safeResolve = (val) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(safetyTimeout);
|
|
resolve(val);
|
|
}
|
|
};
|
|
const safetyTimeout = setTimeout(() => safeResolve(false), 10000);
|
|
const toast = this._emit({
|
|
...opts,
|
|
type: opts.type || 'warning',
|
|
message,
|
|
duration: 0,
|
|
closeButton: false,
|
|
closeOnClick: false,
|
|
draggable: false,
|
|
html: confirmHTML(opts),
|
|
});
|
|
setTimeout(() => {
|
|
const confirmBtn = toast.el?.querySelector('.met-confirm-btn');
|
|
const cancelBtn = toast.el?.querySelector('.met-cancel-btn');
|
|
if (confirmBtn) {
|
|
confirmBtn.addEventListener('click', () => {
|
|
toast.close();
|
|
safeResolve(true);
|
|
});
|
|
}
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener('click', () => {
|
|
toast.close();
|
|
safeResolve(false);
|
|
});
|
|
}
|
|
}, 0);
|
|
});
|
|
},
|
|
prompt(message, opts = {}) {
|
|
if (typeof message !== 'string') {
|
|
console.error('MeToast.prompt: message must be a string');
|
|
return Promise.resolve(null);
|
|
}
|
|
return new Promise((resolve) => {
|
|
let resolved = false;
|
|
const safeResolve = (val) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(safetyTimeout);
|
|
resolve(val);
|
|
}
|
|
};
|
|
const safetyTimeout = setTimeout(() => safeResolve(null), 10000);
|
|
const toast = this._emit({
|
|
...opts,
|
|
type: opts.type || 'info',
|
|
message,
|
|
duration: 0,
|
|
closeButton: false,
|
|
closeOnClick: false,
|
|
draggable: false,
|
|
html: promptHTML(opts),
|
|
});
|
|
setTimeout(() => {
|
|
const input = toast.el?.querySelector('.met-input');
|
|
const submitBtn = toast.el?.querySelector('.met-submit-btn');
|
|
const cancelBtn = toast.el?.querySelector('.met-cancel-btn');
|
|
if (input) {
|
|
input.focus();
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
toast.close();
|
|
safeResolve(input.value);
|
|
}
|
|
});
|
|
}
|
|
if (submitBtn) {
|
|
submitBtn.addEventListener('click', () => {
|
|
toast.close();
|
|
safeResolve(input?.value || null);
|
|
});
|
|
}
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener('click', () => {
|
|
toast.close();
|
|
safeResolve(null);
|
|
});
|
|
}
|
|
}, 0);
|
|
});
|
|
},
|
|
progress(messageOrOpts, opts = {}) {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
|
normalized.duration = 0;
|
|
normalized.closeButton = false;
|
|
normalized.showProgress = false;
|
|
const toast = this._emit({
|
|
...normalized,
|
|
html: progressHTML(opts),
|
|
});
|
|
return {
|
|
id: toast.id,
|
|
setProgress(percent) {
|
|
const fill = toast.el?.querySelector('.met-progress-fill');
|
|
const text = toast.el?.querySelector('.met-progress-text');
|
|
if (fill) {
|
|
fill.style.width = `${Math.min(100, Math.max(0, percent))}%`;
|
|
}
|
|
if (text) {
|
|
text.textContent = `${Math.round(percent)}%`;
|
|
}
|
|
},
|
|
complete(message) {
|
|
this.setProgress(100);
|
|
setTimeout(() => {
|
|
toast.update({
|
|
type: 'success',
|
|
message: message || t('success'),
|
|
html: '',
|
|
});
|
|
setTimeout(() => toast.close(), 1000);
|
|
}, 300);
|
|
},
|
|
error(message) {
|
|
toast.update({
|
|
type: 'error',
|
|
message: message || t('error'),
|
|
html: '',
|
|
});
|
|
setTimeout(() => toast.close(), 2000);
|
|
},
|
|
dismiss() {
|
|
toast.close();
|
|
},
|
|
};
|
|
},
|
|
countdown(message, seconds = 10, opts = {}) {
|
|
if (typeof message !== 'string') {
|
|
console.error('MeToast.countdown: message must be a string');
|
|
return { id: '', cancel: () => { }, pause: () => { }, resume: () => { } };
|
|
}
|
|
let remaining = Math.max(1, parseInt(String(seconds)) || 10);
|
|
let timer = null;
|
|
const toast = this._emit({
|
|
...opts,
|
|
type: opts.type || 'warning',
|
|
message: message.replace(/\{seconds\}/g, String(remaining)),
|
|
duration: 0,
|
|
closeButton: true,
|
|
showProgress: false,
|
|
});
|
|
const tick = () => {
|
|
remaining--;
|
|
if (remaining <= 0) {
|
|
if (timer !== null)
|
|
clearInterval(timer);
|
|
toast.close();
|
|
if (typeof opts.onComplete === 'function') {
|
|
try {
|
|
opts.onComplete();
|
|
}
|
|
catch (e) {
|
|
console.error('countdown onComplete error:', e);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
toast.update({
|
|
message: message.replace(/\{seconds\}/g, String(remaining)),
|
|
});
|
|
};
|
|
timer = setInterval(tick, 1000);
|
|
return {
|
|
id: toast.id,
|
|
cancel() { if (timer !== null)
|
|
clearInterval(timer); toast.close(); },
|
|
pause() { if (timer !== null)
|
|
clearInterval(timer); },
|
|
resume() { if (timer !== null)
|
|
clearInterval(timer); timer = setInterval(tick, 1000); },
|
|
};
|
|
},
|
|
action(messageOrOpts, actions = [], opts = {}) {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
|
normalized.duration = opts.duration ?? 0;
|
|
normalized.closeButton = opts.closeButton ?? true;
|
|
const toast = this._emit({
|
|
...normalized,
|
|
html: (normalized.html || '') + actionHTML(actions),
|
|
});
|
|
if (Array.isArray(actions)) {
|
|
setTimeout(() => {
|
|
actions.forEach((a, i) => {
|
|
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
|
if (btn && typeof a.onClick === 'function') {
|
|
btn.addEventListener('click', () => {
|
|
try {
|
|
a.onClick(toast);
|
|
}
|
|
catch (e) {
|
|
console.error('Action onClick error:', e);
|
|
}
|
|
if (a.close !== false)
|
|
toast.close();
|
|
});
|
|
}
|
|
});
|
|
}, 0);
|
|
}
|
|
return { id: toast.id, toast, dismiss: () => toast.close() };
|
|
},
|
|
queue(messages, opts = {}) {
|
|
if (!Array.isArray(messages)) {
|
|
console.error('MeToast.queue: messages must be an array');
|
|
const p = Promise.resolve();
|
|
return { then: (fn) => p.then(fn), catch: (rj) => p.catch(rj), cancel: () => { } };
|
|
}
|
|
let cancelled = false;
|
|
const promise = new Promise((resolve) => {
|
|
let index = 0;
|
|
const delay = opts.delay || 1000;
|
|
const userOnClose = opts.onClose;
|
|
const showNext = () => {
|
|
if (cancelled || index >= messages.length) {
|
|
resolve();
|
|
return;
|
|
}
|
|
const message = messages[index];
|
|
index++;
|
|
const msg = typeof message === 'string' ? message : (message?.message || '');
|
|
const msgObj = (typeof message === 'object' && message !== null) ? message : {};
|
|
const msgOnClose = msgObj.onClose;
|
|
this._emit({
|
|
...opts,
|
|
...msgObj,
|
|
message: msg,
|
|
duration: msgObj.duration || opts.duration || 3000,
|
|
onClose: (toast) => {
|
|
if (typeof msgOnClose === 'function') {
|
|
try {
|
|
msgOnClose(toast);
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
}
|
|
if (typeof userOnClose === 'function') {
|
|
try {
|
|
userOnClose(toast);
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
}
|
|
setTimeout(showNext, delay);
|
|
},
|
|
});
|
|
};
|
|
showNext();
|
|
});
|
|
return {
|
|
then: (fn, rj) => promise.then(fn, rj),
|
|
catch: (rj) => promise.catch(rj),
|
|
cancel: () => { cancelled = true; },
|
|
};
|
|
},
|
|
stack(messages, opts = {}) {
|
|
if (!Array.isArray(messages)) {
|
|
console.error('MeToast.stack: messages must be an array');
|
|
return;
|
|
}
|
|
messages.forEach((message, index) => {
|
|
setTimeout(() => {
|
|
const msg = typeof message === 'string' ? message : (message?.message || '');
|
|
this._emit({
|
|
...opts,
|
|
...((typeof message === 'object' && message !== null) ? message : {}),
|
|
message: msg,
|
|
});
|
|
}, index * (opts.stagger || 100));
|
|
});
|
|
},
|
|
dismiss(id) {
|
|
if (id) {
|
|
const t = this._toasts.get(id);
|
|
if (t)
|
|
t.close();
|
|
return;
|
|
}
|
|
this._toasts.forEach(t => t.close());
|
|
},
|
|
/**
|
|
* 立即移除指定 Toast(不触发离场动画)
|
|
*/
|
|
removeToast(id) {
|
|
if (!id)
|
|
return;
|
|
const t = this._toasts.get(id);
|
|
if (t)
|
|
t.remove();
|
|
},
|
|
clear(position) {
|
|
this._toasts.forEach(t => {
|
|
if (!position || t.config.position === position)
|
|
t.close();
|
|
});
|
|
},
|
|
getAll() {
|
|
return new Map(this._toasts);
|
|
},
|
|
count() {
|
|
return this._toasts.size;
|
|
},
|
|
group(name) {
|
|
const self = this;
|
|
const methods = ['show', 'success', 'error', 'warning', 'info', 'loading', 'action'];
|
|
const g = { _group: name };
|
|
methods.forEach(m => {
|
|
g[m] = (...args) => {
|
|
const lastArg = args[args.length - 1];
|
|
const isObj = typeof lastArg === 'object' && lastArg !== null && !Array.isArray(lastArg);
|
|
const selfMethods = self;
|
|
if (isObj && args.length === 1) {
|
|
return selfMethods[m]({ ...lastArg, group: name });
|
|
}
|
|
if (isObj) {
|
|
args.pop();
|
|
return selfMethods[m](...args, { ...lastArg, group: name });
|
|
}
|
|
return selfMethods[m](...args, { group: name });
|
|
};
|
|
});
|
|
g.dismiss = () => self.dismissGroup(name);
|
|
g.count = () => self._groupCount(name);
|
|
return g;
|
|
},
|
|
dismissGroup(name) {
|
|
this._toasts.forEach(t => { if (t.group === name)
|
|
t.close(); });
|
|
},
|
|
_groupCount(name) {
|
|
let c = 0;
|
|
this._toasts.forEach(t => { if (t.group === name)
|
|
c++; });
|
|
return c;
|
|
},
|
|
getToasts() {
|
|
return Array.from(this._toasts.values());
|
|
},
|
|
hasToasts() {
|
|
return this._toasts.size > 0;
|
|
},
|
|
getToast(id) {
|
|
if (!id)
|
|
return null;
|
|
return this._toasts.get(id) || null;
|
|
},
|
|
closeAll() {
|
|
this._toasts.forEach(t => t.close());
|
|
},
|
|
clearAll() {
|
|
this._toasts.forEach(t => t.close());
|
|
},
|
|
pauseAll() {
|
|
this._toasts.forEach(t => t._pause());
|
|
},
|
|
resumeAll() {
|
|
this._toasts.forEach(t => t._resume());
|
|
},
|
|
updateAll(partial) {
|
|
if (partial && typeof partial === 'object') {
|
|
this._toasts.forEach(t => t.update(partial));
|
|
}
|
|
},
|
|
findToasts(predicate) {
|
|
if (typeof predicate !== 'function')
|
|
return [];
|
|
return Array.from(this._toasts.values()).filter(predicate);
|
|
},
|
|
findByType(type) {
|
|
return this.findToasts(t => t.type === type);
|
|
},
|
|
findByPosition(position) {
|
|
return this.findToasts(t => t.config.position === position);
|
|
},
|
|
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
|
init(options = {}) {
|
|
Toast.trigger('beforeInit');
|
|
this._destroyed = false;
|
|
if (options.config) {
|
|
this.configure(options.config);
|
|
}
|
|
if (options.theme && this.themes) {
|
|
this.themes.switchTheme(options.theme);
|
|
}
|
|
if (options.locale && this.i18n) {
|
|
this.i18n.switchLocale(options.locale);
|
|
}
|
|
if (options.plugins && Array.isArray(options.plugins)) {
|
|
options.plugins.forEach((p) => {
|
|
this.use(p);
|
|
});
|
|
}
|
|
Toast.trigger('afterInit');
|
|
return this;
|
|
},
|
|
getStatus() {
|
|
return {
|
|
version: VERSION,
|
|
toasts: this._toasts.size,
|
|
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
|
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
|
plugins: this.plugins?.getNames?.() || [],
|
|
animations: this.animations?.getActiveCount?.() || 0,
|
|
};
|
|
},
|
|
getConfig() {
|
|
return { ...this._config };
|
|
},
|
|
updateConfig(config) {
|
|
if (config && typeof config === 'object') {
|
|
Object.assign(this._config, config);
|
|
Toast.trigger('configChange');
|
|
}
|
|
return this;
|
|
},
|
|
resetConfig() {
|
|
const keys = Object.keys(this._config);
|
|
keys.forEach(k => delete this._config[k]);
|
|
Object.assign(this._config, DEFAULTS);
|
|
Toast.trigger('configChange');
|
|
return this;
|
|
},
|
|
use(plugin, options = {}) {
|
|
if (typeof plugin === 'string') {
|
|
const preset = presetPlugins[plugin];
|
|
if (!preset) {
|
|
console.warn(`Preset plugin "${plugin}" not found`);
|
|
return this;
|
|
}
|
|
this.plugins.register(plugin, { ...preset, ...options });
|
|
// 连接插件钩子
|
|
if (plugin === 'accessibility') {
|
|
Toast.on('afterShow', (toast) => {
|
|
const acc = preset;
|
|
if (typeof acc.announce === 'function')
|
|
acc.announce(toast);
|
|
});
|
|
}
|
|
if (plugin === 'persistence') {
|
|
const saved = typeof preset.install === 'function'
|
|
? preset.install(this.plugins)
|
|
: null;
|
|
if (saved)
|
|
this.configure(saved);
|
|
Toast.on('afterClose', () => {
|
|
const p = preset;
|
|
if (typeof p.save === 'function')
|
|
p.save(this.getConfig());
|
|
});
|
|
}
|
|
}
|
|
else if (plugin && typeof plugin === 'object') {
|
|
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options });
|
|
}
|
|
return this;
|
|
},
|
|
destroy() {
|
|
if (this._destroyed)
|
|
return;
|
|
this._destroyed = true;
|
|
Toast.trigger('beforeDestroy');
|
|
this.dismiss();
|
|
// 清理子模块
|
|
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
|
this.plugins.destroy();
|
|
}
|
|
if (this.themes) {
|
|
if (typeof this.themes.clearThemeListeners === 'function') {
|
|
this.themes.clearThemeListeners();
|
|
}
|
|
if (typeof this.themes.unwatchSystemTheme === 'function') {
|
|
this.themes.unwatchSystemTheme();
|
|
}
|
|
}
|
|
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
|
this.i18n.clearLocaleListeners();
|
|
}
|
|
if (this.animations && typeof this.animations.destroy === 'function') {
|
|
this.animations.destroy();
|
|
}
|
|
// 清理 DOM 与静态资源
|
|
if (typeof document !== 'undefined') {
|
|
const containers = document.querySelectorAll('.met-container');
|
|
containers.forEach((c) => { if (c.parentNode)
|
|
c.parentNode.removeChild(c); });
|
|
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
|
|
const el = document.getElementById(id);
|
|
if (el && el.parentNode)
|
|
el.parentNode.removeChild(el);
|
|
});
|
|
}
|
|
this._toasts.clear();
|
|
_containerCache.clear();
|
|
Toast._registry.clear();
|
|
// afterDestroy 必须在清理 hooks 之前触发
|
|
Toast.trigger('afterDestroy');
|
|
Toast._hooks.clear();
|
|
},
|
|
// 子模块引用(由 index.ts 注入实际实现)
|
|
animations: null,
|
|
themes: null,
|
|
i18n: null,
|
|
plugins: null,
|
|
presetPlugins: {},
|
|
};
|
|
// 连接 Toast 静态回调到 meToast 实例
|
|
Toast.setCallbacks({
|
|
onError: (info) => {
|
|
const onError = meToast._config.onError;
|
|
if (typeof onError === 'function') {
|
|
try {
|
|
onError(info);
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
}
|
|
},
|
|
removeToast: (id) => {
|
|
meToast._toasts.delete(id);
|
|
},
|
|
});
|
|
|
|
/**
|
|
* MetonaToast — 轻量级Toast通知库
|
|
* @module metona-toast
|
|
* @version 0.3.0
|
|
* @author thzxx
|
|
* @license MIT
|
|
*/
|
|
/**
|
|
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
|
*/
|
|
const enhancedMeToast = {
|
|
...meToast,
|
|
version: VERSION,
|
|
animations: animationUtils,
|
|
themes: themeUtils,
|
|
i18n: i18nUtils,
|
|
plugins: pluginUtils,
|
|
presetPlugins,
|
|
};
|
|
// 初始化主题和国际化
|
|
if (typeof themeUtils.initTheme === 'function') {
|
|
themeUtils.initTheme();
|
|
}
|
|
if (typeof i18nUtils.initI18n === 'function') {
|
|
i18nUtils.initI18n();
|
|
}
|
|
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
|
|
if (typeof themeUtils.addThemeListener === 'function') {
|
|
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
|
|
}
|
|
if (typeof i18nUtils.addLocaleListener === 'function') {
|
|
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
|
|
}
|
|
// 浏览器环境全局注册
|
|
if (typeof window !== 'undefined') {
|
|
window.MeToast = enhancedMeToast;
|
|
window.Met = enhancedMeToast;
|
|
// Notification API — 页面不可见时自动发送系统通知
|
|
const sendNotification = (toast) => {
|
|
try {
|
|
new Notification(toast.title || toast.type, { body: toast.message });
|
|
}
|
|
catch (_e) { /* noop */ }
|
|
};
|
|
enhancedMeToast._notify = (toast) => {
|
|
if (typeof Notification === 'undefined')
|
|
return;
|
|
if (Notification.permission === 'denied')
|
|
return;
|
|
if (!document.hidden)
|
|
return;
|
|
if (Notification.permission === 'default') {
|
|
Notification.requestPermission().then(p => { if (p === 'granted')
|
|
sendNotification(toast); });
|
|
return;
|
|
}
|
|
sendNotification(toast);
|
|
};
|
|
Toast.on('afterShow', (toast) => {
|
|
const config = toast.config;
|
|
if (config.notifyWhenHidden) {
|
|
(enhancedMeToast._notify)(toast);
|
|
}
|
|
});
|
|
}
|
|
|
|
exports.MeToast = enhancedMeToast;
|
|
exports.Met = enhancedMeToast;
|
|
exports.Toast = Toast;
|
|
exports.VERSION = VERSION;
|
|
exports.default = enhancedMeToast;
|
|
exports.meToast = enhancedMeToast;
|
|
//# sourceMappingURL=metona-toast.cjs.js.map
|