6713 lines
183 KiB
JavaScript
6713 lines
183 KiB
JavaScript
'use strict';
|
||
|
||
Object.defineProperty(exports, '__esModule', { value: true });
|
||
|
||
/**
|
||
* MetonaToast Utils - 工具函数
|
||
* @module utils
|
||
* @version 2.0.0
|
||
* @description 通用工具函数集合
|
||
*/
|
||
|
||
/**
|
||
* 生成唯一ID
|
||
* @returns {string} 唯一ID
|
||
*/
|
||
const generateId = () => {
|
||
return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
||
};
|
||
|
||
/**
|
||
* HTML转义
|
||
* @param {string} s - 输入字符串
|
||
* @returns {string} 转义后的字符串
|
||
*/
|
||
const escapeHTML = (s) => {
|
||
if (typeof document === 'undefined') {
|
||
// Node.js环境
|
||
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;
|
||
};
|
||
|
||
/**
|
||
* 检测暗色模式偏好
|
||
* @returns {boolean} 是否偏好暗色模式
|
||
*/
|
||
const prefersDark = () => {
|
||
return typeof window !== 'undefined' &&
|
||
window.matchMedia &&
|
||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
};
|
||
|
||
/**
|
||
* MetonaToast Icons — 图标SVG定义
|
||
* @module icons
|
||
* @version 2.0.0
|
||
* @description 80+ 内置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 2.0.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: '已断开',
|
||
loading: '加载中',
|
||
loaded: '已加载',
|
||
saving: '保存中',
|
||
saved: '已保存',
|
||
deleting: '删除中',
|
||
deleted: '已删除',
|
||
uploading: '上传中',
|
||
uploaded: '已上传',
|
||
downloading: '下载中',
|
||
downloaded: '已下载',
|
||
processing: '处理中',
|
||
processed: '已处理',
|
||
syncing: '同步中',
|
||
synced: '已同步',
|
||
sending: '发送中',
|
||
sent: '已发送',
|
||
receiving: '接收中',
|
||
received: '已接收',
|
||
connecting: '连接中',
|
||
error: '错误',
|
||
warning: '警告',
|
||
info: '信息',
|
||
success: '成功',
|
||
question: '问题',
|
||
help: '帮助',
|
||
feedback: '反馈',
|
||
report: '报告',
|
||
bug: '错误',
|
||
feature: '功能',
|
||
improvement: '改进',
|
||
suggestion: '建议',
|
||
request: '请求',
|
||
response: '响应',
|
||
status: '状态',
|
||
progress: '进度',
|
||
complete: '完成',
|
||
incomplete: '未完成',
|
||
pending: '待处理',
|
||
processing: '处理中',
|
||
failed: '失败',
|
||
cancelled: '已取消',
|
||
timeout: '超时',
|
||
expired: '已过期',
|
||
invalid: '无效',
|
||
valid: '有效',
|
||
required: '必填',
|
||
optional: '可选',
|
||
enabled: '启用',
|
||
disabled: '禁用',
|
||
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: '工单',
|
||
request: '请求',
|
||
response: '响应',
|
||
message: '消息',
|
||
notification: '通知',
|
||
alert: '警报',
|
||
reminder: '提醒',
|
||
event: '事件',
|
||
activity: '活动',
|
||
log: '日志',
|
||
report: '报告',
|
||
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: '谁',
|
||
myself: '我自己',
|
||
yourself: '你自己',
|
||
himself: '他自己',
|
||
herself: '她自己',
|
||
itself: '它自己',
|
||
ourselves: '我们自己',
|
||
yourselves: '你们自己',
|
||
themselves: '他们自己',
|
||
mine: '我的',
|
||
yours: '你的',
|
||
his: '他的',
|
||
hers: '她的',
|
||
its: '它的',
|
||
ours: '我们的',
|
||
yours: '你们的',
|
||
theirs: '他们的',
|
||
me: '我',
|
||
you: '你',
|
||
him: '他',
|
||
her: '她',
|
||
it: '它',
|
||
us: '我们',
|
||
them: '他们',
|
||
my: '我的',
|
||
your: '你的',
|
||
his: '他的',
|
||
her: '她的',
|
||
its: '它的',
|
||
our: '我们的',
|
||
your: '你们的',
|
||
their: '他们的',
|
||
i: '我',
|
||
you: '你',
|
||
he: '他',
|
||
she: '她',
|
||
it: '它',
|
||
we: '我们',
|
||
they: '他们',
|
||
am: '是',
|
||
is: '是',
|
||
are: '是',
|
||
was: '是',
|
||
were: '是',
|
||
be: '是',
|
||
been: '是',
|
||
being: '是',
|
||
have: '有',
|
||
has: '有',
|
||
had: '有',
|
||
having: '有',
|
||
do: '做',
|
||
does: '做',
|
||
did: '做',
|
||
doing: '做',
|
||
will: '会',
|
||
would: '会',
|
||
shall: '将',
|
||
should: '应该',
|
||
may: '可以',
|
||
might: '可能',
|
||
can: '能',
|
||
could: '能',
|
||
must: '必须',
|
||
need: '需要',
|
||
dare: '敢',
|
||
ought: '应该',
|
||
used: '过去常常',
|
||
to: '到',
|
||
of: '的',
|
||
in: '在',
|
||
for: '为了',
|
||
on: '在',
|
||
with: '和',
|
||
at: '在',
|
||
by: '通过',
|
||
from: '从',
|
||
into: '进入',
|
||
during: '在...期间',
|
||
before: '在...之前',
|
||
after: '在...之后',
|
||
above: '在...上面',
|
||
below: '在...下面',
|
||
between: '在...之间',
|
||
under: '在...下面',
|
||
over: '在...上面',
|
||
across: '穿过',
|
||
through: '通过',
|
||
into: '进入',
|
||
towards: '朝向',
|
||
upon: '在...上面',
|
||
about: '关于',
|
||
against: '反对',
|
||
among: '在...之中',
|
||
along: '沿着',
|
||
around: '周围',
|
||
beyond: '超出',
|
||
but: '但是',
|
||
despite: '尽管',
|
||
except: '除了',
|
||
inside: '里面',
|
||
outside: '外面',
|
||
since: '自从',
|
||
until: '直到',
|
||
unless: '除非',
|
||
whether: '是否',
|
||
while: '当...时候',
|
||
although: '虽然',
|
||
because: '因为',
|
||
if: '如果',
|
||
once: '一旦',
|
||
since: '自从',
|
||
so: '所以',
|
||
that: '那个',
|
||
though: '虽然',
|
||
till: '直到',
|
||
unless: '除非',
|
||
until: '直到',
|
||
when: '当...时候',
|
||
whenever: '每当',
|
||
where: '哪里',
|
||
wherever: '无论哪里',
|
||
whereas: '然而',
|
||
wherever: '无论哪里',
|
||
while: '当...时候',
|
||
why: '为什么',
|
||
},
|
||
|
||
'en-US': {
|
||
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',
|
||
enabled: 'Enabled',
|
||
disabled: 'Disabled',
|
||
active: 'Active',
|
||
inactive: 'Inactive',
|
||
online: 'Online',
|
||
offline: 'Offline',
|
||
connected: 'Connected',
|
||
disconnected: 'Disconnected',
|
||
loading: 'Loading',
|
||
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',
|
||
error: 'Error',
|
||
warning: 'Warning',
|
||
info: 'Info',
|
||
success: 'Success',
|
||
question: 'Question',
|
||
help: 'Help',
|
||
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',
|
||
processing: 'Processing',
|
||
failed: 'Failed',
|
||
cancelled: 'Cancelled',
|
||
timeout: 'Timeout',
|
||
expired: 'Expired',
|
||
invalid: 'Invalid',
|
||
valid: 'Valid',
|
||
required: 'Required',
|
||
optional: 'Optional',
|
||
enabled: 'Enabled',
|
||
disabled: 'Disabled',
|
||
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',
|
||
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',
|
||
group: 'Group',
|
||
team: 'Team',
|
||
organization: 'Organization',
|
||
company: 'Company',
|
||
department: 'Department',
|
||
project: 'Project',
|
||
task: 'Task',
|
||
issue: 'Issue',
|
||
ticket: 'Ticket',
|
||
request: 'Request',
|
||
response: 'Response',
|
||
message: 'Message',
|
||
notification: 'Notification',
|
||
alert: 'Alert',
|
||
reminder: 'Reminder',
|
||
event: 'Event',
|
||
activity: 'Activity',
|
||
log: 'Log',
|
||
report: 'Report',
|
||
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',
|
||
myself: 'Myself',
|
||
yourself: 'Yourself',
|
||
himself: 'Himself',
|
||
herself: 'Herself',
|
||
itself: 'Itself',
|
||
ourselves: 'Ourselves',
|
||
yourselves: 'Yourselves',
|
||
themselves: 'Themselves',
|
||
mine: 'Mine',
|
||
yours: 'Yours',
|
||
his: 'His',
|
||
hers: 'Hers',
|
||
its: 'Its',
|
||
ours: 'Ours',
|
||
yours: 'Yours',
|
||
theirs: 'Theirs',
|
||
me: 'Me',
|
||
you: 'You',
|
||
him: 'Him',
|
||
her: 'Her',
|
||
it: 'It',
|
||
us: 'Us',
|
||
them: 'Them',
|
||
my: 'My',
|
||
your: 'Your',
|
||
his: 'His',
|
||
her: 'Her',
|
||
its: 'Its',
|
||
our: 'Our',
|
||
your: 'Your',
|
||
their: 'Their',
|
||
i: 'I',
|
||
you: 'You',
|
||
he: 'He',
|
||
she: 'She',
|
||
it: 'It',
|
||
we: 'We',
|
||
they: 'They',
|
||
am: 'Am',
|
||
is: 'Is',
|
||
are: 'Are',
|
||
was: 'Was',
|
||
were: 'Were',
|
||
be: 'Be',
|
||
been: 'Been',
|
||
being: 'Being',
|
||
have: 'Have',
|
||
has: 'Has',
|
||
had: 'Had',
|
||
having: 'Having',
|
||
do: 'Do',
|
||
does: 'Does',
|
||
did: 'Did',
|
||
doing: 'Doing',
|
||
will: 'Will',
|
||
would: 'Would',
|
||
shall: 'Shall',
|
||
should: 'Should',
|
||
may: 'May',
|
||
might: 'Might',
|
||
can: 'Can',
|
||
could: 'Could',
|
||
must: 'Must',
|
||
need: 'Need',
|
||
dare: 'Dare',
|
||
ought: 'Ought',
|
||
used: 'Used',
|
||
to: 'To',
|
||
of: 'Of',
|
||
in: 'In',
|
||
for: 'For',
|
||
on: 'On',
|
||
with: 'With',
|
||
at: 'At',
|
||
by: 'By',
|
||
from: 'From',
|
||
into: 'Into',
|
||
during: 'During',
|
||
before: 'Before',
|
||
after: 'After',
|
||
above: 'Above',
|
||
below: 'Below',
|
||
between: 'Between',
|
||
under: 'Under',
|
||
over: 'Over',
|
||
across: 'Across',
|
||
through: 'Through',
|
||
into: 'Into',
|
||
towards: 'Towards',
|
||
upon: 'Upon',
|
||
about: 'About',
|
||
against: 'Against',
|
||
among: 'Among',
|
||
along: 'Along',
|
||
around: 'Around',
|
||
beyond: 'Beyond',
|
||
but: 'But',
|
||
despite: 'Despite',
|
||
except: 'Except',
|
||
inside: 'Inside',
|
||
outside: 'Outside',
|
||
since: 'Since',
|
||
until: 'Until',
|
||
unless: 'Unless',
|
||
whether: 'Whether',
|
||
while: 'While',
|
||
although: 'Although',
|
||
because: 'Because',
|
||
if: 'If',
|
||
once: 'Once',
|
||
since: 'Since',
|
||
so: 'So',
|
||
that: 'That',
|
||
though: 'Though',
|
||
till: 'Till',
|
||
unless: 'Unless',
|
||
until: 'Until',
|
||
when: 'When',
|
||
whenever: 'Whenever',
|
||
where: 'Where',
|
||
wherever: 'Wherever',
|
||
whereas: 'Whereas',
|
||
wherever: 'Wherever',
|
||
while: 'While',
|
||
why: 'Why',
|
||
},
|
||
};
|
||
|
||
/**
|
||
* MetonaToast Constants - 常量定义
|
||
* @module constants
|
||
* @version 2.0.0
|
||
* @description 默认配置、颜色、动画、主题等常量
|
||
*/
|
||
|
||
/**
|
||
* 默认配置
|
||
*/
|
||
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,
|
||
onClose: null,
|
||
onClick: null,
|
||
onUpdate: null,
|
||
|
||
// 高级
|
||
resetTimerOnUpdate: false,
|
||
notifyWhenHidden: false,
|
||
|
||
// 国际化
|
||
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: 'auto',
|
||
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)',
|
||
},
|
||
};
|
||
|
||
/**
|
||
* MetonaToast Styles - 样式管理
|
||
* @module styles
|
||
* @version 2.0.0
|
||
* @description 样式注入、更新和主题管理
|
||
*/
|
||
|
||
let styleElement = null;
|
||
|
||
/**
|
||
* 生成CSS样式
|
||
* @param {Object} theme - 主题配置
|
||
* @returns {string} CSS字符串
|
||
*/
|
||
const generateCSS = (theme) => {
|
||
return `
|
||
/* MetonaToast 基础样式 */
|
||
.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;
|
||
}
|
||
|
||
/* 位置样式 — 顶部位置用 column-reverse,新 toast 出现在最上方 */
|
||
.met-container.top-left {
|
||
top: 0;
|
||
left: 0;
|
||
align-items: flex-start;
|
||
flex-direction: column-reverse;
|
||
}
|
||
|
||
.met-container.top-center {
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
align-items: center;
|
||
flex-direction: column-reverse;
|
||
}
|
||
|
||
.met-container.top-right {
|
||
top: 0;
|
||
right: 0;
|
||
align-items: flex-end;
|
||
flex-direction: column-reverse;
|
||
}
|
||
|
||
.met-container.bottom-left {
|
||
bottom: 0;
|
||
left: 0;
|
||
align-items: flex-start;
|
||
flex-direction: column-reverse;
|
||
}
|
||
|
||
.met-container.bottom-center {
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
align-items: center;
|
||
flex-direction: column-reverse;
|
||
}
|
||
|
||
.met-container.bottom-right {
|
||
bottom: 0;
|
||
right: 0;
|
||
align-items: flex-end;
|
||
flex-direction: column-reverse;
|
||
}
|
||
|
||
/* Toast基础样式 */
|
||
.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));
|
||
}
|
||
|
||
/* 悬停效果 */
|
||
.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 定义 === */
|
||
@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-show === */
|
||
.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 (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 (max-width: 480px) {
|
||
.met-container {
|
||
padding: 12px !important;
|
||
}
|
||
|
||
.met-toast {
|
||
width: 100% !important;
|
||
min-width: 0 !important;
|
||
}
|
||
}
|
||
|
||
/* 暗色主题特定样式 */
|
||
.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);
|
||
}
|
||
|
||
/* 动画性能优化 */
|
||
.met-toast {
|
||
backface-visibility: hidden;
|
||
-webkit-backface-visibility: hidden;
|
||
perspective: 1000;
|
||
}
|
||
|
||
/* 滚动条样式 */
|
||
.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);
|
||
}
|
||
|
||
/* 打印样式 */
|
||
@media print {
|
||
.met-container {
|
||
display: none !important;
|
||
}
|
||
.met-toast {
|
||
display: none !important;
|
||
}
|
||
}
|
||
|
||
/* 高对比度模式 */
|
||
@media (forced-colors: active) {
|
||
.met-toast {
|
||
border: 2px solid ButtonText;
|
||
}
|
||
|
||
.met-close {
|
||
border: 1px solid ButtonText;
|
||
}
|
||
}
|
||
|
||
/* 焦点样式 */
|
||
.met-toast:focus-visible {
|
||
outline: 2px solid #3b82f6;
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.met-close:focus-visible {
|
||
outline: 2px solid #3b82f6;
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* 禁用状态 */
|
||
.met-toast:disabled,
|
||
.met-toast[disabled] {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.met-toast.met-loading {
|
||
cursor: wait;
|
||
}
|
||
|
||
/* 成功状态 */
|
||
.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 {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
/* Toast进入动画 */
|
||
.met-toast.met-enter {
|
||
animation: met-enter 0.3s ease forwards;
|
||
}
|
||
|
||
/* Toast离开动画 */
|
||
.met-toast.met-leave {
|
||
animation: met-leave 0.3s ease forwards;
|
||
}
|
||
|
||
@keyframes met-enter {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
@keyframes met-leave {
|
||
from {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
to {
|
||
opacity: 0;
|
||
transform: translateY(-20px);
|
||
}
|
||
}
|
||
|
||
/* 响应式字体大小 */
|
||
@media (max-width: 768px) {
|
||
.met-toast {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.met-title {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.met-message {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.met-toast {
|
||
font-size: 12px;
|
||
padding: 12px 14px;
|
||
}
|
||
|
||
.met-title {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.met-message {
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
|
||
/* 无障碍支持 */
|
||
.met-toast[role="alert"] {
|
||
/* 警告类型Toast的特殊样式 */
|
||
}
|
||
|
||
.met-toast[role="status"] {
|
||
/* 状态类型Toast的特殊样式 */
|
||
}
|
||
|
||
/* 高对比度模式增强 */
|
||
@media (prefers-contrast: high) {
|
||
.met-toast {
|
||
border-width: 2px;
|
||
}
|
||
|
||
.met-close {
|
||
border: 1px solid currentColor;
|
||
}
|
||
}
|
||
|
||
/* 暗色模式自动检测 */
|
||
@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);
|
||
}
|
||
}
|
||
|
||
/* 动画性能优化 */
|
||
.met-toast {
|
||
transform: translateZ(0);
|
||
-webkit-transform: translateZ(0);
|
||
}
|
||
|
||
/* 触摸设备优化 */
|
||
@media (hover: none) {
|
||
.met-toast:hover {
|
||
box-shadow: inherit;
|
||
}
|
||
}
|
||
|
||
/* 滚动时固定位置 */
|
||
.met-container {
|
||
position: fixed;
|
||
z-index: 9999;
|
||
}
|
||
|
||
/* 全屏模式 */
|
||
:fullscreen .met-container {
|
||
z-index: 2147483647;
|
||
}
|
||
|
||
/* 画中画模式 */
|
||
: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));
|
||
}
|
||
}
|
||
|
||
/* 暗色模式安全区域 */
|
||
@media (prefers-color-scheme: dark) {
|
||
@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));
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 高分辨率屏幕优化 */
|
||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||
.met-toast {
|
||
border-width: 0.5px;
|
||
}
|
||
}
|
||
|
||
/* 触摸设备优化 */
|
||
@media (hover: none) and (pointer: coarse) {
|
||
.met-toast {
|
||
min-height: 48px;
|
||
}
|
||
|
||
.met-close {
|
||
min-width: 48px;
|
||
min-height: 48px;
|
||
}
|
||
}
|
||
|
||
/* 键盘导航优化 */
|
||
.met-toast:focus {
|
||
outline: 2px solid #3b82f6;
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.met-toast:focus:not(:focus-visible) {
|
||
outline: none;
|
||
}
|
||
|
||
.met-toast:focus-visible {
|
||
outline: 2px solid #3b82f6;
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* 减少动画模式 */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.met-toast {
|
||
transition: opacity 0.15s ease !important;
|
||
animation: none !important;
|
||
}
|
||
|
||
.met-bar,
|
||
.met-bar-v {
|
||
transition: none !important;
|
||
}
|
||
|
||
.met-container {
|
||
transition: none !important;
|
||
}
|
||
}
|
||
|
||
/* 高对比度模式 */
|
||
@media (prefers-contrast: high) {
|
||
.met-toast {
|
||
border-width: 3px;
|
||
border-style: solid;
|
||
}
|
||
|
||
.met-close {
|
||
border: 2px solid currentColor;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.met-progress,
|
||
.met-progress-v {
|
||
height: 4px;
|
||
}
|
||
|
||
.met-bar,
|
||
.met-bar-v {
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
/* 低分辨率屏幕优化 */
|
||
@media (max-resolution: 1dppx) {
|
||
.met-toast {
|
||
border-width: 1px;
|
||
}
|
||
}
|
||
|
||
/* 触摸设备优化 */
|
||
@media (hover: none) and (pointer: coarse) {
|
||
.met-toast {
|
||
min-height: 52px;
|
||
padding: 16px 18px;
|
||
}
|
||
|
||
.met-close {
|
||
min-width: 52px;
|
||
min-height: 52px;
|
||
padding: 8px;
|
||
}
|
||
|
||
.met-icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
}
|
||
}
|
||
|
||
/* 大屏幕优化 */
|
||
@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;
|
||
}
|
||
}
|
||
|
||
/* 无障碍支持 */
|
||
.met-toast[aria-live="assertive"] {
|
||
/* 紧急通知样式 */
|
||
}
|
||
|
||
.met-toast[aria-live="polite"] {
|
||
/* 普通通知样式 */
|
||
}
|
||
|
||
/* 焦点陷阱 */
|
||
.met-toast:focus-within {
|
||
/* 包含焦点元素的Toast样式 */
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.met-toast[aria-busy="true"] {
|
||
cursor: wait;
|
||
}
|
||
|
||
/* 禁用状态 */
|
||
.met-toast[aria-disabled="true"] {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 隐藏状态 */
|
||
.met-toast[aria-hidden="true"] {
|
||
display: none;
|
||
}
|
||
|
||
/* 展开状态 */
|
||
.met-toast[aria-expanded="true"] {
|
||
/* 展开状态样式 */
|
||
}
|
||
|
||
/* 选中状态 */
|
||
.met-toast[aria-selected="true"] {
|
||
/* 选中状态样式 */
|
||
}
|
||
|
||
/* 按下状态 */
|
||
.met-toast:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
/* 加载状态动画 */
|
||
.met-toast.met-loading::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: linear-gradient(
|
||
90deg,
|
||
transparent,
|
||
rgba(255, 255, 255, 0.1),
|
||
transparent
|
||
);
|
||
animation: met-loading 1.5s infinite;
|
||
}
|
||
|
||
@keyframes met-loading {
|
||
0% {
|
||
transform: translateX(-100%);
|
||
}
|
||
100% {
|
||
transform: translateX(100%);
|
||
}
|
||
}
|
||
|
||
/* 成功状态动画 */
|
||
.met-toast.met-success::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 4px;
|
||
height: 100%;
|
||
background: #10b981;
|
||
border-radius: 12px 0 0 12px;
|
||
animation: met-success 0.3s ease;
|
||
}
|
||
|
||
@keyframes met-success {
|
||
from {
|
||
transform: scaleY(0);
|
||
}
|
||
to {
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
|
||
/* 错误状态动画 */
|
||
.met-toast.met-error::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 4px;
|
||
height: 100%;
|
||
background: #ef4444;
|
||
border-radius: 12px 0 0 12px;
|
||
animation: met-error 0.3s ease;
|
||
}
|
||
|
||
@keyframes met-error {
|
||
from {
|
||
transform: scaleY(0);
|
||
}
|
||
to {
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
|
||
/* 警告状态动画 */
|
||
.met-toast.met-warning::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 4px;
|
||
height: 100%;
|
||
background: #f59e0b;
|
||
border-radius: 12px 0 0 12px;
|
||
animation: met-warning 0.3s ease;
|
||
}
|
||
|
||
@keyframes met-warning {
|
||
from {
|
||
transform: scaleY(0);
|
||
}
|
||
to {
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
|
||
/* 信息状态动画 */
|
||
.met-toast.met-info::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 4px;
|
||
height: 100%;
|
||
background: #3b82f6;
|
||
border-radius: 12px 0 0 12px;
|
||
animation: met-info 0.3s ease;
|
||
}
|
||
|
||
@keyframes met-info {
|
||
from {
|
||
transform: scaleY(0);
|
||
}
|
||
to {
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
|
||
/* 加载状态动画 */
|
||
.met-toast.met-loading::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 4px;
|
||
height: 100%;
|
||
background: #6366f1;
|
||
border-radius: 12px 0 0 12px;
|
||
animation: met-loading-bar 1s infinite;
|
||
}
|
||
|
||
@keyframes met-loading-bar {
|
||
0% {
|
||
transform: scaleY(0);
|
||
transform-origin: top;
|
||
}
|
||
50% {
|
||
transform: scaleY(1);
|
||
transform-origin: top;
|
||
}
|
||
51% {
|
||
transform-origin: bottom;
|
||
}
|
||
100% {
|
||
transform: scaleY(0);
|
||
transform-origin: bottom;
|
||
}
|
||
}
|
||
|
||
/* 进度条脉冲动画 */
|
||
.met-bar,
|
||
.met-bar-v {
|
||
animation: met-pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes met-pulse {
|
||
0%, 100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
/* Toast堆叠效果 */
|
||
.met-toast + .met-toast {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
/* Toast分组 */
|
||
.met-toast-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* Toast头部 */
|
||
.met-toast-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* Toast底部 */
|
||
.met-toast-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
/* Toast操作按钮 */
|
||
.met-toast-action {
|
||
background: transparent;
|
||
border: 1px solid currentColor;
|
||
padding: 6px 12px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
opacity: 0.7;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.met-toast-action:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Toast进度文本 */
|
||
.met-toast-progress-text {
|
||
font-size: 12px;
|
||
opacity: 0.7;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
/* Toast时间戳 */
|
||
.met-toast-timestamp {
|
||
font-size: 11px;
|
||
opacity: 0.5;
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* Toast头像 */
|
||
.met-toast-avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Toast图片 */
|
||
.met-toast-image {
|
||
max-width: 100%;
|
||
max-height: 200px;
|
||
border-radius: 8px;
|
||
margin-top: 8px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
/* Toast视频 */
|
||
.met-toast-video {
|
||
max-width: 100%;
|
||
max-height: 200px;
|
||
border-radius: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
/* Toast音频 */
|
||
.met-toast-audio {
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
/* Toast代码块 */
|
||
.met-toast-code {
|
||
background: rgba(0, 0, 0, 0.05);
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||
font-size: 12px;
|
||
margin-top: 8px;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-code {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
/* Toast引用 */
|
||
.met-toast-quote {
|
||
border-left: 3px solid currentColor;
|
||
padding-left: 12px;
|
||
margin-top: 8px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* Toast列表 */
|
||
.met-toast-list {
|
||
margin: 8px 0 0 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.met-toast-list li {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* Toast表格 */
|
||
.met-toast-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.met-toast-table th,
|
||
.met-toast-table td {
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
padding: 6px 8px;
|
||
text-align: left;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-table th,
|
||
.met-theme-dark .met-toast-table td {
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.met-toast-table th {
|
||
background: rgba(0, 0, 0, 0.05);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-table th {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
/* Toast分割线 */
|
||
.met-toast-divider {
|
||
height: 1px;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
margin: 12px 0;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-divider {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
/* Toast标签 */
|
||
.met-toast-tag {
|
||
display: inline-block;
|
||
background: rgba(0, 0, 0, 0.05);
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
margin-right: 4px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-tag {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
/* Toast徽章 */
|
||
.met-toast-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #3b82f6;
|
||
color: white;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
min-width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
/* Toast进度环 */
|
||
.met-toast-progress-ring {
|
||
width: 40px;
|
||
height: 40px;
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.met-toast-progress-ring circle {
|
||
fill: none;
|
||
stroke-width: 3;
|
||
stroke-linecap: round;
|
||
}
|
||
|
||
.met-toast-progress-ring .met-progress-ring-bg {
|
||
stroke: rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.met-theme-dark .met-toast-progress-ring .met-progress-ring-bg {
|
||
stroke: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.met-toast-progress-ring .met-progress-ring-fill {
|
||
stroke: #3b82f6;
|
||
stroke-dasharray: 100;
|
||
stroke-dashoffset: 100;
|
||
transition: stroke-dashoffset 0.3s ease;
|
||
}
|
||
|
||
/* Toast滑块 */
|
||
.met-toast-slider {
|
||
width: 100%;
|
||
height: 4px;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
border-radius: 2px;
|
||
margin-top: 8px;
|
||
position: relative;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-slider {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.met-toast-slider-fill {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
height: 100%;
|
||
background: #3b82f6;
|
||
border-radius: 2px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
/* Toast开关 */
|
||
.met-toast-switch {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 36px;
|
||
height: 20px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.met-toast-switch input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.met-toast-switch-slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
border-radius: 20px;
|
||
transition: 0.3s;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-switch-slider {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.met-toast-switch-slider:before {
|
||
content: '';
|
||
position: absolute;
|
||
height: 16px;
|
||
width: 16px;
|
||
left: 2px;
|
||
bottom: 2px;
|
||
background: white;
|
||
border-radius: 50%;
|
||
transition: 0.3s;
|
||
}
|
||
|
||
.met-toast-switch input:checked + .met-toast-switch-slider {
|
||
background: #3b82f6;
|
||
}
|
||
|
||
.met-toast-switch input:checked + .met-toast-switch-slider:before {
|
||
transform: translateX(16px);
|
||
}
|
||
|
||
/* Toast复选框 */
|
||
.met-toast-checkbox {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.met-toast-checkbox input {
|
||
width: 16px;
|
||
height: 16px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Toast单选框 */
|
||
.met-toast-radio {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.met-toast-radio input {
|
||
width: 16px;
|
||
height: 16px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Toast输入框 */
|
||
.met-toast-input {
|
||
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;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-input {
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.met-toast-input:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||
}
|
||
|
||
/* Toast文本域 */
|
||
.met-toast-textarea {
|
||
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;
|
||
resize: vertical;
|
||
min-height: 80px;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-textarea {
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.met-toast-textarea:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||
}
|
||
|
||
/* Toast选择框 */
|
||
.met-toast-select {
|
||
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;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-select {
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.met-toast-select:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||
}
|
||
|
||
/* Toast下拉菜单 */
|
||
.met-toast-dropdown {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.met-toast-dropdown-content {
|
||
display: none;
|
||
position: absolute;
|
||
background: ${theme.bg};
|
||
min-width: 160px;
|
||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||
border-radius: 8px;
|
||
z-index: 1;
|
||
padding: 8px 0;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.met-theme-dark .met-toast-dropdown-content {
|
||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.met-toast-dropdown:hover .met-toast-dropdown-content {
|
||
display: block;
|
||
}
|
||
|
||
.met-toast-dropdown-item {
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.met-toast-dropdown-item:hover {
|
||
background: rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.met-theme-dark .met-toast-dropdown-item:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
/* Toast工具提示 */
|
||
.met-toast-tooltip {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.met-toast-tooltip .met-toast-tooltip-text {
|
||
visibility: hidden;
|
||
width: 120px;
|
||
background: ${theme.bg};
|
||
color: ${theme.text};
|
||
text-align: center;
|
||
border-radius: 6px;
|
||
padding: 6px 8px;
|
||
position: absolute;
|
||
z-index: 1;
|
||
bottom: 125%;
|
||
left: 50%;
|
||
margin-left: -60px;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
font-size: 12px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.met-theme-dark .met-toast-tooltip .met-toast-tooltip-text {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.met-toast-tooltip:hover .met-toast-tooltip-text {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Toast弹出框 */
|
||
.met-toast-popover {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.met-toast-popover .met-toast-popover-content {
|
||
visibility: hidden;
|
||
background: ${theme.bg};
|
||
color: ${theme.text};
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
position: absolute;
|
||
z-index: 1;
|
||
bottom: 125%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
min-width: 200px;
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.met-theme-dark .met-toast-popover .met-toast-popover-content {
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
.met-toast-popover:hover .met-toast-popover-content {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Toast模态框 */
|
||
.met-toast-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10000;
|
||
}
|
||
|
||
.met-toast-modal-content {
|
||
background: ${theme.bg};
|
||
color: ${theme.text};
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
max-width: 500px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* Toast抽屉 */
|
||
.met-toast-drawer {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 10000;
|
||
}
|
||
|
||
.met-toast-drawer-content {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 300px;
|
||
height: 100%;
|
||
background: ${theme.bg};
|
||
color: ${theme.text};
|
||
padding: 24px;
|
||
transform: translateX(100%);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.met-toast-drawer.open .met-toast-drawer-content {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
/* Toast通知栏 */
|
||
.met-toast-notification-bar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: ${theme.bg};
|
||
color: ${theme.text};
|
||
padding: 12px 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
z-index: 9999;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* Toast状态栏 */
|
||
.met-toast-status-bar {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: ${theme.bg};
|
||
color: ${theme.text};
|
||
padding: 8px 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
z-index: 9999;
|
||
font-size: 12px;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
`;
|
||
};
|
||
|
||
/**
|
||
* 注入样式
|
||
* @param {Object} theme - 主题配置
|
||
*/
|
||
const injectStyles = (theme = THEMES.light) => {
|
||
// 检查是否在浏览器环境
|
||
if (typeof document === 'undefined') return;
|
||
|
||
// 检查是否已注入
|
||
if (styleElement && document.getElementById('metona-toast-styles')) {
|
||
return;
|
||
}
|
||
|
||
// 生成CSS
|
||
const css = generateCSS(theme);
|
||
|
||
// 创建样式元素
|
||
styleElement = document.createElement('style');
|
||
styleElement.id = 'metona-toast-styles';
|
||
styleElement.textContent = css.replace(/\s+/g, ' ');
|
||
|
||
// 添加到文档头
|
||
document.head.appendChild(styleElement);
|
||
};
|
||
|
||
/**
|
||
* MetonaToast Themes - 主题管理
|
||
* @module themes
|
||
* @version 2.0.0
|
||
* @description 主题系统、自定义主题和主题切换
|
||
*/
|
||
|
||
|
||
// 当前主题状态
|
||
let currentTheme = 'auto';
|
||
let themeListeners = new Set();
|
||
let systemThemeMediaQuery = null;
|
||
let systemThemeListener = null;
|
||
|
||
/**
|
||
* 获取系统主题
|
||
* @returns {string} 系统主题 ('light' 或 'dark')
|
||
*/
|
||
const getSystemTheme = () => {
|
||
if (typeof window === 'undefined') return 'light';
|
||
return prefersDark() ? 'dark' : 'light';
|
||
};
|
||
|
||
/**
|
||
* 获取主题(别名)
|
||
* @param {string} theme - 主题名称
|
||
* @returns {string} 解析后的主题
|
||
*/
|
||
const getTheme = (theme) => {
|
||
return resolveTheme(theme);
|
||
};
|
||
|
||
/**
|
||
* 解析主题
|
||
* @param {string} theme - 主题名称
|
||
* @returns {string} 解析后的主题
|
||
*/
|
||
const resolveTheme = (theme) => {
|
||
if (theme === 'auto') {
|
||
// 优先使用用户设置的全局主题,否则使用系统主题
|
||
if (currentTheme && currentTheme !== 'auto') {
|
||
return currentTheme;
|
||
}
|
||
return getSystemTheme();
|
||
}
|
||
return theme;
|
||
};
|
||
|
||
/**
|
||
* 获取主题配置
|
||
* @param {string} theme - 主题名称
|
||
* @returns {Object} 主题配置
|
||
*/
|
||
const getThemeConfig = (theme) => {
|
||
const resolved = resolveTheme(theme);
|
||
return THEMES[resolved] || THEMES.light;
|
||
};
|
||
|
||
/**
|
||
* 应用主题
|
||
* @param {string} theme - 主题名称
|
||
*/
|
||
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}`);
|
||
|
||
// 设置CSS变量
|
||
const config = getThemeConfig(theme);
|
||
setThemeVariables(config);
|
||
}
|
||
|
||
// 通知监听器
|
||
notifyThemeListeners(theme, resolved);
|
||
};
|
||
|
||
/**
|
||
* 设置主题CSS变量
|
||
* @param {Object} config - 主题配置
|
||
*/
|
||
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');
|
||
};
|
||
|
||
/**
|
||
* 获取当前主题
|
||
* @returns {string} 当前主题
|
||
*/
|
||
const getCurrentTheme = () => {
|
||
return currentTheme;
|
||
};
|
||
|
||
/**
|
||
* 获取解析后的主题
|
||
* @returns {string} 解析后的主题
|
||
*/
|
||
const getResolvedTheme = () => {
|
||
return resolveTheme(currentTheme);
|
||
};
|
||
|
||
/**
|
||
* 切换主题
|
||
* @param {string} theme - 新主题
|
||
*/
|
||
const switchTheme = (theme) => {
|
||
applyTheme(theme);
|
||
saveTheme(theme);
|
||
};
|
||
|
||
/**
|
||
* 切换亮色/暗色主题
|
||
*/
|
||
const toggleTheme = () => {
|
||
const resolved = getResolvedTheme();
|
||
const newTheme = resolved === 'dark' ? 'light' : 'dark';
|
||
switchTheme(newTheme);
|
||
};
|
||
|
||
/**
|
||
* 重置为自动主题
|
||
*/
|
||
const resetToAuto = () => {
|
||
switchTheme('auto');
|
||
};
|
||
|
||
/**
|
||
* 保存主题到本地存储
|
||
* @param {string} theme - 主题名称
|
||
*/
|
||
const saveTheme = (theme) => {
|
||
if (typeof localStorage !== 'undefined') {
|
||
try {
|
||
localStorage.setItem('metona-toast-theme', theme);
|
||
} catch (e) {
|
||
console.warn('Failed to save theme:', e);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 从本地存储加载主题
|
||
* @returns {string} 主题名称
|
||
*/
|
||
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 = (e) => {
|
||
if (currentTheme === 'auto') {
|
||
applyTheme('auto');
|
||
}
|
||
};
|
||
|
||
systemThemeMediaQuery.addEventListener('change', systemThemeListener);
|
||
};
|
||
|
||
/**
|
||
* 停止监听系统主题变化
|
||
*/
|
||
const unwatchSystemTheme = () => {
|
||
if (systemThemeMediaQuery && systemThemeListener) {
|
||
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
||
systemThemeMediaQuery = null;
|
||
systemThemeListener = null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 添加主题监听器
|
||
* @param {Function} listener - 监听器函数
|
||
* @returns {Function} 移除监听器函数
|
||
*/
|
||
const addThemeListener = (listener) => {
|
||
themeListeners.add(listener);
|
||
|
||
return () => {
|
||
themeListeners.delete(listener);
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 移除主题监听器
|
||
* @param {Function} listener - 监听器函数
|
||
*/
|
||
const removeThemeListener = (listener) => {
|
||
themeListeners.delete(listener);
|
||
};
|
||
|
||
/**
|
||
* 通知主题监听器
|
||
* @param {string} theme - 主题名称
|
||
* @param {string} resolved - 解析后的主题
|
||
*/
|
||
const notifyThemeListeners = (theme, resolved) => {
|
||
themeListeners.forEach((listener) => {
|
||
try {
|
||
listener(theme, resolved);
|
||
} catch (e) {
|
||
console.error('Theme listener error:', e);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 清除所有主题监听器
|
||
*/
|
||
const clearThemeListeners = () => {
|
||
themeListeners.clear();
|
||
};
|
||
|
||
/**
|
||
* 注册自定义主题
|
||
* @param {string} name - 主题名称
|
||
* @param {Object} config - 主题配置
|
||
*/
|
||
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,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 注销自定义主题
|
||
* @param {string} name - 主题名称
|
||
*/
|
||
const unregisterTheme = (name) => {
|
||
if (name === 'light' || name === 'dark' || name === 'auto') {
|
||
console.warn('Cannot unregister built-in theme:', name);
|
||
return;
|
||
}
|
||
|
||
delete THEMES[name];
|
||
};
|
||
|
||
/**
|
||
* 获取所有主题
|
||
* @returns {Object} 主题映射
|
||
*/
|
||
const getAllThemes = () => {
|
||
return { ...THEMES };
|
||
};
|
||
|
||
/**
|
||
* 获取主题名称列表
|
||
* @returns {Array} 主题名称数组
|
||
*/
|
||
const getThemeNames = () => {
|
||
return Object.keys(THEMES);
|
||
};
|
||
|
||
/**
|
||
* 检查主题是否存在
|
||
* @param {string} name - 主题名称
|
||
* @returns {boolean} 是否存在
|
||
*/
|
||
const hasTheme = (name) => {
|
||
return name in THEMES;
|
||
};
|
||
|
||
/**
|
||
* 获取主题预览
|
||
* @param {string} theme - 主题名称
|
||
* @returns {Object} 主题预览
|
||
*/
|
||
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
|
||
* @param {string} theme - 主题名称
|
||
* @returns {string} 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
|
||
* @param {string} theme - 主题名称
|
||
*/
|
||
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,
|
||
};
|
||
|
||
/**
|
||
* 预设主题
|
||
*/
|
||
const presetThemes = {
|
||
/**
|
||
* 浅色主题
|
||
*/
|
||
light: {
|
||
name: '浅色',
|
||
description: '明亮清晰的主题',
|
||
config: THEMES.light,
|
||
},
|
||
|
||
/**
|
||
* 深色主题
|
||
*/
|
||
dark: {
|
||
name: '深色',
|
||
description: '护眼舒适的暗色主题',
|
||
config: THEMES.dark,
|
||
},
|
||
|
||
/**
|
||
* 自动主题
|
||
*/
|
||
auto: {
|
||
name: '自动',
|
||
description: '跟随系统主题设置',
|
||
config: 'auto',
|
||
},
|
||
|
||
warm: {
|
||
name: '暖色',
|
||
description: '温馨舒适的暖色主题',
|
||
config: THEMES.warm,
|
||
},
|
||
};
|
||
|
||
// 注册预设主题
|
||
Object.entries(presetThemes).forEach(([name, theme]) => {
|
||
if (name !== 'auto' && theme.config !== 'auto') {
|
||
THEMES[name] = theme.config;
|
||
}
|
||
});
|
||
|
||
/**
|
||
* MetonaToast i18n - 国际化管理
|
||
* @module i18n
|
||
* @version 2.0.0
|
||
* @description 多语言支持、语言切换和翻译管理
|
||
*/
|
||
|
||
|
||
// 当前语言状态
|
||
let currentLocale = 'zh-CN';
|
||
let localeListeners = new Set();
|
||
let fallbackLocale = 'zh-CN';
|
||
|
||
/**
|
||
* 获取当前语言
|
||
* @returns {string} 当前语言
|
||
*/
|
||
const getCurrentLocale = () => {
|
||
return currentLocale;
|
||
};
|
||
|
||
/**
|
||
* 设置当前语言
|
||
* @param {string} locale - 语言代码
|
||
*/
|
||
const setCurrentLocale = (locale) => {
|
||
if (!LOCALES[locale]) {
|
||
console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`);
|
||
locale = fallbackLocale;
|
||
}
|
||
|
||
currentLocale = locale;
|
||
notifyLocaleListeners(locale);
|
||
saveLocale(locale);
|
||
};
|
||
|
||
/**
|
||
* 获取回退语言
|
||
* @returns {string} 回退语言
|
||
*/
|
||
const getFallbackLocale = () => {
|
||
return fallbackLocale;
|
||
};
|
||
|
||
/**
|
||
* 设置回退语言
|
||
* @param {string} locale - 语言代码
|
||
*/
|
||
const setFallbackLocale = (locale) => {
|
||
if (!LOCALES[locale]) {
|
||
console.warn(`Fallback locale "${locale}" not found`);
|
||
return;
|
||
}
|
||
|
||
fallbackLocale = locale;
|
||
};
|
||
|
||
/**
|
||
* 翻译函数
|
||
* @param {string} key - 翻译键
|
||
* @param {Object} params - 插值参数
|
||
* @returns {string} 翻译后的字符串
|
||
*/
|
||
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;
|
||
};
|
||
|
||
/**
|
||
* 获取翻译
|
||
* @param {string} locale - 语言代码
|
||
* @param {string} key - 翻译键
|
||
* @returns {string|undefined} 翻译字符串
|
||
*/
|
||
const getTranslation = (locale, key) => {
|
||
const localeData = LOCALES[locale];
|
||
if (!localeData) return undefined;
|
||
|
||
// 支持嵌套键 (如 'common.close')
|
||
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;
|
||
};
|
||
|
||
/**
|
||
* 插值函数
|
||
* @param {string} str - 包含占位符的字符串
|
||
* @param {Object} params - 插值参数
|
||
* @returns {string} 插值后的字符串
|
||
*/
|
||
const interpolate = (str, params) => {
|
||
return str.replace(/\{(\w+)\}/g, (match, key) => {
|
||
return params[key] !== undefined ? params[key] : match;
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 检查翻译是否存在
|
||
* @param {string} key - 翻译键
|
||
* @returns {boolean} 是否存在
|
||
*/
|
||
const hasTranslation = (key) => {
|
||
return getTranslation(currentLocale, key) !== undefined ||
|
||
getTranslation(fallbackLocale, key) !== undefined;
|
||
};
|
||
|
||
/**
|
||
* 获取所有翻译
|
||
* @param {string} locale - 语言代码
|
||
* @returns {Object} 翻译对象
|
||
*/
|
||
const getTranslations = (locale) => {
|
||
return LOCALES[locale] || {};
|
||
};
|
||
|
||
/**
|
||
* 添加翻译
|
||
* @param {string} locale - 语言代码
|
||
* @param {Object} translations - 翻译对象
|
||
*/
|
||
const addTranslations = (locale, translations) => {
|
||
if (!LOCALES[locale]) {
|
||
LOCALES[locale] = {};
|
||
}
|
||
|
||
deepMerge(LOCALES[locale], translations);
|
||
};
|
||
|
||
/**
|
||
* 深度合并对象
|
||
* @param {Object} target - 目标对象
|
||
* @param {Object} source - 源对象
|
||
* @returns {Object} 合并后的对象
|
||
*/
|
||
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;
|
||
};
|
||
|
||
/**
|
||
* 移除翻译
|
||
* @param {string} locale - 语言代码
|
||
* @param {string} key - 翻译键
|
||
*/
|
||
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]];
|
||
};
|
||
|
||
/**
|
||
* 清除翻译
|
||
* @param {string} locale - 语言代码
|
||
*/
|
||
const clearTranslations = (locale) => {
|
||
if (LOCALES[locale]) {
|
||
LOCALES[locale] = {};
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 获取支持的语言列表
|
||
* @returns {Array} 语言代码数组
|
||
*/
|
||
const getSupportedLocales = () => {
|
||
return Object.keys(LOCALES);
|
||
};
|
||
|
||
/**
|
||
* 检查语言是否支持
|
||
* @param {string} locale - 语言代码
|
||
* @returns {boolean} 是否支持
|
||
*/
|
||
const isLocaleSupported = (locale) => {
|
||
return locale in LOCALES;
|
||
};
|
||
|
||
/**
|
||
* 获取语言名称
|
||
* @param {string} locale - 语言代码
|
||
* @returns {string} 语言名称
|
||
*/
|
||
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;
|
||
};
|
||
|
||
/**
|
||
* 获取语言方向
|
||
* @param {string} locale - 语言代码
|
||
* @returns {string} 语言方向 ('ltr' 或 'rtl')
|
||
*/
|
||
const getLocaleDirection = (locale) => {
|
||
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug'];
|
||
return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
|
||
};
|
||
|
||
/**
|
||
* 获取语言信息
|
||
* @param {string} locale - 语言代码
|
||
* @returns {Object} 语言信息
|
||
*/
|
||
const getLocaleInfo = (locale) => {
|
||
return {
|
||
code: locale,
|
||
name: getLocaleName(locale),
|
||
direction: getLocaleDirection(locale),
|
||
isSupported: isLocaleSupported(locale),
|
||
isCurrent: locale === currentLocale,
|
||
isFallback: locale === fallbackLocale,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 获取所有语言信息
|
||
* @returns {Array} 语言信息数组
|
||
*/
|
||
const getAllLocaleInfo = () => {
|
||
return getSupportedLocales().map(getLocaleInfo);
|
||
};
|
||
|
||
/**
|
||
* 保存语言到本地存储
|
||
* @param {string} locale - 语言代码
|
||
*/
|
||
const saveLocale = (locale) => {
|
||
if (typeof localStorage !== 'undefined') {
|
||
try {
|
||
localStorage.setItem('metona-toast-locale', locale);
|
||
} catch (e) {
|
||
console.warn('Failed to save locale:', e);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 从本地存储加载语言
|
||
* @returns {string} 语言代码
|
||
*/
|
||
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();
|
||
};
|
||
|
||
/**
|
||
* 获取默认语言
|
||
* @returns {string} 语言代码
|
||
*/
|
||
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);
|
||
};
|
||
|
||
/**
|
||
* 切换语言
|
||
* @param {string} locale - 新语言
|
||
*/
|
||
const switchLocale = (locale) => {
|
||
setCurrentLocale(locale);
|
||
};
|
||
|
||
/**
|
||
* 添加语言监听器
|
||
* @param {Function} listener - 监听器函数
|
||
* @returns {Function} 移除监听器函数
|
||
*/
|
||
const addLocaleListener = (listener) => {
|
||
localeListeners.add(listener);
|
||
|
||
return () => {
|
||
localeListeners.delete(listener);
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 移除语言监听器
|
||
* @param {Function} listener - 监听器函数
|
||
*/
|
||
const removeLocaleListener = (listener) => {
|
||
localeListeners.delete(listener);
|
||
};
|
||
|
||
/**
|
||
* 通知语言监听器
|
||
* @param {string} locale - 语言代码
|
||
*/
|
||
const notifyLocaleListeners = (locale) => {
|
||
localeListeners.forEach((listener) => {
|
||
try {
|
||
listener(locale);
|
||
} catch (e) {
|
||
console.error('Locale listener error:', e);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 清除所有语言监听器
|
||
*/
|
||
const clearLocaleListeners = () => {
|
||
localeListeners.clear();
|
||
};
|
||
|
||
/**
|
||
* 格式化数字
|
||
* @param {number} number - 数字
|
||
* @param {Object} options - 格式化选项
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
const formatNumber = (number, options = {}) => {
|
||
try {
|
||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||
} catch (e) {
|
||
return number.toString();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 格式化货币
|
||
* @param {number} amount - 金额
|
||
* @param {string} currency - 货币代码
|
||
* @param {Object} options - 格式化选项
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
||
try {
|
||
return new Intl.NumberFormat(currentLocale, {
|
||
style: 'currency',
|
||
currency,
|
||
...options,
|
||
}).format(amount);
|
||
} catch (e) {
|
||
return amount.toString();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 格式化百分比
|
||
* @param {number} value - 值
|
||
* @param {Object} options - 格式化选项
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
const formatPercent = (value, options = {}) => {
|
||
try {
|
||
return new Intl.NumberFormat(currentLocale, {
|
||
style: 'percent',
|
||
minimumFractionDigits: 0,
|
||
maximumFractionDigits: 2,
|
||
...options,
|
||
}).format(value / 100);
|
||
} catch (e) {
|
||
return `${value}%`;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 格式化日期
|
||
* @param {Date|number|string} date - 日期
|
||
* @param {Object} options - 格式化选项
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
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 date.toString();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 格式化时间
|
||
* @param {Date|number|string} date - 日期
|
||
* @param {Object} options - 格式化选项
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
const formatTime = (date, options = {}) => {
|
||
return formatDate(date, {
|
||
hour: 'numeric',
|
||
minute: 'numeric',
|
||
second: 'numeric',
|
||
...options,
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 格式化相对时间
|
||
* @param {Date|number|string} date - 日期
|
||
* @param {Object} options - 格式化选项
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
const formatRelativeTime = (date, options = {}) => {
|
||
try {
|
||
const dateObj = date instanceof Date ? date : new Date(date);
|
||
const now = new Date();
|
||
const diff = dateObj - now;
|
||
|
||
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 date.toString();
|
||
} catch (e) {
|
||
return date.toString();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 格式化列表
|
||
* @param {Array} list - 列表
|
||
* @param {Object} options - 格式化选项
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
const formatList = (list, options = {}) => {
|
||
try {
|
||
return new Intl.ListFormat(currentLocale, options).format(list);
|
||
} catch (e) {
|
||
return list.join(', ');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 格式化复数
|
||
* @param {number} count - 数量
|
||
* @param {Object} options - 格式化选项
|
||
* @returns {string} 格式化后的字符串
|
||
*/
|
||
const formatPlural = (count, options = {}) => {
|
||
try {
|
||
return new Intl.PluralRules(currentLocale, options).select(count);
|
||
} catch (e) {
|
||
return count === 1 ? 'one' : 'other';
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 复数翻译
|
||
* @param {string} key - 翻译键
|
||
* @param {number} count - 数量
|
||
* @param {Object} params - 插值参数
|
||
* @returns {string} 翻译后的字符串
|
||
*/
|
||
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 Core - 核心Toast逻辑
|
||
* @module core
|
||
* @version 2.0.0
|
||
* @description 重构后的核心模块,包含Toast类的优化实现
|
||
*/
|
||
|
||
|
||
// 模块级共享容器缓存,所有 Toast 实例共用
|
||
// 使用 Map 而非 WeakMap:document.body 在页面存活期间永不被 GC,
|
||
// 容器需要显式通过 destroy() 清理,WeakMap 在此场景无实际收益。
|
||
const _containerCache = new Map();
|
||
|
||
/**
|
||
* 参数标准化工具
|
||
* 支持多种调用方式:
|
||
* - success('message')
|
||
* - success({ message: 'text' })
|
||
* - success({ title: 'title', message: 'text' })
|
||
* - success('message', { duration: 5000 })
|
||
*/
|
||
const normalizeArgs = (args, defaultType = 'default') => {
|
||
const [first, second] = args;
|
||
|
||
// 情况1: success('message')
|
||
if (typeof first === 'string') {
|
||
return {
|
||
...second,
|
||
type: second?.type || defaultType,
|
||
message: first,
|
||
};
|
||
}
|
||
|
||
// 情况2: success({ message: 'text' }) 或 success({ title: 't', message: 'm' })
|
||
if (first && typeof first === 'object') {
|
||
return {
|
||
...first,
|
||
type: first.type || defaultType,
|
||
};
|
||
}
|
||
|
||
// 情况3: success() 无参数
|
||
return {
|
||
type: defaultType,
|
||
message: '',
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Toast类 - 核心通知组件
|
||
*/
|
||
class Toast {
|
||
// 静态钩子系统
|
||
static _hooks = new Map();
|
||
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));
|
||
}
|
||
static trigger(name, toast) {
|
||
const list = this._hooks.get(name);
|
||
if (list) list.forEach(fn => { try { fn(toast); } catch (e) { console.error('Hook error:', name, e); } });
|
||
}
|
||
|
||
constructor(opts) {
|
||
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.el = null;
|
||
this.barEl = null;
|
||
this.rafId = null;
|
||
this.remaining = this.config.duration;
|
||
this.startedAt = 0;
|
||
this.paused = false;
|
||
this.closing = false;
|
||
this._cleanups = [];
|
||
this.group = opts.group || null;
|
||
}
|
||
|
||
_palette() {
|
||
const theme = getTheme(this.config.theme);
|
||
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;
|
||
|
||
Toast.trigger('beforeShow', this);
|
||
injectStyles();
|
||
|
||
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);
|
||
}
|
||
|
||
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 position = this.config.position || 'top-right';
|
||
const zIndex = this.config.zIndex || 9999;
|
||
|
||
if (_containerCache.has(document.body)) {
|
||
const containers = _containerCache.get(document.body);
|
||
if (containers.has(position)) {
|
||
return containers.get(position);
|
||
}
|
||
} else {
|
||
_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');
|
||
|
||
// cssText 完全接管布局,不依赖 CSS class
|
||
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'));
|
||
if (list.length >= max) {
|
||
const first = list[0];
|
||
const id = first && first.dataset.id;
|
||
if (id) {
|
||
const old = meToast._toasts.get(id);
|
||
if (old) old.close(true);
|
||
}
|
||
}
|
||
}
|
||
|
||
_buildClassName(theme) {
|
||
// CSS 支持的动画类型,未列出的 fallback 到 slide
|
||
const CSS_ANIMS = ['slide','fade','scale','bounce','flip','rotate','zoom',
|
||
'slideUp','slideDown','slideLeft','slideRight'];
|
||
const anim = CSS_ANIMS.includes(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');
|
||
|
||
// 只强制核心布局属性,动画/滤镜/颜色由 CSS class 控制
|
||
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;
|
||
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;
|
||
}
|
||
|
||
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) {
|
||
const mouseEnter = () => this._pause();
|
||
const mouseLeave = () => this._resume();
|
||
|
||
el.addEventListener('mouseenter', mouseEnter);
|
||
el.addEventListener('mouseleave', mouseLeave);
|
||
|
||
this._cleanups.push(() => {
|
||
el.removeEventListener('mouseenter', mouseEnter);
|
||
el.removeEventListener('mouseleave', mouseLeave);
|
||
});
|
||
}
|
||
|
||
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();
|
||
};
|
||
|
||
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;
|
||
};
|
||
|
||
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() {
|
||
if (this.config.duration <= 0) return;
|
||
|
||
this.startedAt = Date.now();
|
||
this.remaining = this.config.duration;
|
||
|
||
const tick = () => {
|
||
if (this.paused || this.closing) return;
|
||
|
||
const elapsed = Date.now() - this.startedAt;
|
||
this.remaining = Math.max(0, this.config.duration - elapsed);
|
||
|
||
if (this.barEl) {
|
||
const ratio = this.remaining / this.config.duration;
|
||
const t = this.config.progressDirection === 'vertical'
|
||
? `scaleY(${ratio})` : `scaleX(${ratio})`;
|
||
this.barEl.style.transform = t;
|
||
}
|
||
|
||
if (this.remaining <= 0) {
|
||
this.close();
|
||
return;
|
||
}
|
||
|
||
this.rafId = requestAnimationFrame(tick);
|
||
};
|
||
|
||
this.rafId = requestAnimationFrame(tick);
|
||
}
|
||
|
||
_pause() {
|
||
if (this.paused || this.config.duration <= 0) return;
|
||
this.paused = true;
|
||
cancelAnimationFrame(this.rafId);
|
||
this.remaining = Math.max(0, this.config.duration - (Date.now() - this.startedAt));
|
||
}
|
||
|
||
_resume() {
|
||
if (!this.paused) return;
|
||
this.paused = false;
|
||
this.startedAt = Date.now() - (this.config.duration - this.remaining);
|
||
this._startTimer();
|
||
}
|
||
|
||
update(partial) {
|
||
Toast.trigger('beforeUpdate', this);
|
||
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; }
|
||
|
||
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) {
|
||
cancelAnimationFrame(this.rafId);
|
||
this.startedAt = Date.now();
|
||
this.remaining = this.config.duration;
|
||
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;
|
||
}
|
||
|
||
close(immediate = false) {
|
||
if (this.closing) return;
|
||
this.closing = true;
|
||
|
||
Toast.trigger('beforeClose', this);
|
||
cancelAnimationFrame(this.rafId);
|
||
this._cleanups.forEach(fn => { try { fn(); } catch (_) {} });
|
||
this._cleanups = [];
|
||
|
||
const el = this.el;
|
||
if (!el) {
|
||
meToast._remove(this.id);
|
||
return;
|
||
}
|
||
|
||
// 立即脱离文档流:记录当前位置,然后设为 absolute,让下方 toast 自然上移
|
||
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);
|
||
meToast._remove(this.id);
|
||
}
|
||
}
|
||
|
||
// ========== HTML模板辅助函数 ==========
|
||
|
||
/**
|
||
* 按钮行模板 — confirm/prompt 共用
|
||
* @param {string} buttons - 按钮HTML
|
||
* @returns {string} 按钮行HTML
|
||
*/
|
||
const _btnRow = (buttons) => `
|
||
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
||
${buttons}
|
||
</div>`;
|
||
|
||
/**
|
||
* 确认对话框模板
|
||
* @param {Object} opts - 选项
|
||
* @returns {string} HTML
|
||
*/
|
||
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>
|
||
`);
|
||
};
|
||
|
||
/**
|
||
* 输入对话框模板
|
||
* @param {Object} opts - 选项
|
||
* @returns {string} HTML
|
||
*/
|
||
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>
|
||
`)}
|
||
`;
|
||
};
|
||
|
||
/**
|
||
* 进度条模板
|
||
* @param {Object} opts - 选项
|
||
* @returns {string} HTML
|
||
*/
|
||
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 模板
|
||
* @param {Array} actions - 操作按钮数组 [{ text, onClick, style }]
|
||
* @returns {string} HTML
|
||
*/
|
||
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);
|
||
};
|
||
|
||
/**
|
||
* 主对象
|
||
*/
|
||
const meToast = {
|
||
_toasts: new Map(),
|
||
_config: { ...DEFAULTS },
|
||
version: '2.0.0',
|
||
|
||
configure(opts) {
|
||
if (!opts || typeof opts !== 'object') return this;
|
||
this._config = { ...this._config, ...opts };
|
||
|
||
if (opts.theme) {
|
||
applyTheme(opts.theme);
|
||
}
|
||
|
||
if (opts.locale) {
|
||
setCurrentLocale(opts.locale);
|
||
}
|
||
|
||
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);
|
||
},
|
||
|
||
/**
|
||
* 显示默认Toast
|
||
* @param {string|Object} messageOrOpts - 消息字符串或选项对象
|
||
* @param {Object} [opts] - 选项对象(当第一个参数是字符串时)
|
||
*/
|
||
show(messageOrOpts, opts = {}) {
|
||
const normalized = normalizeArgs([messageOrOpts, opts], 'default');
|
||
return this._emit(normalized);
|
||
},
|
||
|
||
/**
|
||
* 显示成功Toast
|
||
* @param {string|Object} messageOrOpts - 消息字符串或选项对象
|
||
* @param {Object} [opts] - 选项对象
|
||
*/
|
||
success(messageOrOpts, opts = {}) {
|
||
const normalized = normalizeArgs([messageOrOpts, opts], 'success');
|
||
return this._emit(normalized);
|
||
},
|
||
|
||
/**
|
||
* 显示错误Toast
|
||
* @param {string|Object} messageOrOpts - 消息字符串或选项对象
|
||
* @param {Object} [opts] - 选项对象
|
||
*/
|
||
error(messageOrOpts, opts = {}) {
|
||
const normalized = normalizeArgs([messageOrOpts, opts], 'error');
|
||
return this._emit(normalized);
|
||
},
|
||
|
||
/**
|
||
* 显示警告Toast
|
||
* @param {string|Object} messageOrOpts - 消息字符串或选项对象
|
||
* @param {Object} [opts] - 选项对象
|
||
*/
|
||
warning(messageOrOpts, opts = {}) {
|
||
const normalized = normalizeArgs([messageOrOpts, opts], 'warning');
|
||
return this._emit(normalized);
|
||
},
|
||
|
||
/**
|
||
* 显示信息Toast
|
||
* @param {string|Object} messageOrOpts - 消息字符串或选项对象
|
||
* @param {Object} [opts] - 选项对象
|
||
*/
|
||
info(messageOrOpts, opts = {}) {
|
||
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
||
return this._emit(normalized);
|
||
},
|
||
|
||
/**
|
||
* 显示加载Toast
|
||
* @param {string|Object} messageOrOpts - 消息字符串或选项对象
|
||
* @param {Object} [opts] - 选项对象
|
||
*/
|
||
loading(messageOrOpts, opts = {}) {
|
||
const normalized = normalizeArgs([messageOrOpts, opts], 'loading');
|
||
normalized.duration = 0;
|
||
normalized.closeButton = false;
|
||
normalized.showProgress = false;
|
||
|
||
const t = this._emit(normalized);
|
||
|
||
return {
|
||
id: t.id,
|
||
success: (msg, o = {}) => this._resolve(t, 'success', msg, o),
|
||
error: (msg, o = {}) => this._resolve(t, 'error', msg, o),
|
||
info: (msg, o = {}) => this._resolve(t, 'info', msg, o),
|
||
warning: (msg, o = {}) => this._resolve(t, 'warning', msg, o),
|
||
update: (p) => { t.update(p); return this; },
|
||
dismiss: () => t.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 });
|
||
},
|
||
|
||
/**
|
||
* 确认对话框
|
||
* @param {string} message - 消息内容
|
||
* @param {Object} [opts] - 选项
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
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 t = this._emit({
|
||
...opts,
|
||
type: opts.type || 'warning',
|
||
message,
|
||
duration: 0,
|
||
closeButton: false,
|
||
closeOnClick: false,
|
||
draggable: false,
|
||
html: _confirmHTML(opts),
|
||
});
|
||
|
||
setTimeout(() => {
|
||
const confirmBtn = t.el?.querySelector('.met-confirm-btn');
|
||
const cancelBtn = t.el?.querySelector('.met-cancel-btn');
|
||
|
||
if (confirmBtn) {
|
||
confirmBtn.addEventListener('click', () => {
|
||
t.close();
|
||
safeResolve(true);
|
||
});
|
||
}
|
||
|
||
if (cancelBtn) {
|
||
cancelBtn.addEventListener('click', () => {
|
||
t.close();
|
||
safeResolve(false);
|
||
});
|
||
}
|
||
}, 0);
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 输入对话框
|
||
* @param {string} message - 消息内容
|
||
* @param {Object} [opts] - 选项
|
||
* @returns {Promise<string|null>}
|
||
*/
|
||
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 t = this._emit({
|
||
...opts,
|
||
type: opts.type || 'info',
|
||
message,
|
||
duration: 0,
|
||
closeButton: false,
|
||
closeOnClick: false,
|
||
draggable: false,
|
||
html: _promptHTML(opts),
|
||
});
|
||
|
||
setTimeout(() => {
|
||
const input = t.el?.querySelector('.met-input');
|
||
const submitBtn = t.el?.querySelector('.met-submit-btn');
|
||
const cancelBtn = t.el?.querySelector('.met-cancel-btn');
|
||
|
||
if (input) {
|
||
input.focus();
|
||
|
||
input.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
t.close();
|
||
safeResolve(input.value);
|
||
}
|
||
});
|
||
}
|
||
|
||
if (submitBtn) {
|
||
submitBtn.addEventListener('click', () => {
|
||
t.close();
|
||
safeResolve(input?.value || null);
|
||
});
|
||
}
|
||
|
||
if (cancelBtn) {
|
||
cancelBtn.addEventListener('click', () => {
|
||
t.close();
|
||
safeResolve(null);
|
||
});
|
||
}
|
||
}, 0);
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 进度Toast
|
||
* @param {string|Object} messageOrOpts - 消息或选项
|
||
* @param {Object} [opts] - 选项
|
||
* @returns {Object} 控制对象
|
||
*/
|
||
progress(messageOrOpts, opts = {}) {
|
||
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
||
normalized.duration = 0;
|
||
normalized.closeButton = false;
|
||
normalized.showProgress = false;
|
||
|
||
const t$1 = this._emit({
|
||
...normalized,
|
||
html: _progressHTML(opts),
|
||
});
|
||
|
||
return {
|
||
id: t$1.id,
|
||
|
||
setProgress(percent) {
|
||
const fill = t$1.el?.querySelector('.met-progress-fill');
|
||
const text = t$1.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(() => {
|
||
t$1.update({
|
||
type: 'success',
|
||
message: message || t('success'),
|
||
html: '',
|
||
});
|
||
setTimeout(() => t$1.close(), 1000);
|
||
}, 300);
|
||
},
|
||
|
||
error(message) {
|
||
t$1.update({
|
||
type: 'error',
|
||
message: message || t('error'),
|
||
html: '',
|
||
});
|
||
setTimeout(() => t$1.close(), 2000);
|
||
},
|
||
|
||
dismiss() {
|
||
t$1.close();
|
||
},
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 倒计时Toast
|
||
* @param {string} message - 消息内容
|
||
* @param {number} seconds - 秒数
|
||
* @param {Object} [opts] - 选项
|
||
* @returns {Object} 控制对象
|
||
*/
|
||
countdown(message, seconds = 10, opts = {}) {
|
||
if (typeof message !== 'string') {
|
||
console.error('MeToast.countdown: message must be a string');
|
||
return { cancel: () => {}, pause: () => {}, resume: () => {} };
|
||
}
|
||
|
||
let remaining = Math.max(1, parseInt(seconds) || 10);
|
||
let timer = null;
|
||
|
||
const t = this._emit({
|
||
...opts,
|
||
type: opts.type || 'warning',
|
||
message: message.replace(/\{seconds\}/g, remaining),
|
||
duration: 0,
|
||
closeButton: true,
|
||
showProgress: false,
|
||
});
|
||
|
||
const tick = () => {
|
||
remaining--;
|
||
|
||
if (remaining <= 0) {
|
||
clearInterval(timer);
|
||
t.close();
|
||
|
||
if (typeof opts.onComplete === 'function') {
|
||
try {
|
||
opts.onComplete();
|
||
} catch (e) {
|
||
console.error('countdown onComplete error:', e);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
t.update({
|
||
message: message.replace(/\{seconds\}/g, remaining),
|
||
});
|
||
};
|
||
|
||
timer = setInterval(tick, 1000);
|
||
|
||
return {
|
||
id: t.id,
|
||
|
||
cancel() {
|
||
clearInterval(timer);
|
||
t.close();
|
||
},
|
||
|
||
pause() {
|
||
clearInterval(timer);
|
||
},
|
||
|
||
resume() {
|
||
clearInterval(timer);
|
||
timer = setInterval(tick, 1000);
|
||
},
|
||
};
|
||
},
|
||
|
||
/**
|
||
* Action Toast — 带操作按钮的通知
|
||
* @param {string|Object} messageOrOpts - 消息
|
||
* @param {Array} actions - 操作按钮 [{ text, onClick, color }]
|
||
* @param {Object} [opts] - 选项
|
||
* @returns {Object} toast实例 + dismiss
|
||
*/
|
||
action(messageOrOpts, actions = [], opts = {}) {
|
||
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
||
normalized.duration = opts.duration ?? 0;
|
||
normalized.closeButton = opts.closeButton ?? true;
|
||
|
||
const t = this._emit({
|
||
...normalized,
|
||
html: (normalized.html || '') + _actionHTML(actions),
|
||
});
|
||
|
||
if (Array.isArray(actions)) {
|
||
setTimeout(() => {
|
||
actions.forEach((a, i) => {
|
||
const btn = t.el?.querySelector(`.met-action-btn-${i}`);
|
||
if (btn && typeof a.onClick === 'function') {
|
||
btn.addEventListener('click', () => {
|
||
try { a.onClick(t); } catch (e) { console.error('Action onClick error:', e); }
|
||
if (a.close !== false) t.close();
|
||
});
|
||
}
|
||
});
|
||
}, 0);
|
||
}
|
||
|
||
return { id: t.id, toast: t, dismiss: () => t.close() };
|
||
},
|
||
|
||
/**
|
||
* 队列Toast
|
||
* @param {Array} messages - 消息数组
|
||
* @param {Object} [opts] - 选项
|
||
* @returns {Promise}
|
||
*/
|
||
queue(messages, opts = {}) {
|
||
if (!Array.isArray(messages)) {
|
||
console.error('MeToast.queue: messages must be an array');
|
||
return { then: (fn) => Promise.resolve().then(fn), 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 (_) {}
|
||
}
|
||
if (typeof userOnClose === 'function') {
|
||
try { userOnClose(toast); } catch (_) {}
|
||
}
|
||
setTimeout(showNext, delay);
|
||
},
|
||
});
|
||
};
|
||
|
||
showNext();
|
||
});
|
||
return {
|
||
then: (fn, rj) => promise.then(fn, rj),
|
||
catch: (rj) => promise.catch(rj),
|
||
cancel: () => { cancelled = true; },
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 堆叠Toast
|
||
* @param {Array} messages - 消息数组
|
||
* @param {Object} [opts] - 选项
|
||
*/
|
||
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());
|
||
},
|
||
|
||
clear(position) {
|
||
this._toasts.forEach(t => {
|
||
if (!position || t.config.position === position) t.close();
|
||
});
|
||
},
|
||
|
||
destroy() {
|
||
this.dismiss();
|
||
|
||
if (typeof document !== 'undefined') {
|
||
const containers = document.querySelectorAll('.met-container');
|
||
containers.forEach(c => c.parentNode && c.parentNode.removeChild(c));
|
||
|
||
const style = document.getElementById('metona-toast-styles');
|
||
if (style) style.parentNode.removeChild(style);
|
||
}
|
||
|
||
this._toasts.clear();
|
||
_containerCache.clear();
|
||
},
|
||
|
||
getAll() {
|
||
return new Map(this._toasts);
|
||
},
|
||
|
||
count() {
|
||
return this._toasts.size;
|
||
},
|
||
|
||
/**
|
||
* 创建Toast分组 — 返回一个自动添加 group 参数的子对象
|
||
* @param {string} name - 分组名称
|
||
* @returns {Object}
|
||
*/
|
||
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);
|
||
// 单对象参数:直接合并group
|
||
if (isObj && args.length === 1) {
|
||
return self[m]({ ...lastArg, group: name });
|
||
}
|
||
// 末尾是配置对象:pop后合并
|
||
if (isObj) {
|
||
args.pop();
|
||
return self[m](...args, { ...lastArg, group: name });
|
||
}
|
||
// 无配置对象
|
||
return self[m](...args, { group: name });
|
||
};
|
||
});
|
||
g.dismiss = () => self.dismissGroup(name);
|
||
g.count = () => self._groupCount(name);
|
||
return g;
|
||
},
|
||
|
||
/**
|
||
* 按组关闭Toast
|
||
* @param {string} name - 分组名称
|
||
*/
|
||
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;
|
||
},
|
||
};
|
||
|
||
/**
|
||
* MetonaToast Animations - 动画管理
|
||
* @module animations
|
||
* @version 2.0.0
|
||
* @description 动画效果管理和自定义
|
||
*/
|
||
|
||
|
||
// 动画缓存
|
||
const animationCache = new Map();
|
||
|
||
/**
|
||
* 动画管理器类
|
||
*/
|
||
class AnimationManager {
|
||
constructor() {
|
||
this.animations = new Map();
|
||
this.activeAnimations = new Map();
|
||
this.animationId = 0;
|
||
|
||
// 注册默认动画
|
||
this._registerDefaults();
|
||
}
|
||
|
||
/**
|
||
* 注册默认动画
|
||
*/
|
||
_registerDefaults() {
|
||
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
||
this.register(name, config);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 注册动画
|
||
* @param {string} name - 动画名称
|
||
* @param {Object} config - 动画配置
|
||
*/
|
||
register(name, config) {
|
||
const animation = {
|
||
name,
|
||
enter: config.enter || {},
|
||
leave: config.leave || {},
|
||
duration: config.duration || 300,
|
||
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||
delay: config.delay || 0,
|
||
iterations: config.iterations || 1,
|
||
direction: config.direction || 'normal',
|
||
fillMode: config.fillMode || 'forwards',
|
||
};
|
||
|
||
this.animations.set(name, animation);
|
||
animationCache.set(name, animation);
|
||
}
|
||
|
||
/**
|
||
* 注销动画
|
||
* @param {string} name - 动画名称
|
||
*/
|
||
unregister(name) {
|
||
this.animations.delete(name);
|
||
animationCache.delete(name);
|
||
}
|
||
|
||
/**
|
||
* 获取动画配置
|
||
* @param {string} name - 动画名称
|
||
* @returns {Object|null} 动画配置
|
||
*/
|
||
get(name) {
|
||
return this.animations.get(name) || ANIMATIONS[name] || null;
|
||
}
|
||
|
||
/**
|
||
* 应用动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
* @param {string} animationName - 动画名称
|
||
* @param {Object} options - 额外选项
|
||
* @returns {Promise} 动画完成Promise
|
||
*/
|
||
apply(element, animationName, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
const animation = this.get(animationName);
|
||
if (!animation) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const config = { ...animation, ...options };
|
||
const animId = ++this.animationId;
|
||
|
||
// 设置初始状态
|
||
Object.assign(element.style, config.enter);
|
||
|
||
// 强制重绘
|
||
element.offsetHeight;
|
||
|
||
// 创建动画
|
||
const keyframes = [
|
||
{ ...config.enter },
|
||
{ ...config.leave },
|
||
];
|
||
|
||
const animationOptions = {
|
||
duration: config.duration,
|
||
easing: config.easing,
|
||
delay: config.delay,
|
||
iterations: config.iterations,
|
||
direction: config.direction,
|
||
fill: config.fillMode,
|
||
};
|
||
|
||
// 应用动画
|
||
const anim = element.animate(keyframes, animationOptions);
|
||
|
||
// 存储活动动画
|
||
this.activeAnimations.set(animId, {
|
||
element,
|
||
animation: anim,
|
||
config,
|
||
});
|
||
|
||
// 动画完成处理
|
||
anim.onfinish = () => {
|
||
Object.assign(element.style, config.leave);
|
||
this.activeAnimations.delete(animId);
|
||
resolve();
|
||
};
|
||
|
||
// 动画取消处理 — reject 让调用者感知取消
|
||
anim.oncancel = () => {
|
||
this.activeAnimations.delete(animId);
|
||
reject(new Error(`Animation "${animationName}" was cancelled`));
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 应用进入动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
* @param {string} animationName - 动画名称
|
||
* @param {Object} options - 额外选项
|
||
* @returns {Promise} 动画完成Promise
|
||
*/
|
||
enter(element, animationName, options = {}) {
|
||
const animation = this.get(animationName);
|
||
if (!animation) {
|
||
return Promise.resolve();
|
||
}
|
||
|
||
return this.apply(element, animationName, {
|
||
...options,
|
||
direction: 'normal',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 应用离开动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
* @param {string} animationName - 动画名称
|
||
* @param {Object} options - 额外选项
|
||
* @returns {Promise} 动画完成Promise
|
||
*/
|
||
leave(element, animationName, options = {}) {
|
||
const animation = this.get(animationName);
|
||
if (!animation) {
|
||
return Promise.resolve();
|
||
}
|
||
|
||
return this.apply(element, animationName, {
|
||
...options,
|
||
direction: 'reverse',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 取消所有动画
|
||
*/
|
||
cancelAll() {
|
||
this.activeAnimations.forEach(({ animation }) => {
|
||
animation.cancel();
|
||
});
|
||
|
||
this.activeAnimations.clear();
|
||
}
|
||
|
||
/**
|
||
* 取消元素动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
*/
|
||
cancel(element) {
|
||
this.activeAnimations.forEach(({ element: el, animation }, id) => {
|
||
if (el === element) {
|
||
animation.cancel();
|
||
this.activeAnimations.delete(id);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 暂停所有动画
|
||
*/
|
||
pauseAll() {
|
||
this.activeAnimations.forEach(({ animation }) => {
|
||
animation.pause();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 恢复所有动画
|
||
*/
|
||
resumeAll() {
|
||
this.activeAnimations.forEach(({ animation }) => {
|
||
animation.play();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 暂停元素动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
*/
|
||
pause(element) {
|
||
this.activeAnimations.forEach(({ element: el, animation }) => {
|
||
if (el === element) {
|
||
animation.pause();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 恢复元素动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
*/
|
||
resume(element) {
|
||
this.activeAnimations.forEach(({ element: el, animation }) => {
|
||
if (el === element) {
|
||
animation.play();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取活动动画数量
|
||
* @returns {number} 动画数量
|
||
*/
|
||
getActiveCount() {
|
||
return this.activeAnimations.size;
|
||
}
|
||
|
||
/**
|
||
* 获取所有活动动画
|
||
* @returns {Map} 活动动画映射
|
||
*/
|
||
getActiveAnimations() {
|
||
return new Map(this.activeAnimations);
|
||
}
|
||
|
||
/**
|
||
* 检查元素是否有活动动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
* @returns {boolean} 是否有活动动画
|
||
*/
|
||
hasActiveAnimation(element) {
|
||
for (const { element: el } of this.activeAnimations.values()) {
|
||
if (el === element) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 获取动画配置
|
||
* @returns {Object} 动画配置
|
||
*/
|
||
getConfig() {
|
||
return {
|
||
animations: Array.from(this.animations.keys()),
|
||
activeCount: this.getActiveCount(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 重置动画管理器
|
||
*/
|
||
reset() {
|
||
this.cancelAll();
|
||
this.animations.clear();
|
||
this._registerDefaults();
|
||
}
|
||
|
||
/**
|
||
* 销毁动画管理器
|
||
*/
|
||
destroy() {
|
||
this.cancelAll();
|
||
this.animations.clear();
|
||
this.activeAnimations.clear();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 预设动画效果
|
||
*/
|
||
const presetAnimations = {
|
||
bounce: {
|
||
enter: { transform: 'translateY(-80px)', opacity: 0 },
|
||
leave: { transform: 'translateY(20px)', opacity: 1 },
|
||
duration: 650,
|
||
easing: 'ease',
|
||
},
|
||
slideUp: {
|
||
enter: { transform: 'translateY(60px)', opacity: 0 },
|
||
leave: { transform: 'translateY(0)', opacity: 1 },
|
||
duration: 400,
|
||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||
},
|
||
slideDown: {
|
||
enter: { transform: 'translateY(-60px)', opacity: 0 },
|
||
leave: { transform: 'translateY(0)', opacity: 1 },
|
||
duration: 400,
|
||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||
},
|
||
slideLeft: {
|
||
enter: { transform: 'translateX(-90px)', opacity: 0 },
|
||
leave: { transform: 'translateX(0)', opacity: 1 },
|
||
duration: 400,
|
||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||
},
|
||
slideRight: {
|
||
enter: { transform: 'translateX(90px)', opacity: 0 },
|
||
leave: { transform: 'translateX(0)', opacity: 1 },
|
||
duration: 400,
|
||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||
},
|
||
|
||
};
|
||
|
||
// 注册预设动画
|
||
Object.entries(presetAnimations).forEach(([name, config]) => {
|
||
ANIMATIONS[name] = config;
|
||
});
|
||
|
||
/**
|
||
* 创建动画管理器实例
|
||
* @returns {AnimationManager} 动画管理器实例
|
||
*/
|
||
const createAnimationManager = () => {
|
||
return new AnimationManager();
|
||
};
|
||
|
||
// 创建默认实例
|
||
const defaultAnimationManager = createAnimationManager();
|
||
|
||
/**
|
||
* 动画工具函数
|
||
*/
|
||
const animationUtils = {
|
||
/**
|
||
* 应用动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
* @param {string} animationName - 动画名称
|
||
* @param {Object} options - 额外选项
|
||
* @returns {Promise} 动画完成Promise
|
||
*/
|
||
apply(element, animationName, options = {}) {
|
||
return defaultAnimationManager.apply(element, animationName, options);
|
||
},
|
||
|
||
/**
|
||
* 应用进入动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
* @param {string} animationName - 动画名称
|
||
* @param {Object} options - 额外选项
|
||
* @returns {Promise} 动画完成Promise
|
||
*/
|
||
enter(element, animationName, options = {}) {
|
||
return defaultAnimationManager.enter(element, animationName, options);
|
||
},
|
||
|
||
/**
|
||
* 应用离开动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
* @param {string} animationName - 动画名称
|
||
* @param {Object} options - 额外选项
|
||
* @returns {Promise} 动画完成Promise
|
||
*/
|
||
leave(element, animationName, options = {}) {
|
||
return defaultAnimationManager.leave(element, animationName, options);
|
||
},
|
||
|
||
/**
|
||
* 取消所有动画
|
||
*/
|
||
cancelAll() {
|
||
defaultAnimationManager.cancelAll();
|
||
},
|
||
|
||
/**
|
||
* 取消元素动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
*/
|
||
cancel(element) {
|
||
defaultAnimationManager.cancel(element);
|
||
},
|
||
|
||
/**
|
||
* 暂停所有动画
|
||
*/
|
||
pauseAll() {
|
||
defaultAnimationManager.pauseAll();
|
||
},
|
||
|
||
/**
|
||
* 恢复所有动画
|
||
*/
|
||
resumeAll() {
|
||
defaultAnimationManager.resumeAll();
|
||
},
|
||
|
||
/**
|
||
* 暂停元素动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
*/
|
||
pause(element) {
|
||
defaultAnimationManager.pause(element);
|
||
},
|
||
|
||
/**
|
||
* 恢复元素动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
*/
|
||
resume(element) {
|
||
defaultAnimationManager.resume(element);
|
||
},
|
||
|
||
/**
|
||
* 获取活动动画数量
|
||
* @returns {number} 动画数量
|
||
*/
|
||
getActiveCount() {
|
||
return defaultAnimationManager.getActiveCount();
|
||
},
|
||
|
||
/**
|
||
* 检查元素是否有活动动画
|
||
* @param {HTMLElement} element - 目标元素
|
||
* @returns {boolean} 是否有活动动画
|
||
*/
|
||
hasActiveAnimation(element) {
|
||
return defaultAnimationManager.hasActiveAnimation(element);
|
||
},
|
||
|
||
/**
|
||
* 注册动画
|
||
* @param {string} name - 动画名称
|
||
* @param {Object} config - 动画配置
|
||
*/
|
||
register(name, config) {
|
||
defaultAnimationManager.register(name, config);
|
||
},
|
||
|
||
/**
|
||
* 注销动画
|
||
* @param {string} name - 动画名称
|
||
*/
|
||
unregister(name) {
|
||
defaultAnimationManager.unregister(name);
|
||
},
|
||
|
||
/**
|
||
* 获取动画配置
|
||
* @param {string} name - 动画名称
|
||
* @returns {Object|null} 动画配置
|
||
*/
|
||
get(name) {
|
||
return defaultAnimationManager.get(name);
|
||
},
|
||
|
||
/**
|
||
* 获取所有动画名称
|
||
* @returns {Array} 动画名称数组
|
||
*/
|
||
getAnimationNames() {
|
||
return Array.from(defaultAnimationManager.animations.keys());
|
||
},
|
||
|
||
/**
|
||
* 获取动画配置
|
||
* @returns {Object} 动画配置
|
||
*/
|
||
getConfig() {
|
||
return defaultAnimationManager.getConfig();
|
||
},
|
||
|
||
/**
|
||
* 重置动画管理器
|
||
*/
|
||
reset() {
|
||
defaultAnimationManager.reset();
|
||
},
|
||
|
||
/**
|
||
* 销毁动画管理器
|
||
*/
|
||
destroy() {
|
||
defaultAnimationManager.destroy();
|
||
},
|
||
};
|
||
|
||
/**
|
||
* MetonaToast Plugins - 插件系统
|
||
* @module plugins
|
||
* @version 2.0.0
|
||
* @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility)
|
||
*/
|
||
|
||
|
||
/**
|
||
* 插件管理器
|
||
*/
|
||
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 && !plugin.version)) {
|
||
console.error(`Invalid plugin "${name}"`);
|
||
return this;
|
||
}
|
||
|
||
this.plugins.set(name, { name, ...plugin, installed: false, enabled: true });
|
||
|
||
if (plugin.install) {
|
||
try { plugin.install(this); this.plugins.get(name).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, name) => {
|
||
if (p.destroy) { try { p.destroy(this); } catch (e) {} }
|
||
});
|
||
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;
|
||
this._handler = (e) => {
|
||
if (e.key === 'Escape') {
|
||
// 点击所有关闭按钮
|
||
document.querySelectorAll('.met-toast .met-close').forEach(btn => {
|
||
try { btn.click(); } catch (_) {}
|
||
});
|
||
}
|
||
};
|
||
document.addEventListener('keydown', this._handler);
|
||
},
|
||
|
||
uninstall() {
|
||
if (this._handler && typeof document !== 'undefined') {
|
||
document.removeEventListener('keydown', this._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 saved = localStorage.getItem(this.storageKey);
|
||
return saved ? JSON.parse(saved) : null;
|
||
} catch (_) { return null; }
|
||
}
|
||
return null;
|
||
},
|
||
|
||
save(config) {
|
||
if (typeof localStorage !== 'undefined') {
|
||
try { localStorage.setItem(this.storageKey, JSON.stringify(config)); } catch (_) {}
|
||
}
|
||
},
|
||
|
||
uninstall() {
|
||
if (typeof localStorage !== 'undefined') {
|
||
try { localStorage.removeItem(this.storageKey); } catch (_) {}
|
||
}
|
||
},
|
||
},
|
||
|
||
/**
|
||
* 无障碍插件 — 屏幕阅读器公告
|
||
*/
|
||
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 || null,
|
||
uninstall: config.uninstall || null,
|
||
...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 - 轻量级Toast通知库
|
||
* @module metona-toast
|
||
* @version 2.0.0
|
||
* @author thzxx
|
||
* @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。
|
||
* @license MIT
|
||
*/
|
||
|
||
|
||
// 版本信息
|
||
const VERSION = '2.0.0';
|
||
|
||
/**
|
||
* 主对象增强
|
||
*/
|
||
const enhancedMeToast = {
|
||
...meToast,
|
||
|
||
version: VERSION,
|
||
|
||
animations: animationUtils,
|
||
themes: themeUtils,
|
||
i18n: i18nUtils,
|
||
plugins: pluginUtils,
|
||
presetPlugins,
|
||
|
||
/**
|
||
* 安装插件,并连接必要的生命周期钩子
|
||
*/
|
||
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) => preset.announce(toast));
|
||
}
|
||
if (plugin === 'persistence') {
|
||
const saved = preset.install();
|
||
if (saved) this.configure(saved);
|
||
Toast.on('afterClose', () => { preset.save(this.getConfig()); });
|
||
}
|
||
} else if (plugin && typeof plugin === 'object') {
|
||
const name = plugin.name || 'custom';
|
||
this.plugins.register(name, { ...plugin, ...options });
|
||
}
|
||
|
||
return this;
|
||
},
|
||
|
||
/**
|
||
* 初始化
|
||
*/
|
||
init(options = {}) {
|
||
if (options.config) {
|
||
this.configure(options.config);
|
||
}
|
||
|
||
if (options.theme) {
|
||
this.themes.switchTheme(options.theme);
|
||
}
|
||
|
||
if (options.locale) {
|
||
this.i18n.switchLocale(options.locale);
|
||
}
|
||
|
||
if (options.plugins && Array.isArray(options.plugins)) {
|
||
options.plugins.forEach((plugin) => {
|
||
this.use(plugin);
|
||
});
|
||
}
|
||
|
||
return this;
|
||
},
|
||
|
||
/**
|
||
* 销毁
|
||
*/
|
||
destroy() {
|
||
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.cancelAll === 'function') {
|
||
this.animations.cancelAll();
|
||
}
|
||
|
||
if (typeof document !== 'undefined') {
|
||
const containers = document.querySelectorAll('.met-container');
|
||
containers.forEach((c) => c.parentNode && c.parentNode.removeChild(c));
|
||
|
||
const style = document.getElementById('metona-toast-styles');
|
||
if (style) style.parentNode.removeChild(style);
|
||
}
|
||
|
||
this._toasts.clear();
|
||
},
|
||
|
||
/**
|
||
* 获取状态
|
||
*/
|
||
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') {
|
||
this._config = { ...this._config, ...config };
|
||
}
|
||
return this;
|
||
},
|
||
|
||
/**
|
||
* 重置配置
|
||
*/
|
||
resetConfig() {
|
||
this._config = { ...DEFAULTS };
|
||
return this;
|
||
},
|
||
|
||
/**
|
||
* 获取Toast列表
|
||
*/
|
||
getToasts() {
|
||
return Array.from(this._toasts.values());
|
||
},
|
||
|
||
/**
|
||
* 检查是否有Toast
|
||
*/
|
||
hasToasts() {
|
||
return this._toasts.size > 0;
|
||
},
|
||
|
||
/**
|
||
* 获取Toast
|
||
*/
|
||
getToast(id) {
|
||
if (!id) return null;
|
||
return this._toasts.get(id) || null;
|
||
},
|
||
|
||
/**
|
||
* 关闭所有Toast
|
||
*/
|
||
closeAll() {
|
||
this._toasts.forEach((t) => t.close());
|
||
},
|
||
|
||
/**
|
||
* 清除所有Toast
|
||
*/
|
||
clearAll() {
|
||
this._toasts.forEach((t) => t.close());
|
||
},
|
||
|
||
/**
|
||
* 暂停所有Toast
|
||
*/
|
||
pauseAll() {
|
||
this._toasts.forEach((t) => t._pause());
|
||
},
|
||
|
||
/**
|
||
* 恢复所有Toast
|
||
*/
|
||
resumeAll() {
|
||
this._toasts.forEach((t) => t._resume());
|
||
},
|
||
|
||
/**
|
||
* 更新所有Toast
|
||
*/
|
||
updateAll(partial) {
|
||
if (partial && typeof partial === 'object') {
|
||
this._toasts.forEach((t) => t.update(partial));
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 查找Toast
|
||
*/
|
||
findToasts(predicate) {
|
||
if (typeof predicate !== 'function') return [];
|
||
return Array.from(this._toasts.values()).filter(predicate);
|
||
},
|
||
|
||
/**
|
||
* 按类型查找Toast
|
||
*/
|
||
findByType(type) {
|
||
return this.findToasts((t) => t.type === type);
|
||
},
|
||
|
||
/**
|
||
* 按位置查找Toast
|
||
*/
|
||
findByPosition(position) {
|
||
return this.findToasts((t) => t.config.position === position);
|
||
},
|
||
|
||
};
|
||
// 初始化主题和国际化
|
||
if (typeof themeUtils.initTheme === 'function') {
|
||
themeUtils.initTheme();
|
||
}
|
||
if (typeof i18nUtils.initI18n === 'function') {
|
||
i18nUtils.initI18n();
|
||
}
|
||
|
||
// 浏览器环境全局注册
|
||
if (typeof window !== 'undefined') {
|
||
window.MeToast = enhancedMeToast;
|
||
window.Met = enhancedMeToast;
|
||
|
||
// Notification API — 页面不可见时自动发送系统通知
|
||
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);
|
||
};
|
||
const sendNotification = (toast) => {
|
||
try {
|
||
new Notification(toast.title || toast.type, { body: toast.message });
|
||
} catch (_) {}
|
||
};
|
||
Toast.on('afterShow', (toast) => {
|
||
if (toast.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
|