Files
MetonaToast/dist/metona-toast.cjs.js
T

6713 lines
183 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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 而非 WeakMapdocument.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