Files
MetonaToast/dist/react.js
T
tianhao 95f5652e9c fix: CJS 产物改 .cjs 扩展名并提升 default 导出 — 修复 require() 兼容性
- 包声明 type:module 时 .js 产物被 Node 按 ESM 解析:Node 16/18 下 require 报 ERR_REQUIRE_ESM,Node 20+ 拿到 namespace 对象(success/version 等属性 undefined)
- dist/metona-toast.cjs.js → .cjs,dist/react.cjs.js → .cjs
- 主包 CJS footer 将 default 提升为 module.exports:require() 直接返回 MeToast(兼容 .default / .MeToast / .Met / .Toast / .VERSION 所有用法)
- 版本 0.5.1(0.5.0 已发布带问题产物)
- 真实消费者验证:CJS/ESM/React 子包四路加载全部正常
2026-08-08 15:25:47 +08:00

4613 lines
164 KiB
JavaScript

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