(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MeToast = {})); })(this, (function (exports) { 'use strict'; /** * MetonaToast Utils — 工具函数 * @module utils * @version 0.5.0 */ /** * 生成唯一ID */ const generateId = () => { return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); }; /** * HTML转义 * @param s - 输入字符串 */ const escapeHTML = (s) => { if (typeof document === 'undefined') { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } 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.0 * @description 107 个内置 SVG 图标 */ const ICONS = { success: ``, error: ``, warning: ``, info: ``, loading: ``, close: ``, check: ``, x: ``, alert: ``, question: ``, star: ``, heart: ``, bell: ``, mail: ``, settings: ``, user: ``, home: ``, search: ``, plus: ``, minus: ``, edit: ``, trash: ``, download: ``, upload: ``, share: ``, link: ``, external: ``, clock: ``, calendar: ``, map: ``, compass: ``, globe: ``, wifi: ``, cloud: ``, sun: ``, moon: ``, zap: ``, activity: ``, cpu: ``, database: ``, server: ``, terminal: ``, code: ``, git: ``, package: ``, layers: ``, grid: ``, list: ``, filter: ``, sort: ``, refresh: ``, sync: ``, power: ``, battery: ``, bluetooth: ``, volume: ``, mic: ``, camera: ``, image: ``, video: ``, music: ``, file: ``, folder: ``, clipboard: ``, save: ``, print: ``, eye: ``, eyeOff: ``, lock: ``, unlock: ``, shield: ``, key: ``, flag: ``, bookmark: ``, tag: ``, gift: ``, award: ``, target: ``, crosshair: ``, move: ``, maximize: ``, minimize: ``, copy: ``, cut: ``, paste: ``, rotateCw: ``, rotateCcw: ``, zoomIn: ``, zoomOut: ``, crop: ``, sliders: ``, toggleLeft: ``, toggleRight: ``, checkCircle: ``, xCircle: ``, alertCircle: ``, infoCircle: ``, helpCircle: ``, alertTriangle: ``, checkSquare: ``, square: ``, circle: ``, triangle: ``, hexagon: ``, octagon: ``, pentagon: ``, diamond: ``, }; /** * MetonaToast Locales — 国际化翻译数据 * @module locales * @version 0.5.0 * @description 内置 zh-CN / en-US 完整翻译 */ const LOCALES = { 'zh-CN': { // === 操作 === close: '关闭', loading: '加载中...', success: '操作成功', error: '操作失败', warning: '警告', info: '信息', confirm: '确认', cancel: '取消', yes: '是', no: '否', ok: '确定', retry: '重试', undo: '撤销', redo: '重做', save: '保存', delete: '删除', edit: '编辑', add: '添加', remove: '移除', search: '搜索', filter: '筛选', sort: '排序', refresh: '刷新', sync: '同步', upload: '上传', download: '下载', share: '分享', copy: '复制', cut: '剪切', paste: '粘贴', print: '打印', export: '导出', import: '导入', settings: '设置', help: '帮助', about: '关于', version: '版本', language: '语言', theme: '主题', dark: '暗色', light: '亮色', auto: '自动', notifications: '通知', enable: '启用', disable: '禁用', on: '开启', off: '关闭', max: '最大', min: '最小', expand: '展开', collapse: '折叠', fullscreen: '全屏', exitFullscreen: '退出全屏', zoomIn: '放大', zoomOut: '缩小', reset: '重置', apply: '应用', submit: '提交', send: '发送', receive: '接收', connect: '连接', disconnect: '断开', start: '开始', stop: '停止', pause: '暂停', resume: '继续', next: '下一个', previous: '上一个', first: '第一个', last: '最后一个', back: '返回', forward: '前进', home: '首页', menu: '菜单', more: '更多', less: '更少', show: '显示', hide: '隐藏', visible: '可见', hidden: '隐藏', // === 状态 === enabled: '已启用', disabled: '已禁用', active: '活跃', inactive: '非活跃', online: '在线', offline: '离线', connected: '已连接', disconnected: '已断开', loaded: '已加载', saving: '保存中', saved: '已保存', deleting: '删除中', deleted: '已删除', uploading: '上传中', uploaded: '已上传', downloading: '下载中', downloaded: '已下载', processing: '处理中', processed: '已处理', syncing: '同步中', synced: '已同步', sending: '发送中', sent: '已发送', receiving: '接收中', received: '已接收', connecting: '连接中', // === 交互 === question: '问题', feedback: '反馈', report: '报告', bug: '错误', feature: '功能', improvement: '改进', suggestion: '建议', request: '请求', response: '响应', status: '状态', progress: '进度', complete: '完成', incomplete: '未完成', pending: '待处理', failed: '失败', cancelled: '已取消', timeout: '超时', expired: '已过期', invalid: '无效', valid: '有效', required: '必填', optional: '可选', allowed: '允许', denied: '拒绝', approved: '已批准', rejected: '已拒绝', accepted: '已接受', declined: '已拒绝', confirmed: '已确认', unconfirmed: '未确认', verified: '已验证', unverified: '未验证', authenticated: '已认证', unauthenticated: '未认证', authorized: '已授权', unauthorized: '未授权', // === 范围 / 角色 === public: '公开', private: '私有', protected: '受保护', internal: '内部', external: '外部', local: '本地', remote: '远程', global: '全局', system: '系统', user: '用户', admin: '管理员', moderator: '版主', guest: '访客', member: '成员', owner: '所有者', creator: '创建者', editor: '编辑者', viewer: '查看者', subscriber: '订阅者', follower: '关注者', following: '关注中', friend: '朋友', contact: '联系人', // === 组织 === group: '群组', team: '团队', organization: '组织', company: '公司', department: '部门', project: '项目', task: '任务', issue: '问题', ticket: '工单', message: '消息', notification: '通知', alert: '警报', reminder: '提醒', event: '事件', activity: '活动', log: '日志', analytics: '分析', statistics: '统计', metrics: '指标', performance: '性能', security: '安全', privacy: '隐私', compliance: '合规', policy: '政策', terms: '条款', conditions: '条件', agreement: '协议', contract: '合同', license: '许可证', copyright: '版权', trademark: '商标', patent: '专利', tradeSecret: '商业秘密', confidential: '机密', sensitive: '敏感', critical: '关键', important: '重要', urgent: '紧急', high: '高', medium: '中', low: '低', none: '无', all: '全部', any: '任何', some: '一些', each: '每个', every: '每个', both: '两者', neither: '两者都不', either: '任一', other: '其他', another: '另一个', this: '这个', that: '那个', these: '这些', those: '那些', here: '这里', there: '那里', where: '哪里', when: '何时', why: '为什么', how: '如何', what: '什么', who: '谁', which: '哪个', whose: '谁的', whom: '谁', }, 'en-US': { // === Actions === close: 'Close', loading: 'Loading...', success: 'Success', error: 'Error', warning: 'Warning', info: 'Info', confirm: 'Confirm', cancel: 'Cancel', yes: 'Yes', no: 'No', ok: 'OK', retry: 'Retry', undo: 'Undo', redo: 'Redo', save: 'Save', delete: 'Delete', edit: 'Edit', add: 'Add', remove: 'Remove', search: 'Search', filter: 'Filter', sort: 'Sort', refresh: 'Refresh', sync: 'Sync', upload: 'Upload', download: 'Download', share: 'Share', copy: 'Copy', cut: 'Cut', paste: 'Paste', print: 'Print', export: 'Export', import: 'Import', settings: 'Settings', help: 'Help', about: 'About', version: 'Version', language: 'Language', theme: 'Theme', dark: 'Dark', light: 'Light', auto: 'Auto', notifications: 'Notifications', enable: 'Enable', disable: 'Disable', on: 'On', off: 'Off', max: 'Max', min: 'Min', expand: 'Expand', collapse: 'Collapse', fullscreen: 'Fullscreen', exitFullscreen: 'Exit Fullscreen', zoomIn: 'Zoom In', zoomOut: 'Zoom Out', reset: 'Reset', apply: 'Apply', submit: 'Submit', send: 'Send', receive: 'Receive', connect: 'Connect', disconnect: 'Disconnect', start: 'Start', stop: 'Stop', pause: 'Pause', resume: 'Resume', next: 'Next', previous: 'Previous', first: 'First', last: 'Last', back: 'Back', forward: 'Forward', home: 'Home', menu: 'Menu', more: 'More', less: 'Less', show: 'Show', hide: 'Hide', visible: 'Visible', hidden: 'Hidden', // === Status === enabled: 'Enabled', disabled: 'Disabled', active: 'Active', inactive: 'Inactive', online: 'Online', offline: 'Offline', connected: 'Connected', disconnected: 'Disconnected', loaded: 'Loaded', saving: 'Saving', saved: 'Saved', deleting: 'Deleting', deleted: 'Deleted', uploading: 'Uploading', uploaded: 'Uploaded', downloading: 'Downloading', downloaded: 'Downloaded', processing: 'Processing', processed: 'Processed', syncing: 'Syncing', synced: 'Synced', sending: 'Sending', sent: 'Sent', receiving: 'Receiving', received: 'Received', connecting: 'Connecting', // === Interaction === question: 'Question', feedback: 'Feedback', report: 'Report', bug: 'Bug', feature: 'Feature', improvement: 'Improvement', suggestion: 'Suggestion', request: 'Request', response: 'Response', status: 'Status', progress: 'Progress', complete: 'Complete', incomplete: 'Incomplete', pending: 'Pending', failed: 'Failed', cancelled: 'Cancelled', timeout: 'Timeout', expired: 'Expired', invalid: 'Invalid', valid: 'Valid', required: 'Required', optional: 'Optional', allowed: 'Allowed', denied: 'Denied', approved: 'Approved', rejected: 'Rejected', accepted: 'Accepted', declined: 'Declined', confirmed: 'Confirmed', unconfirmed: 'Unconfirmed', verified: 'Verified', unverified: 'Unverified', authenticated: 'Authenticated', unauthenticated: 'Unauthenticated', authorized: 'Authorized', unauthorized: 'Unauthorized', // === Scope / Role === public: 'Public', private: 'Private', protected: 'Protected', internal: 'Internal', external: 'External', local: 'Local', remote: 'Remote', global: 'Global', system: 'System', user: 'User', admin: 'Admin', moderator: 'Moderator', guest: 'Guest', member: 'Member', owner: 'Owner', creator: 'Creator', editor: 'Editor', viewer: 'Viewer', subscriber: 'Subscriber', follower: 'Follower', following: 'Following', friend: 'Friend', contact: 'Contact', // === Organization === group: 'Group', team: 'Team', organization: 'Organization', company: 'Company', department: 'Department', project: 'Project', task: 'Task', issue: 'Issue', ticket: 'Ticket', message: 'Message', notification: 'Notification', alert: 'Alert', reminder: 'Reminder', event: 'Event', activity: 'Activity', log: 'Log', analytics: 'Analytics', statistics: 'Statistics', metrics: 'Metrics', performance: 'Performance', security: 'Security', privacy: 'Privacy', compliance: 'Compliance', policy: 'Policy', terms: 'Terms', conditions: 'Conditions', agreement: 'Agreement', contract: 'Contract', license: 'License', copyright: 'Copyright', trademark: 'Trademark', patent: 'Patent', tradeSecret: 'Trade Secret', confidential: 'Confidential', sensitive: 'Sensitive', critical: 'Critical', important: 'Important', urgent: 'Urgent', high: 'High', medium: 'Medium', low: 'Low', none: 'None', all: 'All', any: 'Any', some: 'Some', each: 'Each', every: 'Every', both: 'Both', neither: 'Neither', either: 'Either', other: 'Other', another: 'Another', this: 'This', that: 'That', these: 'These', those: 'Those', here: 'Here', there: 'There', where: 'Where', when: 'When', why: 'Why', how: 'How', what: 'What', who: 'Who', which: 'Which', whose: 'Whose', whom: 'Whom', }, }; /** * MetonaToast Constants — 常量定义 * @module constants * @version 0.5.0 */ /** * 版本号 — 唯一来源,发布时只需修改此处 */ const VERSION = '0.5.0'; /** * 默认配置 */ 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.0 */ // 样式缓存 let styleElement = null; /** * 生成CSS样式 */ const generateCSS = () => { return ` .met-container { pointer-events: none; display: flex; flex-direction: column; gap: 12px; box-sizing: border-box; padding: 24px; max-width: 100vw; position: fixed; z-index: 9999; } .met-container.top-left { top: 0; left: 0; align-items: flex-start; } .met-container.top-center { top: 0; left: 0; right: 0; align-items: center; } .met-container.top-right { top: 0; right: 0; align-items: flex-end; } .met-container.bottom-left { bottom: 0; left: 0; align-items: flex-start; } .met-container.bottom-center{ bottom: 0; left: 0; right: 0; align-items: center; } .met-container.bottom-right { bottom: 0; right: 0; align-items: flex-end; } .met-toast { position: relative; display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border-radius: 12px; box-sizing: border-box; backdrop-filter: blur(14px) saturate(180%); -webkit-backdrop-filter: blur(14px) saturate(180%); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; user-select: none; -webkit-user-select: none; transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s ease, opacity 0.25s ease; will-change: transform, opacity; flex-shrink: 0; pointer-events: auto; min-width: 240px; max-width: calc(100vw - 48px); width: 360px; border: 1px solid var(--met-border, rgba(0,0,0,0.06)); background: var(--met-bg, rgba(255,255,255,0.96)); color: var(--met-text, #1f2937); box-shadow: var(--met-shadow, 0 10px 36px -10px rgba(0,0,0,0.18), 0 4px 14px -4px rgba(0,0,0,0.08)); transform: translateZ(0); -webkit-transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; perspective: 1000; } .met-toast:hover { box-shadow: var(--met-hover-shadow, 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10)) !important; } .met-toast.met-clickable { cursor: pointer; } .met-icon { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-top: 1px; } .met-content { flex: 1; min-width: 0; word-wrap: break-word; overflow-wrap: break-word; } .met-title { font-weight: 600; font-size: 14px; letter-spacing: 0.1px; } .met-message { font-size: 13px; opacity: 0.85; margin-top: 2px; } .met-close { flex-shrink: 0; background: transparent; border: 0; padding: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: background 0.2s, color 0.2s; line-height: 0; color: inherit; opacity: 0.5; } .met-close:hover { opacity: 1; background: var(--met-close-hover-bg, rgba(0,0,0,0.06)); } .met-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; overflow: hidden; background: var(--met-progress-bg, rgba(0,0,0,0.06)); border-radius: 0 0 12px 12px; } .met-progress-v { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; overflow: hidden; background: var(--met-progress-bg, rgba(0,0,0,0.06)); border-radius: 12px 0 0 12px; } .met-bar { position: absolute; left: 0; top: 0; height: 100%; width: 100%; transform-origin: left; transform: scaleX(1); } .met-bar-v { position: absolute; left: 0; bottom: 0; width: 100%; height: 100%; transform-origin: bottom; transform: scaleY(1); } .met-side { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; border-radius: 12px 0 0 12px; } .met-spin { animation: met-rot 1s linear infinite; transform-origin: 50% 50%; } .met-toast.met-leaving { pointer-events: none; z-index: 1; } @keyframes met-rot { to { transform: rotate(360deg); } } .met-anim-slide.met-toast, .met-anim-fade.met-toast, .met-anim-scale.met-toast, .met-anim-bounce.met-toast, .met-anim-flip.met-toast, .met-anim-rotate.met-toast, .met-anim-zoom.met-toast, .met-anim-slideUp.met-toast, .met-anim-slideDown.met-toast, .met-anim-slideLeft.met-toast, .met-anim-slideRight.met-toast { opacity: 0; } @keyframes met-slide-in { 0% { transform: translateX(80px); opacity: 0; } 65% { transform: translateX(-6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } @keyframes met-fade-in { 0% { opacity: 0; filter: blur(3px); } 100% { opacity: 1; filter: blur(0); } } @keyframes met-scale-in { 0% { transform: scale(0.55); opacity: 0; } 65% { transform: scale(1.07); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } @keyframes met-bounce-in { 0% { transform: translateY(-80px); opacity: 0; animation-timing-function: ease-in; } 45% { transform: translateY(14px); opacity: 1; animation-timing-function: ease-out; } 65% { transform: translateY(-12px); animation-timing-function: ease-in; } 82% { transform: translateY(4px); animation-timing-function: ease-out; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-flip-in { 0% { transform: perspective(500px) rotateX(-90deg); opacity: 0; } 60% { transform: perspective(500px) rotateX(8deg); opacity: 1; } 85% { transform: perspective(500px) rotateX(-2deg); } 100% { transform: perspective(500px) rotateX(0); opacity: 1; } } @keyframes met-rotate-in { 0% { transform: rotate(-25deg) scale(0.6); opacity: 0; } 60% { transform: rotate(4deg) scale(1.05); opacity: 1; } 85% { transform: rotate(-1deg) scale(0.98); } 100% { transform: rotate(0) scale(1); opacity: 1; } } @keyframes met-zoom-in { 0% { transform: scale(0.1); opacity: 0; } 50% { transform: scale(1.12); opacity: 1; } 75% { transform: scale(0.94); } 100% { transform: scale(1); opacity: 1; } } @keyframes met-slideUp-in { 0% { transform: translateY(60px); opacity: 0; } 70% { transform: translateY(-6px); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-slideDown-in { 0% { transform: translateY(-60px); opacity: 0; } 70% { transform: translateY(6px); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-slideLeft-in { 0% { transform: translateX(-90px); opacity: 0; } 65% { transform: translateX(6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } @keyframes met-slideRight-in { 0% { transform: translateX(90px); opacity: 0; } 65% { transform: translateX(-6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } .met-anim-slide.met-toast.met-show { animation: met-slide-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-fade.met-toast.met-show { animation: met-fade-in 0.50s ease forwards; } .met-anim-scale.met-toast.met-show { animation: met-scale-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } .met-anim-bounce.met-toast.met-show { animation: met-bounce-in 0.65s ease forwards; } .met-anim-flip.met-toast.met-show { animation: met-flip-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-rotate.met-toast.met-show { animation: met-rotate-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-zoom.met-toast.met-show { animation: met-zoom-in 0.50s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } .met-anim-slideUp.met-toast.met-show { animation: met-slideUp-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideDown.met-toast.met-show { animation: met-slideDown-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideLeft.met-toast.met-show { animation: met-slideLeft-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideRight.met-toast.met-show { animation: met-slideRight-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } @media (max-width: 480px) { .met-container { padding: 12px !important; } .met-toast { width: 100% !important; min-width: 0 !important; } } @media (max-width: 768px) { .met-toast { font-size: 13px; } .met-title { font-size: 13px; } .met-message { font-size: 12px; } } @media (min-width: 1200px) { .met-toast { width: 400px; } } @media (min-width: 1920px) { .met-toast { width: 440px; } } @media (max-width: 320px) { .met-toast { width: 100%; min-width: 0; padding: 12px 14px; } .met-container { padding: 8px; } } @media (orientation: landscape) and (max-height: 500px) { .met-container { padding: 12px 24px; } .met-toast { min-height: 44px; padding: 10px 14px; } } @media (orientation: portrait) and (max-width: 500px) { .met-container { padding: 12px; } .met-toast { width: 100%; min-width: 0; } } @media (prefers-reduced-motion: reduce) { .met-toast { transition: opacity 0.15s !important; } .met-toast.met-show { animation: none !important; opacity: 1 !important; } .met-spin { animation-duration: 2.5s !important; } } @media (forced-colors: active) { .met-toast { border: 2px solid ButtonText; } .met-close { border: 1px solid ButtonText; } } @media (prefers-contrast: high) { .met-toast { border-width: 3px; border-style: solid; } .met-close { border: 2px solid currentColor; border-radius: 4px; } } .met-toast:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; } .met-close:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; } @media (hover: none) and (pointer: coarse) { .met-toast:hover { box-shadow: inherit; } .met-toast { min-height: 48px; } .met-close { min-width: 48px; min-height: 48px; } } @media print { .met-container { display: none !important; } .met-toast { display: none !important; } } :fullscreen .met-container, :picture-in-picture .met-container { z-index: 2147483647; } @supports (padding: max(0px)) { .met-container { padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left)); } } .met-toast:active { transform: scale(0.98); } .met-toast.met-success { border-left: 4px solid #10b981; } .met-toast.met-error { border-left: 4px solid #ef4444; } .met-toast.met-warning { border-left: 4px solid #f59e0b; } .met-toast.met-info { border-left: 4px solid #3b82f6; } .met-toast.met-loading { border-left: 4px solid #6366f1; } .met-bar, .met-bar-v { transition: transform 0.1s linear; } /* 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.0 */ // 当前主题状态 let currentTheme = 'auto'; const themeListeners = new Set(); let systemThemeMediaQuery = null; let systemThemeListener = null; /** * 获取系统主题 */ const getSystemTheme = () => { if (typeof window === 'undefined') return 'light'; return prefersDark() ? 'dark' : 'light'; }; /** * 获取主题(别名) */ const getTheme = (theme) => { return resolveTheme(theme); }; /** * 解析主题 — 'auto' 始终按系统主题检测,不依赖当前手动选择的主题 */ const resolveTheme = (theme) => { if (theme === 'auto') { return getSystemTheme(); } return theme; }; /** * 获取主题配置 */ const getThemeConfig = (theme) => { const resolved = resolveTheme(theme); return THEMES[resolved] || THEMES.light; }; /** * 应用主题 */ const applyTheme = (theme) => { currentTheme = theme; const resolved = resolveTheme(theme); if (typeof document !== 'undefined') { document.documentElement.setAttribute('data-met-theme', resolved); document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto'); document.documentElement.classList.add(`met-theme-${resolved}`); const config = getThemeConfig(theme); setThemeVariables(config); } notifyThemeListeners(theme, resolved); }; /** * 设置主题CSS变量 */ const setThemeVariables = (config) => { if (typeof document === 'undefined') return; const root = document.documentElement; root.style.setProperty('--met-bg', config.bg); root.style.setProperty('--met-text', config.text); root.style.setProperty('--met-border', config.border); root.style.setProperty('--met-shadow', config.shadow); root.style.setProperty('--met-hover-shadow', config.hoverShadow); root.style.setProperty('--met-progress-bg', config.progressBg); root.style.setProperty('--met-close-hover-bg', config.closeHoverBg); root.style.setProperty('--met-success', '#10b981'); root.style.setProperty('--met-error', '#ef4444'); root.style.setProperty('--met-warning', '#f59e0b'); root.style.setProperty('--met-info', '#3b82f6'); root.style.setProperty('--met-loading', '#6366f1'); }; /** * 获取当前主题 */ const getCurrentTheme = () => { return currentTheme; }; /** * 获取解析后的主题 */ const getResolvedTheme = () => { return resolveTheme(currentTheme); }; /** * 切换主题 */ const switchTheme = (theme) => { applyTheme(theme); saveTheme(theme); }; /** * 切换亮色/暗色主题 */ const toggleTheme = () => { const resolved = getResolvedTheme(); const newTheme = resolved === 'dark' ? 'light' : 'dark'; switchTheme(newTheme); }; /** * 重置为自动主题 */ const resetToAuto = () => { switchTheme('auto'); }; /** * 保存主题到本地存储 */ const saveTheme = (theme) => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-theme', theme); } catch (e) { console.warn('Failed to save theme:', e); } } }; /** * 从本地存储加载主题 */ const loadTheme = () => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-theme') || 'auto'; } catch (e) { console.warn('Failed to load theme:', e); } } return 'auto'; }; /** * 初始化主题系统 */ const initTheme = () => { const savedTheme = loadTheme(); applyTheme(savedTheme); watchSystemTheme(); }; /** * 监听系统主题变化 */ const watchSystemTheme = () => { if (typeof window === 'undefined' || !window.matchMedia) return; if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); } systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); systemThemeListener = () => { if (currentTheme === 'auto') { applyTheme('auto'); } }; systemThemeMediaQuery.addEventListener('change', systemThemeListener); }; /** * 停止监听系统主题变化 */ const unwatchSystemTheme = () => { if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); systemThemeMediaQuery = null; systemThemeListener = null; } }; /** * 添加主题监听器 */ const addThemeListener = (listener) => { themeListeners.add(listener); return () => { themeListeners.delete(listener); }; }; /** * 移除主题监听器 */ const removeThemeListener = (listener) => { themeListeners.delete(listener); }; /** * 通知主题监听器 */ const notifyThemeListeners = (theme, resolved) => { themeListeners.forEach((listener) => { try { listener(theme, resolved); } catch (e) { console.error('Theme listener error:', e); } }); }; /** * 清除所有主题监听器 */ const clearThemeListeners = () => { themeListeners.clear(); }; /** * 注册自定义主题 */ const registerTheme = (name, config) => { THEMES[name] = { bg: config.bg || THEMES.light.bg, text: config.text || THEMES.light.text, border: config.border || THEMES.light.border, shadow: config.shadow || THEMES.light.shadow, hoverShadow: config.hoverShadow || THEMES.light.hoverShadow, progressBg: config.progressBg || THEMES.light.progressBg, closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg, }; }; /** * 注销自定义主题 */ const unregisterTheme = (name) => { if (name === 'light' || name === 'dark' || name === 'auto') { console.warn('Cannot unregister built-in theme:', name); return; } delete THEMES[name]; }; /** * 获取所有主题 */ const getAllThemes = () => { return { ...THEMES }; }; /** * 获取主题名称列表 */ const getThemeNames = () => { return Object.keys(THEMES); }; /** * 检查主题是否存在 */ const hasTheme = (name) => { return name in THEMES; }; /** * 获取主题预览 */ const getThemePreview = (theme) => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return { name: theme, resolved, colors: { background: config.bg, text: config.text, border: config.border, }, isDark: resolved === 'dark', isLight: resolved === 'light', isAuto: theme === 'auto', }; }; /** * 生成主题CSS */ const generateThemeCSS = (theme) => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return ` .met-theme-${resolved} { --met-bg: ${config.bg}; --met-text: ${config.text}; --met-border: ${config.border}; --met-shadow: ${config.shadow}; --met-hover-shadow: ${config.hoverShadow}; --met-progress-bg: ${config.progressBg}; --met-close-hover-bg: ${config.closeHoverBg}; } `; }; /** * 应用主题CSS */ const applyThemeCSS = (theme) => { if (typeof document === 'undefined') return; const css = generateThemeCSS(theme); const styleId = 'metona-toast-theme-css'; const oldStyle = document.getElementById(styleId); if (oldStyle) { oldStyle.remove(); } const style = document.createElement('style'); style.id = styleId; style.textContent = css; document.head.appendChild(style); }; /** * 移除主题CSS */ const removeThemeCSS = () => { if (typeof document === 'undefined') return; const styleId = 'metona-toast-theme-css'; const style = document.getElementById(styleId); if (style) { style.remove(); } }; /** * 主题工具 — 代理层 */ const themeUtils = { getSystemTheme, resolveTheme, getThemeConfig, applyTheme, getCurrentTheme, getResolvedTheme, switchTheme, toggleTheme, resetToAuto, initTheme, watchSystemTheme, unwatchSystemTheme, addThemeListener, removeThemeListener, clearThemeListeners, registerTheme, unregisterTheme, getAllThemes, getThemeNames, hasTheme, getThemePreview, generateThemeCSS, applyThemeCSS, removeThemeCSS, saveTheme, loadTheme, }; /** * MetonaToast i18n — 国际化管理 * @module i18n * @version 0.5.0 */ // 当前语言状态 let currentLocale = 'zh-CN'; const localeListeners = new Set(); let fallbackLocale = 'zh-CN'; /** * 获取当前语言 */ const getCurrentLocale = () => { return currentLocale; }; /** * 设置当前语言 */ const setCurrentLocale = (locale) => { if (!LOCALES[locale]) { console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`); locale = fallbackLocale; } currentLocale = locale; notifyLocaleListeners(locale); saveLocale(locale); }; /** * 获取回退语言 */ const getFallbackLocale = () => { return fallbackLocale; }; /** * 设置回退语言 */ const setFallbackLocale = (locale) => { if (!LOCALES[locale]) { console.warn(`Fallback locale "${locale}" not found`); return; } fallbackLocale = locale; }; /** * 翻译函数 */ const t = (key, params = {}) => { const currentTranslation = getTranslation(currentLocale, key); if (currentTranslation !== undefined) { return interpolate(currentTranslation, params); } if (currentLocale !== fallbackLocale) { const fallbackTranslation = getTranslation(fallbackLocale, key); if (fallbackTranslation !== undefined) { return interpolate(fallbackTranslation, params); } } console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`); return key; }; /** * 获取翻译 */ const getTranslation = (locale, key) => { const localeData = LOCALES[locale]; if (!localeData) return undefined; const keys = key.split('.'); let result = localeData; for (const k of keys) { if (result && typeof result === 'object' && k in result) { result = result[k]; } else { return undefined; } } return typeof result === 'string' ? result : undefined; }; /** * 插值函数 */ const interpolate = (str, params) => { return str.replace(/\{(\w+)\}/g, (_match, key) => { return params[key] !== undefined ? String(params[key]) : _match; }); }; /** * 检查翻译是否存在 */ const hasTranslation = (key) => { return getTranslation(currentLocale, key) !== undefined || getTranslation(fallbackLocale, key) !== undefined; }; /** * 获取所有翻译 */ const getTranslations = (locale) => { return LOCALES[locale] || {}; }; /** * 添加翻译 */ const addTranslations = (locale, translations) => { if (!LOCALES[locale]) { LOCALES[locale] = {}; } deepMerge(LOCALES[locale], translations); }; /** * 深度合并对象 */ const deepMerge = (target, source) => { for (const key in source) { if (source[key] instanceof Object && key in target && target[key] instanceof Object) { deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }; /** * 移除翻译 */ const removeTranslation = (locale, key) => { const localeData = LOCALES[locale]; if (!localeData) return; const keys = key.split('.'); let current = localeData; for (let i = 0; i < keys.length - 1; i++) { if (current[keys[i]] && typeof current[keys[i]] === 'object') { current = current[keys[i]]; } else { return; } } delete current[keys[keys.length - 1]]; }; /** * 清除翻译 */ const clearTranslations = (locale) => { if (LOCALES[locale]) { LOCALES[locale] = {}; } }; /** * 获取支持的语言列表 */ const getSupportedLocales = () => { return Object.keys(LOCALES); }; /** * 检查语言是否支持 */ const isLocaleSupported = (locale) => { return locale in LOCALES; }; /** * 获取语言名称 */ const getLocaleName = (locale) => { const names = { 'zh-CN': '简体中文', 'zh-TW': '繁體中文', 'en-US': 'English (US)', 'en-GB': 'English (UK)', 'ja': '日本語', 'ko': '한국어', 'fr': 'Français', 'de': 'Deutsch', 'es': 'Español', 'pt': 'Português', 'ru': 'Русский', 'ar': 'العربية', 'hi': 'हिन्दी', 'th': 'ไทย', 'vi': 'Tiếng Việt', 'id': 'Bahasa Indonesia', 'ms': 'Bahasa Melayu', 'tr': 'Türkçe', 'it': 'Italiano', 'nl': 'Nederlands', 'pl': 'Polski', 'uk': 'Українська', 'cs': 'Čeština', 'sv': 'Svenska', 'da': 'Dansk', 'fi': 'Suomi', 'nb': 'Norsk', 'el': 'Ελληνικά', 'he': 'עברית', 'hu': 'Magyar', 'ro': 'Română', 'bg': 'Български', 'hr': 'Hrvatski', 'sk': 'Slovenčina', 'sl': 'Slovenščina', 'et': 'Eesti', 'lv': 'Latviešu', 'lt': 'Lietuvių', 'ca': 'Català', 'gl': 'Galego', 'eu': 'Euskara', 'cy': 'Cymraeg', 'ga': 'Gaeilge', 'mt': 'Malti', 'is': 'Íslenska', 'mk': 'Македонски', 'sq': 'Shqip', 'sr': 'Српски', 'bs': 'Bosanski', 'me': 'Crnogorski', 'ka': 'ქართული', 'hy': 'Հայերեն', 'az': 'Azərbaycan', 'uz': "O'zbek", 'kk': 'Қазақ', 'ky': 'Кыргыз', 'tg': 'Тоҷикӣ', 'tk': 'Türkmen', 'mn': 'Монгол', 'ne': 'नेपाली', 'si': 'සිංහල', 'my': 'မြန်မာ', 'km': 'ខ្មែរ', 'lo': 'ລາວ', 'am': 'አማርኛ', 'sw': 'Kiswahili', 'yo': 'Yorùbá', 'ig': 'Igbo', 'ha': 'Hausa', 'zu': 'isiZulu', 'af': 'Afrikaans', 'xh': 'isiXhosa', 'st': 'Sesotho', 'tn': 'Setswana', 'ts': 'Xitsonga', 'ss': 'siSwati', 've': 'Tshivenda', 'nr': 'isiNdebele', }; return names[locale] || locale; }; /** * 获取语言方向 */ const getLocaleDirection = (locale) => { const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug']; return rtlLocales.includes(locale) ? 'rtl' : 'ltr'; }; /** * 获取语言信息 */ const getLocaleInfo = (locale) => { return { code: locale, name: getLocaleName(locale), direction: getLocaleDirection(locale), isSupported: isLocaleSupported(locale), isCurrent: locale === currentLocale, isFallback: locale === fallbackLocale, }; }; /** * 获取所有语言信息 */ const getAllLocaleInfo = () => { return getSupportedLocales().map(getLocaleInfo); }; /** * 保存语言到本地存储 */ const saveLocale = (locale) => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-locale', locale); } catch (e) { console.warn('Failed to save locale:', e); } } }; /** * 从本地存储加载语言 */ const loadLocale = () => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-locale') || getDefaultLocale(); } catch (e) { console.warn('Failed to load locale:', e); } } return getDefaultLocale(); }; /** * 获取默认语言 */ const getDefaultLocale = () => { if (typeof navigator !== 'undefined') { const browserLocale = navigator.language || navigator.userLanguage; if (browserLocale && isLocaleSupported(browserLocale)) { return browserLocale; } const shortLocale = browserLocale?.split('-')[0]; if (shortLocale && isLocaleSupported(shortLocale)) { return shortLocale; } } return fallbackLocale; }; /** * 初始化国际化系统 */ const initI18n = () => { const savedLocale = loadLocale(); setCurrentLocale(savedLocale); }; /** * 切换语言 */ const switchLocale = (locale) => { setCurrentLocale(locale); }; /** * 添加语言监听器 */ const addLocaleListener = (listener) => { localeListeners.add(listener); return () => { localeListeners.delete(listener); }; }; /** * 移除语言监听器 */ const removeLocaleListener = (listener) => { localeListeners.delete(listener); }; /** * 通知语言监听器 */ const notifyLocaleListeners = (locale) => { localeListeners.forEach((listener) => { try { listener(locale); } catch (e) { console.error('Locale listener error:', e); } }); }; /** * 清除所有语言监听器 */ const clearLocaleListeners = () => { localeListeners.clear(); }; /** * 格式化数字 */ const formatNumber = (number, options = {}) => { try { return new Intl.NumberFormat(currentLocale, options).format(number); } catch { 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.0 */ /** * Toast 类 — 核心通知组件 */ class Toast { /** * 设置 Toast 类级别的回调函数 */ static setCallbacks(callbacks) { if (callbacks.onError !== undefined) Toast._onError = callbacks.onError; if (callbacks.removeToast !== undefined) Toast._removeToast = callbacks.removeToast; } static on(name, fn) { if (!this._hooks.has(name)) this._hooks.set(name, []); this._hooks.get(name).push(fn); // 用类名引用而非 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 ? `