# MetonaToast 轻量、零依赖、精致美观的 Toast 通知库。 [![npm version](https://img.shields.io/npm/v/@metona-team/metona-toast.svg)](https://www.npmjs.com/package/@metona-team/metona-toast) [![bundle size](https://img.shields.io/bundlephobia/minzip/@metona-team/metona-toast)](https://bundlephobia.com/package/@metona-team/metona-toast) [![license](https://img.shields.io/badge/license-MIT-green.svg)](https://git.metona.cn/MetonaTeam/MetonaToast) [![coverage](https://img.shields.io/badge/coverage-96%25-brightgreen.svg)](https://git.metona.cn/MetonaTeam/MetonaToast) ## 特性 - **零依赖** — TypeScript 严格模式源码,gzip 后不到 10KB,无任何运行时依赖 - **107 种图标类型** — success/error/warning/info/loading 及 100+ 扩展类型,覆盖所有常见场景 - **11 种动画 + 自定义** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,`animations.register()` 可注册任意自定义动画(动态注入 keyframes,真实生效) - **主题系统** — light/dark/auto/warm 四种内置主题,`registerTheme()` 注册自定义主题,跟随系统主题自动切换 - **国际化** — 内置 zh-CN / en-US 完整翻译,`addTranslations()` 扩展任意语言,RTL 语言完整适配 - **插件系统** — 4 款预设插件 (keyboard / persistence / accessibility / dedupe) + 自定义插件 - **完整钩子系统** — 17 个生命周期与事件钩子,`beforeShow` / `beforeClose` / `beforeUpdate` 支持返回 `false` 拦截 - **可拖拽关闭** — 拖动 Toast 任意方向即可关闭,阈值 `dragThreshold` 可配置 - **React 适配器** — `@metona-team/metona-toast/react` 子路径导入,`useToast()` hook + 声明式 `` 组件,主包保持零依赖 - **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入 Sentry、DataDog 等监控系统 - **工程质量** — 385 个测试全过,行覆盖率 96.42%,lint 零警告,CI 含覆盖率门禁(≥95%) ## 安装 ```bash npm install @metona-team/metona-toast ``` ### 引用方式 ```javascript // ES Module import MeToast from '@metona-team/metona-toast'; // CommonJS const MeToast = require('@metona-team/metona-toast'); ``` ```html ``` ## 环境与版本支持 | 环境 | 最低版本 | |------|---------| | Chrome | 64+ | | Firefox | 63+ | | Safari | 11+ | | Edge | 79+ | | Opera | 51+ | | iOS Safari | 11+ | | Android Chrome | 64+ | | Node.js | 16+(ESM & CJS 双格式) | | TypeScript | 4.5+(完整类型声明,类型从源码自动生成) | 核心依赖:Web Animations API(Chrome 64+, Firefox 63+, Safari 11+)、Pointer Events、CSS Grid、CSS Custom Properties。 ## 快速开始 浏览器 `window` 上注册两个全局变量:`MeToast` 和 `Met`,完全等价。 ```javascript import MeToast from '@metona-team/metona-toast'; // 也可以使用 Met — 与 MeToast 完全等价 Met.success('使用 Met 同样可用'); // 基础用法 MeToast.success('操作成功!'); MeToast.error('操作失败!'); MeToast.warning('请注意检查'); MeToast.info('系统维护中'); // 带标题 MeToast.success({ title: '保存成功', message: '文件已同步到云端' }); // 加载 → 链式转换(原地更新同一实例,id 稳定) const loading = MeToast.loading('正在提交...'); setTimeout(() => loading.success('提交成功!'), 2000); // Promise 风格 — 自动 loading → success/error await MeToast.promise(fetch('/api/data'), { loading: '加载中...', success: '加载完成!', error: '加载失败', }); ``` ## 全局配置 ```javascript MeToast.configure({ position: 'top-right', // top-left | top-center | top-right | bottom-left | bottom-center | bottom-right duration: 4000, // 显示时长(ms),0 为不自动关闭 max: 6, // 同一位置最多显示条数,超出关闭最早的 gap: 12, // Toast 间距(px) offset: 24, // 容器距屏幕边缘(px) theme: 'auto', // light | dark | auto(跟随系统)| warm animation: 'slide', // 11 种内置动画或自定义动画名 pauseOnHover: true, // 悬停暂停计时 closeOnClick: true, // 点击关闭 draggable: true, // 允许拖拽关闭 dragThreshold: 120, // 拖拽关闭阈值(px) showProgress: true, // 显示进度条 progressDirection: 'horizontal', // horizontal | vertical zIndex: 9999, width: 360, locale: 'zh-CN', // 语言 onError: ({ hook, source, error, toast }) => { // 钩子/定时器异常全局捕获,可接入监控系统 console.error('Toast error:', hook, error); }, }); ``` ## 动画效果 11 种内置动画,每种感官差异明显: | 动画 | 效果描述 | 时长 | |------|----------|------| | `slide` | 从右侧滑入 + 轻微过冲回弹 | 400ms | | `fade` | 纯淡入(blur→清晰),从容优雅 | 500ms | | `scale` | 弹性放大(0.55→1.07→1.0) | 450ms | | `bounce` | 从天而降四段弹跳 | 650ms | | `flip` | 3D 翻转入场 + 回摆 | 500ms | | `rotate` | 旋转摇摆进入 | 500ms | | `zoom` | 从中心爆发式弹出 | 500ms | | `slideUp` | 从下方弹入 | 400ms | | `slideDown` | 从上方弹入 | 400ms | | `slideLeft` | 从左侧滑入 | 400ms | | `slideRight` | 从右侧滑入 | 400ms | ```javascript MeToast.success('弹跳动画', { animation: 'bounce' }); // 注册自定义动画 — 动态注入 keyframes,真实生效 MeToast.animations.register('myAnim', { enter: { transform: 'rotate(-30deg) scale(0.5)', opacity: 0 }, leave: { transform: 'rotate(0) scale(1)', opacity: 1 }, duration: 500, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', }); MeToast.success('自定义动画', { animation: 'myAnim' }); ``` ## 主题 ```javascript // 切换主题(自动持久化到 localStorage) MeToast.themes.switchTheme('dark'); MeToast.themes.switchTheme('warm'); MeToast.themes.toggleTheme(); // light ↔ dark // 注册自定义主题 MeToast.themes.registerTheme('ocean', { bg: 'rgba(240, 249, 255, 0.96)', text: '#0c4a6e', border: 'rgba(14, 165, 233, 0.2)', shadow: '0 10px 36px -10px rgba(14, 165, 233, 0.18)', hoverShadow: '0 14px 48px -10px rgba(14, 165, 233, 0.22)', progressBg: 'rgba(14, 165, 233, 0.1)', closeHoverBg: 'rgba(14, 165, 233, 0.1)', }); MeToast.themes.switchTheme('ocean'); ``` ## 高级功能 ```javascript // 确认对话框 → Promise const ok = await MeToast.confirm('确定删除?', { confirmText: '删除', confirmColor: '#ef4444' }); // 输入对话框 → Promise const name = await MeToast.prompt('请输入姓名', { placeholder: '请输入...' }); // 进度条 const p = MeToast.progress('上传中...'); p.setProgress(60); p.complete('上传完成!'); // 倒计时({seconds} 自动替换) MeToast.countdown('操作将在 {seconds} 秒后执行', 5, { onComplete: () => MeToast.success('已执行'), }); // 队列展示(顺序逐个) await MeToast.queue(['步骤一', '步骤二', '步骤三'], { delay: 800 }); // 堆叠展示(同时错峰) MeToast.stack(['消息1', '消息2', '消息3'], { stagger: 150 }); // Action Toast — 内嵌操作按钮(close:false 可多次点击不关闭) MeToast.action('文件已删除', [ { text: '撤销', onClick: () => restore(), color: '#3b82f6' }, { text: '查看', onClick: () => open(), color: '#10b981', close: false }, ]); // 分组管理 const orders = MeToast.group('orders'); orders.success('订单已创建'); orders.count(); // 该组 toast 数量 orders.dismiss(); // 一键关闭整组 ``` ## 插件 ```javascript // 安装预设插件 MeToast.use('keyboard'); // ESC 关闭所有 Toast MeToast.use('persistence'); // 配置持久化到 localStorage MeToast.use('accessibility'); // 屏幕阅读器朗读公告 MeToast.use('dedupe'); // 相同 type+message 自动去重(更新已有实例) // 自定义插件(install 中注册生命周期钩子) import MeToast, { Toast } from '@metona-team/metona-toast'; MeToast.use({ name: 'my-plugin', install() { Toast.on('afterShow', (toast) => { console.log('Toast shown:', toast.message); }); }, uninstall() { console.log('plugin removed'); }, }); ``` ## 钩子系统 `Toast.on(name, handler)` 注册钩子,返回取消函数。`beforeShow` / `beforeClose` / `beforeUpdate` 的 handler 返回 `false` 可拦截对应操作。 | 钩子 | 触发时机 | 拦截 | |------|----------|------| | `beforeInit` / `afterInit` | `init()` 前后 | — | | `beforeDestroy` / `afterDestroy` | `destroy()` 前后 | — | | `beforeShow` / `afterShow` | Toast 显示前后 | ✅ 返回 false 阻止显示 | | `beforeClose` / `afterClose` | Toast 关闭前后 | ✅ 返回 false 阻止关闭 | | `beforeUpdate` / `afterUpdate` | `update()` 前后 | ✅ 返回 false 阻止更新 | | `configChange` | `configure`/`updateConfig`/`resetConfig` | — | | `themeChange` | 主题切换 | — | | `localeChange` | 语言切换 | — | | `click` | 点击 Toast 主体 | — | | `hover` | 鼠标悬停进出 | — | | `dragStart` / `dragEnd` | 拖拽开始/结束 | — | | `animationStart` / `animationEnd` | 入场动画开始/结束 | — | | `progressStart` / `progressEnd` | 倒计时开始/归零 | — | ```javascript // 拦截示例:指定类型禁止显示 Toast.on('beforeShow', (toast) => { if (toast.type === 'error' && !isAllowed) return false; }); ``` ## 国际化 ```javascript MeToast.i18n.switchLocale('en-US'); // 添加语言(嵌套结构支持深度合并) MeToast.i18n.addTranslations('ja', { success: '成功', error: 'エラー', confirm: '確認', cancel: 'キャンセル', }); // 格式化工具(基于 Intl) MeToast.i18n.formatNumber(1234567); // "1,234,567" MeToast.i18n.formatCurrency(99, 'CNY'); // "¥99.00" MeToast.i18n.formatDate(new Date()); MeToast.i18n.formatRelativeTime(Date.now() + 3600000); // "1 小时后" ``` ## Toast 管理 ```javascript MeToast.count(); // 当前数量 MeToast.getAll(); // 获取原生 Map MeToast.getToasts(); // 获取所有实例数组 MeToast.find(id); // 查找实例(不存在返回 undefined) MeToast.findByType(type); // 按类型过滤 MeToast.findByPosition(pos);// 按位置过滤 MeToast.dismiss(); // 关闭所有 MeToast.dismiss(id); // 关闭指定 MeToast.removeToast(id); // 立即移除(无离场动画) MeToast.clear(position?); // 按位置清除 MeToast.pauseAll(); // 暂停所有计时 MeToast.resumeAll(); // 恢复所有计时 MeToast.updateAll(partial); // 批量更新 MeToast.destroy(); // 完全销毁(支持重复调用,init() 可恢复) ``` ## 回调 ```javascript MeToast.success({ message: '操作成功', onBeforeShow: (toast) => true, // 返回 false 阻止显示 onShow: (toast) => console.log('显示:', toast.id), onClose: (toast) => console.log('关闭:', toast.id), onClick: (toast) => console.log('点击:', toast.id), onUpdate: (toast) => console.log('更新:', toast.id), }); // 全局错误回调 — 捕获钩子异常和定时器错误 MeToast.configure({ onError: ({ hook, source, error, toast }) => { reportError(error, { hook, source, toastId: toast?.id }); }, }); ``` ## React 适配器 主包保持零依赖;React 适配器通过子路径导入,`react` 为 optional peerDependency,未使用 React 的项目不受影响。 ```bash npm install @metona-team/metona-toast react ``` ```tsx import { useToast, Toast } from '@metona-team/metona-toast/react'; function SubmitButton() { // 本组件创建的 Toast 会在组件卸载时自动移除 const toast = useToast(); const submit = async () => { const loading = toast.loading('正在提交...'); try { await api.submit(); loading.success('提交成功!'); } catch (e) { loading.error('提交失败'); } }; return ; } // 声明式 Toast — props 变化时更新内容,组件卸载时自动移除(autoClose 默认 true) function SaveIndicator({ saving }: { saving: boolean }) { return saving ? : null; } ``` ## 工程质量 - **385 个测试** 全部通过(单元 + DOM 交互 + React 渲染 + SSR 环境隔离) - **行覆盖率 96.42%**,jest 覆盖率门禁 `lines >= 95%`,未达标构建即失败 - **lint 零警告**(TypeScript ESLint 严格检查) - **CI**(Gitea Actions):Node 18/20/22/24 矩阵 — typecheck → lint → 测试+覆盖率门禁 → 构建 → bundle size 检查 - **自动发布**:推送 `v*` tag 触发测试、构建并发布到 Gitea npm registry ## License MIT