(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 2.0.0 * @description 通用工具函数集合 */ /** * 生成唯一ID * @returns {string} 唯一ID */ const generateId = () => { return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); }; /** * HTML转义 * @param {string} s - 输入字符串 * @returns {string} 转义后的字符串 */ const escapeHTML = (s) => { if (typeof document === 'undefined') { // Node.js环境 return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } const div = document.createElement('div'); div.textContent = String(s == null ? '' : s); return div.innerHTML; }; /** * 检测暗色模式偏好 * @returns {boolean} 是否偏好暗色模式 */ const prefersDark = () => { return typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; }; /** * MetonaToast Icons — 图标SVG定义 * @module icons * @version 2.0.0 * @description 80+ 内置SVG图标 */ const ICONS = { success: ``, 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 2.0.0 * @description 内置 zh-CN / en-US 完整翻译 */ const LOCALES = { 'zh-CN': { close: '关闭', loading: '加载中...', success: '操作成功', error: '操作失败', warning: '警告', info: '信息', confirm: '确认', cancel: '取消', yes: '是', no: '否', ok: '确定', retry: '重试', undo: '撤销', redo: '重做', save: '保存', delete: '删除', edit: '编辑', add: '添加', remove: '移除', search: '搜索', filter: '筛选', sort: '排序', refresh: '刷新', sync: '同步', upload: '上传', download: '下载', share: '分享', copy: '复制', cut: '剪切', paste: '粘贴', print: '打印', export: '导出', import: '导入', settings: '设置', help: '帮助', about: '关于', version: '版本', language: '语言', theme: '主题', dark: '暗色', light: '亮色', auto: '自动', notifications: '通知', enable: '启用', disable: '禁用', on: '开启', off: '关闭', max: '最大', min: '最小', expand: '展开', collapse: '折叠', fullscreen: '全屏', exitFullscreen: '退出全屏', zoomIn: '放大', zoomOut: '缩小', reset: '重置', apply: '应用', submit: '提交', send: '发送', receive: '接收', connect: '连接', disconnect: '断开', start: '开始', stop: '停止', pause: '暂停', resume: '继续', next: '下一个', previous: '上一个', first: '第一个', last: '最后一个', back: '返回', forward: '前进', home: '首页', menu: '菜单', more: '更多', less: '更少', show: '显示', hide: '隐藏', visible: '可见', hidden: '隐藏', enabled: '已启用', disabled: '已禁用', active: '活跃', inactive: '非活跃', online: '在线', offline: '离线', connected: '已连接', disconnected: '已断开', loading: '加载中', loaded: '已加载', saving: '保存中', saved: '已保存', deleting: '删除中', deleted: '已删除', uploading: '上传中', uploaded: '已上传', downloading: '下载中', downloaded: '已下载', processing: '处理中', processed: '已处理', syncing: '同步中', synced: '已同步', sending: '发送中', sent: '已发送', receiving: '接收中', received: '已接收', connecting: '连接中', error: '错误', warning: '警告', info: '信息', success: '成功', question: '问题', help: '帮助', feedback: '反馈', report: '报告', bug: '错误', feature: '功能', improvement: '改进', suggestion: '建议', request: '请求', response: '响应', status: '状态', progress: '进度', complete: '完成', incomplete: '未完成', pending: '待处理', processing: '处理中', failed: '失败', cancelled: '已取消', timeout: '超时', expired: '已过期', invalid: '无效', valid: '有效', required: '必填', optional: '可选', enabled: '启用', disabled: '禁用', allowed: '允许', denied: '拒绝', approved: '已批准', rejected: '已拒绝', accepted: '已接受', declined: '已拒绝', confirmed: '已确认', unconfirmed: '未确认', verified: '已验证', unverified: '未验证', authenticated: '已认证', unauthenticated: '未认证', authorized: '已授权', unauthorized: '未授权', public: '公开', private: '私有', protected: '受保护', internal: '内部', external: '外部', local: '本地', remote: '远程', global: '全局', system: '系统', user: '用户', admin: '管理员', moderator: '版主', guest: '访客', member: '成员', owner: '所有者', creator: '创建者', editor: '编辑者', viewer: '查看者', subscriber: '订阅者', follower: '关注者', following: '关注中', friend: '朋友', contact: '联系人', group: '群组', team: '团队', organization: '组织', company: '公司', department: '部门', project: '项目', task: '任务', issue: '问题', ticket: '工单', request: '请求', response: '响应', message: '消息', notification: '通知', alert: '警报', reminder: '提醒', event: '事件', activity: '活动', log: '日志', report: '报告', analytics: '分析', statistics: '统计', metrics: '指标', performance: '性能', security: '安全', privacy: '隐私', compliance: '合规', policy: '政策', terms: '条款', conditions: '条件', agreement: '协议', contract: '合同', license: '许可证', copyright: '版权', trademark: '商标', patent: '专利', tradeSecret: '商业秘密', confidential: '机密', sensitive: '敏感', critical: '关键', important: '重要', urgent: '紧急', high: '高', medium: '中', low: '低', none: '无', all: '全部', any: '任何', some: '一些', each: '每个', every: '每个', both: '两者', neither: '两者都不', either: '任一', other: '其他', another: '另一个', this: '这个', that: '那个', these: '这些', those: '那些', here: '这里', there: '那里', where: '哪里', when: '何时', why: '为什么', how: '如何', what: '什么', who: '谁', which: '哪个', whose: '谁的', whom: '谁', myself: '我自己', yourself: '你自己', himself: '他自己', herself: '她自己', itself: '它自己', ourselves: '我们自己', yourselves: '你们自己', themselves: '他们自己', mine: '我的', yours: '你的', his: '他的', hers: '她的', its: '它的', ours: '我们的', yours: '你们的', theirs: '他们的', me: '我', you: '你', him: '他', her: '她', it: '它', us: '我们', them: '他们', my: '我的', your: '你的', his: '他的', her: '她的', its: '它的', our: '我们的', your: '你们的', their: '他们的', i: '我', you: '你', he: '他', she: '她', it: '它', we: '我们', they: '他们', am: '是', is: '是', are: '是', was: '是', were: '是', be: '是', been: '是', being: '是', have: '有', has: '有', had: '有', having: '有', do: '做', does: '做', did: '做', doing: '做', will: '会', would: '会', shall: '将', should: '应该', may: '可以', might: '可能', can: '能', could: '能', must: '必须', need: '需要', dare: '敢', ought: '应该', used: '过去常常', to: '到', of: '的', in: '在', for: '为了', on: '在', with: '和', at: '在', by: '通过', from: '从', into: '进入', during: '在...期间', before: '在...之前', after: '在...之后', above: '在...上面', below: '在...下面', between: '在...之间', under: '在...下面', over: '在...上面', across: '穿过', through: '通过', into: '进入', towards: '朝向', upon: '在...上面', about: '关于', against: '反对', among: '在...之中', along: '沿着', around: '周围', beyond: '超出', but: '但是', despite: '尽管', except: '除了', inside: '里面', outside: '外面', since: '自从', until: '直到', unless: '除非', whether: '是否', while: '当...时候', although: '虽然', because: '因为', if: '如果', once: '一旦', since: '自从', so: '所以', that: '那个', though: '虽然', till: '直到', unless: '除非', until: '直到', when: '当...时候', whenever: '每当', where: '哪里', wherever: '无论哪里', whereas: '然而', wherever: '无论哪里', while: '当...时候', why: '为什么', }, 'en-US': { close: 'Close', loading: 'Loading...', success: 'Success', error: 'Error', warning: 'Warning', info: 'Info', confirm: 'Confirm', cancel: 'Cancel', yes: 'Yes', no: 'No', ok: 'OK', retry: 'Retry', undo: 'Undo', redo: 'Redo', save: 'Save', delete: 'Delete', edit: 'Edit', add: 'Add', remove: 'Remove', search: 'Search', filter: 'Filter', sort: 'Sort', refresh: 'Refresh', sync: 'Sync', upload: 'Upload', download: 'Download', share: 'Share', copy: 'Copy', cut: 'Cut', paste: 'Paste', print: 'Print', export: 'Export', import: 'Import', settings: 'Settings', help: 'Help', about: 'About', version: 'Version', language: 'Language', theme: 'Theme', dark: 'Dark', light: 'Light', auto: 'Auto', notifications: 'Notifications', enable: 'Enable', disable: 'Disable', on: 'On', off: 'Off', max: 'Max', min: 'Min', expand: 'Expand', collapse: 'Collapse', fullscreen: 'Fullscreen', exitFullscreen: 'Exit Fullscreen', zoomIn: 'Zoom In', zoomOut: 'Zoom Out', reset: 'Reset', apply: 'Apply', submit: 'Submit', send: 'Send', receive: 'Receive', connect: 'Connect', disconnect: 'Disconnect', start: 'Start', stop: 'Stop', pause: 'Pause', resume: 'Resume', next: 'Next', previous: 'Previous', first: 'First', last: 'Last', back: 'Back', forward: 'Forward', home: 'Home', menu: 'Menu', more: 'More', less: 'Less', show: 'Show', hide: 'Hide', visible: 'Visible', hidden: 'Hidden', enabled: 'Enabled', disabled: 'Disabled', active: 'Active', inactive: 'Inactive', online: 'Online', offline: 'Offline', connected: 'Connected', disconnected: 'Disconnected', loading: 'Loading', loaded: 'Loaded', saving: 'Saving', saved: 'Saved', deleting: 'Deleting', deleted: 'Deleted', uploading: 'Uploading', uploaded: 'Uploaded', downloading: 'Downloading', downloaded: 'Downloaded', processing: 'Processing', processed: 'Processed', syncing: 'Syncing', synced: 'Synced', sending: 'Sending', sent: 'Sent', receiving: 'Receiving', received: 'Received', connecting: 'Connecting', error: 'Error', warning: 'Warning', info: 'Info', success: 'Success', question: 'Question', help: 'Help', feedback: 'Feedback', report: 'Report', bug: 'Bug', feature: 'Feature', improvement: 'Improvement', suggestion: 'Suggestion', request: 'Request', response: 'Response', status: 'Status', progress: 'Progress', complete: 'Complete', incomplete: 'Incomplete', pending: 'Pending', processing: 'Processing', failed: 'Failed', cancelled: 'Cancelled', timeout: 'Timeout', expired: 'Expired', invalid: 'Invalid', valid: 'Valid', required: 'Required', optional: 'Optional', enabled: 'Enabled', disabled: 'Disabled', allowed: 'Allowed', denied: 'Denied', approved: 'Approved', rejected: 'Rejected', accepted: 'Accepted', declined: 'Declined', confirmed: 'Confirmed', unconfirmed: 'Unconfirmed', verified: 'Verified', unverified: 'Unverified', authenticated: 'Authenticated', unauthenticated: 'Unauthenticated', authorized: 'Authorized', unauthorized: 'Unauthorized', public: 'Public', private: 'Private', protected: 'Protected', internal: 'Internal', external: 'External', local: 'Local', remote: 'Remote', global: 'Global', system: 'System', user: 'User', admin: 'Admin', moderator: 'Moderator', guest: 'Guest', member: 'Member', owner: 'Owner', creator: 'Creator', editor: 'Editor', viewer: 'Viewer', subscriber: 'Subscriber', follower: 'Follower', following: 'Following', friend: 'Friend', contact: 'Contact', group: 'Group', team: 'Team', organization: 'Organization', company: 'Company', department: 'Department', project: 'Project', task: 'Task', issue: 'Issue', ticket: 'Ticket', request: 'Request', response: 'Response', message: 'Message', notification: 'Notification', alert: 'Alert', reminder: 'Reminder', event: 'Event', activity: 'Activity', log: 'Log', report: 'Report', analytics: 'Analytics', statistics: 'Statistics', metrics: 'Metrics', performance: 'Performance', security: 'Security', privacy: 'Privacy', compliance: 'Compliance', policy: 'Policy', terms: 'Terms', conditions: 'Conditions', agreement: 'Agreement', contract: 'Contract', license: 'License', copyright: 'Copyright', trademark: 'Trademark', patent: 'Patent', tradeSecret: 'Trade Secret', confidential: 'Confidential', sensitive: 'Sensitive', critical: 'Critical', important: 'Important', urgent: 'Urgent', high: 'High', medium: 'Medium', low: 'Low', none: 'None', all: 'All', any: 'Any', some: 'Some', each: 'Each', every: 'Every', both: 'Both', neither: 'Neither', either: 'Either', other: 'Other', another: 'Another', this: 'This', that: 'That', these: 'These', those: 'Those', here: 'Here', there: 'There', where: 'Where', when: 'When', why: 'Why', how: 'How', what: 'What', who: 'Who', which: 'Which', whose: 'Whose', whom: 'Whom', myself: 'Myself', yourself: 'Yourself', himself: 'Himself', herself: 'Herself', itself: 'Itself', ourselves: 'Ourselves', yourselves: 'Yourselves', themselves: 'Themselves', mine: 'Mine', yours: 'Yours', his: 'His', hers: 'Hers', its: 'Its', ours: 'Ours', yours: 'Yours', theirs: 'Theirs', me: 'Me', you: 'You', him: 'Him', her: 'Her', it: 'It', us: 'Us', them: 'Them', my: 'My', your: 'Your', his: 'His', her: 'Her', its: 'Its', our: 'Our', your: 'Your', their: 'Their', i: 'I', you: 'You', he: 'He', she: 'She', it: 'It', we: 'We', they: 'They', am: 'Am', is: 'Is', are: 'Are', was: 'Was', were: 'Were', be: 'Be', been: 'Been', being: 'Being', have: 'Have', has: 'Has', had: 'Had', having: 'Having', do: 'Do', does: 'Does', did: 'Did', doing: 'Doing', will: 'Will', would: 'Would', shall: 'Shall', should: 'Should', may: 'May', might: 'Might', can: 'Can', could: 'Could', must: 'Must', need: 'Need', dare: 'Dare', ought: 'Ought', used: 'Used', to: 'To', of: 'Of', in: 'In', for: 'For', on: 'On', with: 'With', at: 'At', by: 'By', from: 'From', into: 'Into', during: 'During', before: 'Before', after: 'After', above: 'Above', below: 'Below', between: 'Between', under: 'Under', over: 'Over', across: 'Across', through: 'Through', into: 'Into', towards: 'Towards', upon: 'Upon', about: 'About', against: 'Against', among: 'Among', along: 'Along', around: 'Around', beyond: 'Beyond', but: 'But', despite: 'Despite', except: 'Except', inside: 'Inside', outside: 'Outside', since: 'Since', until: 'Until', unless: 'Unless', whether: 'Whether', while: 'While', although: 'Although', because: 'Because', if: 'If', once: 'Once', since: 'Since', so: 'So', that: 'That', though: 'Though', till: 'Till', unless: 'Unless', until: 'Until', when: 'When', whenever: 'Whenever', where: 'Where', wherever: 'Wherever', whereas: 'Whereas', wherever: 'Wherever', while: 'While', why: 'Why', }, }; /** * MetonaToast Constants - 常量定义 * @module constants * @version 2.0.0 * @description 默认配置、颜色、动画、主题等常量 */ /** * 默认配置 */ const DEFAULTS = Object.freeze({ // 基础配置 position: 'top-right', duration: 4000, max: 6, gap: 12, offset: 24, // 交互配置 pauseOnHover: true, closeOnClick: true, draggable: true, // 显示配置 showProgress: true, progressDirection: 'horizontal', icon: true, closeButton: true, // 主题配置 theme: 'auto', animation: 'slide', // 布局配置 zIndex: 9999, width: 360, // 自定义配置 className: '', style: {}, // 回调函数 onShow: null, onClose: null, onClick: null, onUpdate: null, // 高级 resetTimerOnUpdate: false, notifyWhenHidden: false, // 国际化 locale: 'zh-CN', // 插件 plugins: [], }); /** * 类型颜色配置 */ const TYPE_COLORS = { success: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, error: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, warning: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, info: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, loading: { fg: '#6366f1', bg: 'rgba(99, 102, 241, 0.1)' }, default: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, check: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, x: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, alert: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, question: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, star: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, heart: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' }, bell: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, mail: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, settings: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, user: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, home: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, search: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, plus: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, minus: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, edit: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, trash: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, download: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, upload: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, share: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, link: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, external: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, clock: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, calendar: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, map: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, compass: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, globe: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, wifi: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, cloud: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, sun: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, moon: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, zap: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, activity: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, cpu: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, database: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, server: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, terminal: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, code: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, git: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, package: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, layers: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, grid: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, list: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, filter: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, sort: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, refresh: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, sync: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, power: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, battery: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, bluetooth: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, volume: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, mic: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, camera: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, image: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, video: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, music: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, file: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, folder: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, clipboard: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, save: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, print: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, eye: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, eyeOff: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, lock: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, unlock: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, shield: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, key: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, flag: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, bookmark: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, tag: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, gift: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' }, award: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, target: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, crosshair: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, move: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, maximize: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, minimize: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, copy: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, cut: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, paste: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, rotateCw: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, rotateCcw: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, zoomIn: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, zoomOut: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, crop: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, sliders: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, toggleLeft: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, toggleRight: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, checkCircle: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, xCircle: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, alertCircle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, infoCircle: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, helpCircle: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, alertTriangle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, checkSquare: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, square: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, circle: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, triangle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, hexagon: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, octagon: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, pentagon: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, diamond: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' }, }; /** * 动画配置 */ const ANIMATIONS = { slide: { enter: { transform: 'translateX(80px)', opacity: 0 }, leave: { transform: 'translateX(120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, fade: { enter: { opacity: 0, filter: 'blur(3px)' }, leave: { opacity: 0, filter: 'blur(3px)' }, duration: 500, easing: 'ease', }, scale: { enter: { transform: 'scale(0.55)', opacity: 0 }, leave: { transform: 'scale(0.55)', opacity: 0 }, duration: 450, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', }, bounce: { enter: { transform: 'translateY(-80px)', opacity: 0 }, leave: { transform: 'translateY(20px)', opacity: 0 }, duration: 650, easing: 'ease', }, flip: { enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: 0 }, leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: 0 }, duration: 500, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, rotate: { enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: 0 }, leave: { transform: 'rotate(25deg) scale(0.6)', opacity: 0 }, duration: 500, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, zoom: { enter: { transform: 'scale(0.1)', opacity: 0 }, leave: { transform: 'scale(0.1)', opacity: 0 }, duration: 500, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', }, slideUp: { enter: { transform: 'translateY(60px)', opacity: 0 }, leave: { transform: 'translateY(-120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, slideDown: { enter: { transform: 'translateY(-60px)', opacity: 0 }, leave: { transform: 'translateY(120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, slideLeft: { enter: { transform: 'translateX(-90px)', opacity: 0 }, leave: { transform: 'translateX(120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, slideRight: { enter: { transform: 'translateX(90px)', opacity: 0 }, leave: { transform: 'translateX(-120%)', opacity: 0 }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, }; /** * 主题配置 */ const THEMES = { light: { bg: 'rgba(255, 255, 255, 0.96)', text: '#1f2937', border: 'rgba(0, 0, 0, 0.06)', shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08)', hoverShadow: '0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10)', progressBg: 'rgba(0, 0, 0, 0.06)', closeHoverBg: 'rgba(0, 0, 0, 0.06)', }, dark: { bg: 'rgba(28, 32, 40, 0.94)', text: '#e6e8eb', border: 'rgba(255, 255, 255, 0.08)', shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4)', hoverShadow: '0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4)', progressBg: 'rgba(255, 255, 255, 0.08)', closeHoverBg: 'rgba(255, 255, 255, 0.08)', }, auto: 'auto', warm: { bg: 'rgba(255, 251, 235, 0.96)', text: '#78350f', border: 'rgba(245, 158, 11, 0.2)', shadow: '0 10px 36px -10px rgba(245, 158, 11, 0.18), 0 4px 14px -4px rgba(245, 158, 11, 0.08)', hoverShadow: '0 14px 48px -10px rgba(245, 158, 11, 0.22), 0 6px 18px -4px rgba(245, 158, 11, 0.10)', progressBg: 'rgba(245, 158, 11, 0.1)', closeHoverBg: 'rgba(245, 158, 11, 0.1)', }, }; /** * MetonaToast Styles - 样式管理 * @module styles * @version 2.0.0 * @description 样式注入、更新和主题管理 */ let styleElement = null; /** * 生成CSS样式 * @param {Object} theme - 主题配置 * @returns {string} CSS字符串 */ const generateCSS = (theme) => { return ` /* MetonaToast 基础样式 */ .met-container { pointer-events: none; display: flex; flex-direction: column; gap: 12px; box-sizing: border-box; padding: 24px; max-width: 100vw; position: fixed; z-index: 9999; } /* 位置样式 — 顶部位置用 column-reverse,新 toast 出现在最上方 */ .met-container.top-left { top: 0; left: 0; align-items: flex-start; flex-direction: column-reverse; } .met-container.top-center { top: 0; left: 0; right: 0; align-items: center; flex-direction: column-reverse; } .met-container.top-right { top: 0; right: 0; align-items: flex-end; flex-direction: column-reverse; } .met-container.bottom-left { bottom: 0; left: 0; align-items: flex-start; flex-direction: column-reverse; } .met-container.bottom-center { bottom: 0; left: 0; right: 0; align-items: center; flex-direction: column-reverse; } .met-container.bottom-right { bottom: 0; right: 0; align-items: flex-end; flex-direction: column-reverse; } /* Toast基础样式 */ .met-toast { position: relative; display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border-radius: 12px; box-sizing: border-box; backdrop-filter: blur(14px) saturate(180%); -webkit-backdrop-filter: blur(14px) saturate(180%); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; user-select: none; -webkit-user-select: none; transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s ease, opacity 0.25s ease; will-change: transform, opacity; flex-shrink: 0; pointer-events: auto; min-width: 240px; max-width: calc(100vw - 48px); width: 360px; border: 1px solid var(--met-border, rgba(0,0,0,0.06)); background: var(--met-bg, rgba(255,255,255,0.96)); color: var(--met-text, #1f2937); box-shadow: var(--met-shadow, 0 10px 36px -10px rgba(0,0,0,0.18), 0 4px 14px -4px rgba(0,0,0,0.08)); } /* 悬停效果 */ .met-toast:hover { box-shadow: var(--met-hover-shadow, 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10)) !important; } /* 可点击状态 */ .met-toast.met-clickable { cursor: pointer; } /* 图标样式 */ .met-icon { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-top: 1px; } /* 内容样式 */ .met-content { flex: 1; min-width: 0; word-wrap: break-word; overflow-wrap: break-word; } /* 标题样式 */ .met-title { font-weight: 600; font-size: 14px; letter-spacing: 0.1px; } /* 消息样式 */ .met-message { font-size: 13px; opacity: 0.85; margin-top: 2px; } /* 关闭按钮样式 */ .met-close { flex-shrink: 0; background: transparent; border: 0; padding: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: background 0.2s, color 0.2s; line-height: 0; color: inherit; opacity: 0.5; } .met-close:hover { opacity: 1; background: var(--met-close-hover-bg, rgba(0,0,0,0.06)); } /* 进度条样式 - 水平 */ .met-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; overflow: hidden; background: var(--met-progress-bg, rgba(0,0,0,0.06)); border-radius: 0 0 12px 12px; } /* 进度条样式 - 垂直 */ .met-progress-v { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; overflow: hidden; background: var(--met-progress-bg, rgba(0,0,0,0.06)); border-radius: 12px 0 0 12px; } /* 进度条指示器 - 水平 */ .met-bar { position: absolute; left: 0; top: 0; height: 100%; width: 100%; transform-origin: left; transform: scaleX(1); } /* 进度条指示器 - 垂直 */ .met-bar-v { position: absolute; left: 0; bottom: 0; width: 100%; height: 100%; transform-origin: bottom; transform: scaleY(1); } /* 侧边指示器 */ .met-side { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; border-radius: 12px 0 0 12px; } /* 加载动画 */ .met-spin { animation: met-rot 1s linear infinite; transform-origin: 50% 50%; } /* 离开状态 */ .met-toast.met-leaving { pointer-events: none; z-index: 1; } /* 动画关键帧 */ @keyframes met-rot { to { transform: rotate(360deg); } } /* === 进入动画:初始隐藏态 === */ .met-anim-slide.met-toast, .met-anim-fade.met-toast, .met-anim-scale.met-toast, .met-anim-bounce.met-toast, .met-anim-flip.met-toast, .met-anim-rotate.met-toast, .met-anim-zoom.met-toast, .met-anim-slideUp.met-toast, .met-anim-slideDown.met-toast, .met-anim-slideLeft.met-toast, .met-anim-slideRight.met-toast { opacity: 0; } /* === 进入动画:@keyframes 定义 === */ @keyframes met-slide-in { 0% { transform: translateX(80px); opacity: 0; } 65% { transform: translateX(-6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } @keyframes met-fade-in { 0% { opacity: 0; filter: blur(3px); } 100% { opacity: 1; filter: blur(0); } } @keyframes met-scale-in { 0% { transform: scale(0.55); opacity: 0; } 65% { transform: scale(1.07); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } @keyframes met-bounce-in { 0% { transform: translateY(-80px); opacity: 0; animation-timing-function: ease-in; } 45% { transform: translateY(14px); opacity: 1; animation-timing-function: ease-out; } 65% { transform: translateY(-12px); animation-timing-function: ease-in; } 82% { transform: translateY(4px); animation-timing-function: ease-out; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-flip-in { 0% { transform: perspective(500px) rotateX(-90deg); opacity: 0; } 60% { transform: perspective(500px) rotateX(8deg); opacity: 1; } 85% { transform: perspective(500px) rotateX(-2deg); } 100% { transform: perspective(500px) rotateX(0); opacity: 1; } } @keyframes met-rotate-in { 0% { transform: rotate(-25deg) scale(0.6); opacity: 0; } 60% { transform: rotate(4deg) scale(1.05); opacity: 1; } 85% { transform: rotate(-1deg) scale(0.98); } 100% { transform: rotate(0) scale(1); opacity: 1; } } @keyframes met-zoom-in { 0% { transform: scale(0.1); opacity: 0; } 50% { transform: scale(1.12); opacity: 1; } 75% { transform: scale(0.94); } 100% { transform: scale(1); opacity: 1; } } @keyframes met-slideUp-in { 0% { transform: translateY(60px); opacity: 0; } 70% { transform: translateY(-6px); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-slideDown-in { 0% { transform: translateY(-60px); opacity: 0; } 70% { transform: translateY(6px); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-slideLeft-in { 0% { transform: translateX(-90px); opacity: 0; } 65% { transform: translateX(6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } @keyframes met-slideRight-in { 0% { transform: translateX(90px); opacity: 0; } 65% { transform: translateX(-6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } /* === 进入动画:绑定到 .met-show === */ .met-anim-slide.met-toast.met-show { animation: met-slide-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-fade.met-toast.met-show { animation: met-fade-in 0.50s ease forwards; } .met-anim-scale.met-toast.met-show { animation: met-scale-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } .met-anim-bounce.met-toast.met-show { animation: met-bounce-in 0.65s ease forwards; } .met-anim-flip.met-toast.met-show { animation: met-flip-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-rotate.met-toast.met-show { animation: met-rotate-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-zoom.met-toast.met-show { animation: met-zoom-in 0.50s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } .met-anim-slideUp.met-toast.met-show { animation: met-slideUp-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideDown.met-toast.met-show { animation: met-slideDown-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideLeft.met-toast.met-show { animation: met-slideLeft-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideRight.met-toast.met-show { animation: met-slideRight-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } /* 响应式设计 */ @media (prefers-reduced-motion: reduce) { .met-toast { transition: opacity 0.15s !important; } .met-toast.met-show { animation: none !important; opacity: 1 !important; } .met-spin { animation-duration: 2.5s !important; } } @media (max-width: 480px) { .met-container { padding: 12px !important; } .met-toast { width: 100% !important; min-width: 0 !important; } } /* 暗色主题特定样式 */ .met-toast.met-theme-dark { background: rgba(28, 32, 40, 0.94); color: #e6e8eb; border-color: rgba(255, 255, 255, 0.08); box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4); } .met-toast.met-theme-dark:hover { box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important; } .met-toast.met-theme-dark .met-close:hover { background: rgba(255, 255, 255, 0.08); } .met-toast.met-theme-dark .met-progress, .met-toast.met-theme-dark .met-progress-v { background: rgba(255, 255, 255, 0.08); } /* 亮色主题特定样式 */ .met-toast.met-theme-light { background: rgba(255, 255, 255, 0.96); color: #1f2937; border-color: rgba(0, 0, 0, 0.06); box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08); } .met-toast.met-theme-light:hover { box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important; } .met-toast.met-theme-light .met-close:hover { background: rgba(0, 0, 0, 0.06); } .met-toast.met-theme-light .met-progress, .met-toast.met-theme-light .met-progress-v { background: rgba(0, 0, 0, 0.06); } /* 自定义主题支持 */ .met-toast[data-theme] { --met-bg: var(--met-theme-bg); --met-text: var(--met-theme-text); --met-border: var(--met-theme-border); --met-shadow: var(--met-theme-shadow); } /* 动画性能优化 */ .met-toast { backface-visibility: hidden; -webkit-backface-visibility: hidden; perspective: 1000; } /* 滚动条样式 */ .met-container::-webkit-scrollbar { width: 6px; height: 6px; } .met-container::-webkit-scrollbar-track { background: transparent; } .met-container::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 3px; } .met-container::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); } /* 打印样式 */ @media print { .met-container { display: none !important; } .met-toast { display: none !important; } } /* 高对比度模式 */ @media (forced-colors: active) { .met-toast { border: 2px solid ButtonText; } .met-close { border: 1px solid ButtonText; } } /* 焦点样式 */ .met-toast:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; } .met-close:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; } /* 禁用状态 */ .met-toast:disabled, .met-toast[disabled] { opacity: 0.5; cursor: not-allowed; } /* 加载状态 */ .met-toast.met-loading { cursor: wait; } /* 成功状态 */ .met-toast.met-success { border-left: 4px solid #10b981; } /* 错误状态 */ .met-toast.met-error { border-left: 4px solid #ef4444; } /* 警告状态 */ .met-toast.met-warning { border-left: 4px solid #f59e0b; } /* 信息状态 */ .met-toast.met-info { border-left: 4px solid #3b82f6; } /* 加载状态 */ .met-toast.met-loading { border-left: 4px solid #6366f1; } /* 进度条动画 */ .met-bar, .met-bar-v { transition: transform 0.1s linear; } /* 容器动画 */ .met-container { transition: all 0.3s ease; } /* Toast进入动画 */ .met-toast.met-enter { animation: met-enter 0.3s ease forwards; } /* Toast离开动画 */ .met-toast.met-leave { animation: met-leave 0.3s ease forwards; } @keyframes met-enter { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } @keyframes met-leave { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-20px); } } /* 响应式字体大小 */ @media (max-width: 768px) { .met-toast { font-size: 13px; } .met-title { font-size: 13px; } .met-message { font-size: 12px; } } @media (max-width: 480px) { .met-toast { font-size: 12px; padding: 12px 14px; } .met-title { font-size: 12px; } .met-message { font-size: 11px; } } /* 无障碍支持 */ .met-toast[role="alert"] { /* 警告类型Toast的特殊样式 */ } .met-toast[role="status"] { /* 状态类型Toast的特殊样式 */ } /* 高对比度模式增强 */ @media (prefers-contrast: high) { .met-toast { border-width: 2px; } .met-close { border: 1px solid currentColor; } } /* 暗色模式自动检测 */ @media (prefers-color-scheme: dark) { .met-theme-auto .met-toast { background: rgba(28, 32, 40, 0.94); color: #e6e8eb; border-color: rgba(255, 255, 255, 0.08); box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4); } .met-theme-auto .met-toast:hover { box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important; } .met-theme-auto .met-close:hover { background: rgba(255, 255, 255, 0.08); } .met-theme-auto .met-progress, .met-theme-auto .met-progress-v { background: rgba(255, 255, 255, 0.08); } } /* 亮色模式自动检测 */ @media (prefers-color-scheme: light) { .met-theme-auto .met-toast { background: rgba(255, 255, 255, 0.96); color: #1f2937; border-color: rgba(0, 0, 0, 0.06); box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08); } .met-theme-auto .met-toast:hover { box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important; } .met-theme-auto .met-close:hover { background: rgba(0, 0, 0, 0.06); } .met-theme-auto .met-progress, .met-theme-auto .met-progress-v { background: rgba(0, 0, 0, 0.06); } } /* 动画性能优化 */ .met-toast { transform: translateZ(0); -webkit-transform: translateZ(0); } /* 触摸设备优化 */ @media (hover: none) { .met-toast:hover { box-shadow: inherit; } } /* 滚动时固定位置 */ .met-container { position: fixed; z-index: 9999; } /* 全屏模式 */ :fullscreen .met-container { z-index: 2147483647; } /* 画中画模式 */ :picture-in-picture .met-container { z-index: 2147483647; } /* 安全区域适配 */ @supports (padding: max(0px)) { .met-container { padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left)); } } /* 暗色模式安全区域 */ @media (prefers-color-scheme: dark) { @supports (padding: max(0px)) { .met-container { padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left)); } } } /* 高分辨率屏幕优化 */ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .met-toast { border-width: 0.5px; } } /* 触摸设备优化 */ @media (hover: none) and (pointer: coarse) { .met-toast { min-height: 48px; } .met-close { min-width: 48px; min-height: 48px; } } /* 键盘导航优化 */ .met-toast:focus { outline: 2px solid #3b82f6; outline-offset: 2px; } .met-toast:focus:not(:focus-visible) { outline: none; } .met-toast:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; } /* 减少动画模式 */ @media (prefers-reduced-motion: reduce) { .met-toast { transition: opacity 0.15s ease !important; animation: none !important; } .met-bar, .met-bar-v { transition: none !important; } .met-container { transition: none !important; } } /* 高对比度模式 */ @media (prefers-contrast: high) { .met-toast { border-width: 3px; border-style: solid; } .met-close { border: 2px solid currentColor; border-radius: 4px; } .met-progress, .met-progress-v { height: 4px; } .met-bar, .met-bar-v { height: 100%; } } /* 低分辨率屏幕优化 */ @media (max-resolution: 1dppx) { .met-toast { border-width: 1px; } } /* 触摸设备优化 */ @media (hover: none) and (pointer: coarse) { .met-toast { min-height: 52px; padding: 16px 18px; } .met-close { min-width: 52px; min-height: 52px; padding: 8px; } .met-icon { width: 28px; height: 28px; } } /* 大屏幕优化 */ @media (min-width: 1200px) { .met-toast { width: 400px; } } /* 超大屏幕优化 */ @media (min-width: 1920px) { .met-toast { width: 440px; } } /* 小屏幕优化 */ @media (max-width: 320px) { .met-toast { width: 100%; min-width: 0; padding: 12px 14px; } .met-container { padding: 8px; } } /* 横屏优化 */ @media (orientation: landscape) and (max-height: 500px) { .met-container { padding: 12px 24px; } .met-toast { min-height: 44px; padding: 10px 14px; } } /* 竖屏优化 */ @media (orientation: portrait) and (max-width: 500px) { .met-container { padding: 12px; } .met-toast { width: 100%; min-width: 0; } } /* 无障碍支持 */ .met-toast[aria-live="assertive"] { /* 紧急通知样式 */ } .met-toast[aria-live="polite"] { /* 普通通知样式 */ } /* 焦点陷阱 */ .met-toast:focus-within { /* 包含焦点元素的Toast样式 */ } /* 加载状态 */ .met-toast[aria-busy="true"] { cursor: wait; } /* 禁用状态 */ .met-toast[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; pointer-events: none; } /* 隐藏状态 */ .met-toast[aria-hidden="true"] { display: none; } /* 展开状态 */ .met-toast[aria-expanded="true"] { /* 展开状态样式 */ } /* 选中状态 */ .met-toast[aria-selected="true"] { /* 选中状态样式 */ } /* 按下状态 */ .met-toast:active { transform: scale(0.98); } /* 加载状态动画 */ .met-toast.met-loading::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 90deg, transparent, rgba(255, 255, 255, 0.1), transparent ); animation: met-loading 1.5s infinite; } @keyframes met-loading { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } /* 成功状态动画 */ .met-toast.met-success::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: #10b981; border-radius: 12px 0 0 12px; animation: met-success 0.3s ease; } @keyframes met-success { from { transform: scaleY(0); } to { transform: scaleY(1); } } /* 错误状态动画 */ .met-toast.met-error::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: #ef4444; border-radius: 12px 0 0 12px; animation: met-error 0.3s ease; } @keyframes met-error { from { transform: scaleY(0); } to { transform: scaleY(1); } } /* 警告状态动画 */ .met-toast.met-warning::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: #f59e0b; border-radius: 12px 0 0 12px; animation: met-warning 0.3s ease; } @keyframes met-warning { from { transform: scaleY(0); } to { transform: scaleY(1); } } /* 信息状态动画 */ .met-toast.met-info::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: #3b82f6; border-radius: 12px 0 0 12px; animation: met-info 0.3s ease; } @keyframes met-info { from { transform: scaleY(0); } to { transform: scaleY(1); } } /* 加载状态动画 */ .met-toast.met-loading::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: #6366f1; border-radius: 12px 0 0 12px; animation: met-loading-bar 1s infinite; } @keyframes met-loading-bar { 0% { transform: scaleY(0); transform-origin: top; } 50% { transform: scaleY(1); transform-origin: top; } 51% { transform-origin: bottom; } 100% { transform: scaleY(0); transform-origin: bottom; } } /* 进度条脉冲动画 */ .met-bar, .met-bar-v { animation: met-pulse 2s infinite; } @keyframes met-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.8; } } /* Toast堆叠效果 */ .met-toast + .met-toast { margin-top: 8px; } /* Toast分组 */ .met-toast-group { display: flex; flex-direction: column; gap: 8px; } /* Toast头部 */ .met-toast-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } /* Toast底部 */ .met-toast-footer { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 12px; } /* Toast操作按钮 */ .met-toast-action { background: transparent; border: 1px solid currentColor; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; opacity: 0.7; transition: opacity 0.2s; } .met-toast-action:hover { opacity: 1; } /* Toast进度文本 */ .met-toast-progress-text { font-size: 12px; opacity: 0.7; margin-left: 8px; } /* Toast时间戳 */ .met-toast-timestamp { font-size: 11px; opacity: 0.5; margin-left: auto; } /* Toast头像 */ .met-toast-avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; flex-shrink: 0; } /* Toast图片 */ .met-toast-image { max-width: 100%; max-height: 200px; border-radius: 8px; margin-top: 8px; object-fit: cover; } /* Toast视频 */ .met-toast-video { max-width: 100%; max-height: 200px; border-radius: 8px; margin-top: 8px; } /* Toast音频 */ .met-toast-audio { width: 100%; margin-top: 8px; } /* Toast代码块 */ .met-toast-code { background: rgba(0, 0, 0, 0.05); padding: 8px 12px; border-radius: 6px; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 12px; margin-top: 8px; overflow-x: auto; } .met-theme-dark .met-toast-code { background: rgba(255, 255, 255, 0.05); } /* Toast引用 */ .met-toast-quote { border-left: 3px solid currentColor; padding-left: 12px; margin-top: 8px; opacity: 0.8; } /* Toast列表 */ .met-toast-list { margin: 8px 0 0 0; padding-left: 20px; } .met-toast-list li { margin-bottom: 4px; } /* Toast表格 */ .met-toast-table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 12px; } .met-toast-table th, .met-toast-table td { border: 1px solid rgba(0, 0, 0, 0.1); padding: 6px 8px; text-align: left; } .met-theme-dark .met-toast-table th, .met-theme-dark .met-toast-table td { border-color: rgba(255, 255, 255, 0.1); } .met-toast-table th { background: rgba(0, 0, 0, 0.05); font-weight: 600; } .met-theme-dark .met-toast-table th { background: rgba(255, 255, 255, 0.05); } /* Toast分割线 */ .met-toast-divider { height: 1px; background: rgba(0, 0, 0, 0.1); margin: 12px 0; } .met-theme-dark .met-toast-divider { background: rgba(255, 255, 255, 0.1); } /* Toast标签 */ .met-toast-tag { display: inline-block; background: rgba(0, 0, 0, 0.05); padding: 2px 8px; border-radius: 4px; font-size: 11px; margin-right: 4px; margin-bottom: 4px; } .met-theme-dark .met-toast-tag { background: rgba(255, 255, 255, 0.05); } /* Toast徽章 */ .met-toast-badge { display: inline-flex; align-items: center; justify-content: center; background: #3b82f6; color: white; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; min-width: 20px; height: 20px; } /* Toast进度环 */ .met-toast-progress-ring { width: 40px; height: 40px; transform: rotate(-90deg); } .met-toast-progress-ring circle { fill: none; stroke-width: 3; stroke-linecap: round; } .met-toast-progress-ring .met-progress-ring-bg { stroke: rgba(0, 0, 0, 0.1); } .met-theme-dark .met-toast-progress-ring .met-progress-ring-bg { stroke: rgba(255, 255, 255, 0.1); } .met-toast-progress-ring .met-progress-ring-fill { stroke: #3b82f6; stroke-dasharray: 100; stroke-dashoffset: 100; transition: stroke-dashoffset 0.3s ease; } /* Toast滑块 */ .met-toast-slider { width: 100%; height: 4px; background: rgba(0, 0, 0, 0.1); border-radius: 2px; margin-top: 8px; position: relative; } .met-theme-dark .met-toast-slider { background: rgba(255, 255, 255, 0.1); } .met-toast-slider-fill { position: absolute; left: 0; top: 0; height: 100%; background: #3b82f6; border-radius: 2px; transition: width 0.3s ease; } /* Toast开关 */ .met-toast-switch { position: relative; display: inline-block; width: 36px; height: 20px; margin-top: 8px; } .met-toast-switch input { opacity: 0; width: 0; height: 0; } .met-toast-switch-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.1); border-radius: 20px; transition: 0.3s; } .met-theme-dark .met-toast-switch-slider { background: rgba(255, 255, 255, 0.1); } .met-toast-switch-slider:before { content: ''; position: absolute; height: 16px; width: 16px; left: 2px; bottom: 2px; background: white; border-radius: 50%; transition: 0.3s; } .met-toast-switch input:checked + .met-toast-switch-slider { background: #3b82f6; } .met-toast-switch input:checked + .met-toast-switch-slider:before { transform: translateX(16px); } /* Toast复选框 */ .met-toast-checkbox { display: flex; align-items: center; gap: 8px; margin-top: 8px; cursor: pointer; } .met-toast-checkbox input { width: 16px; height: 16px; cursor: pointer; } /* Toast单选框 */ .met-toast-radio { display: flex; align-items: center; gap: 8px; margin-top: 8px; cursor: pointer; } .met-toast-radio input { width: 16px; height: 16px; cursor: pointer; } /* Toast输入框 */ .met-toast-input { width: 100%; padding: 8px 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 6px; font-size: 13px; margin-top: 8px; background: transparent; color: inherit; } .met-theme-dark .met-toast-input { border-color: rgba(255, 255, 255, 0.1); } .met-toast-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); } /* Toast文本域 */ .met-toast-textarea { width: 100%; padding: 8px 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 6px; font-size: 13px; margin-top: 8px; background: transparent; color: inherit; resize: vertical; min-height: 80px; } .met-theme-dark .met-toast-textarea { border-color: rgba(255, 255, 255, 0.1); } .met-toast-textarea:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); } /* Toast选择框 */ .met-toast-select { width: 100%; padding: 8px 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 6px; font-size: 13px; margin-top: 8px; background: transparent; color: inherit; cursor: pointer; } .met-theme-dark .met-toast-select { border-color: rgba(255, 255, 255, 0.1); } .met-toast-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); } /* Toast下拉菜单 */ .met-toast-dropdown { position: relative; display: inline-block; } .met-toast-dropdown-content { display: none; position: absolute; background: ${theme.bg}; min-width: 160px; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); border-radius: 8px; z-index: 1; padding: 8px 0; margin-top: 4px; } .met-theme-dark .met-toast-dropdown-content { box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); } .met-toast-dropdown:hover .met-toast-dropdown-content { display: block; } .met-toast-dropdown-item { padding: 8px 16px; cursor: pointer; font-size: 13px; } .met-toast-dropdown-item:hover { background: rgba(0, 0, 0, 0.05); } .met-theme-dark .met-toast-dropdown-item:hover { background: rgba(255, 255, 255, 0.05); } /* Toast工具提示 */ .met-toast-tooltip { position: relative; display: inline-block; } .met-toast-tooltip .met-toast-tooltip-text { visibility: hidden; width: 120px; background: ${theme.bg}; color: ${theme.text}; text-align: center; border-radius: 6px; padding: 6px 8px; position: absolute; z-index: 1; bottom: 125%; left: 50%; margin-left: -60px; opacity: 0; transition: opacity 0.3s; font-size: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .met-theme-dark .met-toast-tooltip .met-toast-tooltip-text { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } .met-toast-tooltip:hover .met-toast-tooltip-text { visibility: visible; opacity: 1; } /* Toast弹出框 */ .met-toast-popover { position: relative; display: inline-block; } .met-toast-popover .met-toast-popover-content { visibility: hidden; background: ${theme.bg}; color: ${theme.text}; border-radius: 8px; padding: 12px 16px; position: absolute; z-index: 1; bottom: 125%; left: 50%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s; min-width: 200px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } .met-theme-dark .met-toast-popover .met-toast-popover-content { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); } .met-toast-popover:hover .met-toast-popover-content { visibility: visible; opacity: 1; } /* Toast模态框 */ .met-toast-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; } .met-toast-modal-content { background: ${theme.bg}; color: ${theme.text}; border-radius: 12px; padding: 24px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; } /* Toast抽屉 */ .met-toast-drawer { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10000; } .met-toast-drawer-content { position: absolute; top: 0; right: 0; width: 300px; height: 100%; background: ${theme.bg}; color: ${theme.text}; padding: 24px; transform: translateX(100%); transition: transform 0.3s ease; } .met-toast-drawer.open .met-toast-drawer-content { transform: translateX(0); } /* Toast通知栏 */ .met-toast-notification-bar { position: fixed; top: 0; left: 0; right: 0; background: ${theme.bg}; color: ${theme.text}; padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; z-index: 9999; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } /* Toast状态栏 */ .met-toast-status-bar { position: fixed; bottom: 0; left: 0; right: 0; background: ${theme.bg}; color: ${theme.text}; padding: 8px 24px; display: flex; align-items: center; justify-content: space-between; z-index: 9999; font-size: 12px; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); } `; }; /** * 注入样式 * @param {Object} theme - 主题配置 */ const injectStyles = (theme = THEMES.light) => { // 检查是否在浏览器环境 if (typeof document === 'undefined') return; // 检查是否已注入 if (styleElement && document.getElementById('metona-toast-styles')) { return; } // 生成CSS const css = generateCSS(theme); // 创建样式元素 styleElement = document.createElement('style'); styleElement.id = 'metona-toast-styles'; styleElement.textContent = css.replace(/\s+/g, ' '); // 添加到文档头 document.head.appendChild(styleElement); }; /** * MetonaToast Themes - 主题管理 * @module themes * @version 2.0.0 * @description 主题系统、自定义主题和主题切换 */ // 当前主题状态 let currentTheme = 'auto'; let themeListeners = new Set(); let systemThemeMediaQuery = null; let systemThemeListener = null; /** * 获取系统主题 * @returns {string} 系统主题 ('light' 或 'dark') */ const getSystemTheme = () => { if (typeof window === 'undefined') return 'light'; return prefersDark() ? 'dark' : 'light'; }; /** * 获取主题(别名) * @param {string} theme - 主题名称 * @returns {string} 解析后的主题 */ const getTheme = (theme) => { return resolveTheme(theme); }; /** * 解析主题 * @param {string} theme - 主题名称 * @returns {string} 解析后的主题 */ const resolveTheme = (theme) => { if (theme === 'auto') { // 优先使用用户设置的全局主题,否则使用系统主题 if (currentTheme && currentTheme !== 'auto') { return currentTheme; } return getSystemTheme(); } return theme; }; /** * 获取主题配置 * @param {string} theme - 主题名称 * @returns {Object} 主题配置 */ const getThemeConfig = (theme) => { const resolved = resolveTheme(theme); return THEMES[resolved] || THEMES.light; }; /** * 应用主题 * @param {string} theme - 主题名称 */ const applyTheme = (theme) => { currentTheme = theme; const resolved = resolveTheme(theme); // 应用主题到文档 if (typeof document !== 'undefined') { document.documentElement.setAttribute('data-met-theme', resolved); document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto'); document.documentElement.classList.add(`met-theme-${resolved}`); // 设置CSS变量 const config = getThemeConfig(theme); setThemeVariables(config); } // 通知监听器 notifyThemeListeners(theme, resolved); }; /** * 设置主题CSS变量 * @param {Object} config - 主题配置 */ const setThemeVariables = (config) => { if (typeof document === 'undefined') return; const root = document.documentElement; root.style.setProperty('--met-bg', config.bg); root.style.setProperty('--met-text', config.text); root.style.setProperty('--met-border', config.border); root.style.setProperty('--met-shadow', config.shadow); root.style.setProperty('--met-hover-shadow', config.hoverShadow); root.style.setProperty('--met-progress-bg', config.progressBg); root.style.setProperty('--met-close-hover-bg', config.closeHoverBg); // 设置颜色变量 root.style.setProperty('--met-success', '#10b981'); root.style.setProperty('--met-error', '#ef4444'); root.style.setProperty('--met-warning', '#f59e0b'); root.style.setProperty('--met-info', '#3b82f6'); root.style.setProperty('--met-loading', '#6366f1'); }; /** * 获取当前主题 * @returns {string} 当前主题 */ const getCurrentTheme = () => { return currentTheme; }; /** * 获取解析后的主题 * @returns {string} 解析后的主题 */ const getResolvedTheme = () => { return resolveTheme(currentTheme); }; /** * 切换主题 * @param {string} theme - 新主题 */ const switchTheme = (theme) => { applyTheme(theme); saveTheme(theme); }; /** * 切换亮色/暗色主题 */ const toggleTheme = () => { const resolved = getResolvedTheme(); const newTheme = resolved === 'dark' ? 'light' : 'dark'; switchTheme(newTheme); }; /** * 重置为自动主题 */ const resetToAuto = () => { switchTheme('auto'); }; /** * 保存主题到本地存储 * @param {string} theme - 主题名称 */ const saveTheme = (theme) => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-theme', theme); } catch (e) { console.warn('Failed to save theme:', e); } } }; /** * 从本地存储加载主题 * @returns {string} 主题名称 */ const loadTheme = () => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-theme') || 'auto'; } catch (e) { console.warn('Failed to load theme:', e); } } return 'auto'; }; /** * 初始化主题系统 */ const initTheme = () => { // 加载保存的主题 const savedTheme = loadTheme(); applyTheme(savedTheme); // 监听系统主题变化 watchSystemTheme(); }; /** * 监听系统主题变化 */ const watchSystemTheme = () => { if (typeof window === 'undefined' || !window.matchMedia) return; // 移除旧的监听器 if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); } // 创建新的监听器 systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); systemThemeListener = (e) => { if (currentTheme === 'auto') { applyTheme('auto'); } }; systemThemeMediaQuery.addEventListener('change', systemThemeListener); }; /** * 停止监听系统主题变化 */ const unwatchSystemTheme = () => { if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); systemThemeMediaQuery = null; systemThemeListener = null; } }; /** * 添加主题监听器 * @param {Function} listener - 监听器函数 * @returns {Function} 移除监听器函数 */ const addThemeListener = (listener) => { themeListeners.add(listener); return () => { themeListeners.delete(listener); }; }; /** * 移除主题监听器 * @param {Function} listener - 监听器函数 */ const removeThemeListener = (listener) => { themeListeners.delete(listener); }; /** * 通知主题监听器 * @param {string} theme - 主题名称 * @param {string} resolved - 解析后的主题 */ const notifyThemeListeners = (theme, resolved) => { themeListeners.forEach((listener) => { try { listener(theme, resolved); } catch (e) { console.error('Theme listener error:', e); } }); }; /** * 清除所有主题监听器 */ const clearThemeListeners = () => { themeListeners.clear(); }; /** * 注册自定义主题 * @param {string} name - 主题名称 * @param {Object} config - 主题配置 */ const registerTheme = (name, config) => { THEMES[name] = { bg: config.bg || THEMES.light.bg, text: config.text || THEMES.light.text, border: config.border || THEMES.light.border, shadow: config.shadow || THEMES.light.shadow, hoverShadow: config.hoverShadow || THEMES.light.hoverShadow, progressBg: config.progressBg || THEMES.light.progressBg, closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg, }; }; /** * 注销自定义主题 * @param {string} name - 主题名称 */ const unregisterTheme = (name) => { if (name === 'light' || name === 'dark' || name === 'auto') { console.warn('Cannot unregister built-in theme:', name); return; } delete THEMES[name]; }; /** * 获取所有主题 * @returns {Object} 主题映射 */ const getAllThemes = () => { return { ...THEMES }; }; /** * 获取主题名称列表 * @returns {Array} 主题名称数组 */ const getThemeNames = () => { return Object.keys(THEMES); }; /** * 检查主题是否存在 * @param {string} name - 主题名称 * @returns {boolean} 是否存在 */ const hasTheme = (name) => { return name in THEMES; }; /** * 获取主题预览 * @param {string} theme - 主题名称 * @returns {Object} 主题预览 */ const getThemePreview = (theme) => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return { name: theme, resolved, colors: { background: config.bg, text: config.text, border: config.border, }, isDark: resolved === 'dark', isLight: resolved === 'light', isAuto: theme === 'auto', }; }; /** * 生成主题CSS * @param {string} theme - 主题名称 * @returns {string} CSS字符串 */ const generateThemeCSS = (theme) => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return ` .met-theme-${resolved} { --met-bg: ${config.bg}; --met-text: ${config.text}; --met-border: ${config.border}; --met-shadow: ${config.shadow}; --met-hover-shadow: ${config.hoverShadow}; --met-progress-bg: ${config.progressBg}; --met-close-hover-bg: ${config.closeHoverBg}; } `; }; /** * 应用主题CSS * @param {string} theme - 主题名称 */ const applyThemeCSS = (theme) => { if (typeof document === 'undefined') return; const css = generateThemeCSS(theme); const styleId = 'metona-toast-theme-css'; // 移除旧样式 const oldStyle = document.getElementById(styleId); if (oldStyle) { oldStyle.remove(); } // 添加新样式 const style = document.createElement('style'); style.id = styleId; style.textContent = css; document.head.appendChild(style); }; /** * 移除主题CSS */ const removeThemeCSS = () => { if (typeof document === 'undefined') return; const styleId = 'metona-toast-theme-css'; const style = document.getElementById(styleId); if (style) { style.remove(); } }; /** * 主题工具 — 代理层,直接引用模块函数 */ const themeUtils = { getSystemTheme, resolveTheme, getThemeConfig, applyTheme, getCurrentTheme, getResolvedTheme, switchTheme, toggleTheme, resetToAuto, initTheme, watchSystemTheme, unwatchSystemTheme, addThemeListener, removeThemeListener, clearThemeListeners, registerTheme, unregisterTheme, getAllThemes, getThemeNames, hasTheme, getThemePreview, generateThemeCSS, applyThemeCSS, removeThemeCSS, saveTheme, loadTheme, }; /** * 预设主题 */ const presetThemes = { /** * 浅色主题 */ light: { name: '浅色', description: '明亮清晰的主题', config: THEMES.light, }, /** * 深色主题 */ dark: { name: '深色', description: '护眼舒适的暗色主题', config: THEMES.dark, }, /** * 自动主题 */ auto: { name: '自动', description: '跟随系统主题设置', config: 'auto', }, warm: { name: '暖色', description: '温馨舒适的暖色主题', config: THEMES.warm, }, }; // 注册预设主题 Object.entries(presetThemes).forEach(([name, theme]) => { if (name !== 'auto' && theme.config !== 'auto') { THEMES[name] = theme.config; } }); /** * MetonaToast i18n - 国际化管理 * @module i18n * @version 2.0.0 * @description 多语言支持、语言切换和翻译管理 */ // 当前语言状态 let currentLocale = 'zh-CN'; let localeListeners = new Set(); let fallbackLocale = 'zh-CN'; /** * 获取当前语言 * @returns {string} 当前语言 */ const getCurrentLocale = () => { return currentLocale; }; /** * 设置当前语言 * @param {string} locale - 语言代码 */ const setCurrentLocale = (locale) => { if (!LOCALES[locale]) { console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`); locale = fallbackLocale; } currentLocale = locale; notifyLocaleListeners(locale); saveLocale(locale); }; /** * 获取回退语言 * @returns {string} 回退语言 */ const getFallbackLocale = () => { return fallbackLocale; }; /** * 设置回退语言 * @param {string} locale - 语言代码 */ const setFallbackLocale = (locale) => { if (!LOCALES[locale]) { console.warn(`Fallback locale "${locale}" not found`); return; } fallbackLocale = locale; }; /** * 翻译函数 * @param {string} key - 翻译键 * @param {Object} params - 插值参数 * @returns {string} 翻译后的字符串 */ const t = (key, params = {}) => { // 尝试当前语言 const currentTranslation = getTranslation(currentLocale, key); if (currentTranslation !== undefined) { return interpolate(currentTranslation, params); } // 尝试回退语言 if (currentLocale !== fallbackLocale) { const fallbackTranslation = getTranslation(fallbackLocale, key); if (fallbackTranslation !== undefined) { return interpolate(fallbackTranslation, params); } } // 返回键名 console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`); return key; }; /** * 获取翻译 * @param {string} locale - 语言代码 * @param {string} key - 翻译键 * @returns {string|undefined} 翻译字符串 */ const getTranslation = (locale, key) => { const localeData = LOCALES[locale]; if (!localeData) return undefined; // 支持嵌套键 (如 'common.close') const keys = key.split('.'); let result = localeData; for (const k of keys) { if (result && typeof result === 'object' && k in result) { result = result[k]; } else { return undefined; } } return typeof result === 'string' ? result : undefined; }; /** * 插值函数 * @param {string} str - 包含占位符的字符串 * @param {Object} params - 插值参数 * @returns {string} 插值后的字符串 */ const interpolate = (str, params) => { return str.replace(/\{(\w+)\}/g, (match, key) => { return params[key] !== undefined ? params[key] : match; }); }; /** * 检查翻译是否存在 * @param {string} key - 翻译键 * @returns {boolean} 是否存在 */ const hasTranslation = (key) => { return getTranslation(currentLocale, key) !== undefined || getTranslation(fallbackLocale, key) !== undefined; }; /** * 获取所有翻译 * @param {string} locale - 语言代码 * @returns {Object} 翻译对象 */ const getTranslations = (locale) => { return LOCALES[locale] || {}; }; /** * 添加翻译 * @param {string} locale - 语言代码 * @param {Object} translations - 翻译对象 */ const addTranslations = (locale, translations) => { if (!LOCALES[locale]) { LOCALES[locale] = {}; } deepMerge(LOCALES[locale], translations); }; /** * 深度合并对象 * @param {Object} target - 目标对象 * @param {Object} source - 源对象 * @returns {Object} 合并后的对象 */ const deepMerge = (target, source) => { for (const key in source) { if (source[key] instanceof Object && key in target && target[key] instanceof Object) { deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }; /** * 移除翻译 * @param {string} locale - 语言代码 * @param {string} key - 翻译键 */ const removeTranslation = (locale, key) => { const localeData = LOCALES[locale]; if (!localeData) return; const keys = key.split('.'); let current = localeData; for (let i = 0; i < keys.length - 1; i++) { if (current[keys[i]] && typeof current[keys[i]] === 'object') { current = current[keys[i]]; } else { return; } } delete current[keys[keys.length - 1]]; }; /** * 清除翻译 * @param {string} locale - 语言代码 */ const clearTranslations = (locale) => { if (LOCALES[locale]) { LOCALES[locale] = {}; } }; /** * 获取支持的语言列表 * @returns {Array} 语言代码数组 */ const getSupportedLocales = () => { return Object.keys(LOCALES); }; /** * 检查语言是否支持 * @param {string} locale - 语言代码 * @returns {boolean} 是否支持 */ const isLocaleSupported = (locale) => { return locale in LOCALES; }; /** * 获取语言名称 * @param {string} locale - 语言代码 * @returns {string} 语言名称 */ const getLocaleName = (locale) => { const names = { 'zh-CN': '简体中文', 'zh-TW': '繁體中文', 'en-US': 'English (US)', 'en-GB': 'English (UK)', 'ja': '日本語', 'ko': '한국어', 'fr': 'Français', 'de': 'Deutsch', 'es': 'Español', 'pt': 'Português', 'ru': 'Русский', 'ar': 'العربية', 'hi': 'हिन्दी', 'th': 'ไทย', 'vi': 'Tiếng Việt', 'id': 'Bahasa Indonesia', 'ms': 'Bahasa Melayu', 'tr': 'Türkçe', 'it': 'Italiano', 'nl': 'Nederlands', 'pl': 'Polski', 'uk': 'Українська', 'cs': 'Čeština', 'sv': 'Svenska', 'da': 'Dansk', 'fi': 'Suomi', 'nb': 'Norsk', 'el': 'Ελληνικά', 'he': 'עברית', 'hu': 'Magyar', 'ro': 'Română', 'bg': 'Български', 'hr': 'Hrvatski', 'sk': 'Slovenčina', 'sl': 'Slovenščina', 'et': 'Eesti', 'lv': 'Latviešu', 'lt': 'Lietuvių', 'ca': 'Català', 'gl': 'Galego', 'eu': 'Euskara', 'cy': 'Cymraeg', 'ga': 'Gaeilge', 'mt': 'Malti', 'is': 'Íslenska', 'mk': 'Македонски', 'sq': 'Shqip', 'sr': 'Српски', 'bs': 'Bosanski', 'me': 'Crnogorski', 'ka': 'ქართული', 'hy': 'Հայերեն', 'az': 'Azərbaycan', 'uz': "O'zbek", 'kk': 'Қазақ', 'ky': 'Кыргыз', 'tg': 'Тоҷикӣ', 'tk': 'Türkmen', 'mn': 'Монгол', 'ne': 'नेपाली', 'si': 'සිංහල', 'my': 'မြန်မာ', 'km': 'ខ្មែរ', 'lo': 'ລາວ', 'am': 'አማርኛ', 'sw': 'Kiswahili', 'yo': 'Yorùbá', 'ig': 'Igbo', 'ha': 'Hausa', 'zu': 'isiZulu', 'af': 'Afrikaans', 'xh': 'isiXhosa', 'st': 'Sesotho', 'tn': 'Setswana', 'ts': 'Xitsonga', 'ss': 'siSwati', 've': 'Tshivenda', 'nr': 'isiNdebele', }; return names[locale] || locale; }; /** * 获取语言方向 * @param {string} locale - 语言代码 * @returns {string} 语言方向 ('ltr' 或 'rtl') */ const getLocaleDirection = (locale) => { const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug']; return rtlLocales.includes(locale) ? 'rtl' : 'ltr'; }; /** * 获取语言信息 * @param {string} locale - 语言代码 * @returns {Object} 语言信息 */ const getLocaleInfo = (locale) => { return { code: locale, name: getLocaleName(locale), direction: getLocaleDirection(locale), isSupported: isLocaleSupported(locale), isCurrent: locale === currentLocale, isFallback: locale === fallbackLocale, }; }; /** * 获取所有语言信息 * @returns {Array} 语言信息数组 */ const getAllLocaleInfo = () => { return getSupportedLocales().map(getLocaleInfo); }; /** * 保存语言到本地存储 * @param {string} locale - 语言代码 */ const saveLocale = (locale) => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-locale', locale); } catch (e) { console.warn('Failed to save locale:', e); } } }; /** * 从本地存储加载语言 * @returns {string} 语言代码 */ const loadLocale = () => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-locale') || getDefaultLocale(); } catch (e) { console.warn('Failed to load locale:', e); } } return getDefaultLocale(); }; /** * 获取默认语言 * @returns {string} 语言代码 */ const getDefaultLocale = () => { if (typeof navigator !== 'undefined') { // 尝试从浏览器获取语言 const browserLocale = navigator.language || navigator.userLanguage; if (browserLocale && isLocaleSupported(browserLocale)) { return browserLocale; } // 尝试语言代码的前两位 const shortLocale = browserLocale?.split('-')[0]; if (shortLocale && isLocaleSupported(shortLocale)) { return shortLocale; } } return fallbackLocale; }; /** * 初始化国际化系统 */ const initI18n = () => { const savedLocale = loadLocale(); setCurrentLocale(savedLocale); }; /** * 切换语言 * @param {string} locale - 新语言 */ const switchLocale = (locale) => { setCurrentLocale(locale); }; /** * 添加语言监听器 * @param {Function} listener - 监听器函数 * @returns {Function} 移除监听器函数 */ const addLocaleListener = (listener) => { localeListeners.add(listener); return () => { localeListeners.delete(listener); }; }; /** * 移除语言监听器 * @param {Function} listener - 监听器函数 */ const removeLocaleListener = (listener) => { localeListeners.delete(listener); }; /** * 通知语言监听器 * @param {string} locale - 语言代码 */ const notifyLocaleListeners = (locale) => { localeListeners.forEach((listener) => { try { listener(locale); } catch (e) { console.error('Locale listener error:', e); } }); }; /** * 清除所有语言监听器 */ const clearLocaleListeners = () => { localeListeners.clear(); }; /** * 格式化数字 * @param {number} number - 数字 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatNumber = (number, options = {}) => { try { return new Intl.NumberFormat(currentLocale, options).format(number); } catch (e) { return number.toString(); } }; /** * 格式化货币 * @param {number} amount - 金额 * @param {string} currency - 货币代码 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatCurrency = (amount, currency = 'USD', options = {}) => { try { return new Intl.NumberFormat(currentLocale, { style: 'currency', currency, ...options, }).format(amount); } catch (e) { return amount.toString(); } }; /** * 格式化百分比 * @param {number} value - 值 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatPercent = (value, options = {}) => { try { return new Intl.NumberFormat(currentLocale, { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2, ...options, }).format(value / 100); } catch (e) { return `${value}%`; } }; /** * 格式化日期 * @param {Date|number|string} date - 日期 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatDate = (date, options = {}) => { try { const dateObj = date instanceof Date ? date : new Date(date); return new Intl.DateTimeFormat(currentLocale, options).format(dateObj); } catch (e) { return date.toString(); } }; /** * 格式化时间 * @param {Date|number|string} date - 日期 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatTime = (date, options = {}) => { return formatDate(date, { hour: 'numeric', minute: 'numeric', second: 'numeric', ...options, }); }; /** * 格式化相对时间 * @param {Date|number|string} date - 日期 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatRelativeTime = (date, options = {}) => { try { const dateObj = date instanceof Date ? date : new Date(date); const now = new Date(); const diff = dateObj - now; const rtf = new Intl.RelativeTimeFormat(currentLocale, { numeric: 'auto', ...options, }); const units = [ { unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 }, { unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 }, { unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 }, { unit: 'day', ms: 24 * 60 * 60 * 1000 }, { unit: 'hour', ms: 60 * 60 * 1000 }, { unit: 'minute', ms: 60 * 1000 }, { unit: 'second', ms: 1000 }, ]; for (const { unit, ms } of units) { if (Math.abs(diff) >= ms || unit === 'second') { const value = Math.round(diff / ms); return rtf.format(value, unit); } } return date.toString(); } catch (e) { return date.toString(); } }; /** * 格式化列表 * @param {Array} list - 列表 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatList = (list, options = {}) => { try { return new Intl.ListFormat(currentLocale, options).format(list); } catch (e) { return list.join(', '); } }; /** * 格式化复数 * @param {number} count - 数量 * @param {Object} options - 格式化选项 * @returns {string} 格式化后的字符串 */ const formatPlural = (count, options = {}) => { try { return new Intl.PluralRules(currentLocale, options).select(count); } catch (e) { return count === 1 ? 'one' : 'other'; } }; /** * 复数翻译 * @param {string} key - 翻译键 * @param {number} count - 数量 * @param {Object} params - 插值参数 * @returns {string} 翻译后的字符串 */ const plural = (key, count, params = {}) => { const pluralForm = formatPlural(count); const pluralKey = `${key}.${pluralForm}`; if (hasTranslation(pluralKey)) { return t(pluralKey, { ...params, count }); } // 尝试通用形式 if (hasTranslation(key)) { return t(key, { ...params, count }); } return key; }; /** * 国际化工具 — 代理层,直接引用模块函数 */ const i18nUtils = { t, plural, getCurrentLocale, setCurrentLocale, switchLocale, getFallbackLocale, setFallbackLocale, hasTranslation, getTranslations, addTranslations, removeTranslation, clearTranslations, getSupportedLocales, isLocaleSupported, getLocaleName, getLocaleDirection, getLocaleInfo, getAllLocaleInfo, formatNumber, formatCurrency, formatPercent, formatDate, formatTime, formatRelativeTime, formatList, formatPlural, addLocaleListener, removeLocaleListener, clearLocaleListeners, initI18n, saveLocale, loadLocale, getDefaultLocale, }; /** * 预设语言包 */ ({ 'zh-CN': { name: '简体中文', nativeName: '简体中文', direction: 'ltr', translations: LOCALES['zh-CN'], }, 'en-US': { name: 'English (US)', nativeName: 'English (US)', direction: 'ltr', translations: LOCALES['en-US'], }, }); /** * MetonaToast Core - 核心Toast逻辑 * @module core * @version 2.0.0 * @description 重构后的核心模块,包含Toast类的优化实现 */ // 模块级共享容器缓存,所有 Toast 实例共用 // 使用 Map 而非 WeakMap:document.body 在页面存活期间永不被 GC, // 容器需要显式通过 destroy() 清理,WeakMap 在此场景无实际收益。 const _containerCache = new Map(); /** * 参数标准化工具 * 支持多种调用方式: * - success('message') * - success({ message: 'text' }) * - success({ title: 'title', message: 'text' }) * - success('message', { duration: 5000 }) */ const normalizeArgs = (args, defaultType = 'default') => { const [first, second] = args; // 情况1: success('message') if (typeof first === 'string') { return { ...second, type: second?.type || defaultType, message: first, }; } // 情况2: success({ message: 'text' }) 或 success({ title: 't', message: 'm' }) if (first && typeof first === 'object') { return { ...first, type: first.type || defaultType, }; } // 情况3: success() 无参数 return { type: defaultType, message: '', }; }; /** * Toast类 - 核心通知组件 */ class Toast { // 静态钩子系统 static _hooks = new Map(); static on(name, fn) { if (!this._hooks.has(name)) this._hooks.set(name, []); this._hooks.get(name).push(fn); return () => this.off(name, fn); } static off(name, fn) { const list = this._hooks.get(name); if (list) this._hooks.set(name, list.filter(f => f !== fn)); } static trigger(name, toast) { const list = this._hooks.get(name); if (list) list.forEach(fn => { try { fn(toast); } catch (e) { console.error('Hook error:', name, e); } }); } constructor(opts) { this.id = opts.id || generateId(); this.type = opts.type || 'default'; this.title = opts.title || ''; this.message = opts.message ?? opts.content ?? ''; this.html = opts.html || ''; this.iconHTML = opts.iconHTML || ''; this.config = { ...DEFAULTS, ...opts }; // 防止嵌套对象被多个 toast 实例共享引用 if (opts.style && typeof opts.style === 'object') { this.config.style = { ...opts.style }; } this.el = null; this.barEl = null; this.rafId = null; this.remaining = this.config.duration; this.startedAt = 0; this.paused = false; this.closing = false; this._cleanups = []; this.group = opts.group || null; } _palette() { const theme = getTheme(this.config.theme); const c = TYPE_COLORS[this.type] || TYPE_COLORS.default; const tc = THEMES[theme] || THEMES.light; const t = { bg: tc.bg, border: tc.border, shadow: tc.shadow }; return { theme, c, t }; } create() { if (typeof window === 'undefined' || typeof document === 'undefined') return this; Toast.trigger('beforeShow', this); injectStyles(); const container = this._getContainer(); const { theme, c, t } = this._palette(); this._limitToasts(container); const el = document.createElement('div'); el.className = this._buildClassName(theme); el.setAttribute('role', (this.type === 'error' || this.type === 'warning') ? 'alert' : 'status'); el.setAttribute('aria-live', this.type === 'error' ? 'assertive' : 'polite'); el.dataset.id = this.id; this._applyStyles(el, theme, t); this._buildContent(el, c); this.el = el; // Chrome bug: column-reverse + appendChild 会导致重叠。改用 column + insertBefore const pos = this.config.position || 'top-right'; if (pos.startsWith('top')) { container.insertBefore(el, container.firstChild); } else { container.appendChild(el); } this._bindEvents(el); this._startTimer(); // 进入动画 requestAnimationFrame(() => { requestAnimationFrame(() => { el.classList.add('met-show'); }); }); if (typeof this.config.onShow === 'function') { try { this.config.onShow(this); } catch (e) { console.error('onShow callback error:', e); } } Toast.trigger('afterShow', this); return this; } _getContainer() { const position = this.config.position || 'top-right'; const zIndex = this.config.zIndex || 9999; if (_containerCache.has(document.body)) { const containers = _containerCache.get(document.body); if (containers.has(position)) { return containers.get(position); } } else { _containerCache.set(document.body, new Map()); } const el = document.createElement('div'); el.className = `met-container ${position}`; el.setAttribute('aria-label', 'Notifications'); el.setAttribute('role', 'region'); // cssText 完全接管布局,不依赖 CSS class const posStyles = { 'top-left': 'top:0;left:0;align-items:flex-start', 'top-center': 'top:0;left:0;right:0;align-items:center', 'top-right': 'top:0;right:0;align-items:flex-end', 'bottom-left': 'bottom:0;left:0;align-items:flex-start', 'bottom-center':'bottom:0;left:0;right:0;align-items:center', 'bottom-right': 'bottom:0;right:0;align-items:flex-end', }; el.style.cssText = ` display:flex; flex-direction:column; gap:${this.config.gap || 12}px; box-sizing:border-box; padding:${this.config.offset || 24}px; max-width:100vw; position:fixed; z-index:${zIndex}; pointer-events:none; ${posStyles[position] || 'top:0;right:0;align-items:flex-end'} `; document.body.appendChild(el); _containerCache.get(document.body).set(position, el); return el; } _limitToasts(container) { const max = this.config.max || 6; const list = Array.from(container.querySelectorAll('.met-toast')); if (list.length >= max) { const first = list[0]; const id = first && first.dataset.id; if (id) { const old = meToast._toasts.get(id); if (old) old.close(true); } } } _buildClassName(theme) { // CSS 支持的动画类型,未列出的 fallback 到 slide const CSS_ANIMS = ['slide','fade','scale','bounce','flip','rotate','zoom', 'slideUp','slideDown','slideLeft','slideRight']; const anim = CSS_ANIMS.includes(this.config.animation) ? this.config.animation : 'slide'; return [ 'met-toast', `met-${this.type}`, `met-theme-${theme}`, `met-anim-${anim}`, (this.config.closeOnClick || this.config.draggable) ? 'met-clickable' : '', this.config.className || '', ].filter(Boolean).join(' '); } _applyStyles(el, theme, t) { const widthValue = typeof this.config.width === 'number' ? `${this.config.width}px` : (typeof this.config.width === 'string' ? this.config.width : '360px'); // 只强制核心布局属性,动画/滤镜/颜色由 CSS class 控制 const set = (p, v) => el.style.setProperty(p, v, 'important'); set('position', 'relative'); set('flex-shrink', '0'); set('min-width', '240px'); set('max-width', 'calc(100vw - 48px)'); set('width', widthValue); // 用户自定义样式最后应用 if (this.config.style && Object.keys(this.config.style).length > 0) { Object.entries(this.config.style).forEach(([k, v]) => { el.style[k] = v; }); } } _buildContent(el, c) { // 自定义渲染函数 — 完全接管 DOM 构建 if (typeof this.config.render === 'function') { el.innerHTML = this.config.render(this); this.barEl = el.querySelector('.met-bar, .met-bar-v'); return; } const showIcon = this.config.icon !== false && (this.iconHTML || ICONS[this.type]); const showClose = this.config.closeButton !== false; const showProgress = this.config.showProgress !== false && this.config.duration > 0; const showSide = (!showIcon && this.type !== 'default'); const safeTitle = this.title ? `