Compare commits

...
21 Commits
Author SHA1 Message Date
tianhao 5f8b9d2f75 chore: 版本回退至 0.5.0 并重建产物(含 CJS 兼容修复)
CI / test (18.x) (push) Successful in 10m8s
CI / test (20.x) (push) Successful in 10m10s
CI / test (22.x) (push) Successful in 10m4s
CI / test (24.x) (push) Successful in 10m2s
2026-08-08 15:40:04 +08:00
tianhao 2516a3a36b fix(ci): 修复 YAML 解析失败导致 CI 不触发
CI / test (18.x) (push) Successful in 10m12s
CI / test (20.x) (push) Successful in 10m7s
CI / test (22.x) (push) Successful in 10m8s
CI / test (24.x) (push) Successful in 10m24s
- 根因:'Run tests (coverage gate: lines >= 95%)' 未加引号,
  plain scalar 中 'coverage gate:' 的冒号被解析为嵌套 mapping,
  整个 workflow 解析失败 → push 不触发(v0.5.0 引入)
- 修复:name 加单引号;runs-on 恢复 debian-latest(runner 实际标签)
- publish.yml 同步恢复 runs-on
- 验证:两个文件 YAML 解析全部通过
2026-08-08 15:33:28 +08:00
tianhao a4a757b15b ci: runs-on 改用 ubuntu-latest(官方 act_runner 默认标签) 2026-08-08 15:30:14 +08:00
tianhao 95f5652e9c fix: CJS 产物改 .cjs 扩展名并提升 default 导出 — 修复 require() 兼容性
- 包声明 type:module 时 .js 产物被 Node 按 ESM 解析:Node 16/18 下 require 报 ERR_REQUIRE_ESM,Node 20+ 拿到 namespace 对象(success/version 等属性 undefined)
- dist/metona-toast.cjs.js → .cjs,dist/react.cjs.js → .cjs
- 主包 CJS footer 将 default 提升为 module.exports:require() 直接返回 MeToast(兼容 .default / .MeToast / .Met / .Toast / .VERSION 所有用法)
- 版本 0.5.1(0.5.0 已发布带问题产物)
- 真实消费者验证:CJS/ESM/React 子包四路加载全部正常
2026-08-08 15:25:47 +08:00
tianhao dd061025e0 docs: 重写 README 与 site 站点 — 精美酷炫全新视觉
- README 全面更新:对齐 v0.5.0(钩子系统完整表、React 适配器、dragThreshold、工程质量章节)
- site 三页全新设计:深色渐变 + 玻璃拟态 + 光球漂浮 + 网格背景 + 终端风格代码块 + 滚动淡入
- index.html:Hero 自动演示、数据统计条、12 特性卡、环境支持、CTA
- docs.html:28 项 API 导航、滚动高亮、补全钩子系统/React/配置项/动画/主题章节
- demo.html:18 交互卡片、新增钩子拦截/dedupe 去重/自定义动画/拖拽阈值演示、107 图标网格
- jsdom 冒烟验证:三页 MeToast 全局注册、toast 渲染、图标网格/卡片/代码块完整
2026-08-08 15:11:04 +08:00
tianhao 1f718c4ef8 release: v0.5.0 — 覆盖率96.42%达标、lint零警告、覆盖率门禁、CI强化、发布自动化
- 覆盖率 96.42%(行):新增 59 测试共 385 个,覆盖 DOM 交互事件、对话框按钮交互、Intl/localStorage 异常路径、React 渲染生命周期
- 测试环境重构:SSR 分支用 defineProperty 真正覆盖 document/window;新增 tests/dom.test.ts、tests/setup.ts
- jest 覆盖率门禁 lines >= 95%;CI lint 改必过;新增 publish.yml(v* tag 自动发布到 Gitea registry)
- 修复:Toast.off 空数组残留、Toast.on 取消函数 this 绑定、action close:false 冒泡关闭、use() 重复注册钩子、i18n catch 引用错误
- 删除死代码 autoApplySystemTheme;lint 零警告(no-console 策略 + caughtErrorsIgnorePattern)
2026-08-08 15:02:51 +08:00
tianhao bbff631f4f release: v0.4.0 — React 适配器、loading链式id稳定、dragThreshold、RTL适配、dedupe插件
- React 适配器子包:@metona-team/metona-toast/react — useToast() hook + 声明式 <Toast /> 组件,组件卸载自动清理;react 为 optional peerDependency,主包保持零依赖
- loading 链式转换改原地 update:id 稳定不重建 DOM,转换后 duration 恢复默认自动关闭;update() 支持 duration 变更动态重启计时器
- _emit 幽灵实例修复:beforeShow 拦截后的 toast 不再注册进 _toasts
- dragThreshold 配置项(默认 120px,原硬编码);RTL 容器 dir 属性 + 进度条/side 条/色条镜像
- dedupe 预设插件:相同 type+message 自动去重,支持 uninstall
- jest 环境隔离测试独立成 tests/ssr.test.ts(resetModules 不再污染共享模块状态);新增 tests/setup.ts 补 TextEncoder
- 文档:README/docs.html 补 React 适配器、dedupe、dragThreshold;CHANGELOG 更新;328 测试全过
2026-08-08 14:45:19 +08:00
tianhao 2937ce5e2d release: v0.3.0 — 自定义动画真实生效、max超限修复、钩子系统完整化、destroy全量清理
- 自定义动画:animations.register() 注册的动画动态注入 keyframes 真实生效(此前仅存 Map 实际 fallback slide)
- max 超限:通过实例注册表真正 close() 最早的 toast(此前仅删内存记录,DOM 残留)
- 钩子收敛:beforeClose/beforeUpdate 支持返回 false 拦截;接入 click/hover/dragStart/dragEnd/animationStart/animationEnd/progressStart/progressEnd/configChange/themeChange/localeChange/beforeInit/afterInit/beforeDestroy/afterDestroy;HOOK_NAMES 移除永不触发的声明
- destroy() 全量清理:hooks、实例注册表、全部 metona-toast-* 样式标签;动画注册表真正清空
- applyThemeVariables 重复注入修复;init() 支持插件对象并复活 _destroyed;AnimationConfig 类型放宽
- VERSION 统一到 constants.ts 唯一维护;删除失效 scripts;新增 .gitattributes 统一 LF
- docs.html 补 v0.2.1 新 API 文档;CHANGELOG 更新;测试 309 用例全过
2026-08-08 14:23:41 +08:00
thzxx cb8dd623db chore: 提交 dist 构建产物
CI / test (22.x) (push) Successful in 9m58s
CI / test (18.x) (push) Successful in 10m1s
CI / test (20.x) (push) Successful in 10m2s
CI / test (24.x) (push) Successful in 9m58s
2026-07-25 15:34:42 +08:00
thzxx 8014dedf93 fix: 移除 lint 脚本中的单引号以兼容 Windows
CI / test (18.x) (push) Successful in 10m2s
CI / test (20.x) (push) Successful in 9m58s
CI / test (22.x) (push) Successful in 9m58s
CI / test (24.x) (push) Successful in 9m56s
2026-07-25 13:02:30 +08:00
thzxx bc1128bd1f release: v0.2.1 — bug修复、代码去重、功能增强、覆盖率88.57% 2026-07-25 13:01:59 +08:00
tianhao cca2675fb4 refactor: 对齐 MetonaEditor — babel-jest 替代 ts-jest
CI / test (18.x) (push) Successful in 9m50s
CI / test (20.x) (push) Successful in 9m49s
CI / test (22.x) (push) Successful in 9m56s
CI / test (24.x) (push) Successful in 9m55s
### Changed
- 测试框架: ts-jest → babel-jest (对齐 MetonaEditor 生产方案)
- jest.config.cjs: babel-jest transform + moduleNameMapper
- test 脚本: jest → jest --coverage (对齐 MetonaEditor)
- 移除 ts-jest, 新增 @babel/core/preset-env/preset-typescript/babel-jest

### Added
- babel.config.cjs (对齐 MetonaEditor)

### Fixed
- 恢复 tslib (@rollup/plugin-typescript 必需依赖)

### Verified
- tsc --noEmit: 零错误
- jest --no-coverage: 136/136 passed
- rollup build: 5 outputs
2026-07-25 11:52:01 +08:00
tianhao 32a3bdf86e fix(ci): 修复 Gitea Actions 测试卡死问题
CI / test (20.x) (push) Canceled after 0s
CI / test (22.x) (push) Canceled after 0s
CI / test (24.x) (push) Canceled after 0s
CI / test (18.x) (push) Canceled after 3s
- 移除 --coverage(CI 环境 ts-jest 覆盖率内存爆炸)
- 添加 --forceExit --maxWorkers=2 --no-cache
- 添加 NODE_OPTIONS=--max-old-space-size=4096 防 OOM
2026-07-25 11:33:52 +08:00
tianhao 9a6cd33d90 release: v0.2.0 — TypeScript 源码重构
CI / test (18.x) (push) Successful in 9m52s
CI / test (22.x) (push) Canceled after 0s
CI / test (24.x) (push) Canceled after 0s
CI / test (20.x) (push) Canceled after 2m49s
### Changed
- 全部源码从 JavaScript 迁移到 TypeScript (strict mode)
- core.js (1244行) 拆分为 toast.ts + api.ts + templates.ts
- 删除手动维护的 types/index.d.ts,类型从源码自动生成
- 构建工具链: Babel → ts-jest, 新增 @rollup/plugin-typescript
- 新增 rollup-plugin-dts 生成合并 .d.ts

### Added
- tsconfig.json (strict: true)
- src/types.ts 核心类型模块 (39 个导出类型)
- .eslintrc.json (@typescript-eslint)
- .gitea/workflows/ci.yml (Node 18/20/22/24 矩阵)
- CHANGELOG.md

### Aligned with metona-starter
- package.json: type:module, engines≥16, prepublishOnly
- rollup.config.js: dts plugin, port 3001
- jest.config.cjs, build.sh, serve.sh, .gitignore (.npmrc)

### Removed
- babel.config.js, types/ 目录, 所有 src/*.js
2026-07-25 11:22:07 +08:00
thzxx ee2b685f64 docs: 更新 README.md 和 site/ 文档页
README.md:
- 特性列表: 80+ → 107 种图标类型,新增全局错误回调和暂停恢复说明
- 全局配置: 添加 onError 示例
- Toast 管理: 添加 getAll() 方法
- 回调: 新增 onError 全局错误回调示例

site/index.html:
- 新增「错误捕获」和「暂停恢复」特性卡片
- TypeScript 卡片更新为「零 phantom 方法」

site/docs.html:
- 回调表添加 onError 条目

site/demo.html:
- 新增 onError 错误捕获演示卡片(含 JS 实现)
2026-07-24 14:59:11 +08:00
thzxx 5c5627179a fix(types): 补全和修正 TypeScript 类型声明
修复:
- ToastInstance: 补充 iconHTML / group 属性
- MeToast: 补充 getAll() 方法
- AnimationUtils: 移除 12 个不存在的 phantom 方法 (apply/enter/leave/cancel/pauseAll等)
- PluginManager: 移除 10 个不存在的 phantom 方法 (use/trigger/middleware等)
- PluginUtils: 补充缺失的 unregister/has/getNames/enable/disable/isEnabled 方法
2026-07-24 14:54:08 +08:00
thzxx f7d74d152d chore: v0.1.2 — 全面修复、优化与增强
修复:
- core.js: _resume 计时器 BUG (startedAt 被 _startTimer 覆盖导致暂停恢复失效)
- plugins.js: register 在 install 前展开导致 handler 实例丢失
- locales.js: zh-CN/en-US 大量重复 key 覆盖正确翻译值
- core.js: configure 改为原地更新避免 _config 引用断裂

优化:
- core.js: update() 类型变更时同步更新 DOM 类名和进度条颜色
- animations.js: getActiveCount 返回已注册动画数量
- themes.js: 移除 presetThemes 死代码和重复注册循环
- constants.js: THEMES.auto 从字符串改为规范化对象
- index.js: destroy 添加防重复调用保护

增强:
- core.js: 新增 onError 全局错误回调,钩子异常和定时器错误可被外部捕获
- styles.js: _resume 校准时间基准补偿注释
- 139 tests 全部通过
2026-07-24 14:42:47 +08:00
thzxx eebd669b7f chore: 版本号重置为 v0.1.1, 更新 site/ 文档页面移除 v2.0 标记 2026-07-24 14:19:53 +08:00
tianhao a6c6714b54 chore: 从git跟踪移除dist目录 2026-07-23 20:05:48 +08:00
tianhao a9a0be8f21 chore: .gitignore添加.npmrc排除 2026-07-23 20:04:49 +08:00
tianhao 7d268648ae chore: 从git跟踪移除.npmrc,加入.gitignore 2026-07-23 20:04:46 +08:00
62 changed files with 29854 additions and 24426 deletions
+30
View File
@@ -0,0 +1,30 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true,
"node": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended"],
"rules": {
"no-console": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "warn",
"prefer-const": "warn",
"no-undef": "off",
"no-empty": "off",
"no-useless-escape": "off",
"no-control-regex": "off",
"no-regex-spaces": "off",
"no-constant-condition": ["warn", { "checkLoops": false }]
},
"ignorePatterns": ["dist/", "coverage/", "node_modules/"]
}
+9
View File
@@ -0,0 +1,9 @@
# 统一文本文件行尾为 LF(跨平台协作)
* text=auto eol=lf
# 二进制文件不进行行尾转换
*.png binary
*.jpg binary
*.ico binary
*.gif binary
*.svg text
+50
View File
@@ -0,0 +1,50 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test:
runs-on: debian-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x, 24.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: 'Run tests (coverage gate: lines >= 95%)'
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
env:
NODE_OPTIONS: --max-old-space-size=4096
- name: Build
run: npm run build
- name: Bundle size
run: |
echo "=== Build Output ==="
ls -lh dist/
echo ""
echo "=== Minified Gzip Size ==="
gzip -c dist/metona-toast.min.js | wc -c | xargs echo "min.js gzip:"
gzip -c dist/metona-toast.esm.js | wc -c | xargs echo "esm.js gzip:"
+36
View File
@@ -0,0 +1,36 @@
name: Publish
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: debian-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://git.metona.cn/api/packages/MetonaTeam/npm/
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Run tests
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
- name: Build
run: npm run build
- name: Publish to Gitea npm registry
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
+4 -1
View File
@@ -1,4 +1,7 @@
node_modules/
dist/
coverage/
.DS_Store
.zcode/
# 本地 npm 发布凭据(含 _auth,勿提交)
.npmrc
-10
View File
@@ -1,10 +0,0 @@
registry=https://registry.npmmirror.com
@metona-team:registry=https://git.metona.cn/api/packages/MetonaTeam/npm/
//git.metona.cn/api/packages/MetonaTeam/npm/:_auth=dGh6eHg6Mjc5MjQ2MDE5MGJkcW4=
//git.metona.cn/api/packages/MetonaTeam/npm/:always-auth=true
# 允许包的 postinstall 脚本(electron 需要下载二进制文件)
ignore-scripts=false
# Electron 二进制下载镜像(国内加速,在终端设置环境变量)
# $env:ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
+125
View File
@@ -0,0 +1,125 @@
# Changelog
All notable changes to MetonaToast will be documented in this file.
## [0.5.0] - 2026-08-08
### Changed
- **覆盖率 96.42%(行)**:新增 59 个测试用例(共 385),覆盖 DOM 交互事件(click/drag/hover/animation)、confirm/prompt/action 按钮交互、Intl/localStorage 异常路径、React 渲染生命周期
- **测试环境重构**:SSR 分支测试用 `Object.defineProperty` 真正覆盖 document/windowjsdom getter 使此前赋值无效);新增 `tests/dom.test.ts`(jsdom 真实 DOM 事件驱动)、`tests/setup.ts`TextEncoder 补丁)
- **jest 覆盖率门禁**`lines >= 95%`,未达标测试即失败
### Fixed
- **`Toast.off` 空数组残留**:移除全部钩子后删除 Map key(此前空数组残留导致 `_hooks.has()` 误报 true
- **`Toast.on` 返回的取消函数 this 绑定**:改用类名引用,任意调用上下文可用
- **action 按钮 `close:false` 失效**:按钮 click 冒泡到 toast 的 closeOnClick 处理器导致总是关闭,已 stopPropagation 修复
- **`use()` 重复注册钩子**accessibility/persistence 预设的钩子注册改为防重复(先卸载旧钩子),并在插件 `uninstall` 中一并清理
- **i18n catch 引用错误**`catch {}` 中残留 `e` 引用导致的 ReferenceErrorsaveLocale/loadLocale/notifyLocaleListeners
- **删除残缺死代码**`styles.autoApplySystemTheme`(无实际效果的占位实现)
### Removed
- **lint 零警告**`no-console` 规则关闭(库的错误报告依赖 console,生产构建已由 terser `drop_console` 剥离);catch 变量通过 `caughtErrorsIgnorePattern` 豁免
### Added
- **Gitea Actions 发布自动化**`publish.yml` — 推送 `v*` tag 时自动 测试+构建+发布到 Gitea npm registry(需配置 `PACKAGES_TOKEN` secret
- **CI 强化**lint 改为必过(移除 continue-on-error);测试命令带覆盖率门禁
- **CJS 产物兼容性修复**:包声明 `type: module``.js` 产物被 Node 按 ESM 解析导致 `require()` 失效,CJS 产物改 `.cjs` 扩展名并将 default 提升为 `module.exports``require()` 直接返回 MeToast,兼容 `.default`/`.MeToast`/`.Met`/`.Toast`/`.VERSION` 全部用法)
## [0.4.0] - 2026-08-08
### Added
- **React 适配器子包**`@metona-team/metona-toast/react``useToast()` hook(组件卸载时自动移除本组件创建的 Toast)+ 声明式 `<Toast />` 组件(props 变化更新、卸载自动清理)。主包保持零依赖,`react` 为 optional peerDependency
- **`dedupe` 预设插件**:相同 type+message 的 Toast 自动去重(更新已有实例而非重复弹出),支持 uninstall
- **`dragThreshold` 配置项**:拖拽关闭阈值可配置(默认 120px,原为硬编码)
- **RTL 完整适配**:容器设置 `dir="rtl"`,进度条/side 条/类型色条/圆角正确镜像
- **`update({ duration })` 支持**:动态变更显示时长并自动重启/停止计时器(此前 update 忽略 duration
- **幽灵实例修复**`_emit` 检查 `create()` 结果,beforeShow 拦截后的 toast 不再注册到 `_toasts`(此前会残留幽灵记录,影响 count 与去重类插件)
### Changed
- **loading 链式转换 id 稳定**`loading.success/error/info/warning` 由"关闭旧 + 新建"改为原地 `update()` 同一实例,返回的 Toast id 与 loading id 一致;转换后 duration 自动恢复默认值(自动关闭)。注:转换不再产生新的入场动画,改为平滑更新;关闭按钮/进度条保持 loading 配置(如需默认外观,请通过 opts 覆盖)
## [0.3.0] - 2026-08-08
### Fixed
- **自定义动画真实生效**`animations.register()` 注册的动画此前仅写入内存 Map,实际渲染始终 fallback 到 slide。现在注册表驱动 `_buildClassName` 并动态注入对应 `@keyframes` + 类规则,README 的注册教程名实相符
- **`max` 超限真正关闭最早的 toast**:此前超限时仅从内存 Map 删除记录,DOM 上的旧 toast 依然显示。现在通过实例注册表找到最早的 toast 并真正 `close()`(触发离场动画与生命周期回调)
- **`destroy()` 全量清理**:新增清理 `Toast._hooks` 钩子、`Toast._registry` 实例注册表、`metona-toast-theme-css` / `metona-toast-custom-styles` / `metona-toast-anim-styles` 样式标签;动画注册表改为真正清空
- **`applyThemeVariables` 重复注入**:修复多次调用追加相同 id 的 style 标签问题(改为先移除旧元素)
- **`init()` 复活**`destroy()` 后调用 `init()` 重置 `_destroyed` 标志,恢复可初始化状态
- **失效脚本清理**:删除 `docs`typedoc 未安装)、`example`(examples/ 目录不存在)脚本,新增 `serve` 脚本
- **`AnimationConfig.enter/leave` 类型放宽**:支持 `opacity: 0` 等数值属性(此前仅 `string`,README 示例在 TS 严格模式下报错)
### Added
- **钩子系统完整化**:此前 `HOOK_NAMES` 声明 20+ 钩子但多数永不触发。现在全部声明均有触发点:
- 拦截语义:`beforeClose` / `beforeUpdate` 返回 `false` 可分别阻止关闭、更新
- 新触发点:`click` / `hover` / `dragStart` / `dragEnd` / `animationStart` / `animationEnd` / `progressStart` / `progressEnd` / `configChange` / `themeChange` / `localeChange` / `beforeInit` / `afterInit` / `beforeDestroy` / `afterDestroy`
- **`init({ plugins })` 支持插件对象**:与 `use()` 一致,可传 `string | Plugin`
### Changed
- **VERSION 唯一来源**:版本号统一在 `src/constants.ts` 维护,`api.ts` / `index.ts` 引用,消除三处硬编码漂移
- **`HOOK_NAMES` 收敛**:移除永不触发的 `progressUpdate` / `custom` 声明
- 版本升级至 **v0.3.0**(含行为增强,按 semver minor 处理)
## [0.2.1] - 2026-07-25
### Fixed
- **`auto` 主题配置死值**`THEMES.auto` 改为合法 CSS 值(复用 light 默认值),避免 `getThemeConfig('auto')` 返回无意义字符串
- **`resolveTheme` 循环逻辑**`resolveTheme('auto')` 始终检测系统主题,不再依赖 `currentTheme` 全局状态
- **`formatList` 国际化**:优先使用 `Intl.ListFormat`,降级到逗号拼接,支持各语言原生列表格式
- **RTL 容器定位**RTL 语言自动翻转 left/right 位置,容器创建时正确适配
- **`_containerCache` 泄露**`index.ts``destroy()` 中增加 `_containerCache.clear()`,与 `api.ts` 保持一致
### Changed
- **代码去重**:消除 `index.ts``api.ts` 之间 11 个方法的重复定义,统一在 `api.ts` 实现
- **Toast 回调注入**`Toast._onError` / `Toast._removeToast` 改为 `Toast.setCallbacks()` 静态方法,消除隐式契约
- **Stub 方法替换**`api.ts` 中 7 个 stub 方法替换为完整实现(init/use/destroy/getStatus/getConfig/updateConfig/resetConfig
- **ESLint 规则**:新增 `@typescript-eslint/no-explicit-any` (warn) 和 `prefer-const` (warn)
### Added
- **`beforeShow` 拦截**:钩子函数返回 `false` 可阻止 Toast 显示;配置 `onBeforeShow` 回调支持同样能力
- **`updatePosition()` 方法**:允许运行时移动 Toast 到不同位置容器
- **`remove()` + `removeToast()` 方法**:立即移除 Toast(不触发离场动画),提供无动画的快速清除 API
## [0.2.0] - 2026-07-25
### Changed
- **TypeScript 源码重构**:全部源码从 JavaScript 迁移到 TypeScript
- 删除手动维护的 `types/index.d.ts`,类型由源码自动生成
- `core.js` (1244行) 拆分为 `toast.ts` + `api.ts` + `templates.ts`
- 构建工具链:Babel → `ts-jest`,新增 `@rollup/plugin-typescript`
- 测试迁移至 TypeScript (`tests/index.test.ts`)
### Added
- `tsconfig.json` (strict: true)
- `src/types.ts` 核心类型模块
- `.eslintrc.json` ESLint 配置
- `.gitea/workflows/ci.yml` CI 工作流
- `CHANGELOG.md`
### Removed
- `babel.config.js`
- `types/` 目录
## [0.1.2] - 2026-07-24
### Fixed
- 补全和修正 TypeScript 类型声明
- 全面修复、优化与增强
## [0.1.1] - 2026-07-23
### Changed
- 版本号重置,更新文档页面
## [0.1.0] - 2026-07-22
### Added
- 初始发布
- 107 种内置图标
- 11 种动画效果
- 4 种主题(light/dark/auto/warm
- 国际化支持 (zh-CN/en-US)
- 插件系统 (keyboard/persistence/accessibility)
- 拖拽关闭、进度条、倒计时等高级特性
- TypeScript 类型声明
- 零依赖
+157 -38
View File
@@ -5,17 +5,21 @@
[![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)
## 特性
- **零依赖** — 纯原生 JavaScriptgzip 后不到 10KB
- **80+ 图标类型** — success/error/warning/info/loading 及更多扩展类型
- **11 种动画** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight每种效果明显不同
- **主题系统** — light/dark/auto/warm 四种内置主题,支持 `registerTheme()` 注册自定义主题
- **国际化** — 内置 zh-CN / en-US 完整翻译,可通过 `addTranslations()` 扩展
- **插件系统** — 3 款预设插件 (keyboard/persistence/accessibility) + 自定义插件
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭
- **TypeScript** — 完整类型定义
- **零依赖** — 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 + 声明式 `<Toast />` 组件,主包保持零依赖
- **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入 Sentry、DataDog 等监控系统
- **工程质量** — 385 个测试全过,行覆盖率 96.42%,lint 零警告,CI 含覆盖率门禁(≥95%)
## 安装
@@ -57,14 +61,14 @@ const MeToast = require('@metona-team/metona-toast');
| Opera | 51+ |
| iOS Safari | 11+ |
| Android Chrome | 64+ |
| Node.js | 18+ESM & CJS 双格式) |
| TypeScript | 4.5+(完整类型声明) |
| Node.js | 16+ESM & CJS 双格式) |
| TypeScript | 4.5+(完整类型声明,类型从源码自动生成 |
核心依赖:Web Animations APIChrome 64+, Firefox 63+, Safari 11+)、Pointer Events、CSS Grid、CSS Custom Properties。
## 快速开始
浏览器 `window`注册两个全局变量:`MeToast``Met`两者完全等价,任选其一使用
浏览器 `window` 上注册两个全局变量:`MeToast``Met`,完全等价。
```javascript
import MeToast from '@metona-team/metona-toast';
@@ -81,11 +85,11 @@ MeToast.info('系统维护中');
// 带标题
MeToast.success({ title: '保存成功', message: '文件已同步到云端' });
// 加载 → 成功链式转换
// 加载 → 链式转换(原地更新同一实例,id 稳定)
const loading = MeToast.loading('正在提交...');
setTimeout(() => loading.success('提交成功!'), 2000);
// Promise 风格
// Promise 风格 — 自动 loading → success/error
await MeToast.promise(fetch('/api/data'), {
loading: '加载中...',
success: '加载完成!',
@@ -99,53 +103,65 @@ await MeToast.promise(fetch('/api/data'), {
MeToast.configure({
position: 'top-right', // top-left | top-center | top-right | bottom-left | bottom-center | bottom-right
duration: 4000, // 显示时长(ms),0 为不自动关闭
max: 6, // 同最多显示条数
theme: 'auto', // light | dark | auto | warm
animation: 'slide', // slide | fade | scale | bounce | flip | rotate | zoom | slideUp | slideDown | slideLeft | slideRight
max: 6, // 同一位置最多显示条数,超出关闭最早的
gap: 12, // Toast 间距(px)
offset: 24, // 容器距屏幕边缘(px)
theme: 'auto', // light | dark | auto(跟随系统)| warm
animation: 'slide', // 11 种内置动画或自定义动画名
pauseOnHover: true, // 悬停暂停计时
closeOnClick: true, // 点击关闭
showProgress: 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 种动画,每种感官差异明显:
11 种内置动画,每种感官差异明显:
| 动画 | 效果描述 |
|------|----------|
| `slide` | 从右侧滑入 + 轻微过冲回弹 |
| `fade` | 纯淡入(blur→清晰),从容优雅 |
| `scale` | 弹性放大(0.55→1.07→1.0 |
| `bounce` | 从天而降四段弹跳 |
| `flip` | 3D 翻转入场 + 回摆 |
| `rotate` | 旋转摇摆进入 |
| `zoom` | 从中心爆发式弹出 |
| `slideUp` | 从下方弹入 |
| `slideDown` | 从上方弹入 |
| `slideLeft` | 从左侧滑入 |
| `slideRight` | 从右侧滑入 |
| 动画 | 效果描述 | 时长 |
|------|----------|------|
| `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', {
@@ -184,6 +200,18 @@ 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(); // 一键关闭整组
```
## 插件
@@ -193,8 +221,9 @@ MeToast.stack(['消息1', '消息2', '消息3'], { stagger: 150 });
MeToast.use('keyboard'); // ESC 关闭所有 Toast
MeToast.use('persistence'); // 配置持久化到 localStorage
MeToast.use('accessibility'); // 屏幕阅读器朗读公告
MeToast.use('dedupe'); // 相同 type+message 自动去重(更新已有实例)
// 自定义插件(通过 install 中注册 Toast 生命周期钩子)
// 自定义插件(install 中注册生命周期钩子)
import MeToast, { Toast } from '@metona-team/metona-toast';
MeToast.use({
@@ -204,6 +233,36 @@ MeToast.use({
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;
});
```
@@ -212,7 +271,7 @@ MeToast.use({
```javascript
MeToast.i18n.switchLocale('en-US');
// 添加语言
// 添加语言(嵌套结构支持深度合并)
MeToast.i18n.addTranslations('ja', {
success: '成功',
error: 'エラー',
@@ -220,21 +279,30 @@ MeToast.i18n.addTranslations('ja', {
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.getToasts(); // 获取所有实例
MeToast.getAll(); // 获取原生 Map<id, ToastInstance>
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.destroy(); // 完全销毁
MeToast.updateAll(partial); // 批量更新
MeToast.destroy(); // 完全销毁(支持重复调用,init() 可恢复)
```
## 回调
@@ -242,12 +310,63 @@ MeToast.destroy(); // 完全销毁
```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 <button onClick={submit}></button>;
}
// 声明式 Toast — props 变化时更新内容,组件卸载时自动移除(autoClose 默认 true
function SaveIndicator({ saving }: { saving: boolean }) {
return saving ? <Toast type="info" message="正在保存..." /> : 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
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }],
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-transform-modules-commonjs',
],
};
-16
View File
@@ -1,16 +0,0 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
modules: 'commonjs',
},
],
],
plugins: [
'@babel/plugin-transform-modules-commonjs',
],
};
+8 -66
View File
@@ -1,83 +1,25 @@
#!/bin/bash
# MetonaToast 构建脚本
# 构建脚本 — 安装依赖 → 测试 → 构建
echo "🍞 MetonaToast 构建脚本"
echo "========================"
echo "🔨 MetonaToast 构建脚本"
# 检查Node.js
if ! command -v node &> /dev/null; then
echo "❌ 错误: 未找到Node.js"
echo "❌ 未找到 Node.js"
exit 1
fi
# 检查npm
if ! command -v npm &> /dev/null; then
echo "❌ 错误: 未找到npm"
exit 1
fi
echo "✅ Node.js: $(node -v)"
echo "✅ Node.js版本: $(node -v)"
echo "✅ npm版本: $(npm -v)"
# 安装依赖
echo ""
echo "📦 安装依赖..."
npm install
npm install || exit 1
if [ $? -ne 0 ]; then
echo "❌ 依赖安装失败"
exit 1
fi
echo "✅ 依赖安装完成"
# 运行测试
echo ""
echo "🧪 运行测试..."
npm test
npm test || exit 1
if [ $? -ne 0 ]; then
echo "❌ 测试失败"
exit 1
fi
echo "🔨 构建..."
npm run build || exit 1
echo "✅ 测试通过"
# 构建项目
echo ""
echo "🔨 构建项目..."
npm run build
if [ $? -ne 0 ]; then
echo "❌ 构建失败"
exit 1
fi
echo "✅ 构建完成"
# 显示构建结果
echo ""
echo "📊 构建结果:"
echo "========================"
ls -lh dist/
echo ""
echo "✅ 构建成功完成!"
echo ""
echo "📁 文件结构:"
echo " - dist/metona-toast.js (UMD格式)"
echo " - dist/metona-toast.esm.js (ES Module格式)"
echo " - dist/metona-toast.cjs.js (CommonJS格式)"
echo " - dist/metona-toast.min.js (压缩版本)"
echo " - dist/metona-toast.d.ts (TypeScript声明)"
echo ""
echo "🚀 使用方法:"
echo " 1. 浏览器: <script src='dist/metona-toast.js'></script>"
echo " 2. ES Module: import MeToast from 'dist/metona-toast.esm.js'"
echo " 3. CommonJS: const MeToast = require('dist/metona-toast.cjs.js')"
echo ""
echo "📝 示例:"
echo " 打开 site/index.html 查看官网"
echo " 打开 site/demo.html 查看演示"
echo ""
+4536
View File
File diff suppressed because it is too large Load Diff
-6712
View File
File diff suppressed because it is too large Load Diff
-1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+517 -1036
View File
File diff suppressed because it is too large Load Diff
+3295 -5472
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+3785 -5962
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+4617
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+509
View File
@@ -0,0 +1,509 @@
/**
* MetonaToast
* @module types
* @version 0.5.0
*/
/** Toast 通知类型(107种内置图标类型) */
type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading' | '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';
/** Toast 显示位置 */
type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
/** Toast 主题 */
type ToastTheme = 'light' | 'dark' | 'auto' | 'warm' | (string & {});
/** Toast 动画类型 */
type ToastAnimation = 'slide' | 'fade' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | (string & {});
/** 进度条方向 */
type ToastProgressDirection = 'horizontal' | 'vertical';
interface TypeColor {
fg: string;
bg: string;
}
interface ToastConfig {
position?: ToastPosition;
duration?: number;
max?: number;
gap?: number;
offset?: number;
pauseOnHover?: boolean;
closeOnClick?: boolean;
draggable?: boolean;
dragThreshold?: number;
showProgress?: boolean;
progressDirection?: ToastProgressDirection;
icon?: boolean;
closeButton?: boolean;
theme?: ToastTheme;
animation?: ToastAnimation;
zIndex?: number;
width?: number | string;
className?: string;
style?: Record<string, string>;
onShow?: ((toast: ToastInstance) => void) | null;
onBeforeShow?: ((toast: ToastInstance) => boolean | void) | null;
onClose?: ((toast: ToastInstance) => void) | null;
onClick?: ((toast: ToastInstance) => void) | null;
onUpdate?: ((toast: ToastInstance) => void) | null;
render?: ((toast: ToastInstance) => string) | null;
resetTimerOnUpdate?: boolean;
notifyWhenHidden?: boolean;
onError?: ((errorInfo: ErrorInfo) => void) | null;
group?: string | null;
locale?: string;
plugins?: Array<string | Plugin>;
}
interface ErrorInfo {
hook?: string;
source?: string;
error: Error;
toast?: ToastInstance;
}
interface ToastOptions extends Partial<ToastConfig> {
type?: string;
title?: string;
message?: string;
content?: string;
html?: string;
iconHTML?: string;
id?: string;
}
interface ToastInstance {
id: string;
type: string;
title: string;
message: string;
html: string;
iconHTML: string;
config: ToastConfig;
el: HTMLElement | null;
barEl: HTMLElement | null;
rafId: number | null;
remaining: number;
startedAt: number;
paused: boolean;
closing: boolean;
group: string | null;
_cleanups: Array<() => void>;
_palette(): {
theme: string;
c: TypeColor;
t: Partial<ThemeConfig>;
};
create(): this;
update(partial: Partial<ToastOptions>): this;
updatePosition(position: string): this;
remove(): void;
close(immediate?: boolean): void;
_pause(): void;
_resume(): void;
_destroy(): void;
}
interface LoadingControl {
id: string;
success(message?: string, opts?: ToastOptions): ToastInstance | null;
error(message?: string, opts?: ToastOptions): ToastInstance | null;
info(message?: string, opts?: ToastOptions): ToastInstance | null;
warning(message?: string, opts?: ToastOptions): ToastInstance | null;
update(partial: Partial<ToastOptions>): unknown;
dismiss(): void;
}
interface ProgressControl {
id: string;
setProgress(percent: number): void;
complete(message?: string): void;
error(message?: string): void;
dismiss(): void;
}
interface CountdownControl {
id: string;
cancel(): void;
pause(): void;
resume(): void;
}
interface ActionControl {
id: string;
toast: ToastInstance;
dismiss(): void;
}
interface QueueControl {
then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<void | TResult>;
cancel(): void;
}
interface GroupAPI {
_group: string;
show: (...args: unknown[]) => ToastInstance;
success: (...args: unknown[]) => ToastInstance;
error: (...args: unknown[]) => ToastInstance;
warning: (...args: unknown[]) => ToastInstance;
info: (...args: unknown[]) => ToastInstance;
loading: (...args: unknown[]) => LoadingControl;
action: (...args: unknown[]) => ActionControl;
dismiss(): void;
count(): number;
}
interface ActionButton {
text: string;
onClick: (toast: ToastInstance) => void;
color?: string;
style?: Record<string, string>;
close?: boolean;
}
interface PromiseOptions extends ToastOptions {
loading?: string;
success?: string;
error?: string;
}
interface ConfirmOptions extends ToastOptions {
confirmText?: string;
confirmColor?: string;
cancelText?: string;
cancelColor?: string;
}
interface PromptOptions extends ToastOptions {
inputType?: string;
placeholder?: string;
defaultValue?: string;
submitText?: string;
submitColor?: string;
cancelText?: string;
cancelColor?: string;
}
interface ProgressOptions extends ToastOptions {
progressColor?: string;
}
interface CountdownOptions extends ToastOptions {
onComplete?: (() => void) | null;
}
interface QueueOptions extends ToastOptions {
delay?: number;
}
interface StackOptions extends ToastOptions {
stagger?: number;
}
interface InitOptions {
config?: Partial<ToastConfig>;
theme?: ToastTheme;
locale?: string;
plugins?: Array<string | Plugin>;
}
interface StatusInfo {
version: string;
toasts: number;
theme: string;
locale: string;
plugins: string[];
animations: number;
}
interface AnimationConfig {
enter: Record<string, string | number>;
leave: Record<string, string | number>;
duration: number;
easing: string;
name?: string;
delay?: number;
iterations?: number;
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
}
interface AnimationUtils {
register(name: string, config: Partial<AnimationConfig>): void;
unregister(name: string): void;
get(name: string): AnimationConfig | null;
getAnimationNames(): string[];
getActiveCount(): number;
cancelAll(): void;
reset(): void;
destroy(): void;
}
interface ThemeConfig {
bg: string;
text: string;
border: string;
shadow: string;
hoverShadow: string;
progressBg: string;
closeHoverBg: string;
backdropFilter?: string;
}
interface ThemePreview {
name: string;
resolved: string;
colors: {
background: string;
text: string;
border: string;
};
isDark: boolean;
isLight: boolean;
isAuto: boolean;
}
interface ThemeUtils {
getSystemTheme(): 'light' | 'dark';
resolveTheme(theme: string): string;
getThemeConfig(theme: string): ThemeConfig;
applyTheme(theme: string): void;
getCurrentTheme(): string;
getResolvedTheme(): string;
switchTheme(theme: string): void;
toggleTheme(): void;
resetToAuto(): void;
initTheme(): void;
watchSystemTheme(): void;
unwatchSystemTheme(): void;
addThemeListener(listener: (theme: string, resolved: string) => void): () => void;
removeThemeListener(listener: (theme: string, resolved: string) => void): void;
clearThemeListeners(): void;
registerTheme(name: string, config: ThemeConfig): void;
unregisterTheme(name: string): void;
getAllThemes(): Record<string, ThemeConfig>;
getThemeNames(): string[];
hasTheme(name: string): boolean;
getThemePreview(theme: string): ThemePreview;
generateThemeCSS(theme: string): string;
applyThemeCSS(theme: string): void;
removeThemeCSS(): void;
saveTheme(theme: string): void;
loadTheme(): string;
}
interface LocaleInfo {
code: string;
name: string;
direction: 'ltr' | 'rtl';
isSupported: boolean;
isCurrent: boolean;
isFallback: boolean;
}
interface I18nUtils {
t(key: string, params?: Record<string, string | number>): string;
plural(key: string, count: number, params?: Record<string, string | number>): string;
getCurrentLocale(): string;
setCurrentLocale(locale: string): void;
switchLocale(locale: string): void;
getFallbackLocale(): string;
setFallbackLocale(locale: string): void;
hasTranslation(key: string): boolean;
getTranslations(locale: string): Record<string, unknown>;
addTranslations(locale: string, translations: Record<string, unknown>): void;
removeTranslation(locale: string, key: string): void;
clearTranslations(locale: string): void;
getSupportedLocales(): string[];
isLocaleSupported(locale: string): boolean;
getLocaleName(locale: string): string;
getLocaleDirection(locale: string): 'ltr' | 'rtl';
getLocaleInfo(locale: string): LocaleInfo;
getAllLocaleInfo(): LocaleInfo[];
formatNumber(number: number, options?: Intl.NumberFormatOptions): string;
formatCurrency(amount: number, currency?: string, options?: Intl.NumberFormatOptions): string;
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
formatDate(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
formatTime(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
formatRelativeTime(date: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
formatList(list: string[], options?: Record<string, unknown>): string;
formatPlural(count: number, options?: Intl.PluralRulesOptions): string;
addLocaleListener(listener: (locale: string) => void): () => void;
removeLocaleListener(listener: (locale: string) => void): void;
clearLocaleListeners(): void;
initI18n(): void;
saveLocale(locale: string): void;
loadLocale(): string;
getDefaultLocale(): string;
}
interface Plugin {
name: string;
version?: string;
description?: string;
hooks?: Record<string, (...args: unknown[]) => void>;
middleware?: (...args: unknown[]) => unknown;
install?: (manager: PluginManager) => void | Record<string, unknown> | null;
uninstall?: (manager: PluginManager) => void;
init?: (manager: PluginManager) => void;
destroy?: (manager: PluginManager) => void;
installed?: boolean;
enabled?: boolean;
[key: string]: unknown;
}
interface PluginManager {
register(name: string, plugin: Plugin): PluginManager;
unregister(name: string): PluginManager;
get(name: string): Plugin | null;
has(name: string): boolean;
getAll(): Plugin[];
getNames(): string[];
enable(name: string): PluginManager;
disable(name: string): PluginManager;
isEnabled(name: string): boolean;
destroy(): void;
}
interface PluginUtils {
createManager(): PluginManager;
register(name: string, plugin: Plugin): PluginManager;
unregister(name: string): PluginManager;
get(name: string): Plugin | null;
has(name: string): boolean;
getAll(): Plugin[];
getNames(): string[];
enable(name: string): PluginManager;
disable(name: string): PluginManager;
isEnabled(name: string): boolean;
getPreset(name: string): Plugin | null;
getAllPresets(): Record<string, Plugin>;
createPlugin(config: Partial<Plugin>): Plugin;
validatePlugin(plugin: Partial<Plugin>): {
valid: boolean;
errors: string[];
};
}
interface MeToast {
version: string;
_toasts: Map<string, ToastInstance>;
_config: ToastConfig;
_destroyed?: boolean;
_emit(opts: ToastOptions): ToastInstance;
_remove(id: string): void;
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
_groupCount(name: string): number;
configure(opts: Partial<ToastConfig>): MeToast;
init(options?: InitOptions): MeToast;
destroy(): void;
getStatus(): StatusInfo;
getConfig(): ToastConfig;
updateConfig(config: Partial<ToastConfig>): MeToast;
resetConfig(): MeToast;
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
group(name: string): GroupAPI;
dismissGroup(name: string): void;
find(id: string): ToastInstance | undefined;
dismiss(id?: string): void;
removeToast(id: string): void;
clear(position?: ToastPosition): void;
getToasts(): ToastInstance[];
count(): number;
hasToasts(): boolean;
getToast(id: string): ToastInstance | null;
closeAll(): void;
clearAll(): void;
pauseAll(): void;
resumeAll(): void;
updateAll(partial: Partial<ToastOptions>): void;
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
findByType(type: ToastType): ToastInstance[];
findByPosition(position: ToastPosition): ToastInstance[];
getAll(): Map<string, ToastInstance>;
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
animations: AnimationUtils;
themes: ThemeUtils;
i18n: I18nUtils;
plugins: PluginUtils;
presetPlugins: Record<string, Plugin>;
}
declare global {
interface Window {
MeToast: MeToast;
Met: MeToast;
}
}
/**
* MeToast meToast
*/
declare const enhancedMeToast: {
version: string;
animations: AnimationUtils;
themes: ThemeUtils;
i18n: I18nUtils;
plugins: PluginUtils;
presetPlugins: Record<string, Plugin>;
_toasts: Map<string, ToastInstance>;
_config: ToastConfig;
_destroyed?: boolean;
_emit(opts: ToastOptions): ToastInstance;
_remove(id: string): void;
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
_groupCount(name: string): number;
configure(opts: Partial<ToastConfig>): MeToast;
init(options?: InitOptions): MeToast;
destroy(): void;
getStatus(): StatusInfo;
getConfig(): ToastConfig;
updateConfig(config: Partial<ToastConfig>): MeToast;
resetConfig(): MeToast;
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
group(name: string): GroupAPI;
dismissGroup(name: string): void;
find(id: string): ToastInstance | undefined;
dismiss(id?: string): void;
removeToast(id: string): void;
clear(position?: ToastPosition): void;
getToasts(): ToastInstance[];
count(): number;
hasToasts(): boolean;
getToast(id: string): ToastInstance | null;
closeAll(): void;
clearAll(): void;
pauseAll(): void;
resumeAll(): void;
updateAll(partial: Partial<ToastOptions>): void;
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
findByType(type: ToastType): ToastInstance[];
findByPosition(position: ToastPosition): ToastInstance[];
getAll(): Map<string, ToastInstance>;
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
};
/**
* MetonaToast React React
* @module react
* @version 0.5.0
* @description useToast hook + <Toast /> React optional peerDependency
*/
/** 支持声明式渲染的 Toast 类型 */
type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
message: string;
type?: ReactToastType;
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
autoClose?: boolean;
}
/**
* API Toast
* 便
*/
declare const createBoundApi: (cleanup: Set<string>) => Record<string, unknown>;
/**
* useToast MeToast API Toast
* "通知随组件生命周期"
*/
declare const useToast: () => Record<string, unknown>;
/**
* Toast Toast props
* autoClose true
*/
declare const Toast: (props: ToastProps) => null;
export { enhancedMeToast as MeToast, ReactToastType, Toast, ToastProps, createBoundApi, useToast };
+4612
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+30
View File
@@ -0,0 +1,30 @@
module.exports = {
testEnvironment: 'jsdom',
setupFiles: ['<rootDir>/tests/setup.ts'],
transform: {
'^.+\\.ts$': 'babel-jest',
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transformIgnorePatterns: [
'/node_modules/(?!(@rollup)/)',
],
moduleFileExtensions: ['ts', 'js', 'json'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/index.ts',
'!src/types.ts',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
coverageThreshold: {
global: {
statements: 85,
branches: 65,
functions: 85,
lines: 95,
},
},
verbose: true,
};
-17
View File
@@ -1,17 +0,0 @@
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.js$': 'babel-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!(@rollup)/)',
],
moduleFileExtensions: ['js', 'json'],
collectCoverageFrom: [
'src/**/*.js',
'!src/index.js',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
verbose: true,
};
+524 -5
View File
@@ -1,34 +1,51 @@
{
"name": "metona-toast",
"version": "2.0.0",
"name": "@metona-team/metona-toast",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metona-toast",
"version": "2.0.0",
"name": "@metona-team/metona-toast",
"version": "0.3.0",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.22.0",
"@babel/plugin-transform-modules-commonjs": "^7.22.0",
"@babel/preset-env": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^11.1.6",
"@types/jest": "^29.5.0",
"@types/react": "^18.3.31",
"@types/react-dom": "^18.3.7",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"babel-jest": "^29.5.0",
"eslint": "^8.40.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"prettier": "^2.8.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rollup": "^3.20.0",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-serve": "^2.0.1",
"tslib": "^2.6.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=14.0.0"
"node": ">=16.0.0"
},
"peerDependencies": {
"react": ">=17.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"node_modules/@babel/code-frame": {
@@ -1570,6 +1587,26 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-typescript": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
"integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.29.7",
"@babel/helper-create-class-features-plugin": "^7.29.7",
"@babel/helper-plugin-utils": "^7.29.7",
"@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
"@babel/plugin-syntax-typescript": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-unicode-escapes": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz",
@@ -1738,6 +1775,26 @@
"@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/preset-typescript": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz",
"integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7",
"@babel/helper-validator-option": "^7.29.7",
"@babel/plugin-syntax-jsx": "^7.29.7",
"@babel/plugin-transform-modules-commonjs": "^7.29.7",
"@babel/plugin-transform-typescript": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/template": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz",
@@ -2468,6 +2525,33 @@
}
}
},
"node_modules/@rollup/plugin-typescript": {
"version": "11.1.6",
"resolved": "https://registry.npmmirror.com/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz",
"integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.14.0||^3.0.0||^4.0.0",
"tslib": "*",
"typescript": ">=3.7.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
},
"tslib": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.4.0",
"resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
@@ -2650,6 +2734,34 @@
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.31",
"resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz",
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.20.2.tgz",
@@ -2688,6 +2800,301 @@
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/type-utils": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.65.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.6.tgz",
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.65.0.tgz",
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.65.0",
"@typescript-eslint/types": "^8.65.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.65.0",
"@typescript-eslint/tsconfig-utils": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.8",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.65.0.tgz",
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.65.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
@@ -3443,6 +3850,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-3.0.2.tgz",
@@ -4073,6 +4487,24 @@
"bser": "2.1.1"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5718,6 +6150,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -6328,6 +6773,33 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
@@ -6670,6 +7142,16 @@
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
@@ -6980,6 +7462,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/tmpl/-/tmpl-1.0.5.tgz",
@@ -7029,6 +7528,26 @@
"node": ">=12"
}
},
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
+40 -20
View File
@@ -1,39 +1,50 @@
{
"name": "@metona-team/metona-toast",
"version": "2.0.1",
"description": "轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。",
"main": "dist/metona-toast.js",
"module": "src/index.js",
"version": "0.5.0",
"description": "轻量、零依赖、精致美观的Toast通知库。TypeScript源码,开箱即用。",
"type": "module",
"main": "dist/metona-toast.cjs",
"module": "dist/metona-toast.esm.js",
"unpkg": "dist/metona-toast.min.js",
"jsdelivr": "dist/metona-toast.min.js",
"types": "types/index.d.ts",
"types": "dist/metona-toast.d.ts",
"exports": {
".": {
"import": "./src/index.js",
"require": "./dist/metona-toast.js",
"types": "./types/index.d.ts"
"import": "./dist/metona-toast.esm.js",
"require": "./dist/metona-toast.cjs",
"types": "./dist/metona-toast.d.ts"
},
"./react": {
"import": "./dist/react.js",
"require": "./dist/react.cjs",
"types": "./dist/react.d.ts"
}
},
"peerDependencies": {
"react": ">=17.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
},
"files": [
"dist/",
"src/",
"types/",
"README.md",
"LICENSE"
],
"scripts": {
"build": "rollup -c",
"build": "tsc --noEmit && rollup -c",
"dev": "rollup -c -w",
"test": "jest",
"test": "jest --coverage",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"format": "prettier --write src/",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"format": "prettier --write src/**/*.ts tests/**/*.ts",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build",
"docs": "jsdoc src/ -d docs",
"example": "serve examples/"
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build",
"serve": "bash serve.sh"
},
"repository": {
"type": "git",
@@ -58,25 +69,34 @@
"homepage": "https://git.metona.cn/MetonaTeam/MetonaToast#readme",
"devDependencies": {
"@babel/core": "^7.22.0",
"@babel/preset-env": "^7.22.0",
"@babel/plugin-transform-modules-commonjs": "^7.22.0",
"@babel/preset-env": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^11.1.6",
"@types/jest": "^29.5.0",
"@types/react": "^18.3.31",
"@types/react-dom": "^18.3.7",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"babel-jest": "^29.5.0",
"eslint": "^8.40.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"prettier": "^2.8.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rollup": "^3.20.0",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-serve": "^2.0.1",
"tslib": "^2.6.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=14.0.0"
"node": ">=16.0.0"
},
"browserslist": [
"> 1%",
+99 -96
View File
@@ -1,5 +1,6 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
import dts from 'rollup-plugin-dts';
import serve from 'rollup-plugin-serve';
@@ -8,128 +9,130 @@ import livereload from 'rollup-plugin-livereload';
const isDev = process.env.ROLLUP_WATCH;
const isProd = process.env.NODE_ENV === 'production';
// 基础配置
const baseConfig = {
input: 'src/index.js',
plugins: [
resolve(),
commonjs(),
],
};
// 基础插件
const basePlugins = [
resolve(),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
declaration: false,
sourceMap: !isProd,
}),
];
// 开发服务器配置
const devPlugins = isDev ? [
serve({
open: true,
contentBase: ['site', 'dist'],
port: 3000,
port: 3001,
}),
livereload({
watch: ['src', 'site'],
}),
] : [];
// 生产环境配置
const prodPlugins = isProd ? [
terser({
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.warn'],
},
format: {
comments: false,
},
}),
] : [];
// 输出配置
export default [
// UMD格式(浏览器
// UMD(开发模式
{
...baseConfig,
input: 'src/index.ts',
output: {
file: 'dist/metona-toast.js',
format: 'umd',
name: 'MeToast',
exports: 'named',
sourcemap: !isProd,
globals: {},
sourcemap: true,
},
plugins: [
...baseConfig.plugins,
...devPlugins,
...prodPlugins,
],
plugins: [...basePlugins, ...devPlugins],
},
// ESM格式(现代浏览器/打包工具)
{
...baseConfig,
output: {
file: 'dist/metona-toast.esm.js',
format: 'es',
exports: 'named',
sourcemap: !isProd,
// 生产构建(非 dev 模式时输出全格式)
...(isDev ? [] : [
// ESM
{
input: 'src/index.ts',
output: {
file: 'dist/metona-toast.esm.js',
format: 'es',
exports: 'named',
sourcemap: true,
},
plugins: basePlugins,
},
plugins: [
...baseConfig.plugins,
...prodPlugins,
],
},
// CommonJS格式(Node.js
{
...baseConfig,
output: {
file: 'dist/metona-toast.cjs.js',
format: 'cjs',
exports: 'named',
sourcemap: !isProd,
// CommonJS.cjs 扩展名:包声明 type:module.js 会被 Node 按 ESM 解析)
// footer 将 default 提升为 module.exports,保证 require() 直接返回 MeToast
{
input: 'src/index.ts',
output: {
file: 'dist/metona-toast.cjs',
format: 'cjs',
exports: 'named',
sourcemap: true,
footer: 'module.exports = Object.assign(exports.default, exports);',
},
plugins: basePlugins,
},
plugins: [
...baseConfig.plugins,
...prodPlugins,
],
},
// 压缩版本(UMD
{
...baseConfig,
output: {
file: 'dist/metona-toast.min.js',
format: 'umd',
name: 'MeToast',
exports: 'named',
sourcemap: false,
globals: {},
// UMD 压缩版
{
input: 'src/index.ts',
output: {
file: 'dist/metona-toast.min.js',
format: 'umd',
name: 'MeToast',
exports: 'named',
sourcemap: false,
},
plugins: [
...basePlugins,
terser({
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.warn'],
passes: 2,
},
format: { comments: false },
mangle: { toplevel: true },
}),
],
},
plugins: [
...baseConfig.plugins,
terser({
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.warn'],
passes: 2,
// TypeScript 声明文件合并
{
input: 'src/index.ts',
output: {
file: 'dist/metona-toast.d.ts',
format: 'es',
},
plugins: [dts()],
},
// React 适配器(ESM + CJS),react 为 optional peer 外部依赖
{
input: 'src/react.ts',
external: ['react'],
output: [
{
file: 'dist/react.js',
format: 'es',
exports: 'named',
sourcemap: true,
},
format: {
comments: false,
{
file: 'dist/react.cjs',
format: 'cjs',
exports: 'named',
sourcemap: true,
},
mangle: {
toplevel: true,
},
}),
],
},
// TypeScript声明文件
{
input: 'types/index.d.ts',
output: {
file: 'dist/metona-toast.d.ts',
format: 'es',
],
plugins: basePlugins,
},
plugins: [dts()],
},
{
input: 'src/react.ts',
external: ['react'],
output: {
file: 'dist/react.d.ts',
format: 'es',
},
plugins: [dts()],
},
]),
];
+1 -1
View File
@@ -15,7 +15,7 @@ else
exit 1
fi
PORT=${1:-3000}
PORT=${1:-3001}
echo "✅ Python: $($PYTHON --version 2>&1)"
echo "🌐 启动服务器..."
+227 -98
View File
@@ -7,51 +7,78 @@
<title>在线演示 — MetonaToast</title>
<style>
:root {
--bg: #0b1121; --surface: #151d33; --border: #1e2d4d;
--text: #e2e8f0; --muted: #64748b; --accent: #6366f1; --accent2: #818cf8;
--bg: #070b14; --surface: rgba(255,255,255,0.03); --surface2: rgba(255,255,255,0.05);
--border: rgba(255,255,255,0.08); --text: #e8ecf4; --muted: #8b94a7;
--accent: #a5b4fc; --grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
--radius: 14px;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); color:var(--text); line-height:1.6; min-height:100vh; }
a { color:var(--accent2); text-decoration:none; }
code { background:rgba(99,102,241,0.12); color:#a5b4fc; padding:1px 6px; border-radius:4px; font-size:0.85em; }
.nav { position:sticky; top:0; z-index:100; background:rgba(11,17,33,0.88); backdrop-filter:blur(14px); border-bottom:1px solid var(--border); }
.nav-inner { max-width:1300px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; padding:0 20px; height:56px; }
.nav-logo { font-size:18px; font-weight:700; background:linear-gradient(135deg,#818cf8,#c084fc,#f472b6); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.nav-links { display:flex; gap:20px; }
.nav-links a { color:var(--muted); font-size:13px; font-weight:500; transition:.2s; }
.nav-links a:hover { color:var(--accent2); }
.hero { text-align:center; padding:40px 20px 30px; border-bottom:1px solid var(--border); background:radial-gradient(ellipse at 50% 0%,rgba(99,102,241,0.12) 0%,transparent 50%); }
.hero h1 { font-size:36px; font-weight:800; }
.hero h1 span { background:linear-gradient(135deg,#818cf8,#c084fc,#f472b6); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
body {
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
background:var(--bg); color:var(--text); line-height:1.65; min-height:100vh;
background-image:
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(99,102,241,0.12), transparent),
radial-gradient(ellipse 50% 40% at 90% 20%, rgba(168,85,247,0.07), transparent);
}
a { color:var(--accent); text-decoration:none; }
code { background:rgba(99,102,241,0.15); color:#a5b4fc; padding:1px 6px; border-radius:5px; font-size:.85em; }
.grad-text { background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; color:transparent; }
.nav { position:sticky; top:0; z-index:100; background:rgba(7,11,20,0.72); backdrop-filter:blur(16px); border-bottom:1px solid var(--border); }
.nav-inner { max-width:1320px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; padding:0 22px; height:58px; }
.nav-logo { font-size:18px; font-weight:800; }
.nav-links { display:flex; gap:22px; }
.nav-links a { color:var(--muted); font-size:13.5px; font-weight:500; transition:.2s; }
.nav-links a:hover { color:var(--accent); }
.hero { text-align:center; padding:38px 20px 28px; }
.hero h1 { font-size:34px; font-weight:800; }
.hero p { color:var(--muted); font-size:14px; margin-top:6px; }
.container { max-width:1300px; margin:0 auto; padding:24px 20px; display:grid; grid-template-columns:repeat(auto-fill,minmax(400px,1fr)); gap:18px; }
.card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:20px; }
.card h2 { font-size:15px; margin-bottom:2px; }
.card > p { color:var(--muted); font-size:11px; margin-bottom:12px; }
.container { max-width:1320px; margin:0 auto; padding:20px; display:grid; grid-template-columns:repeat(auto-fill,minmax(400px,1fr)); gap:16px; }
.card {
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
padding:20px; transition:border-color .2s, transform .2s; position:relative; overflow:hidden;
}
.card:hover { border-color:rgba(129,140,248,.35); }
.card::before {
content:''; position:absolute; top:0; left:0; right:0; height:2px;
background:var(--grad); opacity:0; transition:opacity .25s;
}
.card:hover::before { opacity:.8; }
.card h2 { font-size:14.5px; margin-bottom:2px; font-weight:700; }
.card > p { color:var(--muted); font-size:11.5px; margin-bottom:12px; }
.btn-row { display:flex; flex-wrap:wrap; gap:6px; }
.btn { padding:8px 14px; border:none; border-radius:7px; font-size:12px; font-weight:500; cursor:pointer; transition:.15s; color:#fff; }
.btn:hover { transform:translateY(-1px); filter:brightness(1.15); }
.btn-sm { padding:5px 10px; font-size:11px; border-radius:5px; }
.btn {
padding:8px 14px; border:none; border-radius:8px; font-size:12px; font-weight:600;
cursor:pointer; transition:transform .15s, filter .15s, box-shadow .15s; color:#fff;
}
.btn:hover { transform:translateY(-1.5px); filter:brightness(1.15); box-shadow:0 6px 18px -6px rgba(0,0,0,.5); }
.btn-sm { padding:5.5px 11px; font-size:11px; border-radius:6px; }
.btn-tiny { padding:4px 9px; font-size:10px; border-radius:5px; }
.btn-outline { background:transparent; border:1px solid var(--border); color:var(--text); }
.btn-outline:hover { border-color:var(--accent); background:rgba(99,102,241,0.06); }
.btn-tiny { padding:4px 9px; font-size:10px; border-radius:4px; }
.btn-outline:hover { border-color:rgba(129,140,248,.5); background:rgba(129,140,248,.08); }
.c-s { background:#10b981; } .c-e { background:#ef4444; } .c-w { background:#f59e0b; color:#1e293b; }
.c-i { background:#3b82f6; } .c-p { background:#6366f1; } .c-pp { background:#8b5cf6; }
.c-pk { background:#ec4899; } .c-t { background:#14b8a6; } .c-o { background:#f97316; }
.c-sl { background:#475569; } .c-r { background:#f43f5e; } .c-g { background:#22c55e; }
.label { font-size:10px; text-transform:uppercase; letter-spacing:0.8px; color:var(--muted); margin-bottom:10px; font-weight:600; }
.demo-section { border-top:1px solid var(--border); padding-top:8px; margin-top:8px; }
.c-grad { background:var(--grad); }
.label {
font-size:10px; text-transform:uppercase; letter-spacing:1px; color:var(--muted);
margin-bottom:10px; font-weight:700;
}
.demo-section { border-top:1px solid var(--border); padding-top:10px; margin-top:10px; }
.status-line { font-size:11.5px; color:#34d399; margin-top:10px; font-family:monospace; min-height:18px; }
footer { text-align:center; padding:30px 20px; color:var(--muted); font-size:12px; border-top:1px solid var(--border); }
@media (max-width:768px) { .container { grid-template-columns:1fr; } }
@media (max-width:820px) { .container { grid-template-columns:1fr; } }
</style>
</head>
<body>
<nav class="nav">
<div class="nav-inner">
<div class="nav-logo">MetonaToast</div>
<div class="nav-logo"><span class="grad-text">MetonaToast</span></div>
<div class="nav-links">
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
@@ -62,7 +89,7 @@
</nav>
<header class="hero">
<h1><span>在线演示</span></h1>
<h1><span class="grad-text">在线演示</span></h1>
<p>覆盖项目全部功能的交互演示 — 点击即可体验</p>
</header>
@@ -81,10 +108,10 @@
<div class="demo-section">
<div class="btn-row">
<button class="btn btn-outline btn-sm" onclick="MeToast.success({title:'保存成功',message:'文件已同步到云端'})">带标题</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.info('持续10秒',{duration:10000})">长时10s</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.info('持续 10 秒',{duration:10000})">长时 10s</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.warning('手动关闭',{duration:0,closeButton:true})">手动关闭</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.info('',{icon:false})">无图标</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.info('无按钮',{closeButton:false})">关闭按钮</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.info('无关闭按钮',{closeButton:false})">无按钮</button>
<button class="btn btn-outline btn-sm" onclick="demoCustomIcon()">自定义图标</button>
</div>
</div>
@@ -92,16 +119,17 @@
<!-- 2. Loading 链式 -->
<div class="card">
<div class="label">🔄 Loading 链式转换</div>
<div class="label">🔄 Loading 链式转换 <span style="color:#34d399">id 稳定</span></div>
<div class="btn-row">
<button class="btn c-s" onclick="demoLoading()">提交成功</button>
<button class="btn c-e" onclick="demoLoadingFail()">提交失败</button>
<button class="btn c-w" onclick="demoLoadingWarn()">转为警告</button>
<button class="btn c-i" onclick="demoLoadingUpdate()">原地更新</button>
<button class="btn c-s" onclick="demoLoading('success')">提交成功</button>
<button class="btn c-e" onclick="demoLoading('error')">提交失败</button>
<button class="btn c-w" onclick="demoLoading('warning')">转为警告</button>
<button class="btn c-i" onclick="demoLoading('update')">原地更新</button>
</div>
<p class="status-line" id="loading-status">转换后返回的 toast 与 loading 是同一个实例(id 不变)</p>
</div>
<!-- 3. Promise + confirm + prompt -->
<!-- 3. 对话框 -->
<div class="card">
<div class="label">💬 对话框</div>
<div class="btn-row">
@@ -116,8 +144,8 @@
<div class="card">
<div class="label">📊 进度 & 倒计时</div>
<div class="btn-row">
<button class="btn c-o" onclick="demoProgress()">进度条(水平)</button>
<button class="btn c-g" onclick="demoProgressV()">进度条(垂直)</button>
<button class="btn c-o" onclick="demoProgress('horizontal')">进度条(水平)</button>
<button class="btn c-g" onclick="demoProgress('vertical')">进度条(垂直)</button>
<button class="btn c-pk" onclick="demoCountdown()">倒计时 5s</button>
<button class="btn c-r" onclick="demoCountdownPause()">倒计时(可暂停)</button>
</div>
@@ -136,7 +164,7 @@
<!-- 6. Action Toast -->
<div class="card">
<div class="label">⚡ Action Toast <span style="color:#34d399;font-size:10px;">v2.0</span></div>
<div class="label">⚡ Action Toast <span style="color:#34d399">close:false 不关闭</span></div>
<div class="btn-row">
<button class="btn c-e" onclick="demoActionDelete()">删除+撤销</button>
<button class="btn c-i" onclick="demoActionMulti()">多按钮</button>
@@ -146,7 +174,7 @@
<!-- 7. 分组管理 -->
<div class="card">
<div class="label">📁 分组管理 <span style="color:#34d399;font-size:10px;">v2.0</span></div>
<div class="label">📁 分组管理</div>
<div class="btn-row">
<button class="btn c-p" onclick="demoGroup()">创建分组消息</button>
<button class="btn c-e" onclick="dismissGroup()">关闭分组</button>
@@ -157,7 +185,7 @@
<!-- 8. 动画效果 -->
<div class="card">
<div class="label">🎭 11种动画效果</div>
<div class="label">🎭 11 种动画效果</div>
<div class="btn-row">
<button class="btn btn-outline btn-tiny" onclick="anim('slide')">slide</button>
<button class="btn btn-outline btn-tiny" onclick="anim('fade')">fade</button>
@@ -171,9 +199,14 @@
<button class="btn btn-outline btn-tiny" onclick="anim('slideLeft')">slideLeft</button>
<button class="btn btn-outline btn-tiny" onclick="anim('slideRight')">slideRight</button>
</div>
<div class="demo-section">
<div class="btn-row">
<button class="btn c-grad" onclick="demoCustomAnim()">✨ 注册自定义动画</button>
</div>
</div>
</div>
<!-- 9. 6种位置 -->
<!-- 9. 位置 -->
<div class="card">
<div class="label">📍 全部位置</div>
<div class="btn-row">
@@ -186,7 +219,7 @@
</div>
</div>
<!-- 10. 主题切换 + 自定义 -->
<!-- 10. 主题 -->
<div class="card">
<div class="label">🎨 主题切换 & 自定义</div>
<div class="btn-row">
@@ -194,24 +227,43 @@
<button class="btn btn-outline btn-sm" onclick="switchTheme('dark')">🌙 Dark</button>
<button class="btn btn-outline btn-sm" onclick="switchTheme('auto')">🔄 Auto</button>
<button class="btn btn-outline btn-sm" onclick="switchTheme('warm')">🔥 Warm</button>
<button class="btn btn-outline btn-sm" onclick="demoRegisterTheme()">注册自定义主题</button>
<button class="btn btn-outline btn-sm" onclick="demoRegisterTheme()">注册自定义</button>
</div>
</div>
<!-- 11. 插件系统 -->
<!-- 11. 插件 -->
<div class="card">
<div class="label">🔌 插件系统</div>
<div class="label">🔌 插件系统 <span style="color:#34d399">4 款预设</span></div>
<div class="btn-row">
<button class="btn btn-outline btn-sm" onclick="MeToast.use('keyboard');MeToast.info('已安装 — 按 ESC 关闭所有 Toast')">keyboard ESC关闭</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.use('persistence');MeToast.info('已安装 — 配置持久化到 localStorage')">persistence 持久化</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.use('accessibility');MeToast.info('已安装 — 屏幕阅读器可用')">accessibility 无障碍</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.use('keyboard');MeToast.info('已安装 — 按 ESC 关闭所有 Toast')">keyboard</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.use('persistence');MeToast.info('已安装 — 配置持久化')">persistence</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.use('accessibility');MeToast.info('已安装 — 屏幕阅读器')">accessibility</button>
<button class="btn btn-outline btn-sm" onclick="MeToast.use('dedupe');MeToast.info('已安装 — 同消息去重')">dedupe</button>
<button class="btn btn-outline btn-sm" onclick="demoCustomPlugin()">自定义插件</button>
</div>
<div class="demo-section">
<div class="btn-row">
<button class="btn c-t btn-sm" onclick="demoDedupe()">去重演示</button>
</div>
</div>
</div>
<!-- 12. v2.0 新功能 -->
<!-- 12. 钩子系统 -->
<div class="card">
<div class="label">🆕 v2.0 新功能 <span style="color:#34d399;font-size:10px;">resetTimer · onUpdate · render · notify</span></div>
<div class="label">🔗 钩子系统 <span style="color:#34d399">beforeShow 拦截演示</span></div>
<div class="btn-row">
<button class="btn c-p" onclick="demoHookBlock()">开启拦截 error</button>
<button class="btn c-e" onclick="MeToast.error('这条会被拦截')">触发被拦 error</button>
<button class="btn c-s" onclick="MeToast.success('这条不受影响')">正常 success</button>
<button class="btn btn-outline btn-sm" onclick="demoHookUnblock()">关闭拦截</button>
<button class="btn btn-outline btn-sm" onclick="demoHookCount()">钩子统计</button>
</div>
<p class="status-line" id="hook-status">钩子:未注册</p>
</div>
<!-- 13. 高级特性 -->
<div class="card">
<div class="label">🔥 高级特性</div>
<div class="btn-row">
<button class="btn c-i" onclick="demoResetTimer()">resetTimerOnUpdate</button>
<button class="btn c-p" onclick="demoOnUpdate()">onUpdate 回调</button>
@@ -220,7 +272,7 @@
</div>
</div>
<!-- 13. Toast 管理 -->
<!-- 14. Toast 管理 -->
<div class="card">
<div class="label">🔧 Toast 管理</div>
<div class="btn-row">
@@ -230,7 +282,7 @@
<button class="btn c-sl" onclick="MeToast.pauseAll();MeToast.info('已暂停全部')">暂停全部</button>
<button class="btn c-sl" onclick="MeToast.resumeAll();MeToast.info('已恢复全部')">恢复全部</button>
<button class="btn c-e" onclick="MeToast.dismiss()">关闭全部</button>
<button class="btn c-r" onclick="MeToast.destroy();MeToast.info('已销毁')">销毁</button>
<button class="btn c-r" onclick="MeToast.destroy();MeToast.init({});MeToast.info('已销毁并重建')">销毁重建</button>
</div>
<div class="demo-section">
<div class="btn-row">
@@ -238,23 +290,34 @@
<button class="btn btn-outline btn-tiny" onclick="demoGetAll()">getAll</button>
<button class="btn btn-outline btn-tiny" onclick="demoClearByPos()">clear(位置)</button>
<button class="btn btn-outline btn-tiny" onclick="demoGetConfig()">查看配置</button>
<button class="btn btn-outline btn-tiny" onclick="demoRemove()">removeToast 立即移除</button>
</div>
</div>
</div>
<!-- 14. 交互特性 -->
<!-- 15. onError -->
<div class="card">
<div class="label">🛡️ onError 错误捕获</div>
<div class="btn-row">
<button class="btn c-e" onclick="demoOnError()">演示 onError</button>
<button class="btn c-w" onclick="demoOnErrorReset()">重置回调</button>
</div>
</div>
<!-- 16. 交互特性 -->
<div class="card">
<div class="label">✋ 交互特性</div>
<div class="btn-row">
<button class="btn c-i" onclick="demoDraggable()">拖拽关闭</button>
<button class="btn c-p" onclick="demoPauseHover()">悬停暂停</button>
<button class="btn c-w" onclick="demoCloseOnClick()">点击关闭</button>
<button class="btn c-pp" onclick="demoWidth()">自定义宽度</button>
<button class="btn c-t" onclick="demoClassName()">自定义className</button>
<button class="btn c-p" onclick="demoDragThreshold()">拖拽阈值 300px</button>
<button class="btn c-w" onclick="demoPauseHover()">悬停暂停</button>
<button class="btn c-pp" onclick="demoCloseOnClick()">点击关闭</button>
<button class="btn c-t" onclick="demoWidth()">自定义宽度</button>
<button class="btn c-sl" onclick="demoClassName()">自定义 className</button>
</div>
</div>
<!-- 15. 国际化切换 -->
<!-- 17. 国际化 -->
<div class="card">
<div class="label">🌍 国际化 & 格式化</div>
<div class="btn-row">
@@ -264,16 +327,19 @@
</div>
</div>
<!-- 16. 107种预设图标 -->
<!-- 18. 图标网格 -->
<div class="card" style="grid-column:1/-1;">
<div class="label">🎨 107种预设图标 — 点击任意图标发送Toast</div>
<div class="btn-row" id="icon-grid" style="max-height:300px;overflow-y:auto;padding:4px 0;"></div>
<div class="label">🎨 107 种预设图标 — 点击任意图标发送 Toast</div>
<div class="btn-row" id="icon-grid" style="max-height:320px;overflow-y:auto;padding:4px 0;"></div>
</div>
</main>
<footer>
MetonaToast v2.0.1 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
MetonaToast v0.5.0 · MIT ·
<a href="index.html">首页</a> ·
<a href="docs.html">文档</a> ·
<a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
</footer>
<script src="../dist/metona-toast.js"></script>
@@ -281,27 +347,41 @@
MeToast.configure({ position:'top-right', duration:4000, theme:'dark', animation:'slide', pauseOnHover:true, showProgress:true });
// === Loading ===
function demoLoading() { const l=MeToast.loading('正在提交...'); setTimeout(()=>l.success('提交成功!'),2000); }
function demoLoadingFail() { const l=MeToast.loading('正在处理...'); setTimeout(()=>l.error('处理失败'),2000); }
function demoLoadingWarn() { const l=MeToast.loading('检查中...'); setTimeout(()=>l.warning('发现潜在问题'),2000); }
function demoLoadingUpdate() { const l=MeToast.loading('处理中 0%'); let i=1; const t=setInterval(()=>{if(i>3){clearInterval(t);l.success('完成!');return;} l.update({message:'处理中 '+i*33+'%'}); i++;},600); }
function demoLoading(mode) {
const l = MeToast.loading('正在处理...');
const showId = () => document.getElementById('loading-status').textContent = 'loading id: ' + l.id;
showId();
setTimeout(() => {
const map = { success:['提交成功!','success'], error:['处理失败','error'], warning:['发现潜在问题','warning'], update:null };
if (mode === 'update') {
l.update({ message: '进度 50%' });
setTimeout(() => l.success('完成!'), 1000);
return;
}
const [msg, type] = map[mode];
const t = l[type](msg);
if (t) document.getElementById('loading-status').textContent = '转换后 id: ' + t.id + '' + (t.id === l.id ? '同一实例 ✓' : '不同实例 ✗') + '';
}, 1600);
}
// === Promise / confirm / prompt ===
async function demoPromise() {
const p=new Promise((r,e)=>setTimeout(()=>Math.random()>0.3?r('ok'):e('fail'),2000));
try{await MeToast.promise(p,{loading:'处理中...',success:'成功!',error:'失败'});}catch(_){}
const p = new Promise((r,e)=>setTimeout(()=>Math.random()>0.3?r('ok'):e('fail'),2000));
try{ await MeToast.promise(p,{loading:'处理中...',success:'成功!',error:'失败'}); }catch(_){}
}
async function demoConfirm() { const ok=await MeToast.confirm('确定删除?',{confirmText:'删除',confirmColor:'#ef4444'}); if(ok)MeToast.success('已删除'); }
async function demoPrompt() { const v=await MeToast.prompt('请输入姓名',{placeholder:'请输入...'}); if(v) MeToast.info('你好,'+v+''); }
async function demoPromptPassword() { const v=await MeToast.prompt('请输入密码',{placeholder:'密码',inputType:'password',submitText:'登录'}); if(v) MeToast.info('密码已输入'); }
// === Progress & Countdown ===
function demoProgress() { const p=MeToast.progress('上传中...',{progressColor:'#3b82f6'}); let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('上传完成!');}else p.setProgress(v);},400); }
function demoProgressV() { const p=MeToast.progress('上传中...',{progressColor:'#10b981',progressDirection:'vertical'}); let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('完成!');}else p.setProgress(v);},400); }
function demoProgress(dir) {
const p=MeToast.progress('上传中...',{progressColor: dir==='vertical'?'#10b981':'#3b82f6', progressDirection: dir});
let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('上传完成!');}else p.setProgress(v);},400);
}
function demoCountdown() { MeToast.countdown('{seconds} 秒后执行',5,{onComplete:()=>MeToast.success('已执行')}); }
function demoCountdownPause() {
const c=MeToast.countdown('{seconds}s 可暂停',10,{type:'info',onComplete:()=>MeToast.success('倒计时结束')});
setTimeout(()=>{c.pause();MeToast.warning('已暂停3秒后恢复',{duration:3000})},4000);
setTimeout(()=>{c.pause();MeToast.warning('已暂停 3 秒后恢复',{duration:3000})},4000);
setTimeout(()=>{c.resume();MeToast.info('已恢复倒计时')},7000);
}
@@ -312,14 +392,15 @@
setTimeout(()=>{q.cancel();MeToast.warning('队列已取消',{duration:2000});},3500);
}
function demoStack() { MeToast.stack(['消息1','消息2','消息3','消息4','消息5'],{stagger:120,type:'info'}); }
function demoStackMixed() {
MeToast.stack(['普通消息',{message:'警告消息',type:'warning',duration:5000},{message:'错误消息',type:'error'}],{stagger:150});
}
function demoStackMixed() { MeToast.stack(['普通消息',{message:'警告消息',type:'warning',duration:5000},{message:'错误消息',type:'error'}],{stagger:150}); }
// === Action ===
function demoActionDelete() { MeToast.action('文件 data.json 已删除',[{text:'撤销',onClick:()=>MeToast.success('已撤销'),color:'#3b82f6'}]); }
function demoActionMulti() { MeToast.action('邮件已发送',[{text:'查看',onClick:()=>MeToast.info('打开邮件详情'),color:'#3b82f6'},{text:'撤回',onClick:()=>MeToast.warning('已撤回'),color:'#f59e0b'}]); }
function demoActionNoClose() { MeToast.action('完成后点击关闭',[{text:'我知道了',onClick:()=>{},color:'#10b981'}]); }
function demoActionNoClose() {
let n=0;
MeToast.action('点「+1」不会关闭,可连续点击',[{text:'+1',onClick:(t)=>{n++;t.update({message:'已点击 '+n+' 次(close:false 不关闭)'});},color:'#10b981',close:false}],{duration:8000});
}
// === Group ===
const group = MeToast.group('demo-group');
@@ -333,32 +414,71 @@
// === Animation ===
function anim(name) { MeToast.success('动画: '+name,{animation:name,duration:3000,showProgress:false}); }
function demoCustomAnim() {
MeToast.animations.register('swing', {
enter: { transform: 'rotate(-40deg) scale(0.3)', opacity: 0 },
leave: { transform: 'rotate(20deg) scale(0.5)', opacity: 0 },
duration: 600,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
});
MeToast.success('自定义 swing 动画!', { animation: 'swing', duration: 3000 });
}
// === Theme ===
function switchTheme(t) { MeToast.themes.switchTheme(t); MeToast.configure({theme:t}); MeToast.info('主题: '+t,{theme:t}); }
function demoRegisterTheme() {
try {
MeToast.themes.registerTheme('sunset',{bg:'rgba(255,248,240,0.96)',text:'#92400e',border:'rgba(249,115,22,0.2)',shadow:'0 10px 36px -10px rgba(249,115,22,0.2)',hoverShadow:'0 14px 48px -10px rgba(249,115,22,0.3)',progressBg:'rgba(249,115,22,0.1)',closeHoverBg:'rgba(249,115,22,0.1)'});
MeToast.themes.switchTheme('sunset');
MeToast.configure({theme:'sunset'});
MeToast.themes.switchTheme('sunset'); MeToast.configure({theme:'sunset'});
MeToast.success('已切换到 sunset 主题',{theme:'sunset'});
} catch(_) { MeToast.warning('主题已注册,已切换'); }
}
// === Plugins ===
function demoCustomPlugin() {
MeToast.use({name:'click-counter',version:'1.0',install(){let c=0;Toast.on('afterShow',t=>{c++;setTimeout(()=>t.update({message:t.message.replace(/\\(\\d+\\)/,'')+' (总通知#'+c+')'}),1);});}});
MeToast.use({name:'click-counter',version:'1.0',install(){let c=0;Toast.on('afterShow',t=>{c++;setTimeout(()=>t.update({message:t.message.replace(/\(\d+\)/,'')+' (总通知#'+c+')'}),1);});}});
MeToast.info('自定义插件已安装(消息后追加计数)');
}
// === v2.0 new features ===
function demoResetTimer() {
const t=MeToast.info('条消息不会自动消失(resetTimer',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n+' — 每次更新重置3秒'});},1000);
function demoDedupe() {
if (!MeToast.plugins.has('dedupe')) MeToast.use('dedupe');
MeToast.info('同一条消息');
MeToast.info('同一条消息');
MeToast.info('同一条消息');
MeToast.info('当前仅 ' + MeToast.count() + ' 条 — 相同消息自动合并');
}
function demoOnUpdate() { const t=MeToast.success('onUpdate 演示',{onUpdate:t=>{console.log('更新了:',t.message);}}); setTimeout(()=>t.update({message:'已更新!'}),1000); setTimeout(()=>t.update({message:'再次更新!'}),2000); }
// === Hooks ===
let hookOff = null;
function demoHookBlock() {
if (hookOff) hookOff();
hookOff = Toast.on('beforeShow', (toast) => {
if (toast.type === 'error') { document.getElementById('hook-status').textContent = '钩子:error 已被拦截 ✋'; return false; }
});
document.getElementById('hook-status').textContent = '钩子:已注册(error 类型将被拦截)';
MeToast.info('拦截器已开启 — 试试触发 error');
}
function demoHookUnblock() {
if (hookOff) { hookOff(); hookOff = null; }
document.getElementById('hook-status').textContent = '钩子:已移除';
MeToast.info('拦截器已关闭');
}
function demoHookCount() {
const stats = [];
['afterShow','afterClose','configChange','themeChange'].forEach(name => {
const list = Toast._hooks.get(name);
stats.push(name + ':' + (list ? list.length : 0));
});
MeToast.info('钩子数量 — ' + stats.join(' '), { duration: 5000 });
}
// === 高级特性 ===
function demoResetTimer() {
const t=MeToast.info('这条不会自动消失(每次更新重置 3 秒)',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n});},1000);
}
function demoOnUpdate() { const t=MeToast.success('onUpdate 演示',{onUpdate:t=>{console.log('更新了:',t.message);}}); setTimeout(()=>t.update({message:'已更新!'}),1000); }
function demoCustomRender() {
MeToast.show({render:toast=>'<div style="background:#111;color:#0f0;padding:16px 20px;border-radius:10px;border:1px solid #0f0;font-family:monospace">🟢 <b>'+toast.message+'</b><br><span style="font-size:11px;opacity:0.6"># custom render by thzxx</span></div>',message:'自定义渲染内容',duration:5000});
MeToast.show({render:toast=>'<div style="background:#111;color:#0f0;padding:16px 20px;border-radius:10px;border:1px solid #0f0;font-family:monospace">🟢 <b>'+toast.message+'</b><br><span style="font-size:11px;opacity:0.6"># custom render</span></div>',message:'自定义渲染内容',duration:5000});
}
function demoNotify() {
if(typeof Notification==='undefined'){MeToast.error('浏览器不支持 Notification');return;}
@@ -373,29 +493,38 @@
}
function demoGetAll() { const all=MeToast.getAll(); MeToast.info('当前共 '+all.size+' 条 toast'); }
function demoClearByPos() { MeToast.error('右下角临时消息',{position:'bottom-right',duration:3000}); setTimeout(()=>{MeToast.clear('bottom-right');MeToast.info('右下角已清除');},500); }
function demoGetConfig() { try{const c=MeToast.getConfig();MeToast.info('配置: '+JSON.stringify({position:c.position,duration:c.duration,theme:c.theme,animation:c.animation,locale:c.locale}).replace(/"/g,''),{duration:5000});}catch(e){MeToast.error('无 getConfig 方法');} }
function demoGetConfig() { const c=MeToast.getConfig(); MeToast.info('配置: '+JSON.stringify({position:c.position,duration:c.duration,theme:c.theme,animation:c.animation,locale:c.locale}).replace(/"/g,''),{duration:5000}); }
function demoRemove() { const t=MeToast.warning('这条将被立即移除(无动画)'); setTimeout(()=>{MeToast.removeToast(t.id);MeToast.info('已立即移除');},800); }
// === onError ===
function demoOnError() {
MeToast.configure({ onError: function(info) { MeToast.error('⚠️ 捕获: ' + (info.hook || info.source), { duration: 3000, position: 'bottom-center' }); } });
MeToast.info('已启用 onError 监控', { duration: 3000 });
var badFn = function() { throw new Error('demo-error'); };
Toast.on('afterShow', badFn);
MeToast.success('这条会触发错误回调', { duration: 2000 });
setTimeout(function() { Toast.off('afterShow', badFn); }, 3000);
}
function demoOnErrorReset() { MeToast.configure({ onError: null }); MeToast.info('onError 已重置'); }
// === Interaction ===
function demoDraggable() { MeToast.info('← 试试拖动我关闭',{draggable:true,duration:8000,closeButton:true}); }
function demoPauseHover() { MeToast.info('鼠标悬停在我上面,倒计时会暂停',{pauseOnHover:true,duration:8000}); }
function demoDraggable() { MeToast.info('← 拖动我超过 120px 关闭',{draggable:true,duration:8000,closeButton:true}); }
function demoDragThreshold() { MeToast.info('← 拖动阈值 300px(要拖更远)',{draggable:true,dragThreshold:300,duration:8000,closeButton:true}); }
function demoPauseHover() { MeToast.info('悬停时倒计时会暂停',{pauseOnHover:true,duration:8000}); }
function demoCloseOnClick() { MeToast.warning('点击我即可关闭',{closeOnClick:true,duration:10000}); }
function demoWidth() { MeToast.info('宽550px',{width:550}); setTimeout(()=>MeToast.info('宽280px(窄)',{width:280}),500); }
function demoWidth() { MeToast.info('宽 550px',{width:550}); setTimeout(()=>MeToast.info('宽 280px(窄)',{width:280}),500); }
function demoClassName() { MeToast.success('带自定义 className',{className:'demo-custom-class'}); }
function demoCustomIcon() {
MeToast.info('通用通知', {
iconHTML: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>'
});
MeToast.info('通用通知', { iconHTML: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>' });
}
// === i18n ===
function switchLocale(loc) {
try{MeToast.i18n.switchLocale(loc);MeToast.configure({locale:loc});MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English',{theme:'dark'});}
catch(e){MeToast.info('已切换语言');}
MeToast.i18n.switchLocale(loc); MeToast.configure({locale:loc});
MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English');
}
function demoFormat() {
try{
MeToast.info('数字: '+MeToast.i18n.formatNumber(1234567)+' | 货币: '+MeToast.i18n.formatCurrency(99.5,'CNY')+' | 日期: '+MeToast.i18n.formatDate(new Date()),{duration:8000});
}catch(e){MeToast.info('格式化: 1234567 → 1,234,567');}
MeToast.info('数字: '+MeToast.i18n.formatNumber(1234567)+' | 货币: '+MeToast.i18n.formatCurrency(99.5,'CNY')+' | 日期: '+MeToast.i18n.formatDate(new Date()),{duration:8000});
}
// === 图标网格 ===
+356 -348
View File
@@ -7,446 +7,442 @@
<title>API 文档 — MetonaToast</title>
<style>
:root {
--bg: #0b1121; --surface: #1a2332; --border: #1e2d4d;
--text: #e2e8f0; --muted: #64748b; --accent: #6366f1; --accent2: #818cf8;
--radius: 10px;
--bg: #070b14; --surface: rgba(255,255,255,0.03); --border: rgba(255,255,255,0.08);
--text: #e8ecf4; --muted: #8b94a7; --accent: #a5b4fc; --accent2: #c7d2fe;
--grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
--radius: 12px;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); color:var(--text); line-height:1.7; display:flex; min-height:100vh; }
code { background:rgba(99,102,241,0.12); color:#a5b4fc; padding:1px 6px; border-radius:4px; font-size:0.9em; white-space:nowrap; }
pre { background:#0a0f1a; border:1px solid var(--border); border-radius:8px; padding:14px 18px; overflow-x:auto; font:13px/1.7 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color:#cbd5e1; margin:10px 0; }
pre .kw { color:#c084fc; } pre .fn { color:#60a5fa; } pre .s { color:#34d399; } pre .cm { color:#475569; }
a { color:var(--accent2); text-decoration:none; }
html { scroll-behavior:smooth; }
body {
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
background:var(--bg); color:var(--text); line-height:1.75; display:flex; min-height:100vh;
background-image:
radial-gradient(ellipse 70% 40% at 80% -10%, rgba(99,102,241,0.12), transparent),
radial-gradient(ellipse 50% 40% at 0% 20%, rgba(236,72,153,0.05), transparent);
}
code {
background:rgba(99,102,241,0.15); color:#a5b4fc; padding:1px 6px;
border-radius:5px; font-size:.88em; white-space:nowrap;
}
a { color:var(--accent); text-decoration:none; }
a:hover { color:var(--accent2); }
.sidebar { width:260px; min-width:260px; background:var(--surface); border-right:1px solid var(--border); padding:24px 0; position:sticky; top:0; height:100vh; overflow-y:auto; }
.sidebar h2 { font-size:15px; padding:0 24px; margin-bottom:16px; color:var(--accent2); }
.sidebar a { display:block; padding:7px 24px; font-size:13px; color:var(--muted); transition:.15s; border-left:2px solid transparent; }
.sidebar a:hover, .sidebar a.active { color:var(--accent2); border-left-color:var(--accent); background:rgba(99,102,241,0.06); }
.sidebar .section { font-size:11px; text-transform:uppercase; letter-spacing:1px; padding:16px 24px 6px; color:var(--muted); font-weight:600; }
/* ===== 侧边栏 ===== */
.sidebar {
width:264px; min-width:264px; background:rgba(10,15,26,.8);
border-right:1px solid var(--border); padding:22px 0;
position:sticky; top:0; height:100vh; overflow-y:auto;
backdrop-filter:blur(14px);
}
.sidebar::-webkit-scrollbar { width:5px; }
.sidebar::-webkit-scrollbar-thumb { background:rgba(255,255,255,.12); border-radius:3px; }
.sidebar-logo { font-size:16px; font-weight:800; padding:0 24px; margin-bottom:4px; }
.grad-text { background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; color:transparent; }
.sidebar .back { display:block; font-size:12.5px; padding:6px 24px; color:var(--muted); }
.sidebar .back:hover { color:var(--accent2); }
.sidebar a.snav {
display:block; padding:6.5px 24px; font-size:13px; color:var(--muted);
transition:.15s; border-left:2px solid transparent;
}
.sidebar a.snav:hover { color:var(--accent2); }
.sidebar a.snav.active { color:var(--accent2); border-left-color:#a855f7; background:rgba(139,92,246,.08); }
.sidebar .section {
font-size:11px; text-transform:uppercase; letter-spacing:1.2px;
padding:18px 24px 6px; color:var(--muted); font-weight:700; opacity:.75;
}
.main { flex:1; padding:40px 48px; max-width:950px; }
.main h1 { font-size:30px; font-weight:700; margin-bottom:8px; }
.main h2 { font-size:22px; font-weight:700; margin:44px 0 14px; padding-top:22px; border-top:1px solid var(--border); }
.main h3 { font-size:17px; font-weight:600; margin:28px 0 10px; color:var(--accent2); }
/* ===== 主内容 ===== */
.main { flex:1; padding:44px 52px; max-width:980px; min-width:0; }
.main h1 { font-size:32px; font-weight:800; margin-bottom:6px; letter-spacing:-.5px; }
.main .h1-desc { color:var(--muted); font-size:14px; margin-bottom:8px; }
.main h2 {
font-size:22px; font-weight:700; margin:52px 0 12px; padding-top:22px;
border-top:1px solid var(--border); scroll-margin-top:24px;
}
.main h2::before { content:'#'; margin-right:8px; background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
.main h3 { font-size:16.5px; font-weight:700; margin:26px 0 10px; color:var(--accent2); }
.main p { margin:8px 0; color:var(--muted); font-size:14px; }
.main table { width:100%; border-collapse:collapse; margin:14px 0 20px; font-size:13px; }
.main p code { font-size:.9em; }
.main table { width:100%; border-collapse:collapse; margin:14px 0 22px; font-size:13px; }
.main th, .main td { text-align:left; padding:9px 14px; border-bottom:1px solid var(--border); vertical-align:top; }
.main th { color:var(--muted); font-weight:500; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; }
.main td:first-child { color:var(--accent2); font-family:monospace; font-size:12px; white-space:nowrap; }
.main td:nth-child(3) { font-family:monospace; font-size:11px; color:var(--muted); }
.badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:10px; font-weight:700; margin-left:6px; vertical-align:middle; }
.badge-new { background:rgba(16,185,129,0.2); color:#34d399; }
.hint { font-size:12px; color:var(--muted); margin-top:2px; }
.return-box { background:rgba(16,185,129,0.08); border:1px solid rgba(16,185,129,0.2); border-radius:8px; padding:14px 18px; margin:12px 0; }
.return-box h4 { font-size:12px; color:#34d399; margin-bottom:6px; text-transform:uppercase; letter-spacing:0.5px; }
.main th { color:var(--muted); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.8px; }
.main td:first-child { color:var(--accent); font-family:monospace; font-size:12px; white-space:nowrap; }
.main td:nth-child(3) { font-family:monospace; font-size:11.5px; color:var(--muted); }
.badge {
display:inline-block; padding:1px 8px; border-radius:4px; font-size:10px;
font-weight:700; margin-left:6px; vertical-align:middle;
}
.badge-new { background:rgba(16,185,129,.18); color:#34d399; border:1px solid rgba(16,185,129,.3); }
.badge-v5 { background:rgba(168,85,247,.2); color:#d8b4fe; border:1px solid rgba(168,85,247,.4); }
.hint { font-size:12.5px; color:var(--muted); margin-top:2px; }
.return-box {
background:rgba(16,185,129,.06); border:1px solid rgba(16,185,129,.2);
border-radius:10px; padding:14px 18px; margin:14px 0;
}
.return-box h4 { font-size:11.5px; color:#34d399; margin-bottom:8px; text-transform:uppercase; letter-spacing:.8px; }
@media (max-width:768px) {
.terminal {
background:#0a0f1c; border:1px solid var(--border); border-radius:10px;
overflow:hidden; margin:12px 0 20px;
}
.terminal-bar { display:flex; align-items:center; gap:7px; padding:9px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid var(--border); }
.terminal-bar .t-dot { width:10px; height:10px; border-radius:50%; }
.terminal-bar .t-dot:nth-child(1){ background:#ff5f57; } .terminal-bar .t-dot:nth-child(2){ background:#febc2e; } .terminal-bar .t-dot:nth-child(3){ background:#28c840; }
.terminal-bar .t-title { margin-left:10px; font-size:11px; color:var(--muted); }
.terminal pre { padding:16px 18px; overflow-x:auto; font:12.5px/1.75 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color:#cbd5e1; }
.terminal pre .kw { color:#c084fc; } .terminal pre .fn { color:#60a5fa; } .terminal pre .s { color:#34d399; } .terminal pre .cm { color:#475569; }
@media (max-width:860px) {
body { flex-direction:column; }
.sidebar { width:100%; min-width:0; height:auto; position:static; border-right:none; border-bottom:1px solid var(--border); padding:12px 16px; }
.sidebar a { display:inline-block; padding:4px 10px; border-left:none; }
.main { padding:24px 20px; }
.sidebar .section, .sidebar .back { padding:4px 8px; }
.sidebar a.snav { display:inline-block; padding:4px 10px; border-left:none; font-size:12.5px; }
.sidebar a.snav.active { border-left:none; border-radius:6px; }
.main { padding:26px 20px; }
}
</style>
</head>
<body>
<nav class="sidebar">
<h2>📘 MetonaToast 文档</h2>
<a href="index.html">← 返回首页</a>
<a href="demo.html">🎮 在线演示</a>
<div class="sidebar-logo"><span class="grad-text">MetonaToast 文档</span></div>
<a class="back" href="index.html">← 返回首页</a>
<a class="back" href="demo.html">🎮 在线演示</a>
<div class="section">基础通知</div>
<a href="#show">show()</a>
<a href="#success">success()</a>
<a href="#error">error()</a>
<a href="#warning">warning()</a>
<a href="#info">info()</a>
<a href="#loading">loading()</a>
<a class="snav" href="#show">show()</a>
<a class="snav" href="#success">success()</a>
<a class="snav" href="#error">error()</a>
<a class="snav" href="#warning">warning()</a>
<a class="snav" href="#info">info()</a>
<a class="snav" href="#loading">loading()</a>
<div class="section">高级功能</div>
<a href="#promise">promise()</a>
<a href="#confirm">confirm()</a>
<a href="#prompt">prompt()</a>
<a href="#progress">progress()</a>
<a href="#countdown">countdown()</a>
<a href="#queue">queue()</a>
<a href="#stack">stack()</a>
<a href="#action">action() <span class="badge badge-new">NEW</span></a>
<a class="snav" href="#promise">promise()</a>
<a class="snav" href="#confirm">confirm()</a>
<a class="snav" href="#prompt">prompt()</a>
<a class="snav" href="#progress">progress()</a>
<a class="snav" href="#countdown">countdown()</a>
<a class="snav" href="#queue">queue()</a>
<a class="snav" href="#stack">stack()</a>
<a class="snav" href="#action">action()</a>
<div class="section">管理方法</div>
<a href="#group">group() <span class="badge badge-new">NEW</span></a>
<a href="#dismiss">dismiss()</a>
<a href="#clear">clear()</a>
<a href="#configure">configure()</a>
<a href="#use">use()</a>
<a href="#destroy">destroy()</a>
<div class="section">配置参考</div>
<a href="#config">全部配置项</a>
<a href="#callbacks">回调函数</a>
<a href="#animations">动画列表</a>
<a href="#themes">主题参考</a>
<a class="snav" href="#group">group()</a>
<a class="snav" href="#dismiss">dismiss()</a>
<a class="snav" href="#clear">clear()</a>
<a class="snav" href="#updatePosition">updatePosition()</a>
<a class="snav" href="#remove">remove() / removeToast()</a>
<a class="snav" href="#configure">configure()</a>
<a class="snav" href="#use">use()</a>
<a class="snav" href="#destroy">destroy()</a>
<div class="section">扩展</div>
<a class="snav" href="#hooks">钩子系统</a>
<a class="snav" href="#react">React 适配器</a>
<a class="snav" href="#config">全部配置项</a>
<a class="snav" href="#callbacks">回调函数</a>
<a class="snav" href="#animations">动画列表</a>
<a class="snav" href="#themes">主题参考</a>
</nav>
<main class="main">
<h1>API 文档</h1>
<p>MetonaToast v2.0.1 完整 API 参考。所有基础通知方法show/success/error/warning/info/loading支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
<p class="h1-desc">MetonaToast v0.5.0 完整 API 参考。所有基础通知方法支持字符串与对象两种调用形式,均可传入任何<a href="#config">配置项</a>作为可选参数。</p>
<!-- ===== 基础通知 ===== -->
<h2 id="show">show(message, opts?)</h2>
<p>显示默认类型 Toast 通知,无特定颜色图标。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>message</td><td>string | object</td><td></td><td>消息字符串,或包含 title/message 等属性的配置对象</td></tr>
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置对象,覆盖 <a href="#config">全部配置项</a>。仅当 message 为字符串时有效</td></tr>
</table>
<p>显示默认类型 Toast,无特定颜色图标。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">show.js</span></div>
<pre><span class="cm">// 字符串形式</span>
MeToast.<span class="fn">show</span>(<span class="s">'默认消息'</span>);
MeToast.<span class="fn">show</span>(<span class="s">'自定义'</span>, { <span class="fn">duration</span>: 2000, <span class="fn">position</span>: <span class="s">'bottom-center'</span> });
<span class="cm">// 对象形式(所有 opts 作为一级属性)</span>
MeToast.<span class="fn">show</span>({ <span class="fn">title</span>: <span class="s">'标题'</span>, <span class="fn">message</span>: <span class="s">'内容'</span>, <span class="fn">duration</span>: 3000 });</pre>
MeToast.<span class="fn">show</span>(<span class="s">'自定义'</span>, { <span class="fn">duration</span>: <span class="num">2000</span>, <span class="fn">position</span>: <span class="s">'bottom-center'</span> });
<span class="cm">// 对象形式</span>
MeToast.<span class="fn">show</span>({ <span class="fn">title</span>: <span class="s">'标题'</span>, <span class="fn">message</span>: <span class="s">'内容'</span>, <span class="fn">duration</span>: <span class="num">3000</span> });</pre></div>
<h2 id="success">success(message, opts?)</h2>
<p>显示成功通知。绿色对勾图标 <code>#10b981</code>type = success。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>message</td><td>string | object</td><td></td><td>消息字符串或配置对象</td></tr>
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 success</td></tr>
</table>
<p>绿色对勾图标 <code>#10b981</code>type = success。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">success.js</span></div>
<pre>MeToast.<span class="fn">success</span>(<span class="s">'保存成功!'</span>);
MeToast.<span class="fn">success</span>({ <span class="fn">title</span>: <span class="s">'已保存'</span>, <span class="fn">message</span>: <span class="s">'数据已同步'</span> });
Met.<span class="fn">success</span>(<span class="s">'MeToast和Met等价'</span>);</pre>
Met.<span class="fn">success</span>(<span class="s">'Met 与 MeToast 等价'</span>);</pre></div>
<h2 id="error">error(message, opts?)</h2>
<p>显示错误通知。红色叉号图标 <code>#ef4444</code>type = error。aria-live 设为 assertive。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>message</td><td>string | object</td><td></td><td>消息字符串或配置对象</td></tr>
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 error</td></tr>
</table>
<p>红色叉号图标 <code>#ef4444</code>type = error。aria-live = assertive。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">error.js</span></div>
<pre>MeToast.<span class="fn">error</span>(<span class="s">'网络错误,请重试'</span>);
MeToast.<span class="fn">error</span>({ <span class="fn">title</span>: <span class="s">'提交失败'</span>, <span class="fn">message</span>: <span class="s">'服务器不可达'</span> });</pre>
MeToast.<span class="fn">error</span>({ <span class="fn">title</span>: <span class="s">'提交失败'</span>, <span class="fn">message</span>: <span class="s">'服务器不可达'</span> });</pre></div>
<h2 id="warning">warning(message, opts?)</h2>
<p>显示警告通知。黄色三角图标 <code>#f59e0b</code>type = warning。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>message</td><td>string | object</td><td></td><td>消息字符串或配置对象</td></tr>
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 warning</td></tr>
</table>
<pre>MeToast.<span class="fn">warning</span>(<span class="s">'请注意检查输入内容'</span>);</pre>
<p>黄色三角图标 <code>#f59e0b</code>type = warning。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">warning.js</span></div>
<pre>MeToast.<span class="fn">warning</span>(<span class="s">'请注意检查输入内容'</span>);</pre></div>
<h2 id="info">info(message, opts?)</h2>
<p>显示信息通知。蓝色圆形图标 <code>#3b82f6</code>type = info。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>message</td><td>string | object</td><td></td><td>消息字符串或配置对象</td></tr>
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 info</td></tr>
</table>
<pre>MeToast.<span class="fn">info</span>(<span class="s">'系统将于 22:00 维护'</span>);</pre>
<p>蓝色圆形图标 <code>#3b82f6</code>type = info。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">info.js</span></div>
<pre>MeToast.<span class="fn">info</span>(<span class="s">'系统将于 22:00 维护'</span>);</pre></div>
<h2 id="loading">loading(message, opts?)</h2>
<p>显示加载状态。type = loadingduration 强制 0(不自动关闭)closeButton 和 showProgress 强制为 false。返回 LoadingControl 对象</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>message</td><td>string | object</td><td></td><td>加载提示文本或配置对象</td></tr>
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖</td></tr>
</table>
<p>type = loadingduration 强制 0(不自动关闭)。返回 LoadingControl。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">loading.js</span></div>
<pre><span class="kw">const</span> loading = MeToast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), 2000);</pre>
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), <span class="num">2000</span>);</pre></div>
<div class="return-box">
<h4>返回 LoadingControl</h4>
<h4>LoadingControl(链式转换 id 稳定)</h4>
<table>
<tr><th>方法</th><th>签名</th><th>说明</th></tr>
<tr><td>success</td><td>(msg, opts?) => ToastInstance</td><td>关闭加载 toast,原地替换为 success 类型</td></tr>
<tr><td>error</td><td>(msg, opts?) => ToastInstance</td><td>替换为 error 类型</td></tr>
<tr><td>info</td><td>(msg, opts?) => ToastInstance</td><td>替换为 info 类型</td></tr>
<tr><td>warning</td><td>(msg, opts?) => ToastInstance</td><td>替换为 warning 类型</td></tr>
<tr><td>update</td><td>(partial) => MeToast</td><td>不关闭加载 toast,原地更新其内容(支持 resetTimerOnUpdate</td></tr>
<tr><td>dismiss</td><td>()</td><td>直接关闭加载 toast 不替换</td></tr>
<tr><th>方法</th><th>说明</th></tr>
<tr><td>success/error/info/warning</td><td>原地更新为对应类型(同一实例,id 不变),duration 自动恢复默认值</td></tr>
<tr><td>update(partial)</td><td>原地更新内容,不改变类型</td></tr>
<tr><td>dismiss()</td><td>直接关闭,不转换</td></tr>
</table>
</div>
<!-- ===== 高级功能 ===== -->
<h2 id="promise">promise(promise, opts)</h2>
<p>监听 Promise 生命周期自动显示 loading → 根据 resolve/reject 自动切换 success/error返回原 Promise,支持 await 获取结果</p>
<p>监听 Promise 生命周期自动 loading → resolve/reject 切换 success/error返回原 Promise。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
<tr><td>promise</td><td>Promise</td><td></td><td>要监听的 Promise 对象。非 Promise 打印错误并返回 rejected Promise</td></tr>
<tr><td>opts.loading</td><td>string</td><td>"加载中..."</td><td>加载中显示的文本</td></tr>
<tr><td>opts.success</td><td>string</td><td>"操作成功"</td><td>resolve 后显示的文本</td></tr>
<tr><td>opts.error</td><td>string</td><td>"操作失败"</td><td>reject 后显示的文本</td></tr>
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
<tr><td>promise</td><td>Promise</td><td></td><td>要监听的 Promise。非 Promise 打印错误并返回 rejected Promise</td></tr>
<tr><td>opts.loading / success / error</td><td>string</td><td>加载中... / 操作成功 / 操作失败</td><td>各阶段文本</td></tr>
</table>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">promise.js</span></div>
<pre><span class="kw">try</span> {
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
<span class="fn">loading</span>: <span class="s">'加载中...'</span>,
<span class="fn">success</span>: <span class="s">'加载完成!'</span>,
<span class="fn">error</span>: <span class="s">'加载失败'</span>,
<span class="fn">loading</span>: <span class="s">'加载中...'</span>, <span class="fn">success</span>: <span class="s">'完成!'</span>, <span class="fn">error</span>: <span class="s">'失败'</span>,
});
} <span class="kw">catch</span> (<span class="fn">e</span>) { <span class="cm">/* promise reject 会继续抛出 */</span> }</pre>
} <span class="kw">catch</span> (e) { <span class="cm">/* reject 会继续抛出 */</span> }</pre></div>
<h2 id="confirm">confirm(message, opts?)</h2>
<p>确认对话框返回 <code>Promise&lt;boolean&gt;</code>内置 10 秒安全超时,超时自动 resolve(false)。</p>
<p>确认对话框返回 <code>Promise&lt;boolean&gt;</code>内置 10 秒安全超时自动 resolve(false)。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
<tr><td>message</td><td>string</td><td></td><td>对话框消息。非字符串会打印错误并 resolve(false)</td></tr>
<tr><td>opts.confirmText</td><td>string</td><td>"确认"</td><td>确认按钮文字</td></tr>
<tr><td>opts.confirmColor</td><td>string</td><td>"#10b981"</td><td>确认按钮背景色</td></tr>
<tr><td>opts.cancelText</td><td>string</td><td>"取消"</td><td>取消按钮文字</td></tr>
<tr><td>opts.cancelColor</td><td>string</td><td>"#6b7280"</td><td>取消按钮背景色</td></tr>
<tr><td>opts.type</td><td>string</td><td>"warning"</td><td>Toast 类型(影响图标和颜色)</td></tr>
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
<tr><td>confirmText / confirmColor</td><td>确认 / #10b981</td><td>确认按钮文字与颜色</td></tr>
<tr><td>cancelText / cancelColor</td><td>取消 / #6b7280</td><td>取消按钮文字与颜色</td></tr>
<tr><td>type</td><td>warning</td><td>Toast 类型</td></tr>
</table>
<pre><span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, {
<span class="fn">confirmText</span>: <span class="s">'删除'</span>,
<span class="fn">confirmColor</span>: <span class="s">'#ef4444'</span>,
<span class="fn">cancelText</span>: <span class="s">'保留'</span>,
});
<span class="kw">if</span> (ok) MeToast.<span class="fn">success</span>(<span class="s">'已删除'</span>);</pre>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">confirm.js</span></div>
<pre><span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span>, <span class="fn">confirmColor</span>: <span class="s">'#ef4444'</span> });
<span class="kw">if</span> (ok) MeToast.<span class="fn">success</span>(<span class="s">'已删除'</span>);</pre></div>
<h2 id="prompt">prompt(message, opts?)</h2>
<p>输入对话框返回 <code>Promise&lt;string|null&gt;</code>Enter 或点击提交按钮返回输入值,取消返回 null。内置 10 秒安全超时。</p>
<p>输入对话框返回 <code>Promise&lt;string|null&gt;</code>。Enter / 提交返回输入值,取消返回 null10 秒超时。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
<tr><td>message</td><td>string</td><td></td><td>对话框消息。非字符串会打印错误并 resolve(null)</td></tr>
<tr><td>opts.placeholder</td><td>string</td><td>""</td><td>输入框占位文字</td></tr>
<tr><td>opts.defaultValue</td><td>string</td><td>""</td><td>输入框默认值</td></tr>
<tr><td>opts.inputType</td><td>string</td><td>"text"</td><td>input 标签 type 属性(如 password/email/number</td></tr>
<tr><td>opts.submitText</td><td>string</td><td>"确认"</td><td>提交按钮文字</td></tr>
<tr><td>opts.submitColor</td><td>string</td><td>"#3b82f6"</td><td>提交按钮背景色</td></tr>
<tr><td>opts.cancelText</td><td>string</td><td>"取消"</td><td>取消按钮文字</td></tr>
<tr><td>opts.cancelColor</td><td>string</td><td>"#6b7280"</td><td>取消按钮背景色</td></tr>
<tr><td>opts.type</td><td>string</td><td>"info"</td><td>Toast 类型</td></tr>
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
<tr><td>placeholder / defaultValue</td><td>""</td><td>输入框占位与默认值</td></tr>
<tr><td>inputType</td><td>text</td><td>input 的 typepassword/email/number 等)</td></tr>
<tr><td>submitText / submitColor</td><td>确认 / #3b82f6</td><td>提交按钮</td></tr>
<tr><td>cancelText / cancelColor</td><td>取消 / #6b7280</td><td>取消按钮</td></tr>
</table>
<pre><span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>, {
<span class="fn">placeholder</span>: <span class="s">'请输入...'</span>,
<span class="fn">defaultValue</span>: <span class="s">'张三'</span>,
<span class="fn">submitText</span>: <span class="s">'确定'</span>,
});
<span class="kw">if</span> (name) MeToast.<span class="fn">info</span>(<span class="s">'你好,'</span> + name);</pre>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">prompt.js</span></div>
<pre><span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>, { <span class="fn">placeholder</span>: <span class="s">'请输入...'</span> });
<span class="kw">if</span> (name) MeToast.<span class="fn">info</span>(<span class="s">'你好,'</span> + name);</pre></div>
<h2 id="progress">progress(message, opts?)</h2>
<p>进度条通知不自动关闭。返回 ProgressControl 对象以手动更新进度</p>
<p>进度条通知不自动关闭。返回 ProgressControl。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>message</td><td>string | object</td><td></td><td>进度提示文本或配置对象</td></tr>
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置。type 默认 info</td></tr>
<tr><td>opts.progressColor</td><td>string</td><td>"#3b82f6"</td><td>进度条填充颜色</td></tr>
<tr><th>方法</th><th>说明</th></tr>
<tr><td>setProgress(percent)</td><td>设置进度 0~100,自动 clamp</td></tr>
<tr><td>complete(message?)</td><td>跳 100%300ms 后转 success1s 后自动关闭</td></tr>
<tr><td>error(message?)</td><td>转 error2s 后自动关闭</td></tr>
<tr><td>dismiss()</td><td>直接关闭</td></tr>
</table>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">progress.js</span></div>
<pre><span class="kw">const</span> p = MeToast.<span class="fn">progress</span>(<span class="s">'上传中...'</span>, { <span class="fn">progressColor</span>: <span class="s">'#10b981'</span> });
p.<span class="fn">setProgress</span>(45); <span class="cm">// → 45%</span>
p.<span class="fn">setProgress</span>(90); <span class="cm">// → 90%</span>
p.<span class="fn">complete</span>(<span class="s">'上传完成!'</span>); <span class="cm">// → 100% → success</span>
<span class="cm">// 或</span>
p.<span class="fn">error</span>(<span class="s">'上传失败'</span>);
p.<span class="fn">dismiss</span>();</pre>
<div class="return-box">
<h4>返回 ProgressControl</h4>
<table>
<tr><th>方法</th><th>签名</th><th>说明</th></tr>
<tr><td>setProgress</td><td>(percent: number)</td><td>设置进度 0~100,自动 clamp。更新进度条宽度和百分比文字</td></tr>
<tr><td>complete</td><td>(message?: string)</td><td>跳到 100%300ms 后替换为 success toast1s 后自动关闭</td></tr>
<tr><td>error</td><td>(message?: string)</td><td>替换为 error toast2s 后自动关闭</td></tr>
<tr><td>dismiss</td><td>()</td><td>直接关闭</td></tr>
</table>
</div>
p.<span class="fn">setProgress</span>(<span class="num">45</span>);
p.<span class="fn">complete</span>(<span class="s">'上传完成!'</span>);</pre></div>
<h2 id="countdown">countdown(message, seconds, opts?)</h2>
<p>倒计时 Toast<code>{seconds}</code> 占位符每秒自动替换为剩余秒数</p>
<p>倒计时 Toast<code>{seconds}</code> 占位符每秒自动替换。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
<tr><td>message</td><td>string</td><td></td><td>消息文本。支持 <code>{seconds}</code> 占位符。非字符串打印错误并返回空控制对象</td></tr>
<tr><td>seconds</td><td>number</td><td>10</td><td>倒计时秒数,最小 1</td></tr>
<tr><td>opts.onComplete</td><td>function</td><td></td><td>倒计时归零时的回调</td></tr>
<tr><td>opts.type</td><td>string</td><td>"warning"</td><td>Toast 类型</td></tr>
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
<tr><td>seconds</td><td>10</td><td>倒计时秒数,最小 1</td></tr>
<tr><td>opts.onComplete</td><td></td><td>归零回调</td></tr>
</table>
<pre>MeToast.<span class="fn">countdown</span>(<span class="s">'{seconds} 秒后执行'</span>, 5, {
<span class="fn">onComplete</span>: () => MeToast.<span class="fn">success</span>(<span class="s">'已执行'</span>),
});</pre>
<div class="return-box">
<h4>返回 CountdownControl</h4>
<table>
<tr><th>方法</th><th>说明</th></tr>
<tr><td>cancel()</td><td>清除计时器并关闭 toast</td></tr>
<tr><td>pause()</td><td>暂停倒计时</td></tr>
<tr><td>resume()</td><td>恢复倒计时</td></tr>
</table>
</div>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">countdown.js</span></div>
<pre>MeToast.<span class="fn">countdown</span>(<span class="s">'{seconds} 秒后执行'</span>, <span class="num">5</span>, { <span class="fn">onComplete</span>: () => MeToast.<span class="fn">success</span>(<span class="s">'已执行'</span>) });
<span class="cm">// 返回 { cancel(), pause(), resume() }</span></pre></div>
<h2 id="queue">queue(messages, opts?)</h2>
<p>顺序逐个显示消息队列。前一条关闭后延时显示下一条。返回 QueueControlthenable + cancel</p>
<p>顺序逐个显示消息队列,返回 thenable + cancel。支持 await</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
<tr><td>messages</td><td>Array&lt;string|object&gt;</td><td></td><td>消息数组。可为字符串或带 message/type/duration/onClose 的对象。非数组打印错误并返回空控制对象</td></tr>
<tr><td>opts.delay</td><td>number</td><td>1000</td><td>每条消息关闭后到显示下一条的间隔(ms)</td></tr>
<tr><td>opts.duration</td><td>number</td><td>3000</td><td>每条消息显示时长(ms),可被消息级 duration 覆盖</td></tr>
<tr><td>opts.onClose</td><td>function</td><td></td><td>全部队列完成后的回调。消息级 onClose 和队列级 onClose 都会依次调用</td></tr>
<tr><td>opts.type</td><td>string</td><td></td><td>默认类型</td></tr>
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
<tr><td>opts.delay</td><td>1000</td><td>每条关闭后到下一条的间隔(ms)</td></tr>
<tr><td>opts.duration</td><td>3000</td><td>每条显示时长,可被消息级 duration 覆盖</td></tr>
</table>
<pre><span class="kw">const</span> q = MeToast.<span class="fn">queue</span>([
<span class="s">'步骤一'</span>,
{ <span class="fn">message</span>: <span class="s">'步骤二'</span>, <span class="fn">duration</span>: 5000, <span class="fn">type</span>: <span class="s">'warning'</span> },
<span class="s">'步骤三'</span>,
], { <span class="fn">delay</span>: 800, <span class="fn">duration</span>: 2000, <span class="fn">type</span>: <span class="s">'info'</span> });
q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
<span class="kw">await</span> q; <span class="cm">// 等待完成(thenable 支持 await</span></pre>
<div class="return-box">
<h4>返回 QueueControl (thenable)</h4>
<table>
<tr><th>方法</th><th>说明</th></tr>
<tr><td>.then(fn, rj)</td><td>Promise.then 代理,支持 await</td></tr>
<tr><td>.catch(rj)</td><td>Promise.catch 代理</td></tr>
<tr><td>.cancel()</td><td>设置取消标志,不再显示下一条消息</td></tr>
</table>
</div>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">queue.js</span></div>
<pre><span class="kw">const</span> q = MeToast.<span class="fn">queue</span>([<span class="s">'步骤一'</span>, <span class="s">'步骤二'</span>, <span class="s">'步骤三'</span>], { <span class="fn">delay</span>: <span class="num">800</span>, <span class="fn">duration</span>: <span class="num">2000</span> });
q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
<span class="kw">await</span> q; <span class="cm">// 等待完成</span></pre></div>
<h2 id="stack">stack(messages, opts?)</h2>
<p>同时错峰显示多条消息。每条间隔 stagger 毫秒依次出现,全部叠加在屏幕上。</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>messages</td><td>Array&lt;string|object&gt;</td><td></td><td>消息数组。可为字符串或带 message/type/duration 的对象。非数组打印错误</td></tr>
<tr><td>opts.stagger</td><td>number</td><td>100</td><td>每条消息之间的显示间隔(ms)</td></tr>
<tr><td>opts.type</td><td>string</td><td></td><td>默认类型</td></tr>
</table>
<pre>MeToast.<span class="fn">stack</span>([<span class="s">'消息1'</span>, <span class="s">'消息2'</span>, { <span class="fn">message</span>: <span class="s">'警告'</span>, <span class="fn">type</span>: <span class="s">'warning'</span> }], {
<span class="fn">stagger</span>: 150,
<span class="fn">type</span>: <span class="s">'info'</span>,
});</pre>
<p>同时错峰显示多条消息。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">stack.js</span></div>
<pre>MeToast.<span class="fn">stack</span>([<span class="s">'消息1'</span>, <span class="s">'消息2'</span>, { <span class="fn">message</span>: <span class="s">'警告'</span>, <span class="fn">type</span>: <span class="s">'warning'</span> }], { <span class="fn">stagger</span>: <span class="num">150</span> });</pre></div>
<h2 id="action">action(message, actions, opts?) <span class="badge badge-new">v2.0</span></h2>
<p>Action Toast内嵌操作按钮。默认 duration=0 不自动关闭closeButton=true</p>
<h2 id="action">action(message, actions, opts?)</h2>
<p>内嵌操作按钮。默认 duration=0 不自动关闭。点击按钮不会误触 closeOnClick(已隔离冒泡)</p>
<table>
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
<tr><td>message</td><td>string | object</td><td></td><td>消息字符串或配置对象</td></tr>
<tr><td>actions</td><td>ActionButton[]</td><td>[]</td><td>按钮数组。每个按钮可配 text/onClick/color/style/close</td></tr>
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置。duration 默认 0closeButton 默认 true</td></tr>
</table>
<table>
<tr><th>ActionButton 字段</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>text</td><td>string</td><td>—(必填)</td><td>按钮显示文字</td></tr>
<tr><td>onClick</td><td>(toast) => void</td><td>—(必填)</td><td>点击回调函数,参数为当前 toast 实例</td></tr>
<tr><td>color</td><td>string</td><td>"#6366f1"</td><td>按钮背景色</td></tr>
<tr><td>style</td><td>object</td><td>{}</td><td>按钮内联样式,可覆盖 colorstyle.background 优先)</td></tr>
<tr><td>close</td><td>boolean</td><td>true</td><td>点击后是否自动关闭 toast。设为 false 可多次点击</td></tr>
<tr><th>ActionButton</th><th>默认</th><th>说明</th></tr>
<tr><td>text</td><td>(必填)</td><td>按钮文字</td></tr>
<tr><td>onClick</td><td>—(必填)</td><td>点击回调,参数为当前 toast</td></tr>
<tr><td>color / style</td><td>#6366f1</td><td>背景色 / 内联样式(style.background 优先)</td></tr>
<tr><td>close</td><td>true</td><td>点击后是否关闭。false 可多次点击</td></tr>
</table>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">action.js</span></div>
<pre>MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>(), <span class="fn">color</span>: <span class="s">'#3b82f6'</span> },
{ <span class="fn">text</span>: <span class="s">'查看详情'</span>, <span class="fn">onClick</span>: (t) => <span class="fn">openFile</span>(), <span class="fn">color</span>: <span class="s">'#10b981'</span>, <span class="fn">close</span>: false },
]);</pre>
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>(), <span class="fn">color</span>: <span class="s">'#10b981'</span>, <span class="fn">close</span>: <span class="kw">false</span> },
]);</pre></div>
<!-- ===== 管理方法 ===== -->
<h2 id="group">group(name) <span class="badge badge-new">v2.0</span></h2>
<p>创建 Toast 分组,返回 GroupAPI 对象。该对象所有方法自动<code>group: name</code>,按组管理</p>
<table>
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>name</td><td>string</td><td>分组名称。GroupAPI 和 dismissGroup 通过此名称关联</td></tr>
</table>
<h2 id="group">group(name)</h2>
<p>创建分组,返回 GroupAPI所有方法自动<code>group: name</code></p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">group.js</span></div>
<pre><span class="kw">const</span> orders = MeToast.<span class="fn">group</span>(<span class="s">'orders'</span>);
orders.<span class="fn">success</span>(<span class="s">'订单已创建'</span>);
orders.<span class="fn">error</span>(<span class="s">'支付失败'</span>, { <span class="fn">duration</span>: 5000 });
orders.<span class="fn">count</span>(); <span class="cm">// 该组当前 toast 数量</span>
orders.<span class="fn">dismiss</span>(); <span class="cm">// 关闭该组全部 toast</span>
<span class="cm">// 也支持主对象关闭</span>
MeToast.<span class="fn">dismissGroup</span>(<span class="s">'orders'</span>);</pre>
<div class="return-box">
<h4>返回 GroupAPI</h4>
<table>
<tr><th>方法</th><th>说明</th></tr>
<tr><td>show(msg, opts?)</td><td>等价 MeToast.show,自动注入 group</td></tr>
<tr><td>success(msg, opts?)</td><td>等价 MeToast.success</td></tr>
<tr><td>error(msg, opts?)</td><td>等价 MeToast.error</td></tr>
<tr><td>warning(msg, opts?)</td><td>等价 MeToast.warning</td></tr>
<tr><td>info(msg, opts?)</td><td>等价 MeToast.info</td></tr>
<tr><td>loading(msg, opts?)</td><td>等价 MeToast.loading</td></tr>
<tr><td>action(msg, actions, opts?)</td><td>等价 MeToast.action</td></tr>
<tr><td>dismiss()</td><td>关闭该组所有 toast</td></tr>
<tr><td>count()</td><td>返回该组当前 toast 数量</td></tr>
</table>
</div>
orders.<span class="fn">count</span>(); <span class="cm">// 该组数量</span>
orders.<span class="fn">dismiss</span>(); <span class="cm">// 关闭整组</span>
MeToast.<span class="fn">dismissGroup</span>(<span class="s">'orders'</span>);</pre></div>
<h2 id="dismiss">dismiss(id?)</h2>
<p>关闭 Toast。无参数关闭全部。</p>
<table>
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>id</td><td>string</td><td>可选。Toast 的 id,不传则关闭所有</td></tr>
</table>
<pre>MeToast.<span class="fn">dismiss</span>(); <span class="cm">// 关闭所有</span>
MeToast.<span class="fn">dismiss</span>(toast.id); <span class="cm">// 关闭指定</span></pre>
<p>关闭 Toast。无参数关闭全部,传入 id 关闭指定</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">dismiss.js</span></div>
<pre>MeToast.<span class="fn">dismiss</span>(); <span class="cm">// 全部</span>
MeToast.<span class="fn">dismiss</span>(toast.id); <span class="cm">// 指定</span></pre></div>
<h2 id="clear">clear(position?)</h2>
<p>按位置清除 Toast。不传参数清除所有位置</p>
<table>
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>position</td><td>string</td><td>可选。位置如 'top-right',不传则清除全部</td></tr>
</table>
<pre>MeToast.<span class="fn">clear</span>(); <span class="cm">// 全部</span>
MeToast.<span class="fn">clear</span>(<span class="s">'bottom-right'</span>); <span class="cm">// 仅右下角</span></pre>
<p>按位置清除。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">clear.js</span></div>
<pre>MeToast.<span class="fn">clear</span>(); <span class="cm">// 全部</span>
MeToast.<span class="fn">clear</span>(<span class="s">'bottom-right'</span>); <span class="cm">// 仅右下角</span></pre></div>
<h2 id="updatePosition">updatePosition(position) <span class="badge badge-v5">v0.3</span></h2>
<p>运行时将 Toast 移动到新位置容器,立即生效。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">position.js</span></div>
<pre><span class="kw">const</span> t = MeToast.<span class="fn">info</span>(<span class="s">'可移动的 Toast'</span>);
t.<span class="fn">updatePosition</span>(<span class="s">'bottom-left'</span>);</pre></div>
<h2 id="remove">remove() / removeToast(id) <span class="badge badge-new">立即移除</span></h2>
<p>立即从 DOM 和内存移除,<b>不触发离场动画</b>。适用于无动画快速清除。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">remove.js</span></div>
<pre>t.<span class="fn">remove</span>(); <span class="cm">// 实例方法</span>
MeToast.<span class="fn">removeToast</span>(t.id); <span class="cm">// 按 id(等价)</span></pre></div>
<h2 id="configure">configure(opts)</h2>
<p>全局配置,影响后续所有 Toast。themelocale 变化触发相应副作用(应用主题 CSS / 切换语言)</p>
<table>
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>opts</td><td>object</td><td>包含任意 <a href="#config">配置项</a> 的对象</td></tr>
</table>
<p>全局配置,影响后续所有 Toast。theme/locale 变化触发副作用。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">configure.js</span></div>
<pre>MeToast.<span class="fn">configure</span>({
<span class="fn">position</span>: <span class="s">'top-right'</span>,
<span class="fn">duration</span>: 4000,
<span class="fn">theme</span>: <span class="s">'dark'</span>,
<span class="fn">animation</span>: <span class="s">'slide'</span>,
<span class="fn">locale</span>: <span class="s">'zh-CN'</span>,
});</pre>
<span class="fn">position</span>: <span class="s">'top-right'</span>, <span class="fn">duration</span>: <span class="num">4000</span>, <span class="fn">theme</span>: <span class="s">'dark'</span>,
<span class="fn">animation</span>: <span class="s">'slide'</span>, <span class="fn">locale</span>: <span class="s">'zh-CN'</span>,
});</pre></div>
<h2 id="use">use(plugin)</h2>
<p>安装插件。支持字符串(内置预设名或插件对象。已被拒绝注册的无效插件会打印错误。</p>
<table>
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
<tr><td>plugin</td><td>string | object</td><td>预设名 'keyboard'/'persistence'/'accessibility' 或自定义插件对象 { name, version?, install, uninstall? }</td></tr>
</table>
<pre>MeToast.<span class="fn">use</span>(<span class="s">'keyboard'</span>); <span class="cm">// ESC 关闭所有 Toast</span>
MeToast.<span class="fn">use</span>(<span class="s">'persistence'</span>); <span class="cm">// 配置自动保存到 localStorage</span>
MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <span class="cm">// 屏幕阅读器实时朗读 toast 内容</span></pre>
<p class="hint">内置插件详情:keyboard 监听 keydown Escape 键;persistence 将配置写入 localStorage 并在页面加载时恢复;accessibility 在 afterShow/afterUpdate 钩子中通过 aria-live 区域朗读 toast 内容。</p>
<p>安装插件:字符串预设名或插件对象。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">use.js</span></div>
<pre>MeToast.<span class="fn">use</span>(<span class="s">'keyboard'</span>); <span class="cm">// ESC 关闭所有</span>
MeToast.<span class="fn">use</span>(<span class="s">'persistence'</span>); <span class="cm">// 配置持久化 localStorage</span>
MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <span class="cm">// 屏幕阅读器朗读</span>
MeToast.<span class="fn">use</span>(<span class="s">'dedupe'</span>); <span class="cm">// 相同 type+message 自动去重</span></pre></div>
<p class="hint">dedupe:检测已有相同 toast,更新它并阻止重复弹出(beforeShow 钩子实现,支持 uninstall 卸载钩子)。</p>
<h2 id="destroy">destroy()</h2>
<p>完全销毁关闭所有 Toast、移除所有 DOM 容器注入样式标签、清空内存缓存。</p>
<pre>MeToast.<span class="fn">destroy</span>();</pre>
<p>完全销毁关闭所有 Toast、移除全部 DOM 容器注入样式、清空内存缓存与钩子。支持重复调用,<code>init()</code> 可恢复</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">destroy.js</span></div>
<pre>MeToast.<span class="fn">destroy</span>();
MeToast.<span class="fn">init</span>({ <span class="fn">config</span>: { <span class="fn">duration</span>: <span class="num">3000</span> } }); <span class="cm">// 恢复</span></pre></div>
<!-- ===== 钩子系统 ===== -->
<h2 id="hooks">钩子系统 <span class="badge badge-v5">v0.3</span></h2>
<p><code>Toast.on(name, handler)</code> 注册钩子并返回取消函数。<code>beforeShow</code> / <code>beforeClose</code> / <code>beforeUpdate</code> 的 handler 返回 <code>false</code> 可拦截对应操作。</p>
<table>
<tr><th>钩子</th><th>触发时机</th><th>拦截</th></tr>
<tr><td>beforeInit / afterInit</td><td>init() 前后</td><td></td></tr>
<tr><td>beforeDestroy / afterDestroy</td><td>destroy() 前后</td><td></td></tr>
<tr><td>beforeShow / afterShow</td><td>显示前后</td><td>✅ 返回 false 阻止</td></tr>
<tr><td>beforeClose / afterClose</td><td>关闭前后</td><td>✅ 返回 false 阻止</td></tr>
<tr><td>beforeUpdate / afterUpdate</td><td>update() 前后</td><td>✅ 返回 false 阻止</td></tr>
<tr><td>configChange</td><td>configure / updateConfig / resetConfig</td><td></td></tr>
<tr><td>themeChange / localeChange</td><td>主题 / 语言切换</td><td></td></tr>
<tr><td>click / hover</td><td>点击 / 悬停进出</td><td></td></tr>
<tr><td>dragStart / dragEnd</td><td>拖拽开始 / 结束</td><td></td></tr>
<tr><td>animationStart / animationEnd</td><td>入场动画开始 / 结束</td><td></td></tr>
<tr><td>progressStart / progressEnd</td><td>倒计时开始 / 归零</td><td></td></tr>
</table>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">hooks.js</span></div>
<pre><span class="kw">import</span> MeToast, { <span class="fn">Toast</span> } <span class="kw">from</span> <span class="s">'@metona-team/metona-toast'</span>;
<span class="cm">// 拦截:非允许时段禁止错误提示</span>
<span class="kw">const</span> off = <span class="fn">Toast</span>.<span class="fn">on</span>(<span class="s">'beforeShow'</span>, (toast) =&gt; {
<span class="kw">if</span> (toast.type === <span class="s">'error'</span> &amp;&amp; !isAllowed) <span class="kw">return</span> <span class="kw">false</span>;
});
<span class="cm">// ...</span>
off(); <span class="cm">// 取消注册</span></pre></div>
<!-- ===== React ===== -->
<h2 id="react">React 适配器 <span class="badge badge-v5">v0.4</span></h2>
<p>主包保持零依赖。通过子路径 <code>@metona-team/metona-toast/react</code> 导入,<code>react</code> 为 optional peerDependency。</p>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">react.tsx</span></div>
<pre><span class="kw">import</span> { <span class="fn">useToast</span>, <span class="fn">Toast</span> } <span class="kw">from</span> <span class="s">'@metona-team/metona-toast/react'</span>;
<span class="kw">function</span> <span class="fn">SubmitButton</span>() {
<span class="kw">const</span> toast = <span class="fn">useToast</span>(); <span class="cm">// 组件卸载自动清理本组件创建的 Toast</span>
<span class="kw">const</span> submit = <span class="kw">async</span> () =&gt; {
<span class="kw">const</span> loading = toast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
<span class="kw">try</span> { <span class="kw">await</span> api(); loading.<span class="fn">success</span>(<span class="s">'完成!'</span>); }
<span class="kw">catch</span> (e) { loading.<span class="fn">error</span>(<span class="s">'失败'</span>); }
};
<span class="kw">return</span> &lt;button onClick={submit}&gt;提交&lt;/button&gt;;
}
<span class="cm">// 声明式 Toast:props 变化更新,卸载自动移除(autoClose 默认 true</span>
<span class="kw">function</span> <span class="fn">SaveIndicator</span>({ saving }) {
<span class="kw">return</span> saving ? &lt;<span class="fn">Toast</span> type=<span class="s">"info"</span> message=<span class="s">"正在保存..."</span> /&gt; : null;
}</pre></div>
<!-- ===== 配置参考 ===== -->
<h2 id="config">全部配置项</h2>
<p>以下配置可用于 <code>configure()</code><code>init()</code> 或单个 Toast 方法的 opts 参数。带 <span class="badge badge-new">v2.0</span> 标识的为 v2.0 新增</p>
<p>可用于 <code>configure()</code><code>init()</code> 或单个 Toast 方法的 opts。</p>
<table>
<tr><th>配置项</th><th>类型</th><th>默认值</th><th>说明</th></tr>
<tr><td>position</td><td>string</td><td>'top-right'</td><td>位置:top-left / top-center / top-right / bottom-left / bottom-center / bottom-right</td></tr>
<tr><td>duration</td><td>number</td><td>4000</td><td>显示时长(ms)0=不自动关闭</td></tr>
<tr><td>max</td><td>number</td><td>6</td><td>位置最多同时显示条数,超出关闭最早的</td></tr>
<tr><td>gap</td><td>number</td><td>12</td><td>Toast 之间的间距(px)</td></tr>
<tr><td>offset</td><td>number</td><td>24</td><td>容器屏幕边缘的距离(px)</td></tr>
<tr><td>pauseOnHover</td><td>boolean</td><td>true</td><td>鼠标悬停暂停 duration 倒计时和进度条</td></tr>
<tr><td>closeOnClick</td><td>boolean</td><td>true</td><td>点击 Toast 任意位置关闭。关闭按钮始终触发关闭</td></tr>
<tr><td>draggable</td><td>boolean</td><td>true</td><td>允许拖拽关闭。拖拽超过 120px 触发关闭</td></tr>
<tr><td>showProgress</td><td>boolean</td><td>true</td><td>显示 duration 倒计时进度条</td></tr>
<tr><td>progressDirection</td><td>string</td><td>'horizontal'</td><td>进度条方向:horizontal(底部水平) / vertical(右侧垂直)</td></tr>
<tr><td>icon</td><td>boolean</td><td>true</td><td>显示类型对应图标(success→对勾等)</td></tr>
<tr><td>closeButton</td><td>boolean</td><td>true</td><td>显示右上角关闭 × 按钮</td></tr>
<tr><td>theme</td><td>string</td><td>'auto'</td><td>主题:light / dark / auto(跟随系统)/ warm / 自定义注册名</td></tr>
<tr><td>animation</td><td>string</td><td>'slide'</td><td>入场动画名称,支持 CSS 动画列表见下</td></tr>
<tr><td>zIndex</td><td>number</td><td>9999</td><td>容器 CSS z-index</td></tr>
<tr><td>width</td><td>number|string</td><td>360</td><td>Toast 宽度。数字表示 px</td></tr>
<tr><td>className</td><td>string</td><td>''</td><td>附加到 Toast 元素上的 CSS 类名</td></tr>
<tr><td>style</td><td>object</td><td>{}</td><td>附加到 Toast 元素上的内联样式对象</td></tr>
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码(zh-CN / en-US 等)</td></tr>
<tr><td>resetTimerOnUpdate <span class="badge badge-new">v2.0</span></td><td>boolean</td><td>false</td><td>调用 update() 时重置 duration 倒计时</td></tr>
<tr><td>notifyWhenHidden <span class="badge badge-new">v2.0</span></td><td>boolean</td><td>false</td><td>页面不可见时自动通过 Notification API 发送系统通知</td></tr>
<tr><td>render <span class="badge badge-new">v2.0</span></td><td>function</td><td></td><td>自定义渲染函数 <code>(toast) => htmlString</code>,完全接管 DOM 构建</td></tr>
<tr><td>position</td><td>string</td><td>'top-right'</td><td>6 个位置之一(RTL 自动翻转)</td></tr>
<tr><td>duration</td><td>number</td><td>4000</td><td>显示时长(ms)0 = 不自动关闭</td></tr>
<tr><td>max</td><td>number</td><td>6</td><td>同位置最多条数,超出真正关闭最早的</td></tr>
<tr><td>gap</td><td>number</td><td>12</td><td>Toast 间距(px)</td></tr>
<tr><td>offset</td><td>number</td><td>24</td><td>容器屏幕边缘(px)</td></tr>
<tr><td>pauseOnHover</td><td>boolean</td><td>true</td><td>悬停暂停倒计时</td></tr>
<tr><td>closeOnClick</td><td>boolean</td><td>true</td><td>点击关闭</td></tr>
<tr><td>draggable</td><td>boolean</td><td>true</td><td>允许拖拽关闭</td></tr>
<tr><td>dragThreshold</td><td>number</td><td>120</td><td>拖拽关闭阈值(px)</td></tr>
<tr><td>showProgress</td><td>boolean</td><td>true</td><td>显示倒计时进度条</td></tr>
<tr><td>progressDirection</td><td>string</td><td>'horizontal'</td><td>horizontal / vertical</td></tr>
<tr><td>icon</td><td>boolean</td><td>true</td><td>显示类型图标</td></tr>
<tr><td>closeButton</td><td>boolean</td><td>true</td><td>显示关闭 × 按钮</td></tr>
<tr><td>theme</td><td>string</td><td>'auto'</td><td>light / dark / auto / warm / 自定义名</td></tr>
<tr><td>animation</td><td>string</td><td>'slide'</td><td>11 种内置或自定义动画名</td></tr>
<tr><td>zIndex</td><td>number</td><td>9999</td><td>容器 z-index</td></tr>
<tr><td>width</td><td>number|string</td><td>360</td><td>宽度,数字表示 px</td></tr>
<tr><td>className</td><td>string</td><td>''</td><td>附加 CSS 类名</td></tr>
<tr><td>style</td><td>object</td><td>{}</td><td>附加内联样式</td></tr>
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码</td></tr>
<tr><td>resetTimerOnUpdate</td><td>boolean</td><td>false</td><td>update() 时重置倒计时</td></tr>
<tr><td>notifyWhenHidden</td><td>boolean</td><td>false</td><td>页面不可见时发系统通知</td></tr>
<tr><td>render</td><td>function</td><td></td><td>自定义渲染函数,完全接管 DOM</td></tr>
<tr><td>onBeforeShow</td><td>function</td><td></td><td>返回 false 阻止显示</td></tr>
<tr><td>onError</td><td>function</td><td></td><td>钩子/定时器异常全局回调</td></tr>
</table>
<h2 id="callbacks">回调函数</h2>
<table>
<tr><th>回调</th><th>签名</th><th>触发时机</th></tr>
<tr><td>onShow</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 创建并播放入场动画后</td></tr>
<tr><td>onClose</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 被移除后(离场动画完成时)</td></tr>
<tr><td>onClick</td><td>(toast: ToastInstance) => void</td><td>Toast 被点击时(closeOnClick 为 true 时还会自动关闭</td></tr>
<tr><td>onUpdate <span class="badge badge-new">v2.0</span></td><td>(toast: ToastInstance) => void</td><td>Toast 内容通过 update() 更新后</td></tr>
<tr><td>onBeforeShow</td><td>(toast) =&gt; boolean | void</td><td>DOM 创建前,返回 false 阻止显示</td></tr>
<tr><td>onShow</td><td>(toast) =&gt; void</td><td>创建并播放入场动画后</td></tr>
<tr><td>onClose</td><td>(toast) =&gt; void</td><td>DOM 移除后(离场完成</td></tr>
<tr><td>onClick</td><td>(toast) =&gt; void</td><td>点击时(closeOnClick=true 时还会关闭)</td></tr>
<tr><td>onUpdate</td><td>(toast) =&gt; void</td><td>update() 后</td></tr>
<tr><td>onError</td><td>({ hook, source, error, toast }) =&gt; void</td><td>钩子/定时器异常时</td></tr>
</table>
<h2 id="animations">动画列表</h2>
<p>配置 <code>animation</code> 的值,支持以下 CSS 动画。未在此列表中的值将 fallback 到 slide。</p>
<p>未注册的动画名自动 fallback 到 slide。</p>
<table>
<tr><th>名称</th><th>效果</th><th>时长</th></tr>
<tr><td>slide</td><td>从右侧滑入 + 回弹</td><td>400ms</td></tr>
@@ -456,20 +452,13 @@ MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <sp
<tr><td>flip</td><td>3D 翻转入场 + 回摆</td><td>500ms</td></tr>
<tr><td>rotate</td><td>旋转摇摆进入</td><td>500ms</td></tr>
<tr><td>zoom</td><td>从中心爆发式弹出</td><td>500ms</td></tr>
<tr><td>slideUp</td><td>从下方弹入</td><td>400ms</td></tr>
<tr><td>slideDown</td><td>上方弹</td><td>400ms</td></tr>
<tr><td>slideLeft</td><td>从左侧滑入</td><td>400ms</td></tr>
<tr><td>slideRight</td><td>从右侧滑入</td><td>400ms</td></tr>
<tr><td>slideUp / slideDown</td><td>从下方 / 上方弹入</td><td>400ms</td></tr>
<tr><td>slideLeft / slideRight</td><td>左侧 / 右侧滑</td><td>400ms</td></tr>
</table>
<h2 id="themes">主题参考</h2>
<pre><span class="cm">// 内置主题</span>
light <span class="cm">— 白色玻璃质感</span>
dark <span class="cm">— 深色玻璃质感</span>
auto <span class="cm">— 跟随系统主题设置</span>
warm <span class="cm">— 暖色调</span>
<span class="cm">// 注册自定义主题</span>
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">theme.js</span></div>
<pre><span class="cm">// 内置主题:light · dark · auto(跟随系统)· warm</span>
MeToast.themes.<span class="fn">registerTheme</span>(<span class="s">'ocean'</span>, {
<span class="fn">bg</span>: <span class="s">'rgba(240,249,255,0.96)'</span>,
<span class="fn">text</span>: <span class="s">'#0c4a6e'</span>,
@@ -479,19 +468,38 @@ MeToast.themes.<span class="fn">registerTheme</span>(<span class="s">'ocean'</sp
<span class="fn">progressBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
<span class="fn">closeHoverBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
});
MeToast.themes.<span class="fn">switchTheme</span>(<span class="s">'ocean'</span>);</pre>
MeToast.themes.<span class="fn">switchTheme</span>(<span class="s">'ocean'</span>);</pre></div>
</main>
<script src="../dist/metona-toast.js"></script>
<script>
MeToast.configure({ position: 'top-right', duration: 4000, theme: 'dark', animation: 'slide' });
document.querySelectorAll('.sidebar a[href^="#"]').forEach(a => {
a.addEventListener('click', () => {
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
a.classList.add('active');
});
// 侧边栏滚动高亮
const links = document.querySelectorAll('.sidebar a.snav');
const sections = [];
links.forEach(l => {
const id = l.getAttribute('href').slice(1);
const el = document.getElementById(id);
if (el) sections.push({ id, el, link: l });
});
const spy = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
links.forEach(l => l.classList.remove('active'));
const hit = sections.find(s => s.el === e.target);
if (hit) hit.link.classList.add('active');
}
});
}, { rootMargin: '-15% 0px -70% 0px' });
sections.forEach(s => spy.observe(s.el));
// 点击关闭高亮
links.forEach(l => l.addEventListener('click', () => {
links.forEach(x => x.classList.remove('active'));
l.classList.add('active');
}));
</script>
</body>
</html>
</html>
+375 -156
View File
@@ -5,62 +5,231 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<title>MetonaToast — 轻量精致的 Toast 通知库</title>
<meta name="description" content="MetonaToast 是轻量、零依赖、精致美观的 Toast 通知库。80+ 图标类型、11 种动画、主题系统、国际化、插件架构。">
<meta name="description" content="MetonaToast 是轻量、零依赖、精致美观的 Toast 通知库。107 种图标、11 种动画、主题系统、国际化、插件架构、React 适配器。">
<style>
:root {
--bg: #0b1121; --bg2: #111827; --surface: #1a2332;
--border: #1e2d4d; --text: #e2e8f0; --muted: #64748b;
--accent: #6366f1; --accent2: #818cf8; --radius: 14px;
--bg: #070b14;
--surface: rgba(255,255,255,0.03);
--surface-hover: rgba(255,255,255,0.06);
--border: rgba(255,255,255,0.08);
--text: #e8ecf4;
--muted: #8b94a7;
--accent1: #6366f1;
--accent2: #a855f7;
--accent3: #ec4899;
--grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
--radius: 16px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: var(--bg); color: var(--text); line-height: 1.7; }
a { color: var(--accent2); text-decoration: none; }
code { background: rgba(99,102,241,0.12); color: #a5b4fc; padding: 2px 7px; border-radius: 5px; font-size: 0.9em; }
pre { background: #0a0f1a; border: 1px solid var(--border); border-radius: 10px; padding: 18px 22px; overflow-x: auto; font: 13px/1.8 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color: #cbd5e1; }
pre .kw { color: #c084fc; } pre .fn { color: #60a5fa; } pre .s { color: #34d399; } pre .cm { color: #475569; }
* { margin:0; padding:0; box-sizing:border-box; }
html { scroll-behavior:smooth; }
body {
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
background:var(--bg); color:var(--text); line-height:1.7; overflow-x:hidden;
background-image:
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99,102,241,0.15), transparent),
radial-gradient(ellipse 60% 40% at 90% 10%, rgba(168,85,247,0.08), transparent),
radial-gradient(ellipse 50% 40% at 10% 30%, rgba(236,72,153,0.06), transparent);
}
/* 背景网格 */
body::before {
content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
background-image:
linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
background-size:56px 56px;
mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 40%, transparent 100%);
-webkit-mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 40%, transparent 100%);
}
a { color:#a5b4fc; text-decoration:none; transition:color .2s; }
a:hover { color:#c7d2fe; }
::selection { background:rgba(139,92,246,.4); }
.nav { position: sticky; top: 0; z-index: 100; background: rgba(11,17,33,0.88); backdrop-filter: blur(14px); border-bottom: 1px solid var(--border); }
.nav-inner { max-width: 1200px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; height: 56px; }
.nav-logo { font-size: 18px; font-weight: 700; background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.nav-links { display: flex; gap: 24px; }
.nav-links a { color: var(--muted); font-size: 14px; font-weight: 500; transition: .2s; }
.nav-links a:hover { color: var(--accent2); }
.grad-text {
background:var(--grad);
-webkit-background-clip:text; background-clip:text;
-webkit-text-fill-color:transparent; color:transparent;
}
.hero { text-align: center; padding: 100px 20px 70px; background: radial-gradient(ellipse at 50% 0%, rgba(99,102,241,0.18) 0%, transparent 60%), linear-gradient(180deg, #0f172a 0%, var(--bg) 100%); border-bottom: 1px solid var(--border); }
.hero h1 { font-size: 52px; font-weight: 800; }
.hero h1 span { background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.hero p { color: var(--muted); font-size: 17px; margin-top: 12px; max-width: 600px; margin-left: auto; margin-right: auto; }
.hero .badges { display: flex; gap: 12px; justify-content: center; margin-top: 24px; flex-wrap: wrap; }
.hero .badges span { background: var(--surface); border: 1px solid var(--border); border-radius: 20px; padding: 6px 18px; font-size: 13px; color: var(--accent2); }
.hero-actions { margin-top: 32px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.btn { padding: 11px 28px; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: .2s; border: none; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent2); transform: translateY(-1px); }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-outline:hover { border-color: var(--accent); color: var(--accent2); }
/* ===== Nav ===== */
.nav {
position:sticky; top:0; z-index:100;
background:rgba(7,11,20,0.7);
backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
border-bottom:1px solid var(--border);
}
.nav-inner {
max-width:1180px; margin:0 auto; display:flex; align-items:center;
justify-content:space-between; padding:0 28px; height:60px;
}
.nav-logo { font-size:19px; font-weight:800; letter-spacing:.2px; cursor:pointer; }
.nav-links { display:flex; gap:26px; }
.nav-links a { color:var(--muted); font-size:14px; font-weight:500; position:relative; }
.nav-links a::after {
content:''; position:absolute; left:0; right:100%; bottom:-6px; height:2px;
background:var(--grad); border-radius:2px; transition:right .25s;
}
.nav-links a:hover::after { right:0; }
.nav-links a:hover { color:var(--text); }
.container { max-width: 1100px; margin: 0 auto; padding: 60px 24px; }
.section-title { font-size: 28px; font-weight: 700; text-align: center; margin-bottom: 8px; }
.section-desc { color: var(--muted); text-align: center; margin-bottom: 40px; font-size: 15px; }
/* ===== Hero ===== */
.hero { text-align:center; padding:110px 24px 60px; position:relative; z-index:1; }
.hero-badge {
display:inline-flex; align-items:center; gap:8px;
background:rgba(99,102,241,0.1); border:1px solid rgba(99,102,241,0.3);
color:#a5b4fc; font-size:12px; font-weight:600; letter-spacing:1px;
padding:6px 16px; border-radius:999px; margin-bottom:26px;
}
.hero-badge .dot {
width:7px; height:7px; border-radius:50%; background:#34d399;
box-shadow:0 0 12px #34d399; animation:pulse 2s infinite;
}
@keyframes pulse { 50% { opacity:.4; } }
.hero h1 { font-size:clamp(42px, 7vw, 76px); font-weight:800; letter-spacing:-1px; line-height:1.1; }
.hero h1 .grad-text { animation:shimmer 6s linear infinite; }
@keyframes shimmer {
0%,100% { filter:hue-rotate(0deg); }
50% { filter:hue-rotate(30deg); }
}
.hero-sub {
color:var(--muted); font-size:clamp(15px, 2.2vw, 18px);
max-width:640px; margin:18px auto 0;
}
.hero-badges { display:flex; gap:10px; justify-content:center; margin-top:26px; flex-wrap:wrap; }
.hero-badges span {
background:var(--surface); border:1px solid var(--border); border-radius:999px;
padding:5px 14px; font-size:12.5px; color:#cbd5e1;
transition:border-color .2s, transform .2s;
}
.hero-badges span:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); }
.hero-actions { margin-top:36px; display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
.btn {
padding:12px 30px; border-radius:12px; font-size:14.5px; font-weight:600;
cursor:pointer; transition:transform .2s, box-shadow .2s, border-color .2s;
border:1px solid transparent;
}
.btn-primary {
background:var(--grad); color:#fff;
box-shadow:0 8px 30px -8px rgba(139,92,246,.6);
}
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 12px 40px -8px rgba(139,92,246,.8); }
.btn-ghost { background:var(--surface); border-color:var(--border); color:var(--text); }
.btn-ghost:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); background:var(--surface-hover); }
.features { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 18px; }
.feature { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; transition: .2s; }
.feature:hover { border-color: var(--accent); }
.feature-icon { font-size: 28px; margin-bottom: 12px; }
.feature h3 { font-size: 16px; margin-bottom: 6px; }
.feature p { color: var(--muted); font-size: 13px; line-height: 1.6; }
/* 浮动光球 */
.orb { position:absolute; border-radius:50%; filter:blur(70px); pointer-events:none; z-index:0; }
.orb-1 { width:340px; height:340px; background:rgba(99,102,241,.25); top:-80px; left:12%; animation:float 12s ease-in-out infinite; }
.orb-2 { width:260px; height:260px; background:rgba(236,72,153,.18); top:40px; right:10%; animation:float 15s ease-in-out infinite reverse; }
.orb-3 { width:200px; height:200px; background:rgba(168,85,247,.2); bottom:-40px; left:40%; animation:float 18s ease-in-out infinite 2s; }
@keyframes float { 0%,100%{transform:translateY(0) translateX(0);} 50%{transform:translateY(-30px) translateX(20px);} }
.code-block { max-width: 750px; margin: 0 auto; }
/* ===== 数据统计 ===== */
.stats { position:relative; z-index:1; max-width:1000px; margin:10px auto 0; padding:0 24px; }
.stats-grid {
display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:1px;
background:var(--border); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden;
}
.stat { background:rgba(10,15,26,.85); padding:22px 12px; text-align:center; backdrop-filter:blur(8px); }
.stat-num { font-size:26px; font-weight:800; }
.stat-label { font-size:12px; color:var(--muted); margin-top:2px; letter-spacing:.5px; }
.theme-demo { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin: 24px 0; }
.theme-btn { padding: 10px 20px; border-radius: 8px; border: 1px solid var(--border); cursor: pointer; background: var(--surface); color: var(--text); font-size: 13px; font-weight: 500; transition: .2s; }
.theme-btn:hover { border-color: var(--accent); }
/* ===== 通用 Section ===== */
.container { position:relative; z-index:1; max-width:1100px; margin:0 auto; padding:80px 24px; }
.section-title { font-size:30px; font-weight:800; text-align:center; letter-spacing:-.5px; }
.section-desc { color:var(--muted); text-align:center; margin-top:8px; font-size:15px; }
footer { text-align: center; padding: 40px 24px; color: var(--muted); font-size: 13px; border-top: 1px solid var(--border); }
/* ===== 特性卡片 ===== */
.features { display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:16px; margin-top:44px; }
.feature {
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
padding:26px 24px; transition:transform .25s, border-color .25s, background .25s;
position:relative; overflow:hidden;
}
.feature::before {
content:''; position:absolute; top:0; left:0; right:0; height:2px;
background:var(--grad); opacity:0; transition:opacity .25s;
}
.feature:hover { transform:translateY(-4px); border-color:rgba(129,140,248,.4); background:var(--surface-hover); }
.feature:hover::before { opacity:1; }
.feature-icon { font-size:26px; margin-bottom:12px; display:inline-block; }
.feature h3 { font-size:15.5px; margin-bottom:6px; font-weight:700; }
.feature p { color:var(--muted); font-size:13px; line-height:1.65; }
.feature code {
background:rgba(99,102,241,0.15); color:#a5b4fc;
padding:1px 6px; border-radius:5px; font-size:12px;
}
@media (max-width: 768px) {
.hero h1 { font-size: 32px; }
.nav-links { gap: 14px; }
/* ===== 代码块 ===== */
.code-block { max-width:760px; margin:36px auto 0; }
.terminal {
background:#0a0f1c; border:1px solid var(--border); border-radius:12px; overflow:hidden;
box-shadow:0 20px 60px -20px rgba(0,0,0,.6);
}
.terminal-bar {
display:flex; align-items:center; gap:7px; padding:11px 16px;
background:rgba(255,255,255,0.03); border-bottom:1px solid var(--border);
}
.terminal-bar .t-dot { width:11px; height:11px; border-radius:50%; }
.terminal-bar .t-dot:nth-child(1){ background:#ff5f57; } .terminal-bar .t-dot:nth-child(2){ background:#febc2e; } .terminal-bar .t-dot:nth-child(3){ background:#28c840; }
.terminal-bar .t-title { margin-left:10px; font-size:11.5px; color:var(--muted); letter-spacing:.5px; }
.terminal pre {
padding:20px 22px; overflow-x:auto;
font:13px/1.8 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace;
color:#cbd5e1;
}
.terminal pre .kw { color:#c084fc; } .terminal pre .fn { color:#60a5fa; }
.terminal pre .s { color:#34d399; } .terminal pre .cm { color:#475569; }
.terminal pre .num { color:#f472b6; }
/* ===== 主题预览 ===== */
.theme-demo { display:flex; gap:12px; justify-content:center; flex-wrap:wrap; margin-top:36px; }
.theme-btn {
padding:11px 22px; border-radius:10px; border:1px solid var(--border);
cursor:pointer; background:var(--surface); color:var(--text);
font-size:13.5px; font-weight:500; transition:all .2s;
}
.theme-btn:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); }
.theme-btn.active {
background:var(--grad); color:#fff; border-color:transparent;
box-shadow:0 6px 24px -6px rgba(139,92,246,.6);
}
/* ===== 环境支持 ===== */
.env-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(160px, 1fr)); gap:12px; margin-top:40px; }
.env-card {
background:var(--surface); border:1px solid var(--border); border-radius:12px;
padding:20px 14px; text-align:center; transition:border-color .2s, transform .2s;
}
.env-card:hover { border-color:rgba(129,140,248,.4); transform:translateY(-3px); }
.env-card .e-ico { font-size:26px; }
.env-card h3 { font-size:14px; margin:8px 0 2px; }
.env-card p { font-size:12.5px; color:var(--muted); }
/* ===== CTA ===== */
.cta {
max-width:760px; margin:0 auto; text-align:center;
background:var(--grad); border-radius:20px; padding:56px 32px;
position:relative; overflow:hidden;
}
.cta::before {
content:''; position:absolute; inset:0;
background:radial-gradient(ellipse 60% 60% at 50% 120%, rgba(255,255,255,.25), transparent);
}
.cta h2 { font-size:30px; font-weight:800; color:#fff; position:relative; }
.cta p { color:rgba(255,255,255,.85); margin-top:10px; position:relative; }
.cta .btn { margin-top:26px; background:#fff; color:#6d28d9; position:relative; }
.cta .btn:hover { transform:translateY(-2px); }
footer { text-align:center; padding:44px 24px; color:var(--muted); font-size:13px; border-top:1px solid var(--border); position:relative; z-index:1; }
footer a { color:#a5b4fc; }
/* 滚动淡入 */
.reveal { opacity:0; transform:translateY(24px); transition:opacity .6s ease, transform .6s ease; }
.reveal.visible { opacity:1; transform:none; }
@media (max-width:768px) {
.hero { padding:80px 20px 44px; }
.nav-links { gap:16px; }
.container { padding:56px 20px; }
}
</style>
</head>
@@ -68,7 +237,7 @@
<nav class="nav">
<div class="nav-inner">
<div class="nav-logo">MetonaToast</div>
<div class="nav-logo"><span class="grad-text">MetonaToast</span></div>
<div class="nav-links">
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
@@ -79,107 +248,117 @@
</nav>
<header class="hero">
<h1><span>MetonaToast</span></h1>
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。纯原生 JavaScriptgzip 不到 10KB。</p>
<div class="badges">
<span>📦 &lt;10KB gzip</span><span>🎨 107 图标</span><span>🎭 11 动画</span><span>🌍 国际化</span><span>🔌 插件</span><span>📘 TypeScript</span>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
<div class="hero-badge"><span class="dot"></span> v0.5.0 · 385 测试 · 覆盖率 96%</div>
<h1><span class="grad-text">MetonaToast</span></h1>
<p class="hero-sub">轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式,gzip 不到 10KB,一切开箱即用。</p>
<div class="hero-badges">
<span>📦 &lt;10KB gzip</span>
<span>🎨 107 图标</span>
<span>🎭 11 动画</span>
<span>🧩 4 插件</span>
<span>🔗 17 钩子</span>
<span>⚛️ React 适配器</span>
<span>🌍 国际化</span>
</div>
<div class="hero-actions">
<button class="btn btn-primary" onclick="demoQuickStart()">快速预览</button>
<button class="btn btn-outline" onclick="location.href='docs.html'">查看文档</button>
<button class="btn btn-primary" onclick="demoQuickStart()">快速预览</button>
<button class="btn btn-ghost" onclick="location.href='docs.html'">📖 查看文档</button>
<button class="btn btn-ghost" onclick="location.href='demo.html'">🎮 在线演示</button>
</div>
</header>
<section class="stats reveal">
<div class="stats-grid">
<div class="stat"><div class="stat-num grad-text">&lt;10KB</div><div class="stat-label">gzip 体积</div></div>
<div class="stat"><div class="stat-num grad-text">107</div><div class="stat-label">内置图标</div></div>
<div class="stat"><div class="stat-num grad-text">11</div><div class="stat-label">动画效果</div></div>
<div class="stat"><div class="stat-num grad-text">17</div><div class="stat-label">生命周期钩子</div></div>
<div class="stat"><div class="stat-num grad-text">385</div><div class="stat-label">测试用例</div></div>
<div class="stat"><div class="stat-num grad-text">96%</div><div class="stat-label">行覆盖率</div></div>
</div>
</section>
<main class="container">
<h2 class="section-title">核心特性</h2>
<p class="section-desc">一切你需要的,都在一个不到 10KB 的包中</p>
<h2 class="section-title reveal">核心特性</h2>
<p class="section-desc reveal">你需要的一切,都在一个不到 10KB 的包中</p>
<div class="features">
<div class="feature">
<div class="feature-icon">🎨</div>
<h3>107 种图标类型</h3>
<p>success / error / warning / info / loading 以及 100+ 扩展类型,覆盖所有常见场景。</p>
</div>
<div class="feature">
<div class="feature-icon">🎭</div>
<h3>11 种动画效果</h3>
<p>slide / fade / scale / bounce / flip / rotate / zoom / slideUp / slideDown / slideLeft / slideRight,每种感官差异明显。</p>
</div>
<div class="feature">
<div class="feature-icon">🖌️</div>
<h3>主题系统</h3>
<p>内置 light / dark / auto / warm 四种主题,支持 <code>registerTheme()</code> 自定义注册任意主题。</p>
</div>
<div class="feature">
<div class="feature-icon">🌍</div>
<h3>国际化</h3>
<p>内置 zh-CN 和 en-US 完整翻译,400+ 词汇。支持 <code>addTranslations()</code> 扩展任意语言。</p>
</div>
<div class="feature">
<div class="feature-icon">🔌</div>
<h3>插件系统</h3>
<p>3 款预设插件 + 自定义插件架构。keyboard / persistence / accessibility 开箱即用。</p>
</div>
<div class="feature">
<div class="feature-icon"></div>
<h3>拖拽关闭</h3>
<p>任意方向拖动 Toast 即可关闭。支持 Pointer Events,移动端和桌面端体验一致。</p>
</div>
<div class="feature">
<div class="feature reveal">
<div class="feature-icon"></div>
<h3>零依赖</h3>
<p>纯原生 JavaScript 实现,无任何运行时依赖。兼容所有现代浏览器。</p>
<p>TypeScript 严格模式,纯原生实现,无任何运行时依赖。兼容所有现代浏览器。</p>
</div>
<div class="feature">
<div class="feature reveal">
<div class="feature-icon">🎨</div>
<h3>107 种图标类型</h3>
<p>success / error / warning / info / loading 及 100+ 扩展类型,覆盖所有常见场景。</p>
</div>
<div class="feature reveal">
<div class="feature-icon">🎭</div>
<h3>11 种动画 + 自定义</h3>
<p>每种感官差异明显,<code>animations.register()</code> 注册自定义动画,动态注入 keyframes 真实生效。</p>
</div>
<div class="feature reveal">
<div class="feature-icon">🖌️</div>
<h3>主题系统</h3>
<p>light / dark / auto / warm 内置主题,<code>registerTheme()</code> 自定义,跟随系统自动切换。</p>
</div>
<div class="feature reveal">
<div class="feature-icon">🌍</div>
<h3>国际化</h3>
<p>zh-CN / en-US 完整翻译,<code>addTranslations()</code> 扩展任意语言,RTL 完整适配。</p>
</div>
<div class="feature reveal">
<div class="feature-icon">🧩</div>
<h3>插件系统</h3>
<p>4 款预设:keyboard / persistence / accessibility / dedupe,支持自定义插件架构。</p>
</div>
<div class="feature reveal">
<div class="feature-icon">🔗</div>
<h3>17 个钩子</h3>
<p>完整生命周期钩子,<code>beforeShow</code> / <code>beforeClose</code> / <code>beforeUpdate</code> 支持拦截。</p>
</div>
<div class="feature reveal">
<div class="feature-icon"></div>
<h3>拖拽关闭</h3>
<p>任意方向拖动即关,阈值 <code>dragThreshold</code> 可配置。Pointer Events 全端一致。</p>
</div>
<div class="feature reveal">
<div class="feature-icon">⚛️</div>
<h3>React 适配器</h3>
<p><code>@metona-team/metona-toast/react</code> — useToast hook + 声明式 Toast 组件,主包保持零依赖。</p>
</div>
<div class="feature reveal">
<div class="feature-icon">🛡️</div>
<h3>错误捕获</h3>
<p>全局 <code>onError</code> 回调捕获钩子与定时器异常,方便接入 Sentry、DataDog。</p>
</div>
<div class="feature reveal">
<div class="feature-icon">⏱️</div>
<h3>暂停恢复</h3>
<p>悬停暂停、拖拽暂停、手动 pause/resume,剩余时间精确保持不丢失。</p>
</div>
<div class="feature reveal">
<div class="feature-icon">📘</div>
<h3>TypeScript</h3>
<p>完整类型定义,智能提示全覆盖。<code>MeToast.success()</code><code>.confirm()</code><code>.action()</code> 全部有类型</p>
<h3>类型安全</h3>
<p>严格模式源码,类型自动生成零漂移。加载态转换 id 稳定,原地更新</p>
</div>
</div>
</main>
<section class="container">
<h2 class="section-title">环境支持</h2>
<p class="section-desc">所有现代浏览器原生支持,无需 polyfill</p>
<div class="env-grid" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(165px,1fr)); gap:12px;">
<div class="feature" style="text-align:center;padding:20px 16px;">
<div style="font-size:28px;margin-bottom:6px;">🌐</div>
<h3>Chrome</h3>
<p>64+</p>
</div>
<div class="feature" style="text-align:center;padding:20px 16px;">
<div style="font-size:28px;margin-bottom:6px;">🦊</div>
<h3>Firefox</h3>
<p>63+</p>
</div>
<div class="feature" style="text-align:center;padding:20px 16px;">
<div style="font-size:28px;margin-bottom:6px;">🧭</div>
<h3>Safari</h3>
<p>11+</p>
</div>
<div class="feature" style="text-align:center;padding:20px 16px;">
<div style="font-size:28px;margin-bottom:6px;">🪟</div>
<h3>Edge</h3>
<p>79+</p>
</div>
<div class="feature" style="text-align:center;padding:20px 16px;">
<div style="font-size:28px;margin-bottom:6px;">📦</div>
<h3>Node.js</h3>
<p>18+</p>
</div>
<div class="feature" style="text-align:center;padding:20px 16px;">
<div style="font-size:28px;margin-bottom:6px;">📘</div>
<h3>TypeScript</h3>
<p>4.5+</p>
</div>
</div>
<p style="text-align:center;color:var(--muted);font-size:13px;margin-top:16px">核心依赖:Web Animations API · Pointer Events · CSS Grid · CSS Custom Properties</p>
</section>
<section class="container">
<h2 class="section-title">快速开始</h2>
<p class="section-desc">浏览器 window 上仅注册两个全局变量:<code>MeToast</code><code>Met</code>,完全等价</p>
<div class="code-block">
<pre><span class="cm"># npm 安装</span>
<section class="container" style="padding-top:0">
<h2 class="section-title reveal">快速开始</h2>
<p class="section-desc reveal">浏览器上仅注册两个全局变量:<code>MeToast</code><code>Met</code>,完全等价</p>
<div class="code-block reveal">
<div class="terminal">
<div class="terminal-bar">
<span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span>
<span class="t-title">quickstart.js</span>
</div>
<pre><span class="cm"># 安装</span>
npm install @metona-team/metona-toast
<span class="cm">// 导入</span>
@@ -192,43 +371,45 @@ MeToast.<span class="fn">loading</span>(<span class="s">'加载中...'</span>);
<span class="cm">// CDN 直接引用</span>
<span class="kw">&lt;script</span> <span class="fn">src</span>=<span class="s">"https://git.metona.cn/MetonaTeam/MetonaToast/raw/branch/master/dist/metona-toast.min.js"</span><span class="kw">&gt;&lt;/script&gt;</span></pre>
</div>
</div>
</section>
<section class="container">
<h2 class="section-title">高级功能速览</h2>
<p class="section-desc">不仅仅是一个 Toast——确认框、输入框、进度条、倒计时、队列,应有尽有</p>
<div class="code-block">
<pre><span class="cm">// Promise 风格 — 自动 loading → success/error</span>
<section class="container" style="padding-top:0">
<h2 class="section-title reveal">高级功能速览</h2>
<p class="section-desc reveal">不仅仅是 Toast确认框、输入框、进度条、倒计时、队列、Action,应有尽有</p>
<div class="code-block reveal">
<div class="terminal">
<div class="terminal-bar">
<span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span>
<span class="t-title">advanced.js</span>
</div>
<pre><span class="cm">// Promise 风格 — 自动 loading → success/error</span>
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
<span class="fn">loading</span>: <span class="s">'加载中...'</span>,
<span class="fn">success</span>: <span class="s">'加载完成!'</span>,
<span class="fn">error</span>: <span class="s">'加载失败'</span>,
<span class="fn">loading</span>: <span class="s">'加载中...'</span>, <span class="fn">success</span>: <span class="s">'完成'</span>, <span class="fn">error</span>: <span class="s">'失败'</span>
});
<span class="cm">// 确认对话框 → Promise&lt;boolean&gt;</span>
<span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span> });
<span class="cm">// 输入对话框 → Promise&lt;string|null&gt;</span>
<span class="cm">// 确认 / 输入对话框</span>
<span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span> });
<span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>);
<span class="cm">// Action Toast — 内嵌操作按钮</span>
MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>() },
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>() },
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>(), <span class="fn">close</span>: <span class="kw">false</span> },
]);
<span class="cm">// Toast 分组管理</span>
<span class="kw">const</span> orderGroup = MeToast.<span class="fn">group</span>(<span class="s">'orders'</span>);
orderGroup.<span class="fn">success</span>(<span class="s">'订单已创建'</span>);
orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭整组</span></pre>
<span class="cm">// 加载 → 链式转换(id 稳定)</span>
<span class="kw">const</span> loading = MeToast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), <span class="num">2000</span>);</pre>
</div>
</div>
</section>
<section class="container">
<h2 class="section-title">主题切换</h2>
<p class="section-desc">点击按钮实时预览四种内置主题</p>
<div class="theme-demo">
<section class="container" style="padding-top:0">
<h2 class="section-title reveal">主题切换</h2>
<p class="section-desc reveal">点击按钮实时预览内置主题,切换结果自动持久化</p>
<div class="theme-demo reveal">
<button class="theme-btn" onclick="switchTheme('light')">☀️ 浅色 Light</button>
<button class="theme-btn" onclick="switchTheme('dark')">🌙 深色 Dark</button>
<button class="theme-btn" onclick="switchTheme('auto')">🔄 自动 Auto</button>
@@ -236,8 +417,34 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
</div>
</section>
<section class="container" style="padding-top:0">
<h2 class="section-title reveal">环境支持</h2>
<p class="section-desc reveal">所有现代浏览器原生支持,无需 polyfill</p>
<div class="env-grid reveal">
<div class="env-card"><div class="e-ico">🌐</div><h3>Chrome</h3><p>64+</p></div>
<div class="env-card"><div class="e-ico">🦊</div><h3>Firefox</h3><p>63+</p></div>
<div class="env-card"><div class="e-ico">🧭</div><h3>Safari</h3><p>11+</p></div>
<div class="env-card"><div class="e-ico">🪟</div><h3>Edge</h3><p>79+</p></div>
<div class="env-card"><div class="e-ico">📱</div><h3>iOS Safari</h3><p>11+</p></div>
<div class="env-card"><div class="e-ico">🤖</div><h3>Android</h3><p>64+</p></div>
<div class="env-card"><div class="e-ico">📦</div><h3>Node.js</h3><p>16+</p></div>
<div class="env-card"><div class="e-ico">📘</div><h3>TypeScript</h3><p>4.5+</p></div>
</div>
</section>
<section class="container" style="padding-top:0">
<div class="cta reveal">
<h2>开始使用 MetonaToast</h2>
<p>一条命令安装,立即体验精致优雅的 Toast 通知</p>
<button class="btn" onclick="location.href='demo.html'">🎮 打开在线演示</button>
</div>
</section>
<footer>
<p>MetonaToast v2.0.1 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
MetonaToast v0.5.0 · MIT License ·
<a href="https://git.metona.cn/MetonaTeam/MetonaToast" target="_blank">Gitea</a> ·
<a href="docs.html">文档</a> ·
<a href="demo.html">演示</a>
</footer>
<script src="../dist/metona-toast.js"></script>
@@ -246,16 +453,28 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
function demoQuickStart() {
MeToast.success('✨ 欢迎使用 MetonaToast');
setTimeout(() => MeToast.info('💡 试试点击右上角的 Toast 来关闭'), 800);
setTimeout(() => MeToast.warning('⚠️ 你也可以拖动 Toast 关闭'), 1600);
setTimeout(() => MeToast.info('💡 点击 Toast 或点击 × 即可关闭'), 800);
setTimeout(() => MeToast.warning('⚠️ 拖动 Toast 也能关闭', { draggable: true }), 1600);
setTimeout(() => MeToast.success('🎭 试试 bounce 动画', { animation: 'bounce' }), 2400);
}
function switchTheme(t) {
MeToast.themes.switchTheme(t);
MeToast.configure({ theme: t });
document.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
const names = { light: '浅色', dark: '深色', auto: '自动', warm: '暖色' };
MeToast.info('已切换到「' + names[t] + '」主题', { theme: t });
}
// 滚动淡入
const observer = new IntersectionObserver(entries => {
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); observer.unobserve(e.target); } });
}, { threshold: 0.12 });
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
// 页面加载自动演示
setTimeout(demoQuickStart, 600);
</script>
</body>
</html>
-111
View File
@@ -1,111 +0,0 @@
/**
* MetonaToast Animations - 动画管理精简版 v2.0.1
* @module animations
* @description 动画注册与管理移除未使用的 Web Animations API dead code
*/
import { ANIMATIONS } from './constants.js';
// 动画缓存
const animationMap = new Map();
/**
* 注册默认动画
*/
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, config);
});
/**
* 动画工具函数
*/
export const animationUtils = {
/**
* 注册自定义动画
* @param {string} name - 动画名称
* @param {Object} config - 动画配置 { enter, leave, duration, easing }
*/
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)',
});
},
/**
* 注销动画
* @param {string} name - 动画名称
*/
unregister(name) {
animationMap.delete(name);
},
/**
* 获取动画配置
* @param {string} name - 动画名称
* @returns {Object|null} 动画配置
*/
get(name) {
return animationMap.get(name) || ANIMATIONS[name] || null;
},
/**
* 获取所有动画名称
* @returns {Array} 动画名称数组
*/
getAnimationNames() {
return Array.from(animationMap.keys());
},
/**
* 获取活动动画数量兼容API当前CSS动画不跟踪活动实例
* @returns {number} 动画数量
*/
getActiveCount() {
return 0;
},
/**
* 取消所有动画兼容API当前CSS动画由浏览器管理
*/
cancelAll() {
// CSS动画由浏览器原生管理,无需手动取消
},
/**
* 重置动画管理器
*/
reset() {
animationMap.clear();
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, config);
});
},
/**
* 销毁动画管理器
*/
destroy() {
animationMap.clear();
},
};
/**
* 动画预设
*/
export const animationPresets = {};
/**
* 创建自定义动画配置
* @param {Object} config - 动画配置
* @returns {Object} 动画配置
*/
export const createAnimation = (config) => ({
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
+150
View File
@@ -0,0 +1,150 @@
/**
* MetonaToast Animations
* @module animations
* @version 0.2.1
*/
import { ANIMATIONS, ANIMATION_TYPES } from './constants.js';
import type { AnimationConfig, AnimationUtils } from './types.js';
// 动画缓存
const animationMap: Map<string, AnimationConfig> = new Map();
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
const BUILTIN_ANIMATIONS: Set<string> = new Set(ANIMATION_TYPES);
// 动态注入的自定义动画样式(惰性创建)
let animStyleElement: HTMLStyleElement | null = null;
const injectedAnimations: Set<string> = new Set();
/**
* CSS CSS
*/
const toCss = (props: Record<string, string | number>): string =>
Object.entries(props)
.map(([key, value]) => `${key}:${value}`)
.join(';');
/**
* keyframes +
*/
const buildAnimationCSS = (name: string, config: AnimationConfig): string => {
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
*/
export const ensureAnimationCSS = (name: string): void => {
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') as HTMLStyleElement | null;
if (!animStyleElement) {
animStyleElement = document.createElement('style');
animStyleElement.id = 'metona-toast-anim-styles';
document.head.appendChild(animStyleElement);
}
}
animStyleElement.textContent += buildAnimationCSS(name, config);
injectedAnimations.add(name);
};
/**
*
*/
export const hasAnimation = (name: string): boolean => 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,
});
});
/**
*
*/
export const animationUtils: AnimationUtils = {
register(name: string, config: Partial<AnimationConfig>): void {
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: string): void {
animationMap.delete(name);
},
get(name: string): AnimationConfig | null {
return animationMap.get(name) || null;
},
getAnimationNames(): string[] {
return Array.from(animationMap.keys());
},
getActiveCount(): number {
return animationMap.size;
},
cancelAll(): void {
// CSS动画由浏览器原生管理,无需手动取消
},
reset(): void {
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(): void {
animationMap.clear();
injectedAnimations.clear();
animStyleElement = null;
},
};
/**
* API
*/
export const animationPresets: Record<string, AnimationConfig> = {};
/**
*
*/
export const createAnimation = (config: Partial<AnimationConfig>): AnimationConfig => ({
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
+761
View File
@@ -0,0 +1,761 @@
/**
* MetonaToast API meToast API
* @module api
* @version 0.5.0
*/
import { Toast, _containerCache } from './toast.js';
import { DEFAULTS, VERSION } from './constants.js';
import { t, setCurrentLocale } from './i18n.js';
import { applyTheme } from './themes.js';
import { confirmHTML, promptHTML, progressHTML, actionHTML } from './templates.js';
import { presetPlugins } from './plugins.js';
import type {
ToastConfig, ToastOptions, ToastInstance,
LoadingControl, ProgressControl, CountdownControl,
ActionControl, QueueControl, GroupAPI,
ActionButton, PromiseOptions, ConfirmOptions,
PromptOptions, ProgressOptions, CountdownOptions,
QueueOptions, StackOptions, MeToast, ErrorInfo, Plugin,
StatusInfo, InitOptions,
} from './types.js';
/**
*
*/
const normalizeArgs = (args: unknown[], defaultType = 'default'): ToastOptions => {
const [first, second] = args;
if (typeof first === 'string') {
return {
...((second as Record<string, unknown>) || {}),
type: (second as Record<string, unknown>)?.type as string || defaultType,
message: first,
};
}
if (first && typeof first === 'object') {
return {
...(first as Record<string, unknown>),
type: (first as Record<string, unknown>).type as string || defaultType,
} as ToastOptions;
}
return {
type: defaultType,
message: '',
};
};
/**
* meToast API
*/
const meToast: MeToast = {
_toasts: new Map<string, ToastInstance>(),
_config: { ...DEFAULTS } as unknown as ToastConfig,
version: VERSION,
configure(opts: Partial<ToastConfig>): MeToast {
if (!opts || typeof opts !== 'object') return this;
Object.assign(this._config, opts);
if (opts.theme) {
applyTheme(opts.theme);
}
if (opts.locale) {
setCurrentLocale(opts.locale);
}
Toast.trigger('configChange');
return this;
},
_emit(opts: ToastOptions): Toast {
const merged: ToastOptions = { ...this._config, ...opts };
if (merged.content && !merged.message) merged.message = merged.content;
const t = new Toast(merged);
t.create();
// beforeShow/onBeforeShow 拦截(返回 false)时 el 为 null,不注册幽灵实例
if (t.el !== null) {
this._toasts.set(t.id, t);
}
return t;
},
_remove(id: string): void {
this._toasts.delete(id);
},
find(id: string): ToastInstance | undefined {
if (!id) return undefined;
return this._toasts.get(id);
},
show(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
const normalized = normalizeArgs([messageOrOpts, opts], 'default');
return this._emit(normalized);
},
success(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
const normalized = normalizeArgs([messageOrOpts, opts], 'success');
return this._emit(normalized);
},
error(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
const normalized = normalizeArgs([messageOrOpts, opts], 'error');
return this._emit(normalized);
},
warning(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
const normalized = normalizeArgs([messageOrOpts, opts], 'warning');
return this._emit(normalized);
},
info(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
return this._emit(normalized);
},
loading(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): LoadingControl {
const normalized = normalizeArgs([messageOrOpts, opts], 'loading');
normalized.duration = 0;
normalized.closeButton = false;
normalized.showProgress = false;
const toast = this._emit(normalized);
return {
id: toast.id,
success: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'success', msg || t('success'), o),
error: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'error', msg || t('error'), o),
info: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'info', msg || t('info'), o),
warning: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'warning', msg || t('warning'), o),
update: (p: Partial<ToastOptions>) => { toast.update(p); return this; },
dismiss: () => toast.close(),
};
},
promise<T>(promise: Promise<T>, opts: PromiseOptions = {}): Promise<T> {
if (!promise || typeof (promise as unknown as { then?: unknown }).then !== 'function') {
console.error('MeToast.promise: first argument must be a Promise');
return Promise.reject(new Error('Invalid promise'));
}
const loadingMsg = opts.loading || t('loading');
const successMsg = opts.success || t('success');
const errorMsg = opts.error || t('error');
const ctrl = this.loading(loadingMsg);
return Promise.resolve(promise)
.then((data: T) => {
ctrl.success(successMsg);
return data;
})
.catch((err: unknown) => {
ctrl.error(errorMsg);
throw err;
});
},
/**
* loading update id DOM
* duration 0 使 toast
*/
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null {
const old = this._toasts.get(loadingToast.id);
if (!old) return null;
old.update({
...opts,
type,
message,
duration: opts.duration ?? (old.config.duration || this._config.duration),
} as ToastOptions);
return old;
},
confirm(message: string, opts: ConfirmOptions = {}): Promise<boolean> {
if (typeof message !== 'string') {
console.error('MeToast.confirm: message must be a string');
return Promise.resolve(false);
}
return new Promise<boolean>((resolve) => {
let resolved = false;
const safeResolve = (val: boolean) => {
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
};
const safetyTimeout = setTimeout(() => safeResolve(false), 10000);
const toast = this._emit({
...opts,
type: opts.type || 'warning',
message,
duration: 0,
closeButton: false,
closeOnClick: false,
draggable: false,
html: confirmHTML(opts),
});
setTimeout(() => {
const confirmBtn = toast.el?.querySelector('.met-confirm-btn') as HTMLElement | null;
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
if (confirmBtn) {
confirmBtn.addEventListener('click', () => {
toast.close();
safeResolve(true);
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
toast.close();
safeResolve(false);
});
}
}, 0);
});
},
prompt(message: string, opts: PromptOptions = {}): Promise<string | null> {
if (typeof message !== 'string') {
console.error('MeToast.prompt: message must be a string');
return Promise.resolve(null);
}
return new Promise<string | null>((resolve) => {
let resolved = false;
const safeResolve = (val: string | null) => {
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
};
const safetyTimeout = setTimeout(() => safeResolve(null), 10000);
const toast = this._emit({
...opts,
type: opts.type || 'info',
message,
duration: 0,
closeButton: false,
closeOnClick: false,
draggable: false,
html: promptHTML(opts),
});
setTimeout(() => {
const input = toast.el?.querySelector('.met-input') as HTMLInputElement | null;
const submitBtn = toast.el?.querySelector('.met-submit-btn') as HTMLElement | null;
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
if (input) {
input.focus();
input.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Enter') {
toast.close();
safeResolve(input.value);
}
});
}
if (submitBtn) {
submitBtn.addEventListener('click', () => {
toast.close();
safeResolve(input?.value || null);
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
toast.close();
safeResolve(null);
});
}
}, 0);
});
},
progress(messageOrOpts: string | ToastOptions, opts: ProgressOptions = {}): ProgressControl {
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
normalized.duration = 0;
normalized.closeButton = false;
normalized.showProgress = false;
const toast = this._emit({
...normalized,
html: progressHTML(opts),
});
return {
id: toast.id,
setProgress(percent: number) {
const fill = toast.el?.querySelector('.met-progress-fill') as HTMLElement | null;
const text = toast.el?.querySelector('.met-progress-text') as HTMLElement | null;
if (fill) {
fill.style.width = `${Math.min(100, Math.max(0, percent))}%`;
}
if (text) {
text.textContent = `${Math.round(percent)}%`;
}
},
complete(message?: string) {
this.setProgress(100);
setTimeout(() => {
toast.update({
type: 'success',
message: message || t('success'),
html: '',
});
setTimeout(() => toast.close(), 1000);
}, 300);
},
error(message?: string) {
toast.update({
type: 'error',
message: message || t('error'),
html: '',
});
setTimeout(() => toast.close(), 2000);
},
dismiss() {
toast.close();
},
};
},
countdown(message: string, seconds = 10, opts: CountdownOptions = {}): CountdownControl {
if (typeof message !== 'string') {
console.error('MeToast.countdown: message must be a string');
return { id: '', cancel: () => {}, pause: () => {}, resume: () => {} };
}
let remaining = Math.max(1, parseInt(String(seconds)) || 10);
let timer: ReturnType<typeof setInterval> | null = null;
const toast = this._emit({
...opts,
type: opts.type || 'warning',
message: message.replace(/\{seconds\}/g, String(remaining)),
duration: 0,
closeButton: true,
showProgress: false,
});
const tick = (): void => {
remaining--;
if (remaining <= 0) {
if (timer !== null) clearInterval(timer);
toast.close();
if (typeof opts.onComplete === 'function') {
try { opts.onComplete(); } catch (e) { console.error('countdown onComplete error:', e); }
}
return;
}
toast.update({
message: message.replace(/\{seconds\}/g, String(remaining)),
});
};
timer = setInterval(tick, 1000);
return {
id: toast.id,
cancel() { if (timer !== null) clearInterval(timer); toast.close(); },
pause() { if (timer !== null) clearInterval(timer); },
resume() { if (timer !== null) clearInterval(timer); timer = setInterval(tick, 1000); },
};
},
action(messageOrOpts: string | ToastOptions, actions: ActionButton[] = [], opts: ToastOptions = {}): ActionControl {
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
normalized.duration = opts.duration ?? 0;
normalized.closeButton = opts.closeButton ?? true;
const toast = this._emit({
...normalized,
html: (normalized.html || '') + actionHTML(actions),
});
if (Array.isArray(actions)) {
setTimeout(() => {
actions.forEach((a, i) => {
const btn = toast.el?.querySelector(`.met-action-btn-${i}`) as HTMLElement | null;
if (btn && typeof a.onClick === 'function') {
btn.addEventListener('click', (e: Event) => {
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
e.stopPropagation();
try { a.onClick(toast); } catch (err) { console.error('Action onClick error:', err); }
if (a.close !== false) toast.close();
});
}
});
}, 0);
}
return { id: toast.id, toast, dismiss: () => toast.close() };
},
queue(messages: Array<string | ToastOptions>, opts: QueueOptions = {}): QueueControl {
if (!Array.isArray(messages)) {
console.error('MeToast.queue: messages must be an array');
const p = Promise.resolve();
return { then: (fn) => p.then(fn), catch: (rj) => p.catch(rj), cancel: () => {} };
}
let cancelled = false;
const promise = new Promise<void>((resolve) => {
let index = 0;
const delay = opts.delay || 1000;
const userOnClose = opts.onClose;
const showNext = (): void => {
if (cancelled || index >= messages.length) {
resolve();
return;
}
const message = messages[index];
index++;
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
const msgObj: ToastOptions = (typeof message === 'object' && message !== null) ? message as ToastOptions : {};
const msgOnClose = msgObj.onClose;
this._emit({
...opts,
...msgObj,
message: msg,
duration: msgObj.duration || opts.duration || 3000,
onClose: (toast: ToastInstance) => {
if (typeof msgOnClose === 'function') {
try { msgOnClose(toast); } catch (_e) { /* noop */ }
}
if (typeof userOnClose === 'function') {
try { userOnClose(toast); } catch (_e) { /* noop */ }
}
setTimeout(showNext, delay);
},
} as ToastOptions);
};
showNext();
});
return {
then: (fn, rj) => promise.then(fn, rj),
catch: (rj) => promise.catch(rj),
cancel: () => { cancelled = true; },
};
},
stack(messages: Array<string | ToastOptions>, opts: StackOptions = {}): void {
if (!Array.isArray(messages)) {
console.error('MeToast.stack: messages must be an array');
return;
}
messages.forEach((message, index) => {
setTimeout(() => {
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
this._emit({
...opts,
...((typeof message === 'object' && message !== null) ? message as ToastOptions : {}),
message: msg,
} as ToastOptions);
}, index * (opts.stagger || 100));
});
},
dismiss(id?: string): void {
if (id) {
const t = this._toasts.get(id);
if (t) t.close();
return;
}
this._toasts.forEach(t => t.close());
},
/**
* Toast
*/
removeToast(id: string): void {
if (!id) return;
const t = this._toasts.get(id);
if (t) t.remove();
},
clear(position?: string): void {
this._toasts.forEach(t => {
if (!position || t.config.position === position) t.close();
});
},
getAll(): Map<string, ToastInstance> {
return new Map(this._toasts);
},
count(): number {
return this._toasts.size;
},
group(name: string): GroupAPI {
const self = this;
const methods = ['show', 'success', 'error', 'warning', 'info', 'loading', 'action'];
const g: GroupAPI = { _group: name } as unknown as GroupAPI;
methods.forEach(m => {
(g as unknown as Record<string, (...args: unknown[]) => unknown>)[m] = (...args: unknown[]) => {
const lastArg = args[args.length - 1];
const isObj = typeof lastArg === 'object' && lastArg !== null && !Array.isArray(lastArg);
const selfMethods = self as unknown as Record<string, (...a: unknown[]) => unknown>;
if (isObj && args.length === 1) {
return selfMethods[m]({ ...(lastArg as Record<string, unknown>), group: name });
}
if (isObj) {
args.pop();
return selfMethods[m](...args, { ...(lastArg as Record<string, unknown>), group: name });
}
return selfMethods[m](...args, { group: name });
};
});
g.dismiss = () => self.dismissGroup(name);
g.count = () => self._groupCount(name);
return g;
},
dismissGroup(name: string): void {
this._toasts.forEach(t => { if (t.group === name) t.close(); });
},
_groupCount(name: string): number {
let c = 0;
this._toasts.forEach(t => { if (t.group === name) c++; });
return c;
},
getToasts(): ToastInstance[] {
return Array.from(this._toasts.values());
},
hasToasts(): boolean {
return this._toasts.size > 0;
},
getToast(id: string): ToastInstance | null {
if (!id) return null;
return this._toasts.get(id) || null;
},
closeAll(): void {
this._toasts.forEach(t => t.close());
},
clearAll(): void {
this._toasts.forEach(t => t.close());
},
pauseAll(): void {
this._toasts.forEach(t => t._pause());
},
resumeAll(): void {
this._toasts.forEach(t => t._resume());
},
updateAll(partial: Partial<ToastOptions>): void {
if (partial && typeof partial === 'object') {
this._toasts.forEach(t => t.update(partial));
}
},
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] {
if (typeof predicate !== 'function') return [];
return Array.from(this._toasts.values()).filter(predicate);
},
findByType(type: string): ToastInstance[] {
return this.findToasts(t => t.type === type);
},
findByPosition(position: string): ToastInstance[] {
return this.findToasts(t => t.config.position === position);
},
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
init(options: InitOptions = {}): MeToast {
Toast.trigger('beforeInit');
this._destroyed = false;
if (options.config) {
this.configure(options.config);
}
if (options.theme && this.themes) {
this.themes.switchTheme(options.theme);
}
if (options.locale && this.i18n) {
this.i18n.switchLocale(options.locale);
}
if (options.plugins && Array.isArray(options.plugins)) {
options.plugins.forEach((p) => {
this.use(p);
});
}
Toast.trigger('afterInit');
return this;
},
getStatus(): StatusInfo {
return {
version: VERSION,
toasts: this._toasts.size,
theme: this.themes?.getCurrentTheme?.() || 'auto',
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
plugins: this.plugins?.getNames?.() || [],
animations: this.animations?.getActiveCount?.() || 0,
};
},
getConfig(): ToastConfig {
return { ...this._config };
},
updateConfig(config: Partial<ToastConfig>): MeToast {
if (config && typeof config === 'object') {
Object.assign(this._config, config);
Toast.trigger('configChange');
}
return this;
},
resetConfig(): MeToast {
const keys = Object.keys(this._config);
keys.forEach(k => delete (this._config as Record<string, unknown>)[k]);
Object.assign(this._config, DEFAULTS);
Toast.trigger('configChange');
return this;
},
use(plugin: string | Plugin, options: Record<string, unknown> = {}): MeToast {
if (typeof plugin === 'string') {
const preset = presetPlugins[plugin];
if (!preset) {
console.warn(`Preset plugin "${plugin}" not found`);
return this;
}
this.plugins.register(plugin, { ...preset, ...options });
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
const stored = this.plugins.get(plugin) as (Record<string, unknown> & Plugin) | null;
if (plugin === 'accessibility') {
if (stored && typeof stored._off === 'function') { (stored._off as () => void)(); }
const acc = preset as Record<string, (t: ToastInstance) => void>;
if (stored) {
stored._off = Toast.on('afterShow', (toast: ToastInstance) => {
if (typeof acc.announce === 'function') acc.announce(toast);
});
}
}
if (plugin === 'persistence') {
if (stored && typeof stored._saveOff === 'function') { (stored._saveOff as () => void)(); }
const saved = typeof preset.install === 'function'
? preset.install(this.plugins as unknown as import('./types.js').PluginManager)
: null;
if (saved) this.configure(saved as Partial<ToastConfig>);
const p = preset as Record<string, (c: unknown) => void>;
if (stored) {
stored._saveOff = Toast.on('afterClose', () => {
if (typeof p.save === 'function') p.save(this.getConfig());
});
}
}
} else if (plugin && typeof plugin === 'object') {
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options } as Plugin);
}
return this;
},
destroy(): void {
if (this._destroyed) return;
this._destroyed = true;
Toast.trigger('beforeDestroy');
this.dismiss();
// 清理子模块
if (this.plugins && typeof (this.plugins as unknown as Record<string, unknown>).destroy === 'function') {
(this.plugins as unknown as Record<string, () => void>).destroy();
}
if (this.themes) {
if (typeof this.themes.clearThemeListeners === 'function') {
this.themes.clearThemeListeners();
}
if (typeof this.themes.unwatchSystemTheme === 'function') {
this.themes.unwatchSystemTheme();
}
}
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
this.i18n.clearLocaleListeners();
}
if (this.animations && typeof this.animations.destroy === 'function') {
this.animations.destroy();
}
// 清理 DOM 与静态资源
if (typeof document !== 'undefined') {
const containers = document.querySelectorAll('.met-container');
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
const el = document.getElementById(id);
if (el && el.parentNode) el.parentNode.removeChild(el);
});
}
this._toasts.clear();
_containerCache.clear();
Toast._registry.clear();
// afterDestroy 必须在清理 hooks 之前触发
Toast.trigger('afterDestroy');
Toast._hooks.clear();
},
// 子模块引用(由 index.ts 注入实际实现)
animations: null as unknown as MeToast['animations'],
themes: null as unknown as MeToast['themes'],
i18n: null as unknown as MeToast['i18n'],
plugins: null as unknown as MeToast['plugins'],
presetPlugins: {} as MeToast['presetPlugins'],
};
// 连接 Toast 静态回调到 meToast 实例
Toast.setCallbacks({
onError: (info: ErrorInfo) => {
const onError = meToast._config.onError;
if (typeof onError === 'function') {
try { onError(info); } catch (_e) { /* noop */ }
}
},
removeToast: (id: string) => {
meToast._toasts.delete(id);
},
});
export { meToast as default, meToast };
+76 -70
View File
@@ -1,69 +1,58 @@
/**
* MetonaToast Constants -
* MetonaToast Constants
* @module constants
* @version 2.0.1
* @description
* @version 0.5.0
*/
import { ICONS } from './icons.js';
import { LOCALES } from './locales.js';
export { ICONS, LOCALES };
/**
*
*/
export const VERSION = '0.5.0';
/**
*
*/
export 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',
progressDirection: 'horizontal' as const,
icon: true,
closeButton: true,
// 主题配置
theme: 'auto',
animation: 'slide',
// 布局配置
zIndex: 9999,
width: 360,
// 自定义配置
className: '',
style: {},
// 回调函数
onShow: null,
onClose: null,
onClick: null,
onUpdate: null,
// 高级
style: {} as Record<string, string>,
onShow: null as ((...args: unknown[]) => void) | null,
onBeforeShow: null as ((...args: unknown[]) => boolean | void) | null,
onClose: null as ((...args: unknown[]) => void) | null,
onClick: null as ((...args: unknown[]) => void) | null,
onUpdate: null as ((...args: unknown[]) => void) | null,
resetTimerOnUpdate: false,
notifyWhenHidden: false,
// 国际化
onError: null as ((...args: unknown[]) => void) | null,
locale: 'zh-CN',
// 插件
plugins: [],
plugins: [] as Array<string | Record<string, unknown>>,
});
// Icons — 从 icons.js 导入并重新导出(保持向后兼容)
import { ICONS } from './icons.js';
export { ICONS };
/**
*
*/
export const TYPE_COLORS = {
export const TYPE_COLORS: Record<string, { fg: string; bg: string }> = {
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)' },
@@ -176,70 +165,75 @@ export const TYPE_COLORS = {
/**
*
*/
export const ANIMATIONS = {
export const ANIMATIONS: Record<string, {
enter: Record<string, string>;
leave: Record<string, string>;
duration: number;
easing: string;
}> = {
slide: {
enter: { transform: 'translateX(80px)', opacity: 0 },
leave: { transform: 'translateX(120%)', opacity: 0 },
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)' },
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 },
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 },
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 },
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 },
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 },
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 },
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 },
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 },
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 },
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)',
},
@@ -248,7 +242,15 @@ export const ANIMATIONS = {
/**
*
*/
export const THEMES = {
export const THEMES: Record<string, {
bg: string;
text: string;
border: string;
shadow: string;
hoverShadow: string;
progressBg: string;
closeHoverBg: string;
}> = {
light: {
bg: 'rgba(255, 255, 255, 0.96)',
text: '#1f2937',
@@ -267,7 +269,15 @@ export const THEMES = {
progressBg: 'rgba(255, 255, 255, 0.08)',
closeHoverBg: 'rgba(255, 255, 255, 0.08)',
},
auto: 'auto',
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',
@@ -280,7 +290,7 @@ export const THEMES = {
};
/**
*
*
*/
export const POSITIONS = [
'top-left',
@@ -289,10 +299,10 @@ export const POSITIONS = [
'bottom-left',
'bottom-center',
'bottom-right',
];
] as const;
/**
*
*
*/
export const TYPES = [
'default', 'success', 'error', 'warning', 'info', 'loading',
@@ -311,23 +321,19 @@ export const TYPES = [
'checkCircle', 'xCircle', 'alertCircle', 'infoCircle', 'helpCircle',
'alertTriangle', 'checkSquare', 'square', 'circle', 'triangle',
'hexagon', 'octagon', 'pentagon', 'diamond',
];
// Locales — 从 locales.js 导入并重新导出(保持向后兼容)
import { LOCALES } from './locales.js';
export { LOCALES };
] as const;
/**
*
*/
export const PROGRESS_DIRECTIONS = ['horizontal', 'vertical'];
export const PROGRESS_DIRECTIONS = ['horizontal', 'vertical'] as const;
/**
*
*/
export const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
export const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'] as const;
/**
*
*/
export const THEME_TYPES = ['light', 'dark', 'auto'];
export const THEME_TYPES = ['light', 'dark', 'auto'] as const;
-1206
View File
File diff suppressed because it is too large Load Diff
+115 -274
View File
@@ -1,35 +1,33 @@
/**
* MetonaToast i18n -
* MetonaToast i18n
* @module i18n
* @version 2.0.1
* @description
* @version 0.5.0
*/
import { LOCALES } from './constants.js';
import type { I18nUtils, LocaleInfo } from './types.js';
// 当前语言状态
let currentLocale = 'zh-CN';
let localeListeners = new Set();
const localeListeners: Set<(locale: string) => void> = new Set();
let fallbackLocale = 'zh-CN';
/**
*
* @returns {string}
*/
export const getCurrentLocale = () => {
export const getCurrentLocale = (): string => {
return currentLocale;
};
/**
*
* @param {string} locale -
*/
export const setCurrentLocale = (locale) => {
export const setCurrentLocale = (locale: string): void => {
if (!LOCALES[locale]) {
console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`);
locale = fallbackLocale;
}
currentLocale = locale;
notifyLocaleListeners(locale);
saveLocale(locale);
@@ -37,130 +35,104 @@ export const setCurrentLocale = (locale) => {
/**
* 退
* @returns {string} 退
*/
export const getFallbackLocale = () => {
export const getFallbackLocale = (): string => {
return fallbackLocale;
};
/**
* 退
* @param {string} locale -
*/
export const setFallbackLocale = (locale) => {
export const setFallbackLocale = (locale: string): void => {
if (!LOCALES[locale]) {
console.warn(`Fallback locale "${locale}" not found`);
return;
}
fallbackLocale = locale;
};
/**
*
* @param {string} key -
* @param {Object} params -
* @returns {string}
*/
export const t = (key, params = {}) => {
// 尝试当前语言
export const t = (key: string, params: Record<string, string | number> = {}): string => {
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 getTranslation = (locale: string, key: string): string | undefined => {
const localeData = LOCALES[locale];
if (!localeData) return undefined;
// 支持嵌套键 (如 'common.close')
const keys = key.split('.');
let result = localeData;
let result: unknown = localeData;
for (const k of keys) {
if (result && typeof result === 'object' && k in result) {
result = result[k];
if (result && typeof result === 'object' && k in (result as Record<string, unknown>)) {
result = (result as Record<string, unknown>)[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;
const interpolate = (str: string, params: Record<string, string | number>): string => {
return str.replace(/\{(\w+)\}/g, (_match, key: string) => {
return params[key] !== undefined ? String(params[key]) : _match;
});
};
/**
*
* @param {string} key -
* @returns {boolean}
*/
export const hasTranslation = (key) => {
export const hasTranslation = (key: string): boolean => {
return getTranslation(currentLocale, key) !== undefined ||
getTranslation(fallbackLocale, key) !== undefined;
getTranslation(fallbackLocale, key) !== undefined;
};
/**
*
* @param {string} locale -
* @returns {Object}
*/
export const getTranslations = (locale) => {
export const getTranslations = (locale: string): Record<string, unknown> => {
return LOCALES[locale] || {};
};
/**
*
* @param {string} locale -
* @param {Object} translations -
*/
export const addTranslations = (locale, translations) => {
export const addTranslations = (locale: string, translations: Record<string, unknown>): void => {
if (!LOCALES[locale]) {
LOCALES[locale] = {};
}
deepMerge(LOCALES[locale], translations);
deepMerge(LOCALES[locale], translations as Record<string, string>);
};
/**
*
* @param {Object} target -
* @param {Object} source -
* @returns {Object}
*/
const deepMerge = (target, source) => {
const deepMerge = (target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> => {
for (const key in source) {
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
deepMerge(target[key], source[key]);
deepMerge(target[key] as Record<string, unknown>, source[key] as Record<string, unknown>);
} else {
target[key] = source[key];
}
@@ -170,32 +142,29 @@ const deepMerge = (target, source) => {
/**
*
* @param {string} locale -
* @param {string} key -
*/
export const removeTranslation = (locale, key) => {
export const removeTranslation = (locale: string, key: string): void => {
const localeData = LOCALES[locale];
if (!localeData) return;
const keys = key.split('.');
let current = localeData;
let current: Record<string, unknown> = localeData;
for (let i = 0; i < keys.length - 1; i++) {
if (current[keys[i]] && typeof current[keys[i]] === 'object') {
current = current[keys[i]];
current = current[keys[i]] as Record<string, unknown>;
} else {
return;
}
}
delete current[keys[keys.length - 1]];
};
/**
*
* @param {string} locale -
*/
export const clearTranslations = (locale) => {
export const clearTranslations = (locale: string): void => {
if (LOCALES[locale]) {
LOCALES[locale] = {};
}
@@ -203,28 +172,23 @@ export const clearTranslations = (locale) => {
/**
*
* @returns {Array}
*/
export const getSupportedLocales = () => {
export const getSupportedLocales = (): string[] => {
return Object.keys(LOCALES);
};
/**
*
* @param {string} locale -
* @returns {boolean}
*/
export const isLocaleSupported = (locale) => {
export const isLocaleSupported = (locale: string): boolean => {
return locale in LOCALES;
};
/**
*
* @param {string} locale -
* @returns {string}
*/
export const getLocaleName = (locale) => {
const names = {
export const getLocaleName = (locale: string): string => {
const names: Record<string, string> = {
'zh-CN': '简体中文',
'zh-TW': '繁體中文',
'en-US': 'English (US)',
@@ -304,26 +268,22 @@ export const getLocaleName = (locale) => {
've': 'Tshivenda',
'nr': 'isiNdebele',
};
return names[locale] || locale;
};
/**
*
* @param {string} locale -
* @returns {string} ('ltr' 'rtl')
*/
export const getLocaleDirection = (locale) => {
export const getLocaleDirection = (locale: string): 'ltr' | 'rtl' => {
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug'];
return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
};
/**
*
* @param {string} locale -
* @returns {Object}
*/
export const getLocaleInfo = (locale) => {
export const getLocaleInfo = (locale: string): LocaleInfo => {
return {
code: locale,
name: getLocaleName(locale),
@@ -336,17 +296,15 @@ export const getLocaleInfo = (locale) => {
/**
*
* @returns {Array}
*/
export const getAllLocaleInfo = () => {
export const getAllLocaleInfo = (): LocaleInfo[] => {
return getSupportedLocales().map(getLocaleInfo);
};
/**
*
* @param {string} locale -
*/
export const saveLocale = (locale) => {
export const saveLocale = (locale: string): void => {
if (typeof localStorage !== 'undefined') {
try {
localStorage.setItem('metona-toast-locale', locale);
@@ -358,9 +316,8 @@ export const saveLocale = (locale) => {
/**
*
* @returns {string}
*/
export const loadLocale = () => {
export const loadLocale = (): string => {
if (typeof localStorage !== 'undefined') {
try {
return localStorage.getItem('metona-toast-locale') || getDefaultLocale();
@@ -373,50 +330,43 @@ export const loadLocale = () => {
/**
*
* @returns {string}
*/
export const getDefaultLocale = () => {
export const getDefaultLocale = (): string => {
if (typeof navigator !== 'undefined') {
// 尝试从浏览器获取语言
const browserLocale = navigator.language || navigator.userLanguage;
const browserLocale = navigator.language || (navigator as unknown as { userLanguage?: string }).userLanguage;
if (browserLocale && isLocaleSupported(browserLocale)) {
return browserLocale;
}
// 尝试语言代码的前两位
const shortLocale = browserLocale?.split('-')[0];
if (shortLocale && isLocaleSupported(shortLocale)) {
return shortLocale;
}
}
return fallbackLocale;
};
/**
*
*/
export const initI18n = () => {
export const initI18n = (): void => {
const savedLocale = loadLocale();
setCurrentLocale(savedLocale);
};
/**
*
* @param {string} locale -
*/
export const switchLocale = (locale) => {
export const switchLocale = (locale: string): void => {
setCurrentLocale(locale);
};
/**
*
* @param {Function} listener -
* @returns {Function}
*/
export const addLocaleListener = (listener) => {
export const addLocaleListener = (listener: (locale: string) => void): () => void => {
localeListeners.add(listener);
return () => {
localeListeners.delete(listener);
};
@@ -424,17 +374,15 @@ export const addLocaleListener = (listener) => {
/**
*
* @param {Function} listener -
*/
export const removeLocaleListener = (listener) => {
export const removeLocaleListener = (listener: (locale: string) => void): void => {
localeListeners.delete(listener);
};
/**
*
* @param {string} locale -
*/
const notifyLocaleListeners = (locale) => {
const notifyLocaleListeners = (locale: string): void => {
localeListeners.forEach((listener) => {
try {
listener(locale);
@@ -447,50 +395,40 @@ const notifyLocaleListeners = (locale) => {
/**
*
*/
export const clearLocaleListeners = () => {
export const clearLocaleListeners = (): void => {
localeListeners.clear();
};
/**
*
* @param {number} number -
* @param {Object} options -
* @returns {string}
*/
export const formatNumber = (number, options = {}) => {
export const formatNumber = (number: number, options: Intl.NumberFormatOptions = {}): string => {
try {
return new Intl.NumberFormat(currentLocale, options).format(number);
} catch (e) {
} catch {
return number.toString();
}
};
/**
*
* @param {number} amount -
* @param {string} currency -
* @param {Object} options -
* @returns {string}
*/
export const formatCurrency = (amount, currency = 'USD', options = {}) => {
export const formatCurrency = (amount: number, currency = 'USD', options: Intl.NumberFormatOptions = {}): string => {
try {
return new Intl.NumberFormat(currentLocale, {
style: 'currency',
currency,
...options,
}).format(amount);
} catch (e) {
} catch {
return amount.toString();
}
};
/**
*
* @param {number} value -
* @param {Object} options -
* @returns {string}
*/
export const formatPercent = (value, options = {}) => {
export const formatPercent = (value: number, options: Intl.NumberFormatOptions = {}): string => {
try {
return new Intl.NumberFormat(currentLocale, {
style: 'percent',
@@ -498,33 +436,27 @@ export const formatPercent = (value, options = {}) => {
maximumFractionDigits: 2,
...options,
}).format(value / 100);
} catch (e) {
} catch {
return `${value}%`;
}
};
/**
*
* @param {Date|number|string} date -
* @param {Object} options -
* @returns {string}
*/
export const formatDate = (date, options = {}) => {
export const formatDate = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => {
try {
const dateObj = date instanceof Date ? date : new Date(date);
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
} catch (e) {
return date.toString();
} catch {
return String(date);
}
};
/**
*
* @param {Date|number|string} date -
* @param {Object} options -
* @returns {string}
*/
export const formatTime = (date, options = {}) => {
export const formatTime = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => {
return formatDate(date, {
hour: 'numeric',
minute: 'numeric',
@@ -535,22 +467,19 @@ export const formatTime = (date, options = {}) => {
/**
*
* @param {Date|number|string} date -
* @param {Object} options -
* @returns {string}
*/
export const formatRelativeTime = (date, options = {}) => {
export const formatRelativeTime = (date: Date | number | string, options: Intl.RelativeTimeFormatOptions = {}): string => {
try {
const dateObj = date instanceof Date ? date : new Date(date);
const now = new Date();
const diff = dateObj - now;
const diff = dateObj.getTime() - now.getTime();
const rtf = new Intl.RelativeTimeFormat(currentLocale, {
numeric: 'auto',
...options,
});
const units = [
const units: Array<{ unit: Intl.RelativeTimeFormatUnit; ms: number }> = [
{ unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
{ unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
{ unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 },
@@ -559,143 +488,93 @@ export const formatRelativeTime = (date, options = {}) => {
{ 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();
return String(date);
} catch {
return String(date);
}
};
/**
*
* @param {Array} list -
* @param {Object} options -
* @returns {string}
* 使 Intl.ListFormat
*/
export const formatList = (list, options = {}) => {
try {
return new Intl.ListFormat(currentLocale, options).format(list);
} catch (e) {
return list.join(', ');
export const formatList = (list: string[], _options: Record<string, unknown> = {}): string => {
const intl = Intl as typeof Intl & { ListFormat?: new (locale: string, options?: Record<string, unknown>) => { format: (items: string[]) => string } };
if (typeof intl.ListFormat === 'function') {
try {
return new intl.ListFormat(currentLocale, {
style: 'long',
type: 'conjunction',
}).format(list);
} catch (_e) { /* fallback below */ }
}
return list.join(', ');
};
/**
*
* @param {number} count -
* @param {Object} options -
* @returns {string}
*/
export const formatPlural = (count, options = {}) => {
export const formatPlural = (count: number, options: Intl.PluralRulesOptions = {}): string => {
try {
return new Intl.PluralRules(currentLocale, options).select(count);
} catch (e) {
} catch {
return count === 1 ? 'one' : 'other';
}
};
/**
*
* @param {string} key -
* @param {number} count -
* @param {Object} params -
* @returns {string}
*/
export const plural = (key, count, params = {}) => {
export const plural = (key: string, count: number, params: Record<string, string | number> = {}): string => {
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;
};
/**
*
*/
export const dateTimeFormats = {
short: {
year: 'numeric',
month: 'short',
day: 'numeric',
},
medium: {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
},
time: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
},
date: {
year: 'numeric',
month: 'long',
day: 'numeric',
},
weekday: {
weekday: 'long',
},
month: {
month: 'long',
},
year: {
year: 'numeric',
},
export const dateTimeFormats: Record<string, Intl.DateTimeFormatOptions> = {
short: { year: 'numeric', month: 'short', day: 'numeric' },
medium: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: 'numeric', minute: 'numeric', second: 'numeric' },
time: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
date: { year: 'numeric', month: 'long', day: 'numeric' },
weekday: { weekday: 'long' },
month: { month: 'long' },
year: { year: 'numeric' },
};
/**
*
*/
export const numberFormats = {
integer: {
maximumFractionDigits: 0,
},
decimal: {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
percent: {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: 2,
},
currency: {
style: 'currency',
currency: 'USD',
},
export const numberFormats: Record<string, Intl.NumberFormatOptions> = {
integer: { maximumFractionDigits: 0 },
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2 },
currency: { style: 'currency', currency: 'USD' },
};
/**
*
*
*/
export const i18nUtils = {
export const i18nUtils: I18nUtils = {
t,
plural,
getCurrentLocale,
@@ -734,14 +613,18 @@ export const i18nUtils = {
/**
*
*/
export const presetLocales = {
export const presetLocales: Record<string, {
name: string;
nativeName: string;
direction: 'ltr' | 'rtl';
translations: Record<string, string>;
}> = {
'zh-CN': {
name: '简体中文',
nativeName: '简体中文',
direction: 'ltr',
translations: LOCALES['zh-CN'],
},
'en-US': {
name: 'English (US)',
nativeName: 'English (US)',
@@ -750,46 +633,4 @@ export const presetLocales = {
},
};
/**
*
* @returns {Object}
*/
export const createI18nManager = () => {
return {
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,
};
};
export { i18nUtils as default };
+109 -109
View File
@@ -1,134 +1,134 @@
/**
* MetonaToast Icons SVG定义
* @module icons
* @version 2.0.1
* @description 80+ SVG图标
* @version 0.5.0
* @description 107 SVG
*/
export const ICONS = {
export const ICONS: Record<string, string> = {
success: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>`,
error: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>`,
warning: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>`,
info: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>`,
loading: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 1 1-6.219-8.56" class="met-spin"/>
</svg>`,
close: `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>`,
check: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>`,
x: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>`,
alert: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>`,
question: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>`,
star: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>`,
heart: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>`,
bell: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
</svg>`,
mail: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>`,
settings: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>`,
user: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>`,
home: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>`,
search: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>`,
plus: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>`,
minus: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>`,
edit: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>`,
trash: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
</svg>`,
download: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>`,
upload: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>`,
share: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="18" cy="5" r="3"/>
<circle cx="6" cy="12" r="3"/>
@@ -136,58 +136,58 @@ export const ICONS = {
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>`,
link: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>`,
external: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>`,
clock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>`,
calendar: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>`,
map: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
<line x1="8" y1="2" x2="8" y2="18"/>
<line x1="16" y1="6" x2="16" y2="22"/>
</svg>`,
compass: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>
</svg>`,
globe: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>`,
wifi: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12.55a11 11 0 0 1 14.08 0"/>
<path d="M1.42 9a16 16 0 0 1 21.16 0"/>
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
<line x1="12" y1="20" x2="12.01" y2="20"/>
</svg>`,
cloud: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
</svg>`,
sun: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
@@ -199,19 +199,19 @@ export const ICONS = {
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>`,
moon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>`,
zap: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</svg>`,
activity: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>`,
cpu: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/>
<rect x="9" y="9" width="6" height="6"/>
@@ -224,57 +224,57 @@ export const ICONS = {
<line x1="1" y1="9" x2="4" y2="9"/>
<line x1="1" y1="14" x2="4" y2="14"/>
</svg>`,
database: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="12" cy="5" rx="9" ry="3"/>
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
</svg>`,
server: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
<line x1="6" y1="6" x2="6.01" y2="6"/>
<line x1="6" y1="18" x2="6.01" y2="18"/>
</svg>`,
terminal: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>`,
code: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="16 18 22 12 16 6"/>
<polyline points="8 6 2 12 8 18"/>
</svg>`,
git: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="18" cy="18" r="3"/>
<circle cx="6" cy="6" r="3"/>
<path d="M13 6h3a2 2 0 0 1 2 2v7"/>
<line x1="6" y1="9" x2="6" y2="21"/>
</svg>`,
package: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>`,
layers: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
<polyline points="2 17 12 22 22 17"/>
<polyline points="2 12 12 17 22 12"/>
</svg>`,
grid: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>`,
list: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="8" y1="6" x2="21" y2="6"/>
<line x1="8" y1="12" x2="21" y2="12"/>
@@ -283,144 +283,144 @@ export const ICONS = {
<line x1="3" y1="12" x2="3.01" y2="12"/>
<line x1="3" y1="18" x2="3.01" y2="18"/>
</svg>`,
filter: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>
</svg>`,
sort: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<polyline points="19 12 12 19 5 12"/>
</svg>`,
refresh: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"/>
<polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>`,
sync: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"/>
<polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>`,
power: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
<line x1="12" y1="2" x2="12" y2="12"/>
</svg>`,
battery: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="1" y="6" width="18" height="12" rx="2" ry="2"/>
<line x1="23" y1="13" x2="23" y2="11"/>
</svg>`,
bluetooth: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"/>
</svg>`,
volume: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>
</svg>`,
mic: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>`,
camera: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
<circle cx="12" cy="13" r="4"/>
</svg>`,
image: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>`,
video: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="23 7 16 12 23 17 23 7"/>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
</svg>`,
music: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18V5l12-2v13"/>
<circle cx="6" cy="18" r="3"/>
<circle cx="18" cy="16" r="3"/>
</svg>`,
file: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
<polyline points="13 2 13 9 20 9"/>
</svg>`,
folder: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>`,
clipboard: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
</svg>`,
save: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>`,
print: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 6 2 18 2 18 9"/>
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/>
<rect x="6" y="14" width="12" height="8"/>
</svg>`,
eye: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>`,
eyeOff: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
<line x1="1" y1="1" x2="23" y2="23"/>
</svg>`,
lock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>`,
unlock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 9.9-1"/>
</svg>`,
shield: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>`,
key: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
</svg>`,
flag: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
<line x1="4" y1="22" x2="4" y2="15"/>
</svg>`,
bookmark: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
</svg>`,
tag: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
<line x1="7" y1="7" x2="7.01" y2="7"/>
</svg>`,
gift: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 12 20 22 4 22 4 12"/>
<rect x="2" y="7" width="20" height="5"/>
@@ -428,18 +428,18 @@ export const ICONS = {
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/>
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>
</svg>`,
award: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="8" r="7"/>
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/>
</svg>`,
target: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<circle cx="12" cy="12" r="6"/>
<circle cx="12" cy="12" r="2"/>
</svg>`,
crosshair: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="22" y1="12" x2="18" y2="12"/>
@@ -447,7 +447,7 @@ export const ICONS = {
<line x1="12" y1="6" x2="12" y2="2"/>
<line x1="12" y1="22" x2="12" y2="18"/>
</svg>`,
move: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="5 9 2 12 5 15"/>
<polyline points="9 5 12 2 15 5"/>
@@ -456,20 +456,20 @@ export const ICONS = {
<line x1="2" y1="12" x2="22" y2="12"/>
<line x1="12" y1="2" x2="12" y2="22"/>
</svg>`,
maximize: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
</svg>`,
minimize: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
</svg>`,
copy: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>`,
cut: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="6" cy="6" r="3"/>
<circle cx="6" cy="18" r="3"/>
@@ -477,40 +477,40 @@ export const ICONS = {
<line x1="14.47" y1="14.48" x2="20" y2="20"/>
<line x1="8.12" y1="8.12" x2="12" y2="12"/>
</svg>`,
paste: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
</svg>`,
rotateCw: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>`,
rotateCcw: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
</svg>`,
zoomIn: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
<line x1="11" y1="8" x2="11" y2="14"/>
<line x1="8" y1="11" x2="14" y2="11"/>
</svg>`,
zoomOut: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
<line x1="8" y1="11" x2="14" y2="11"/>
</svg>`,
crop: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"/>
<path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"/>
</svg>`,
sliders: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="21" x2="4" y2="14"/>
<line x1="4" y1="10" x2="4" y2="3"/>
@@ -522,81 +522,81 @@ export const ICONS = {
<line x1="9" y1="8" x2="15" y2="8"/>
<line x1="17" y1="16" x2="23" y2="16"/>
</svg>`,
toggleLeft: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/>
<circle cx="8" cy="12" r="3"/>
</svg>`,
toggleRight: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/>
<circle cx="16" cy="12" r="3"/>
</svg>`,
checkCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>`,
xCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>`,
alertCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>`,
infoCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>`,
helpCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>`,
alertTriangle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>`,
checkSquare: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 11 12 14 22 4"/>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
</svg>`,
square: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
</svg>`,
circle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
</svg>`,
triangle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
</svg>`,
hexagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
</svg>`,
octagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/>
</svg>`,
pentagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 22 8.5 19 20 5 20 2 8.5 12 2"/>
</svg>`,
diamond: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="4.5" y="4.5" width="15" height="15" rx="1" transform="rotate(45 12 12)"/>
</svg>`,
-283
View File
@@ -1,283 +0,0 @@
/**
* MetonaToast - 轻量级Toast通知库
* @module metona-toast
* @version 2.0.1
* @author thzxx
* @description 轻量零依赖精致美观的Toast通知库单文件开箱即用
* @license MIT
*/
import { meToast, Toast } from './core.js';
import { animationUtils } from './animations.js';
import { themeUtils } from './themes.js';
import { i18nUtils } from './i18n.js';
import { pluginUtils, presetPlugins } from './plugins.js';
import { DEFAULTS } from './constants.js';
// 版本信息
const VERSION = '2.0.1';
/**
* 主对象增强
*/
const enhancedMeToast = {
...meToast,
version: VERSION,
animations: animationUtils,
themes: themeUtils,
i18n: i18nUtils,
plugins: pluginUtils,
presetPlugins,
/**
* 安装插件并连接必要的生命周期钩子
*/
use(plugin, options = {}) {
if (typeof plugin === 'string') {
const preset = presetPlugins[plugin];
if (!preset) {
console.warn(`Preset plugin "${plugin}" not found`);
return this;
}
this.plugins.register(plugin, { ...preset, ...options });
// 连接插件钩子
if (plugin === 'accessibility') {
Toast.on('afterShow', (toast) => preset.announce(toast));
}
if (plugin === 'persistence') {
const saved = preset.install();
if (saved) this.configure(saved);
Toast.on('afterClose', () => { preset.save(this.getConfig()); });
}
} else if (plugin && typeof plugin === 'object') {
const name = plugin.name || 'custom';
this.plugins.register(name, { ...plugin, ...options });
}
return this;
},
/**
* 初始化
*/
init(options = {}) {
if (options.config) {
this.configure(options.config);
}
if (options.theme) {
this.themes.switchTheme(options.theme);
}
if (options.locale) {
this.i18n.switchLocale(options.locale);
}
if (options.plugins && Array.isArray(options.plugins)) {
options.plugins.forEach((plugin) => {
this.use(plugin);
});
}
return this;
},
/**
* 销毁
*/
destroy() {
this.dismiss();
if (this.plugins && typeof this.plugins.destroy === 'function') {
this.plugins.destroy();
}
if (this.themes) {
if (typeof this.themes.clearThemeListeners === 'function') {
this.themes.clearThemeListeners();
}
if (typeof this.themes.unwatchSystemTheme === 'function') {
this.themes.unwatchSystemTheme();
}
}
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
this.i18n.clearLocaleListeners();
}
if (this.animations && typeof this.animations.cancelAll === 'function') {
this.animations.cancelAll();
}
if (typeof document !== 'undefined') {
const containers = document.querySelectorAll('.met-container');
containers.forEach((c) => c.parentNode && c.parentNode.removeChild(c));
const style = document.getElementById('metona-toast-styles');
if (style) style.parentNode.removeChild(style);
}
this._toasts.clear();
},
/**
* 获取状态
*/
getStatus() {
return {
version: VERSION,
toasts: this._toasts.size,
theme: this.themes?.getCurrentTheme?.() || 'auto',
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
plugins: this.plugins?.getNames?.() || [],
animations: this.animations?.getActiveCount?.() || 0,
};
},
/**
* 获取配置
*/
getConfig() {
return { ...this._config };
},
/**
* 更新配置
*/
updateConfig(config) {
if (config && typeof config === 'object') {
this._config = { ...this._config, ...config };
}
return this;
},
/**
* 重置配置
*/
resetConfig() {
this._config = { ...DEFAULTS };
return this;
},
/**
* 获取Toast列表
*/
getToasts() {
return Array.from(this._toasts.values());
},
/**
* 检查是否有Toast
*/
hasToasts() {
return this._toasts.size > 0;
},
/**
* 获取Toast
*/
getToast(id) {
if (!id) return null;
return this._toasts.get(id) || null;
},
/**
* 关闭所有Toast
*/
closeAll() {
this._toasts.forEach((t) => t.close());
},
/**
* 清除所有Toast
*/
clearAll() {
this._toasts.forEach((t) => t.close());
},
/**
* 暂停所有Toast
*/
pauseAll() {
this._toasts.forEach((t) => t._pause());
},
/**
* 恢复所有Toast
*/
resumeAll() {
this._toasts.forEach((t) => t._resume());
},
/**
* 更新所有Toast
*/
updateAll(partial) {
if (partial && typeof partial === 'object') {
this._toasts.forEach((t) => t.update(partial));
}
},
/**
* 查找Toast
*/
findToasts(predicate) {
if (typeof predicate !== 'function') return [];
return Array.from(this._toasts.values()).filter(predicate);
},
/**
* 按类型查找Toast
*/
findByType(type) {
return this.findToasts((t) => t.type === type);
},
/**
* 按位置查找Toast
*/
findByPosition(position) {
return this.findToasts((t) => t.config.position === position);
},
};
// 初始化主题和国际化
if (typeof themeUtils.initTheme === 'function') {
themeUtils.initTheme();
}
if (typeof i18nUtils.initI18n === 'function') {
i18nUtils.initI18n();
}
// 浏览器环境全局注册
if (typeof window !== 'undefined') {
window.MeToast = enhancedMeToast;
window.Met = enhancedMeToast;
// Notification API — 页面不可见时自动发送系统通知
enhancedMeToast._notify = (toast) => {
if (typeof Notification === 'undefined') return;
if (Notification.permission === 'denied') return;
if (!document.hidden) return;
if (Notification.permission === 'default') {
Notification.requestPermission().then(p => { if (p === 'granted') sendNotification(toast); });
return;
}
sendNotification(toast);
};
const sendNotification = (toast) => {
try {
new Notification(toast.title || toast.type, { body: toast.message });
} catch (_) {}
};
Toast.on('afterShow', (toast) => {
if (toast.config.notifyWhenHidden) enhancedMeToast._notify(toast);
});
}
// 导出
export default enhancedMeToast;
export { enhancedMeToast as meToast, enhancedMeToast as Met, enhancedMeToast as MeToast, Toast, VERSION };
+122
View File
@@ -0,0 +1,122 @@
/**
* MetonaToast Toast通知库
* @module metona-toast
* @version 0.5.0
* @author thzxx
* @license MIT
*/
import { meToast } from './api.js';
import { Toast } from './toast.js';
import { animationUtils } from './animations.js';
import { themeUtils } from './themes.js';
import { i18nUtils } from './i18n.js';
import { pluginUtils, presetPlugins } from './plugins.js';
import { VERSION } from './constants.js';
import type { ToastInstance } from './types.js';
/**
* MeToast meToast
*/
const enhancedMeToast = {
...meToast,
version: VERSION,
animations: animationUtils,
themes: themeUtils,
i18n: i18nUtils,
plugins: pluginUtils,
presetPlugins,
};
// 初始化主题和国际化
if (typeof themeUtils.initTheme === 'function') {
themeUtils.initTheme();
}
if (typeof i18nUtils.initI18n === 'function') {
i18nUtils.initI18n();
}
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
if (typeof themeUtils.addThemeListener === 'function') {
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
}
if (typeof i18nUtils.addLocaleListener === 'function') {
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
}
// 浏览器环境全局注册
if (typeof window !== 'undefined') {
window.MeToast = enhancedMeToast;
window.Met = enhancedMeToast;
// Notification API — 页面不可见时自动发送系统通知
const sendNotification = (toast: ToastInstance): void => {
try {
new Notification(toast.title || toast.type, { body: toast.message });
} catch (_e) { /* noop */ }
};
(enhancedMeToast as unknown as Record<string, unknown>)._notify = (toast: ToastInstance) => {
if (typeof Notification === 'undefined') return;
if (Notification.permission === 'denied') return;
if (!document.hidden) return;
if (Notification.permission === 'default') {
Notification.requestPermission().then(p => { if (p === 'granted') sendNotification(toast); });
return;
}
sendNotification(toast);
};
Toast.on('afterShow', (toast: ToastInstance) => {
const config = toast.config;
if (config.notifyWhenHidden) {
((enhancedMeToast as unknown as Record<string, (t: ToastInstance) => void>)._notify)(toast);
}
});
}
// 导出
export default enhancedMeToast;
export { enhancedMeToast as meToast, enhancedMeToast as Met, enhancedMeToast as MeToast, Toast, VERSION };
// 类型重导出(供 TypeScript 消费者 import type 使用)
export type {
ToastType,
ToastPosition,
ToastTheme,
ToastAnimation,
ToastProgressDirection,
ToastConfig,
ToastOptions,
ToastInstance,
TypeColor,
ErrorInfo,
LoadingControl,
ProgressControl,
CountdownControl,
ActionControl,
QueueControl,
GroupAPI,
ActionButton,
PromiseOptions,
ConfirmOptions,
PromptOptions,
ProgressOptions,
CountdownOptions,
QueueOptions,
StackOptions,
InitOptions,
StatusInfo,
AnimationConfig,
AnimationUtils,
ThemeConfig,
ThemePreview,
ThemeUtils,
LocaleInfo,
I18nUtils,
Plugin,
PluginManager,
PluginUtils,
} from './types.js';
+21 -277
View File
@@ -1,12 +1,13 @@
/**
* MetonaToast Locales
* @module locales
* @version 2.0.1
* @version 0.5.0
* @description zh-CN / en-US
*/
export const LOCALES = {
export const LOCALES: Record<string, Record<string, string>> = {
'zh-CN': {
// === 操作 ===
close: '关闭',
loading: '加载中...',
success: '操作成功',
@@ -87,6 +88,8 @@ export const LOCALES = {
hide: '隐藏',
visible: '可见',
hidden: '隐藏',
// === 状态 ===
enabled: '已启用',
disabled: '已禁用',
active: '活跃',
@@ -95,7 +98,6 @@ export const LOCALES = {
offline: '离线',
connected: '已连接',
disconnected: '已断开',
loading: '加载中',
loaded: '已加载',
saving: '保存中',
saved: '已保存',
@@ -114,12 +116,9 @@ export const LOCALES = {
receiving: '接收中',
received: '已接收',
connecting: '连接中',
error: '错误',
warning: '警告',
info: '信息',
success: '成功',
// === 交互 ===
question: '问题',
help: '帮助',
feedback: '反馈',
report: '报告',
bug: '错误',
@@ -133,7 +132,6 @@ export const LOCALES = {
complete: '完成',
incomplete: '未完成',
pending: '待处理',
processing: '处理中',
failed: '失败',
cancelled: '已取消',
timeout: '超时',
@@ -142,8 +140,6 @@ export const LOCALES = {
valid: '有效',
required: '必填',
optional: '可选',
enabled: '启用',
disabled: '禁用',
allowed: '允许',
denied: '拒绝',
approved: '已批准',
@@ -158,6 +154,8 @@ export const LOCALES = {
unauthenticated: '未认证',
authorized: '已授权',
unauthorized: '未授权',
// === 范围 / 角色 ===
public: '公开',
private: '私有',
protected: '受保护',
@@ -181,6 +179,8 @@ export const LOCALES = {
following: '关注中',
friend: '朋友',
contact: '联系人',
// === 组织 ===
group: '群组',
team: '团队',
organization: '组织',
@@ -190,8 +190,6 @@ export const LOCALES = {
task: '任务',
issue: '问题',
ticket: '工单',
request: '请求',
response: '响应',
message: '消息',
notification: '通知',
alert: '警报',
@@ -199,7 +197,6 @@ export const LOCALES = {
event: '事件',
activity: '活动',
log: '日志',
report: '报告',
analytics: '分析',
statistics: '统计',
metrics: '指标',
@@ -251,134 +248,10 @@ export const LOCALES = {
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': {
// === Actions ===
close: 'Close',
loading: 'Loading...',
success: 'Success',
@@ -459,6 +332,8 @@ export const LOCALES = {
hide: 'Hide',
visible: 'Visible',
hidden: 'Hidden',
// === Status ===
enabled: 'Enabled',
disabled: 'Disabled',
active: 'Active',
@@ -467,7 +342,6 @@ export const LOCALES = {
offline: 'Offline',
connected: 'Connected',
disconnected: 'Disconnected',
loading: 'Loading',
loaded: 'Loaded',
saving: 'Saving',
saved: 'Saved',
@@ -486,12 +360,9 @@ export const LOCALES = {
receiving: 'Receiving',
received: 'Received',
connecting: 'Connecting',
error: 'Error',
warning: 'Warning',
info: 'Info',
success: 'Success',
// === Interaction ===
question: 'Question',
help: 'Help',
feedback: 'Feedback',
report: 'Report',
bug: 'Bug',
@@ -505,7 +376,6 @@ export const LOCALES = {
complete: 'Complete',
incomplete: 'Incomplete',
pending: 'Pending',
processing: 'Processing',
failed: 'Failed',
cancelled: 'Cancelled',
timeout: 'Timeout',
@@ -514,8 +384,6 @@ export const LOCALES = {
valid: 'Valid',
required: 'Required',
optional: 'Optional',
enabled: 'Enabled',
disabled: 'Disabled',
allowed: 'Allowed',
denied: 'Denied',
approved: 'Approved',
@@ -530,6 +398,8 @@ export const LOCALES = {
unauthenticated: 'Unauthenticated',
authorized: 'Authorized',
unauthorized: 'Unauthorized',
// === Scope / Role ===
public: 'Public',
private: 'Private',
protected: 'Protected',
@@ -553,6 +423,8 @@ export const LOCALES = {
following: 'Following',
friend: 'Friend',
contact: 'Contact',
// === Organization ===
group: 'Group',
team: 'Team',
organization: 'Organization',
@@ -562,8 +434,6 @@ export const LOCALES = {
task: 'Task',
issue: 'Issue',
ticket: 'Ticket',
request: 'Request',
response: 'Response',
message: 'Message',
notification: 'Notification',
alert: 'Alert',
@@ -571,7 +441,6 @@ export const LOCALES = {
event: 'Event',
activity: 'Activity',
log: 'Log',
report: 'Report',
analytics: 'Analytics',
statistics: 'Statistics',
metrics: 'Metrics',
@@ -623,130 +492,5 @@ export const LOCALES = {
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',
},
};
-206
View File
@@ -1,206 +0,0 @@
/**
* MetonaToast Plugins - 插件系统
* @module plugins
* @version 2.0.1
* @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility)
*/
import { t } from './i18n.js';
/**
* 插件管理器
*/
class PluginManager {
constructor() {
this.plugins = new Map();
this.initialized = false;
}
register(name, plugin) {
if (this.plugins.has(name)) {
console.warn(`Plugin "${name}" is already registered`);
return this;
}
if (!plugin || typeof plugin !== 'object' || (!plugin.name && !plugin.version)) {
console.error(`Invalid plugin "${name}"`);
return this;
}
this.plugins.set(name, { name, ...plugin, installed: false, enabled: true });
if (plugin.install) {
try { plugin.install(this); this.plugins.get(name).installed = true; }
catch (e) { console.error(`Failed to install plugin "${name}":`, e); }
}
return this;
}
unregister(name) {
const plugin = this.plugins.get(name);
if (!plugin) return this;
if (plugin.uninstall) {
try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); }
}
this.plugins.delete(name);
return this;
}
get(name) { return this.plugins.get(name) || null; }
has(name) { return this.plugins.has(name); }
getAll() { return Array.from(this.plugins.values()); }
getNames() { return Array.from(this.plugins.keys()); }
enable(name) {
const p = this.plugins.get(name);
if (p) p.enabled = true;
return this;
}
disable(name) {
const p = this.plugins.get(name);
if (p) p.enabled = false;
return this;
}
isEnabled(name) {
const p = this.plugins.get(name);
return p ? p.enabled : false;
}
destroy() {
this.plugins.forEach((p, name) => {
if (p.destroy) { try { p.destroy(this); } catch (e) {} }
});
this.plugins.clear();
}
}
/**
* 预设插件
*/
const presetPlugins = {
/**
* 键盘快捷键插件 ESC 关闭所有 Toast
*/
keyboard: {
name: 'keyboard',
version: '1.0.0',
description: 'ESC 关闭所有 Toast',
_handler: null,
install() {
if (typeof document === 'undefined') return;
this._handler = (e) => {
if (e.key === 'Escape') {
// 点击所有关闭按钮
document.querySelectorAll('.met-toast .met-close').forEach(btn => {
try { btn.click(); } catch (_) {}
});
}
};
document.addEventListener('keydown', this._handler);
},
uninstall() {
if (this._handler && typeof document !== 'undefined') {
document.removeEventListener('keydown', this._handler);
this._handler = null;
}
},
},
/**
* 持久化插件 自动保存/加载配置到 localStorage
*/
persistence: {
name: 'persistence',
version: '1.0.0',
description: '自动持久化配置到 localStorage',
storageKey: 'metona-toast-config',
install() {
// 加载已保存的配置
if (typeof localStorage !== 'undefined') {
try {
const saved = localStorage.getItem(this.storageKey);
return saved ? JSON.parse(saved) : null;
} catch (_) { return null; }
}
return null;
},
save(config) {
if (typeof localStorage !== 'undefined') {
try { localStorage.setItem(this.storageKey, JSON.stringify(config)); } catch (_) {}
}
},
uninstall() {
if (typeof localStorage !== 'undefined') {
try { localStorage.removeItem(this.storageKey); } catch (_) {}
}
},
},
/**
* 无障碍插件 屏幕阅读器公告
*/
accessibility: {
name: 'accessibility',
version: '1.0.0',
description: '通过屏幕阅读器朗读 Toast 内容',
announce(toast) {
if (typeof document === 'undefined') return;
const el = document.createElement('div');
el.setAttribute('aria-live', 'assertive');
el.setAttribute('aria-atomic', 'true');
el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
const typeName = t(toast.type) || toast.type;
el.textContent = `${typeName}: ${toast.title || ''} ${toast.message || ''}`;
document.body.appendChild(el);
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
},
},
};
/**
* 插件工具
*/
const pluginUtils = {
createManager() { return new PluginManager(); },
register(name, plugin) { return defaultPluginManager.register(name, plugin); },
unregister(name) { return defaultPluginManager.unregister(name); },
get(name) { return defaultPluginManager.get(name); },
has(name) { return defaultPluginManager.has(name); },
getAll() { return defaultPluginManager.getAll(); },
getNames() { return defaultPluginManager.getNames(); },
enable(name) { return defaultPluginManager.enable(name); },
disable(name) { return defaultPluginManager.disable(name); },
isEnabled(name) { return defaultPluginManager.isEnabled(name); },
getPreset(name) { return presetPlugins[name] || null; },
getAllPresets() { return { ...presetPlugins }; },
createPlugin(config) {
return {
name: config.name || 'custom',
version: config.version || '1.0.0',
description: config.description || '',
hooks: config.hooks || {},
install: config.install || null,
uninstall: config.uninstall || null,
...config,
};
},
validatePlugin(plugin) {
const errors = [];
if (!plugin || typeof plugin !== 'object') errors.push('Plugin must be an object');
if (!plugin.name && !plugin.version) errors.push('Plugin must have a name or version');
return { valid: errors.length === 0, errors };
},
};
const defaultPluginManager = new PluginManager();
export { presetPlugins, pluginUtils, defaultPluginManager, PluginManager };
+266
View File
@@ -0,0 +1,266 @@
/**
* MetonaToast Plugins
* @module plugins
* @version 0.5.0
*/
import { t } from './i18n.js';
import { Toast } from './toast.js';
import type { Plugin, PluginManager as IPluginManager, PluginUtils, ToastInstance } from './types.js';
/**
*
*/
class PluginManager implements IPluginManager {
plugins: Map<string, Plugin>;
initialized: boolean;
constructor() {
this.plugins = new Map();
this.initialized = false;
}
register(name: string, plugin: Plugin): PluginManager {
if (this.plugins.has(name)) {
console.warn(`Plugin "${name}" is already registered`);
return this;
}
if (!plugin || typeof plugin !== 'object' || !plugin.name) {
console.error(`Invalid plugin "${name}": must be an object with a "name" property`);
return this;
}
const stored: Plugin = { ...plugin, installed: false, enabled: true };
if (!stored.name) stored.name = name;
this.plugins.set(name, stored);
if (stored.install) {
try {
stored.install(this);
stored.installed = true;
} catch (e) {
console.error(`Failed to install plugin "${name}":`, e);
}
}
return this;
}
unregister(name: string): PluginManager {
const plugin = this.plugins.get(name);
if (!plugin) return this;
if (plugin.uninstall) {
try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); }
}
this.plugins.delete(name);
return this;
}
get(name: string): Plugin | null { return this.plugins.get(name) || null; }
has(name: string): boolean { return this.plugins.has(name); }
getAll(): Plugin[] { return Array.from(this.plugins.values()); }
getNames(): string[] { return Array.from(this.plugins.keys()); }
enable(name: string): PluginManager {
const p = this.plugins.get(name);
if (p) p.enabled = true;
return this;
}
disable(name: string): PluginManager {
const p = this.plugins.get(name);
if (p) p.enabled = false;
return this;
}
isEnabled(name: string): boolean {
const p = this.plugins.get(name);
return p ? !!p.enabled : false;
}
destroy(): void {
this.plugins.forEach((p) => {
if (p.destroy) { try { p.destroy(this); } catch (_e) { /* noop */ } }
});
this.plugins.clear();
}
}
/**
*
*/
const presetPlugins: Record<string, Plugin> = {
/**
* ESC Toast
*/
keyboard: {
name: 'keyboard',
version: '1.0.0',
description: 'ESC 关闭所有 Toast',
_handler: null as ((e: KeyboardEvent) => void) | null,
install(this: Plugin) {
if (typeof document === 'undefined') return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
document.querySelectorAll('.met-toast .met-close').forEach(btn => {
try { (btn as HTMLElement).click(); } catch (_e) { /* noop */ }
});
}
};
(this as Record<string, unknown>)._handler = handler;
document.addEventListener('keydown', handler);
},
uninstall(this: Plugin) {
const handler = (this as Record<string, unknown>)._handler as ((e: KeyboardEvent) => void) | null;
if (handler && typeof document !== 'undefined') {
document.removeEventListener('keydown', handler);
(this as Record<string, unknown>)._handler = null;
}
},
},
/**
* / localStorage
*/
persistence: {
name: 'persistence',
version: '1.0.0',
description: '自动持久化配置到 localStorage',
storageKey: 'metona-toast-config',
install(this: Plugin) {
if (typeof localStorage !== 'undefined') {
try {
const key = (this as Record<string, unknown>).storageKey as string;
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : null;
} catch (_e) { return null; }
}
return null;
},
save(this: Plugin, config: Record<string, unknown>) {
if (typeof localStorage !== 'undefined') {
try {
const key = (this as Record<string, unknown>).storageKey as string;
localStorage.setItem(key, JSON.stringify(config));
} catch (_e) { /* noop */ }
}
},
uninstall(this: Plugin) {
if (typeof localStorage !== 'undefined') {
try {
const key = (this as Record<string, unknown>).storageKey as string;
localStorage.removeItem(key);
} catch (_e) { /* noop */ }
}
// 卸载 use() 注册的配置保存钩子
const meta = this as Record<string, unknown>;
if (typeof meta._saveOff === 'function') {
(meta._saveOff as () => void)();
meta._saveOff = null;
}
},
},
/**
*
*/
accessibility: {
name: 'accessibility',
version: '1.0.0',
description: '通过屏幕阅读器朗读 Toast 内容',
announce(toast: { type: string; title?: string; message?: string }) {
if (typeof document === 'undefined') return;
const el = document.createElement('div');
el.setAttribute('aria-live', 'assertive');
el.setAttribute('aria-atomic', 'true');
el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
const typeName = t(toast.type) || toast.type;
el.textContent = `${typeName}: ${toast.title || ''} ${toast.message || ''}`;
document.body.appendChild(el);
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
},
uninstall(this: Plugin) {
// 卸载 use() 注册的朗读钩子
const meta = this as Record<string, unknown>;
if (typeof meta._off === 'function') {
(meta._off as () => void)();
meta._off = null;
}
},
},
/**
* type + message toast
*/
dedupe: {
name: 'dedupe',
version: '1.0.0',
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
_off: null as (() => void) | null,
install(this: Plugin) {
const off = Toast.on('beforeShow', (toast: ToastInstance) => {
const existing = Array.from(Toast._registry.values()).find(
t => !t.closing && t.type === toast.type && t.message === toast.message
);
if (existing) {
existing.update({ title: toast.title || existing.title });
return false;
}
return undefined;
});
(this as Record<string, unknown>)._off = off;
},
uninstall(this: Plugin) {
const off = (this as Record<string, unknown>)._off as (() => void) | null;
if (off) off();
(this as Record<string, unknown>)._off = null;
},
},
};
/**
*
*/
const pluginUtils: PluginUtils = {
createManager(): PluginManager { return new PluginManager(); },
register(name: string, plugin: Plugin): PluginManager { return defaultPluginManager.register(name, plugin); },
unregister(name: string): PluginManager { return defaultPluginManager.unregister(name); },
get(name: string): Plugin | null { return defaultPluginManager.get(name); },
has(name: string): boolean { return defaultPluginManager.has(name); },
getAll(): Plugin[] { return defaultPluginManager.getAll(); },
getNames(): string[] { return defaultPluginManager.getNames(); },
enable(name: string): PluginManager { return defaultPluginManager.enable(name); },
disable(name: string): PluginManager { return defaultPluginManager.disable(name); },
isEnabled(name: string): boolean { return defaultPluginManager.isEnabled(name); },
getPreset(name: string): Plugin | null { return presetPlugins[name] || null; },
getAllPresets(): Record<string, Plugin> { return { ...presetPlugins }; },
createPlugin(config: Partial<Plugin>): Plugin {
return {
name: config.name || 'custom',
version: config.version || '1.0.0',
description: config.description || '',
hooks: config.hooks || {},
install: config.install || undefined,
uninstall: config.uninstall || undefined,
...config,
};
},
validatePlugin(plugin: Partial<Plugin>): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!plugin || typeof plugin !== 'object') errors.push('Plugin must be an object');
if (!plugin.name && !plugin.version) errors.push('Plugin must have a name or version');
return { valid: errors.length === 0, errors };
},
};
const defaultPluginManager = new PluginManager();
export { presetPlugins, pluginUtils, defaultPluginManager, PluginManager };
+122
View File
@@ -0,0 +1,122 @@
/**
* MetonaToast React React
* @module react
* @version 0.5.0
* @description useToast hook + <Toast /> React optional peerDependency
*/
import { useEffect, useMemo, useRef } from 'react';
import MeToast from './index.js';
import type { ToastInstance, ToastOptions } from './types.js';
/** 支持声明式渲染的 Toast 类型 */
export type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
export interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
message: string;
type?: ReactToastType;
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
autoClose?: boolean;
}
const SHOW_METHODS = ['success', 'error', 'warning', 'info', 'show'] as const;
type ShowMethod = (typeof SHOW_METHODS)[number];
/** 有 id 的可追踪对象(ToastInstance / LoadingControl */
interface Tracked {
id: string;
}
const showToast = (method: ShowMethod, message: string, opts: Omit<ToastProps, 'message' | 'type' | 'autoClose'>): ToastInstance => {
switch (method) {
case 'success': return MeToast.success(message, opts);
case 'error': return MeToast.error(message, opts);
case 'warning': return MeToast.warning(message, opts);
case 'info': return MeToast.info(message, opts);
default: return MeToast.show(message, opts);
}
};
/**
* API Toast
* 便
*/
export const createBoundApi = (cleanup: Set<string>): Record<string, unknown> => {
const track = <T extends Tracked | null>(fn: () => T): T => {
const result = fn();
if (result && result.id) cleanup.add(result.id);
return result;
};
const bound: Record<string, unknown> = {
...MeToast,
success: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
track(() => MeToast.success(messageOrOpts, opts)),
error: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
track(() => MeToast.error(messageOrOpts, opts)),
warning: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
track(() => MeToast.warning(messageOrOpts, opts)),
info: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
track(() => MeToast.info(messageOrOpts, opts)),
show: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
track(() => MeToast.show(messageOrOpts, opts)),
loading: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
track(() => MeToast.loading(messageOrOpts, opts)),
// 通过 id 关闭时同步清理集合,避免卸载时重复移除
dismiss: (id?: string) => {
if (id) cleanup.delete(id);
return MeToast.dismiss(id);
},
removeToast: (id: string) => {
cleanup.delete(id);
return MeToast.removeToast(id);
},
};
return bound;
};
/**
* useToast MeToast API Toast
* "通知随组件生命周期"
*/
export const useToast = (): Record<string, unknown> => {
const cleanupRef = useRef<Set<string>>(new Set());
useEffect(() => {
const set = cleanupRef.current;
return () => {
set.forEach(id => MeToast.removeToast(id));
set.clear();
};
}, []);
return useMemo(() => createBoundApi(cleanupRef.current), []);
};
/**
* Toast Toast props
* autoClose true
*/
export const Toast = (props: ToastProps): null => {
const { message, type, autoClose = true, ...rest } = props;
const toastRef = useRef<ToastInstance | null>(null);
const restKey = JSON.stringify(rest);
useEffect(() => {
const method: ShowMethod = SHOW_METHODS.includes(type as ShowMethod) ? (type as ShowMethod) : 'show';
toastRef.current = showToast(method, message, rest);
return () => {
if (autoClose) toastRef.current?.remove();
toastRef.current = null;
};
// autoClose 不参与重建依赖:它只影响卸载时的清理行为
}, [message, type, restKey]);
return null;
};
export { MeToast };
+60 -79
View File
@@ -1,20 +1,19 @@
/**
* MetonaToast Styles - v2.0.1
* MetonaToast Styles
* @module styles
* @description 使
* @version 0.5.0
*/
import { THEMES } from './constants.js';
import type { ThemeConfig } from './types.js';
// 样式缓存
let styleElement = null;
let styleElement: HTMLStyleElement | null = null;
/**
* CSS样式
*/
const generateCSS = (theme) => {
const generateCSS = (): string => {
return `
/* MetonaToast 基础样式 */
.met-container {
pointer-events: none;
display: flex;
@@ -27,7 +26,6 @@ const generateCSS = (theme) => {
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; }
@@ -35,7 +33,6 @@ const generateCSS = (theme) => {
.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; }
/* Toast基础样式 */
.met-toast {
position: relative;
display: flex;
@@ -69,17 +66,14 @@ const generateCSS = (theme) => {
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;
@@ -90,7 +84,6 @@ const generateCSS = (theme) => {
margin-top: 1px;
}
/* 内容样式 */
.met-content {
flex: 1;
min-width: 0;
@@ -98,21 +91,18 @@ const generateCSS = (theme) => {
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;
@@ -134,7 +124,6 @@ const generateCSS = (theme) => {
background: var(--met-close-hover-bg, rgba(0,0,0,0.06));
}
/* 进度条样式 - 水平 */
.met-progress {
position: absolute;
left: 0; right: 0; bottom: 0;
@@ -144,7 +133,6 @@ const generateCSS = (theme) => {
border-radius: 0 0 12px 12px;
}
/* 进度条样式 - 垂直 */
.met-progress-v {
position: absolute;
left: 0; top: 0; bottom: 0;
@@ -154,7 +142,6 @@ const generateCSS = (theme) => {
border-radius: 12px 0 0 12px;
}
/* 进度条指示器 - 水平 */
.met-bar {
position: absolute;
left: 0; top: 0;
@@ -163,7 +150,6 @@ const generateCSS = (theme) => {
transform: scaleX(1);
}
/* 进度条指示器 - 垂直 */
.met-bar-v {
position: absolute;
left: 0; bottom: 0;
@@ -172,7 +158,6 @@ const generateCSS = (theme) => {
transform: scaleY(1);
}
/* 侧边指示器 */
.met-side {
position: absolute;
left: 0; top: 0; bottom: 0;
@@ -180,13 +165,11 @@ const generateCSS = (theme) => {
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;
@@ -196,7 +179,6 @@ const generateCSS = (theme) => {
to { transform: rotate(360deg); }
}
/* === 进入动画:初始隐藏态 === */
.met-anim-slide.met-toast,
.met-anim-fade.met-toast,
.met-anim-scale.met-toast,
@@ -211,7 +193,6 @@ const generateCSS = (theme) => {
opacity: 0;
}
/* === 进入动画:@keyframes 定义 === */
@keyframes met-slide-in {
0% { transform: translateX(80px); opacity: 0; }
65% { transform: translateX(-6px); opacity: 1; }
@@ -272,7 +253,6 @@ const generateCSS = (theme) => {
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; }
@@ -285,7 +265,6 @@ const generateCSS = (theme) => {
.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; }
@@ -320,10 +299,6 @@ const generateCSS = (theme) => {
.met-toast { width: 100%; min-width: 0; }
}
/* 无障碍支持 */
.met-toast[role="alert"] { }
.met-toast[role="status"] { }
@media (prefers-reduced-motion: reduce) {
.met-toast { transition: opacity 0.15s !important; }
.met-toast.met-show { animation: none !important; opacity: 1 !important; }
@@ -340,7 +315,6 @@ const generateCSS = (theme) => {
.met-close { border: 2px solid currentColor; border-radius: 4px; }
}
/* 焦点样式 */
.met-toast:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
@@ -350,32 +324,22 @@ const generateCSS = (theme) => {
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 (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 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))
@@ -385,30 +349,60 @@ const generateCSS = (theme) => {
}
}
/* 按下状态 */
.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;
@@ -426,7 +420,6 @@ const generateCSS = (theme) => {
background: rgba(255, 255, 255, 0.08);
}
/* 亮色主题特定样式 */
.met-toast.met-theme-light {
background: rgba(255, 255, 255, 0.96);
color: #1f2937;
@@ -444,7 +437,6 @@ const generateCSS = (theme) => {
background: rgba(0, 0, 0, 0.06);
}
/* 自定义主题支持 */
.met-toast[data-theme] {
--met-bg: var(--met-theme-bg);
--met-text: var(--met-theme-text);
@@ -452,7 +444,6 @@ const generateCSS = (theme) => {
--met-shadow: var(--met-theme-shadow);
}
/* 暗色模式自动检测 */
@media (prefers-color-scheme: dark) {
.met-theme-auto .met-toast {
background: rgba(28, 32, 40, 0.94);
@@ -488,14 +479,14 @@ const generateCSS = (theme) => {
/**
*
*/
export const injectStyles = (theme = THEMES.light) => {
export const injectStyles = (): void => {
if (typeof document === 'undefined') return;
if (styleElement && document.getElementById('metona-toast-styles')) {
return;
}
const css = generateCSS(theme);
const css = generateCSS();
styleElement = document.createElement('style');
styleElement.id = 'metona-toast-styles';
@@ -507,31 +498,30 @@ export const injectStyles = (theme = THEMES.light) => {
/**
*
*/
export const updateStyles = (theme) => {
export const updateStyles = (): void => {
if (!styleElement) {
injectStyles(theme);
injectStyles();
return;
}
const css = generateCSS(theme);
const css = generateCSS();
styleElement.textContent = css.replace(/\s+/g, ' ');
};
/**
*
*/
export const removeStyles = () => {
export const removeStyles = (): void => {
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
styleElement = null;
};
/**
* CSS变量
*/
export const generateThemeVariables = (theme) => {
export const generateThemeVariables = (theme: ThemeConfig): string => {
return `
:root {
--met-theme-bg: ${theme.bg};
@@ -548,9 +538,14 @@ export const generateThemeVariables = (theme) => {
/**
*
*/
export const applyThemeVariables = (theme) => {
export const applyThemeVariables = (theme: ThemeConfig): void => {
if (typeof document === 'undefined') return;
const oldStyle = document.getElementById('metona-toast-custom-styles');
if (oldStyle && oldStyle.parentNode) {
oldStyle.parentNode.removeChild(oldStyle);
}
const css = generateThemeVariables(theme);
const customStyle = document.createElement('style');
customStyle.id = 'metona-toast-custom-styles';
@@ -561,7 +556,7 @@ export const applyThemeVariables = (theme) => {
/**
*
*/
export const clearThemeVariables = () => {
export const clearThemeVariables = (): void => {
const customStyle = document.getElementById('metona-toast-custom-styles');
if (customStyle && customStyle.parentNode) {
customStyle.parentNode.removeChild(customStyle);
@@ -571,26 +566,26 @@ export const clearThemeVariables = () => {
/**
*
*/
export const getSystemTheme = () => {
export const getSystemTheme = (): string => {
if (typeof window === 'undefined') return 'light';
return window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
};
/**
*
*/
export const watchSystemTheme = (callback) => {
export const watchSystemTheme = (callback: (theme: string) => void): (() => void) => {
if (typeof window === 'undefined' || !window.matchMedia) {
return () => {};
}
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e) => {
const handler = (e: MediaQueryListEvent) => {
callback(e.matches ? 'dark' : 'light');
};
@@ -600,17 +595,3 @@ export const watchSystemTheme = (callback) => {
mediaQuery.removeEventListener('change', handler);
};
};
/**
*
*/
export const autoApplySystemTheme = () => {
const applyTheme = (theme) => {
updateStyles(THEMES[theme]);
applyThemeVariables(THEMES[theme]);
};
applyTheme(getSystemTheme());
return watchSystemTheme(applyTheme);
};
+79
View File
@@ -0,0 +1,79 @@
/**
* MetonaToast Templates HTML
* @module templates
* @version 0.5.0
* @description confirm / prompt / progress / action DOM
*/
import { escapeHTML } from './utils.js';
import { t } from './i18n.js';
import type { ActionButton, ConfirmOptions, PromptOptions, ProgressOptions } from './types.js';
/**
* confirm/prompt
*/
export const btnRow = (buttons: string): string => `
<div style="display: flex; gap: 8px; margin-top: 12px;">
${buttons}
</div>`;
/**
*
*/
export const confirmHTML = (opts: ConfirmOptions): string => {
const confirmText = opts.confirmText || t('confirm');
const cancelText = opts.cancelText || t('cancel');
const confirmColor = opts.confirmColor || '#10b981';
const cancelColor = opts.cancelColor || '#6b7280';
return btnRow(`
<button class="met-confirm-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${confirmColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(confirmText)}</button>
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
`);
};
/**
*
*/
export const promptHTML = (opts: PromptOptions): string => {
const submitText = opts.submitText || t('confirm');
const cancelText = opts.cancelText || t('cancel');
const submitColor = opts.submitColor || '#3b82f6';
const cancelColor = opts.cancelColor || '#6b7280';
return `
<input type="${opts.inputType || 'text'}"
class="met-input"
placeholder="${escapeHTML(opts.placeholder || '')}"
value="${escapeHTML(opts.defaultValue || '')}"
style="width:100%;padding:8px 12px;border:1px solid rgba(0,0,0,0.1);border-radius:6px;font-size:13px;margin-top:8px;background:transparent;color:inherit;box-sizing:border-box">
${btnRow(`
<button class="met-submit-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${submitColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(submitText)}</button>
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
`)}
`;
};
/**
*
*/
export const progressHTML = (opts: ProgressOptions): string => {
const color = opts.progressColor || '#3b82f6';
return `
<div class="met-progress-bar" style="width:100%;height:8px;background:rgba(0,0,0,0.1);border-radius:4px;margin-top:12px;overflow:hidden">
<div class="met-progress-fill" style="width:0%;height:100%;background:${color};border-radius:4px;transition:width 0.3s ease"></div>
</div>
<div class="met-progress-text" style="font-size:12px;opacity:0.7;margin-top:4px;text-align:right">0%</div>
`;
};
/**
* Action Toast
*/
export const actionHTML = (actions: ActionButton[]): string => {
if (!Array.isArray(actions) || actions.length === 0) return '';
const buttons = actions.map((a, i) => {
const bg = a.style?.background || a.color || '#6366f1';
const cls = `met-action-btn-${i}`;
return `<button class="${cls}" style="flex:1;padding:6px 12px;border:none;border-radius:6px;background:${bg};color:white;cursor:pointer;font-size:12px;font-weight:500">${escapeHTML(a.text || '')}</button>`;
}).join('');
return btnRow(buttons);
};
+72 -209
View File
@@ -1,48 +1,39 @@
/**
* MetonaToast Themes -
* MetonaToast Themes
* @module themes
* @version 2.0.1
* @description
* @version 0.5.0
*/
import { THEMES } from './constants.js';
import { prefersDark } from './utils.js';
import type { ThemeConfig, ThemePreview, ThemeUtils } from './types.js';
// 当前主题状态
let currentTheme = 'auto';
let themeListeners = new Set();
let systemThemeMediaQuery = null;
let systemThemeListener = null;
const themeListeners: Set<(theme: string, resolved: string) => void> = new Set();
let systemThemeMediaQuery: MediaQueryList | null = null;
let systemThemeListener: ((e: MediaQueryListEvent) => void) | null = null;
/**
*
* @returns {string} ('light' 'dark')
*/
export const getSystemTheme = () => {
export const getSystemTheme = (): 'light' | 'dark' => {
if (typeof window === 'undefined') return 'light';
return prefersDark() ? 'dark' : 'light';
};
/**
*
* @param {string} theme -
* @returns {string}
*/
export const getTheme = (theme) => {
export const getTheme = (theme: string): string => {
return resolveTheme(theme);
};
/**
*
* @param {string} theme -
* @returns {string}
* 'auto'
*/
export const resolveTheme = (theme) => {
export const resolveTheme = (theme: string): string => {
if (theme === 'auto') {
// 优先使用用户设置的全局主题,否则使用系统主题
if (currentTheme && currentTheme !== 'auto') {
return currentTheme;
}
return getSystemTheme();
}
return theme;
@@ -50,44 +41,37 @@ export const resolveTheme = (theme) => {
/**
*
* @param {string} theme -
* @returns {Object}
*/
export const getThemeConfig = (theme) => {
export const getThemeConfig = (theme: string): ThemeConfig => {
const resolved = resolveTheme(theme);
return THEMES[resolved] || THEMES.light;
return (THEMES[resolved] as ThemeConfig) || (THEMES.light as ThemeConfig);
};
/**
*
* @param {string} theme -
*/
export const applyTheme = (theme) => {
export const applyTheme = (theme: string): void => {
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 -
*/
export const setThemeVariables = (config) => {
export const setThemeVariables = (config: ThemeConfig): void => {
if (typeof document === 'undefined') return;
const root = document.documentElement;
root.style.setProperty('--met-bg', config.bg);
root.style.setProperty('--met-text', config.text);
@@ -96,8 +80,7 @@ export const setThemeVariables = (config) => {
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');
@@ -108,51 +91,39 @@ export const setThemeVariables = (config) => {
/**
* CSS变量
*/
export const clearThemeVariables = () => {
export const clearThemeVariables = (): void => {
if (typeof document === 'undefined') return;
const root = document.documentElement;
const variables = [
'--met-bg',
'--met-text',
'--met-border',
'--met-shadow',
'--met-hover-shadow',
'--met-progress-bg',
'--met-close-hover-bg',
'--met-success',
'--met-error',
'--met-warning',
'--met-info',
'--met-loading',
'--met-bg', '--met-text', '--met-border', '--met-shadow',
'--met-hover-shadow', '--met-progress-bg', '--met-close-hover-bg',
'--met-success', '--met-error', '--met-warning', '--met-info', '--met-loading',
];
variables.forEach((variable) => {
root.style.removeProperty(variable);
variables.forEach((v) => {
root.style.removeProperty(v);
});
};
/**
*
* @returns {string}
*/
export const getCurrentTheme = () => {
export const getCurrentTheme = (): string => {
return currentTheme;
};
/**
*
* @returns {string}
*/
export const getResolvedTheme = () => {
export const getResolvedTheme = (): string => {
return resolveTheme(currentTheme);
};
/**
*
* @param {string} theme -
*/
export const switchTheme = (theme) => {
export const switchTheme = (theme: string): void => {
applyTheme(theme);
saveTheme(theme);
};
@@ -160,7 +131,7 @@ export const switchTheme = (theme) => {
/**
* /
*/
export const toggleTheme = () => {
export const toggleTheme = (): void => {
const resolved = getResolvedTheme();
const newTheme = resolved === 'dark' ? 'light' : 'dark';
switchTheme(newTheme);
@@ -169,15 +140,14 @@ export const toggleTheme = () => {
/**
*
*/
export const resetToAuto = () => {
export const resetToAuto = (): void => {
switchTheme('auto');
};
/**
*
* @param {string} theme -
*/
export const saveTheme = (theme) => {
export const saveTheme = (theme: string): void => {
if (typeof localStorage !== 'undefined') {
try {
localStorage.setItem('metona-toast-theme', theme);
@@ -189,9 +159,8 @@ export const saveTheme = (theme) => {
/**
*
* @returns {string}
*/
export const loadTheme = () => {
export const loadTheme = (): string => {
if (typeof localStorage !== 'undefined') {
try {
return localStorage.getItem('metona-toast-theme') || 'auto';
@@ -205,41 +174,36 @@ export const loadTheme = () => {
/**
*
*/
export const initTheme = () => {
// 加载保存的主题
export const initTheme = (): void => {
const savedTheme = loadTheme();
applyTheme(savedTheme);
// 监听系统主题变化
watchSystemTheme();
};
/**
*
*/
export const watchSystemTheme = () => {
export const watchSystemTheme = (): void => {
if (typeof window === 'undefined' || !window.matchMedia) return;
// 移除旧的监听器
if (systemThemeMediaQuery && systemThemeListener) {
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
}
// 创建新的监听器
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
systemThemeListener = (e) => {
systemThemeListener = () => {
if (currentTheme === 'auto') {
applyTheme('auto');
}
};
systemThemeMediaQuery.addEventListener('change', systemThemeListener);
};
/**
*
*/
export const unwatchSystemTheme = () => {
export const unwatchSystemTheme = (): void => {
if (systemThemeMediaQuery && systemThemeListener) {
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
systemThemeMediaQuery = null;
@@ -249,12 +213,9 @@ export const unwatchSystemTheme = () => {
/**
*
* @param {Function} listener -
* @returns {Function}
*/
export const addThemeListener = (listener) => {
export const addThemeListener = (listener: (theme: string, resolved: string) => void): () => void => {
themeListeners.add(listener);
return () => {
themeListeners.delete(listener);
};
@@ -262,18 +223,15 @@ export const addThemeListener = (listener) => {
/**
*
* @param {Function} listener -
*/
export const removeThemeListener = (listener) => {
export const removeThemeListener = (listener: (theme: string, resolved: string) => void): void => {
themeListeners.delete(listener);
};
/**
*
* @param {string} theme -
* @param {string} resolved -
*/
const notifyThemeListeners = (theme, resolved) => {
const notifyThemeListeners = (theme: string, resolved: string): void => {
themeListeners.forEach((listener) => {
try {
listener(theme, resolved);
@@ -286,74 +244,64 @@ const notifyThemeListeners = (theme, resolved) => {
/**
*
*/
export const clearThemeListeners = () => {
export const clearThemeListeners = (): void => {
themeListeners.clear();
};
/**
*
* @param {string} name -
* @param {Object} config -
*/
export 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,
export const registerTheme = (name: string, config: ThemeConfig): void => {
(THEMES as Record<string, ThemeConfig>)[name] = {
bg: config.bg || (THEMES.light as ThemeConfig).bg,
text: config.text || (THEMES.light as ThemeConfig).text,
border: config.border || (THEMES.light as ThemeConfig).border,
shadow: config.shadow || (THEMES.light as ThemeConfig).shadow,
hoverShadow: config.hoverShadow || (THEMES.light as ThemeConfig).hoverShadow,
progressBg: config.progressBg || (THEMES.light as ThemeConfig).progressBg,
closeHoverBg: config.closeHoverBg || (THEMES.light as ThemeConfig).closeHoverBg,
};
};
/**
*
* @param {string} name -
*/
export const unregisterTheme = (name) => {
export const unregisterTheme = (name: string): void => {
if (name === 'light' || name === 'dark' || name === 'auto') {
console.warn('Cannot unregister built-in theme:', name);
return;
}
delete THEMES[name];
delete (THEMES as Record<string, ThemeConfig>)[name];
};
/**
*
* @returns {Object}
*/
export const getAllThemes = () => {
return { ...THEMES };
export const getAllThemes = (): Record<string, ThemeConfig> => {
return { ...THEMES } as Record<string, ThemeConfig>;
};
/**
*
* @returns {Array}
*/
export const getThemeNames = () => {
export const getThemeNames = (): string[] => {
return Object.keys(THEMES);
};
/**
*
* @param {string} name -
* @returns {boolean}
*/
export const hasTheme = (name) => {
export const hasTheme = (name: string): boolean => {
return name in THEMES;
};
/**
*
* @param {string} theme -
* @returns {Object}
*/
export const getThemePreview = (theme) => {
export const getThemePreview = (theme: string): ThemePreview => {
const config = getThemeConfig(theme);
const resolved = resolveTheme(theme);
return {
name: theme,
resolved,
@@ -370,13 +318,11 @@ export const getThemePreview = (theme) => {
/**
* CSS
* @param {string} theme -
* @returns {string} CSS字符串
*/
export const generateThemeCSS = (theme) => {
export const generateThemeCSS = (theme: string): string => {
const config = getThemeConfig(theme);
const resolved = resolveTheme(theme);
return `
.met-theme-${resolved} {
--met-bg: ${config.bg};
@@ -392,21 +338,18 @@ export const generateThemeCSS = (theme) => {
/**
* CSS
* @param {string} theme -
*/
export const applyThemeCSS = (theme) => {
export const applyThemeCSS = (theme: string): void => {
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;
@@ -416,9 +359,9 @@ export const applyThemeCSS = (theme) => {
/**
* CSS
*/
export const removeThemeCSS = () => {
export const removeThemeCSS = (): void => {
if (typeof document === 'undefined') return;
const styleId = 'metona-toast-theme-css';
const style = document.getElementById(styleId);
if (style) {
@@ -427,9 +370,9 @@ export const removeThemeCSS = () => {
};
/**
*
*
*/
export const themeUtils = {
export const themeUtils: ThemeUtils = {
getSystemTheme,
resolveTheme,
getThemeConfig,
@@ -458,84 +401,4 @@ export const themeUtils = {
loadTheme,
};
/**
*
*/
export 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;
}
});
/**
*
* @returns {Object}
*/
export const createThemeManager = () => {
return {
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,
};
};
export { themeUtils as default };
+691
View File
@@ -0,0 +1,691 @@
/**
* MetonaToast Toast Toast
* @module toast
* @version 0.5.0
*/
import { generateId, escapeHTML } from './utils.js';
import { DEFAULTS, ICONS, TYPE_COLORS, THEMES } from './constants.js';
import { injectStyles } from './styles.js';
import { getTheme } from './themes.js';
import { t, getLocaleDirection, getCurrentLocale } from './i18n.js';
import { hasAnimation, ensureAnimationCSS } from './animations.js';
import type { ToastConfig, ToastOptions, ToastInstance, ErrorInfo, TypeColor, ThemeConfig } from './types.js';
/**
* Toast
*/
export class Toast implements ToastInstance {
// 静态钩子系统
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>> = new Map();
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
static _registry: Map<string, Toast> = new Map();
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
private static _onError: ((errorInfo: ErrorInfo) => void) | null = null;
private static _removeToast: ((id: string) => void) | null = null;
/**
* Toast
*/
static setCallbacks(callbacks: {
onError?: ((errorInfo: ErrorInfo) => void) | null;
removeToast?: ((id: string) => void) | null;
}): void {
if (callbacks.onError !== undefined) Toast._onError = callbacks.onError;
if (callbacks.removeToast !== undefined) Toast._removeToast = callbacks.removeToast;
}
static on(name: string, fn: (toast: Toast) => boolean | void): () => void {
if (!this._hooks.has(name)) this._hooks.set(name, []);
this._hooks.get(name)!.push(fn);
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
return () => Toast.off(name, fn);
}
static off(name: string, fn: (toast: Toast) => boolean | void): void {
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: string, toast: Toast | null = null): boolean {
const list = this._hooks.get(name);
if (!list || list.length === 0) return true;
let allow = true;
list.forEach(fn => {
try {
const result = fn(toast as Toast);
if (result === false) allow = false;
}
catch (e) {
console.error('Hook error:', name, e);
if (Toast._onError) {
try { Toast._onError({ hook: name, error: e as Error, toast: toast as ToastInstance }); } catch (_e) { /* noop */ }
}
}
});
return allow;
}
id: string;
type: string;
title: string;
message: string;
html: string;
iconHTML: string;
config: ToastConfig;
el: HTMLElement | null = null;
barEl: HTMLElement | null = null;
rafId: number | null = null;
remaining: number;
startedAt: number = 0;
paused: boolean = false;
closing: boolean = false;
group: string | null = null;
_cleanups: Array<() => void> = [];
constructor(opts: ToastOptions) {
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 as unknown as ToastConfig, ...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(): { theme: string; c: TypeColor; t: Partial<ThemeConfig> } {
const theme = getTheme(this.config.theme || 'auto');
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
const tc = (THEMES[theme] as ThemeConfig) || (THEMES.light as ThemeConfig);
const t: Partial<ThemeConfig> = { bg: tc.bg, border: tc.border, shadow: tc.shadow };
return { theme, c, t };
}
create(): this {
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(): HTMLElement {
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: Record<string, string> = {
'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: HTMLElement): void {
const max = this.config.max || 6;
const list = Array.from(container.querySelectorAll('.met-toast'));
while (list.length >= max) {
const el = list.shift() as HTMLElement;
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: string): string {
const anim = hasAnimation(this.config.animation || '') ? (this.config.animation as string) : '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: HTMLElement, _theme: string, _t: Partial<ThemeConfig>): void {
const widthValue = typeof this.config.width === 'number'
? `${this.config.width}px`
: (typeof this.config.width === 'string' ? this.config.width : '360px');
const set = (p: string, v: string) => 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 as unknown as Record<string, string>)[k] = v; });
}
}
_buildContent(el: HTMLElement, c: TypeColor): void {
// 自定义渲染函数 — 完全接管 DOM 构建
if (typeof this.config.render === 'function') {
el.innerHTML = this.config.render(this);
this.barEl = el.querySelector('.met-bar, .met-bar-v');
return;
}
const showIcon = this.config.icon !== false && (this.iconHTML || ICONS[this.type]);
const showClose = this.config.closeButton !== false;
const showProgress = this.config.showProgress !== false && (this.config.duration || 0) > 0;
const showSide = (!showIcon && this.type !== 'default');
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
const safeMessage = this.html
? `<div class="met-message">${this.html}</div>`
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
const iconHTML = showIcon
? `<div class="met-icon" style="color:${c.fg}">${this.iconHTML || ICONS[this.type]}</div>` : '';
const closeHTML = showClose
? `<button class="met-close" type="button" aria-label="${t('close')}">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>`
: '';
const progressHTML = showProgress
? (this.config.progressDirection === 'vertical'
? `<div class="met-progress-v"><div class="met-bar-v" style="background:${c.fg}"></div></div>`
: `<div class="met-progress"><div class="met-bar" style="background:${c.fg}"></div></div>`)
: '';
const sideHTML = showSide
? `<div class="met-side" style="background:${c.fg}"></div>` : '';
el.innerHTML = `
${iconHTML}
${sideHTML}
<div class="met-content">
${safeTitle}
${safeMessage}
</div>
${closeHTML}
${progressHTML}
`;
this.barEl = el.querySelector('.met-bar, .met-bar-v');
}
_bindEvents(el: HTMLElement): void {
const eventHandler = (e: Event) => {
const target = e.target as HTMLElement;
if (target.closest('.met-close')) {
e.stopPropagation();
this.close();
return;
}
Toast.trigger('click', this);
if (this.config.closeOnClick && !target.closest('.met-close')) {
if (typeof this.config.onClick === 'function') {
try {
this.config.onClick(this);
} catch (err) {
console.error('onClick callback error:', err);
}
}
this.close();
return;
}
};
el.addEventListener('click', eventHandler);
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
const mouseEnter = () => {
Toast.trigger('hover', this);
this._pause();
};
const mouseLeave = () => {
Toast.trigger('hover', this);
this._resume();
};
el.addEventListener('mouseenter', mouseEnter);
el.addEventListener('mouseleave', mouseLeave);
this._cleanups.push(() => {
el.removeEventListener('mouseenter', mouseEnter);
el.removeEventListener('mouseleave', mouseLeave);
});
}
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
const animStart = () => Toast.trigger('animationStart', this);
const animEnd = () => Toast.trigger('animationEnd', this);
el.addEventListener('animationstart', animStart);
el.addEventListener('animationend', animEnd);
this._cleanups.push(() => {
el.removeEventListener('animationstart', animStart);
el.removeEventListener('animationend', animEnd);
});
if (this.config.draggable) {
this._bindDrag(el);
}
}
_bindDrag(el: HTMLElement): void {
let sx = 0, sy = 0, dx = 0, dy = 0, dragging = false;
const down = (e: PointerEvent) => {
if ((e.target as HTMLElement).closest('.met-close')) return;
dragging = true;
sx = e.clientX;
sy = e.clientY;
el.setPointerCapture(e.pointerId);
el.style.transition = 'none';
this._pause();
Toast.trigger('dragStart', this);
};
const move = (e: PointerEvent) => {
if (!dragging) return;
dx = e.clientX - sx;
dy = e.clientY - sy;
el.style.transform = `translate(${dx}px, ${dy}px) rotate(${dx * 0.1}deg)`;
el.style.opacity = String(Math.max(0, 1 - Math.abs(dx) / 200));
};
const up = (e: PointerEvent) => {
if (!dragging) return;
dragging = false;
el.releasePointerCapture(e.pointerId);
el.style.transition = '';
const threshold = this.config.dragThreshold ?? 120;
if (Math.abs(dx) > threshold) {
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
el.style.opacity = '0';
setTimeout(() => this.close(true), 250);
} else {
el.style.transform = '';
el.style.opacity = '';
this._resume();
}
dx = dy = 0;
Toast.trigger('dragEnd', this);
};
el.addEventListener('pointerdown', down);
el.addEventListener('pointermove', move);
el.addEventListener('pointerup', up);
el.addEventListener('pointercancel', up);
this._cleanups.push(() => {
el.removeEventListener('pointerdown', down);
el.removeEventListener('pointermove', move);
el.removeEventListener('pointerup', up);
el.removeEventListener('pointercancel', up);
});
}
_startTimer(resuming = false): void {
if ((this.config.duration || 0) <= 0) return;
if (!resuming) {
this.startedAt = Date.now();
this.remaining = this.config.duration || 0;
Toast.trigger('progressStart', this);
}
const tick = (): void => {
if (this.paused || this.closing) return;
try {
const elapsed = Date.now() - this.startedAt;
this.remaining = Math.max(0, (this.config.duration || 0) - elapsed);
if (this.barEl) {
const ratio = this.remaining / (this.config.duration || 1);
const t = this.config.progressDirection === 'vertical'
? `scaleY(${ratio})` : `scaleX(${ratio})`;
this.barEl.style.transform = t;
}
if (this.remaining <= 0) {
Toast.trigger('progressEnd', this);
this.close();
return;
}
} catch (e) {
console.error('Timer tick error:', e);
if (Toast._onError) {
try { Toast._onError({ source: 'timer', error: e as Error, toast: this }); } catch (_e) { /* noop */ }
}
}
this.rafId = requestAnimationFrame(tick);
};
this.rafId = requestAnimationFrame(tick);
}
_pause(): void {
if (this.paused || (this.config.duration || 0) <= 0) return;
this.paused = true;
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
this.remaining = Math.max(0, (this.config.duration || 0) - (Date.now() - this.startedAt));
}
_resume(): void {
if (!this.paused) return;
this.paused = false;
this.startedAt = Date.now() - ((this.config.duration || 0) - this.remaining);
this._startTimer(true);
}
update(partial: Partial<ToastOptions>): this {
// beforeUpdate 钩子 — 返回 false 可阻止更新
if (!Toast.trigger('beforeUpdate', this)) return this;
const typeChanged = partial.type && partial.type !== this.type;
if (partial.type) this.type = partial.type;
if (partial.title !== undefined) this.title = partial.title;
if (partial.message !== undefined) this.message = partial.message;
if (partial.html !== undefined) this.html = partial.html;
if (!this.el) { Toast.trigger('afterUpdate', this); return this; }
// 类型变更时同步更新 DOM 类名、边框颜色和进度条颜色
if (typeChanged) {
const typeClasses = ['met-success', 'met-error', 'met-warning', 'met-info', 'met-loading', 'met-default'];
typeClasses.forEach(c => this.el!.classList.remove(c));
this.el.classList.add(`met-${this.type}`);
if (this.barEl) {
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
this.barEl.style.background = c.fg;
}
const side = this.el.querySelector('.met-side') as HTMLElement | null;
if (side) {
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
side.style.background = c.fg;
}
}
const content = this.el.querySelector('.met-content');
if (content) {
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
const safeMessage = this.html
? `<div class="met-message">${this.html}</div>`
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
content.innerHTML = `${safeTitle}${safeMessage}`;
}
// duration 变更:重启或停止计时器(loading 0 → N 转换、动态调整时长等场景)
if (partial.duration !== undefined && partial.duration !== this.config.duration) {
this.config.duration = partial.duration;
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
this.rafId = null;
this.remaining = Math.max(0, partial.duration);
if (partial.duration > 0 && !this.paused) {
this.startedAt = Date.now();
this._startTimer();
}
}
// resetTimerOnUpdate: 更新内容后重置计时器
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
this.startedAt = Date.now();
this.remaining = this.config.duration || 0;
this._startTimer();
}
if (typeof this.config.onUpdate === 'function') {
try { this.config.onUpdate(this); } catch (e) { console.error('onUpdate callback error:', e); }
}
Toast.trigger('afterUpdate', this);
return this;
}
/**
* Toast
*/
updatePosition(position: string): this {
if (!position || !this.el) return this;
const oldContainer = this.el.parentNode;
this.config.position = position as ToastConfig['position'];
const newContainer = this._getContainer();
if (oldContainer === newContainer) return this;
// 从旧容器移除,加入新容器
if (oldContainer && this.el.parentNode === oldContainer) {
oldContainer.removeChild(this.el);
}
const pos = position;
if (pos.startsWith('top')) {
newContainer.insertBefore(this.el, newContainer.firstChild);
} else {
newContainer.appendChild(this.el);
}
return this;
}
/**
* DOM
*/
remove(): void {
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } });
this._cleanups = [];
if (this.el && this.el.parentNode) {
this.el.parentNode.removeChild(this.el);
}
this.el = null;
Toast._registry.delete(this.id);
if (Toast._removeToast) Toast._removeToast(this.id);
}
close(immediate = false): void {
if (this.closing) return;
// beforeClose 钩子 — 返回 false 可阻止关闭
if (!Toast.trigger('beforeClose', this)) return;
this.closing = true;
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } });
this._cleanups = [];
const el = this.el;
if (!el) {
if (Toast._removeToast) Toast._removeToast(this.id);
return;
}
// 立即脱离文档流
const container = el.parentNode;
if (container && !immediate) {
const elRect = el.getBoundingClientRect();
const containerRect = (container as HTMLElement).getBoundingClientRect();
el.style.position = 'absolute';
el.style.top = (elRect.top - containerRect.top) + 'px';
el.style.left = (elRect.left - containerRect.left) + 'px';
el.style.width = elRect.width + 'px';
el.style.margin = '0';
}
el.classList.add('met-leaving');
el.classList.remove('met-show');
const pos = this.config.position || 'top-right';
const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl';
let transform = 'scale(.96)';
if (pos.includes('right')) transform = isRTL ? 'translateX(-120%)' : 'translateX(120%)';
else if (pos.includes('left')) transform = isRTL ? 'translateX(120%)' : 'translateX(-120%)';
else if (pos.startsWith('top')) transform = 'translateY(-20px)';
else transform = 'translateY(20px)';
el.style.transform = transform;
el.style.opacity = '0';
setTimeout(() => this._destroy(), immediate ? 0 : 300);
}
_destroy(): void {
if (this.el && this.el.parentNode) {
this.el.parentNode.removeChild(this.el);
}
this.el = null;
if (typeof this.config.onClose === 'function') {
try {
this.config.onClose(this);
} catch (e) {
console.error('onClose callback error:', e);
}
}
Toast.trigger('afterClose', this);
Toast._registry.delete(this.id);
if (Toast._removeToast) Toast._removeToast(this.id);
}
}
/**
* Toast
*/
export const _containerCache: Map<Node, Map<string, HTMLElement>> = new Map();
+555
View File
@@ -0,0 +1,555 @@
/**
* MetonaToast
* @module types
* @version 0.5.0
*/
// ========== 基础类型 ==========
/** Toast 通知类型(107种内置图标类型) */
export type ToastType =
| 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading'
| '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';
/** Toast 显示位置 */
export type ToastPosition =
| 'top-left' | 'top-center' | 'top-right'
| 'bottom-left' | 'bottom-center' | 'bottom-right';
/** Toast 主题 */
export type ToastTheme = 'light' | 'dark' | 'auto' | 'warm' | (string & {});
/** Toast 动画类型 */
export type ToastAnimation =
| 'slide' | 'fade' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom'
| 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight'
| (string & {});
/** 进度条方向 */
export type ToastProgressDirection = 'horizontal' | 'vertical';
/** 语言代码 */
export type ToastLocale =
| 'zh-CN' | 'zh-TW' | 'en-US' | 'en-GB' | 'ja' | 'ko' | 'fr' | 'de'
| 'es' | 'pt' | 'ru' | 'ar' | 'hi' | 'th' | 'vi' | 'id' | 'ms' | 'tr'
| 'it' | 'nl' | 'pl' | 'uk' | 'cs' | 'sv' | 'da' | 'fi' | 'nb' | 'el'
| 'he' | 'hu' | 'ro' | 'bg' | 'hr' | 'sk' | 'sl' | 'et' | 'lv' | 'lt'
| 'ca' | 'gl' | 'eu' | 'cy' | 'ga' | 'mt' | 'is' | 'mk' | 'sq' | 'sr'
| 'bs' | 'me' | 'ka' | 'hy' | 'az' | 'uz' | 'kk' | 'ky' | 'tg' | 'tk'
| 'mn' | 'ne' | 'si' | 'my' | 'km' | 'lo' | 'am' | 'sw' | 'yo' | 'ig'
| 'ha' | 'zu' | 'af' | 'xh' | 'st' | 'tn' | 'ts' | 'ss' | 've' | 'nr';
// ========== 颜色配置 ==========
export interface TypeColor {
fg: string;
bg: string;
}
// ========== Toast 配置 ==========
export interface ToastConfig {
position?: ToastPosition;
duration?: number;
max?: number;
gap?: number;
offset?: number;
pauseOnHover?: boolean;
closeOnClick?: boolean;
draggable?: boolean;
dragThreshold?: number;
showProgress?: boolean;
progressDirection?: ToastProgressDirection;
icon?: boolean;
closeButton?: boolean;
theme?: ToastTheme;
animation?: ToastAnimation;
zIndex?: number;
width?: number | string;
className?: string;
style?: Record<string, string>;
onShow?: ((toast: ToastInstance) => void) | null;
onBeforeShow?: ((toast: ToastInstance) => boolean | void) | null;
onClose?: ((toast: ToastInstance) => void) | null;
onClick?: ((toast: ToastInstance) => void) | null;
onUpdate?: ((toast: ToastInstance) => void) | null;
render?: ((toast: ToastInstance) => string) | null;
resetTimerOnUpdate?: boolean;
notifyWhenHidden?: boolean;
onError?: ((errorInfo: ErrorInfo) => void) | null;
group?: string | null;
locale?: string;
plugins?: Array<string | Plugin>;
}
export interface ErrorInfo {
hook?: string;
source?: string;
error: Error;
toast?: ToastInstance;
}
// ========== Toast 选项与实例 ==========
export interface ToastOptions extends Partial<ToastConfig> {
type?: string;
title?: string;
message?: string;
content?: string;
html?: string;
iconHTML?: string;
id?: string;
}
export interface ToastInstance {
id: string;
type: string;
title: string;
message: string;
html: string;
iconHTML: string;
config: ToastConfig;
el: HTMLElement | null;
barEl: HTMLElement | null;
rafId: number | null;
remaining: number;
startedAt: number;
paused: boolean;
closing: boolean;
group: string | null;
_cleanups: Array<() => void>;
_palette(): { theme: string; c: TypeColor; t: Partial<ThemeConfig> };
create(): this;
update(partial: Partial<ToastOptions>): this;
updatePosition(position: string): this;
remove(): void;
close(immediate?: boolean): void;
_pause(): void;
_resume(): void;
_destroy(): void;
}
// ========== 控制对象 ==========
export interface LoadingControl {
id: string;
success(message?: string, opts?: ToastOptions): ToastInstance | null;
error(message?: string, opts?: ToastOptions): ToastInstance | null;
info(message?: string, opts?: ToastOptions): ToastInstance | null;
warning(message?: string, opts?: ToastOptions): ToastInstance | null;
update(partial: Partial<ToastOptions>): unknown;
dismiss(): void;
}
export interface ProgressControl {
id: string;
setProgress(percent: number): void;
complete(message?: string): void;
error(message?: string): void;
dismiss(): void;
}
export interface CountdownControl {
id: string;
cancel(): void;
pause(): void;
resume(): void;
}
export interface ActionControl {
id: string;
toast: ToastInstance;
dismiss(): void;
}
export interface QueueControl {
then<TResult1 = void, TResult2 = never>(
onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2>;
catch<TResult = never>(
onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
): Promise<void | TResult>;
cancel(): void;
}
export interface GroupAPI {
_group: string;
show: (...args: unknown[]) => ToastInstance;
success: (...args: unknown[]) => ToastInstance;
error: (...args: unknown[]) => ToastInstance;
warning: (...args: unknown[]) => ToastInstance;
info: (...args: unknown[]) => ToastInstance;
loading: (...args: unknown[]) => LoadingControl;
action: (...args: unknown[]) => ActionControl;
dismiss(): void;
count(): number;
}
// ========== Action 按钮 ==========
export interface ActionButton {
text: string;
onClick: (toast: ToastInstance) => void;
color?: string;
style?: Record<string, string>;
close?: boolean;
}
// ========== Options 接口 ==========
export interface PromiseOptions extends ToastOptions {
loading?: string;
success?: string;
error?: string;
}
export interface ConfirmOptions extends ToastOptions {
confirmText?: string;
confirmColor?: string;
cancelText?: string;
cancelColor?: string;
}
export interface PromptOptions extends ToastOptions {
inputType?: string;
placeholder?: string;
defaultValue?: string;
submitText?: string;
submitColor?: string;
cancelText?: string;
cancelColor?: string;
}
export interface ProgressOptions extends ToastOptions {
progressColor?: string;
}
export interface CountdownOptions extends ToastOptions {
onComplete?: (() => void) | null;
}
export interface QueueOptions extends ToastOptions {
delay?: number;
}
export interface StackOptions extends ToastOptions {
stagger?: number;
}
export interface InitOptions {
config?: Partial<ToastConfig>;
theme?: ToastTheme;
locale?: string;
plugins?: Array<string | Plugin>;
}
export interface StatusInfo {
version: string;
toasts: number;
theme: string;
locale: string;
plugins: string[];
animations: number;
}
// ========== 动画 ==========
export interface AnimationConfig {
enter: Record<string, string | number>;
leave: Record<string, string | number>;
duration: number;
easing: string;
name?: string;
delay?: number;
iterations?: number;
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
}
export interface AnimationUtils {
register(name: string, config: Partial<AnimationConfig>): void;
unregister(name: string): void;
get(name: string): AnimationConfig | null;
getAnimationNames(): string[];
getActiveCount(): number;
cancelAll(): void;
reset(): void;
destroy(): void;
}
// ========== 主题 ==========
export interface ThemeConfig {
bg: string;
text: string;
border: string;
shadow: string;
hoverShadow: string;
progressBg: string;
closeHoverBg: string;
backdropFilter?: string;
}
export interface ThemePreview {
name: string;
resolved: string;
colors: {
background: string;
text: string;
border: string;
};
isDark: boolean;
isLight: boolean;
isAuto: boolean;
}
export interface ThemeUtils {
getSystemTheme(): 'light' | 'dark';
resolveTheme(theme: string): string;
getThemeConfig(theme: string): ThemeConfig;
applyTheme(theme: string): void;
getCurrentTheme(): string;
getResolvedTheme(): string;
switchTheme(theme: string): void;
toggleTheme(): void;
resetToAuto(): void;
initTheme(): void;
watchSystemTheme(): void;
unwatchSystemTheme(): void;
addThemeListener(listener: (theme: string, resolved: string) => void): () => void;
removeThemeListener(listener: (theme: string, resolved: string) => void): void;
clearThemeListeners(): void;
registerTheme(name: string, config: ThemeConfig): void;
unregisterTheme(name: string): void;
getAllThemes(): Record<string, ThemeConfig>;
getThemeNames(): string[];
hasTheme(name: string): boolean;
getThemePreview(theme: string): ThemePreview;
generateThemeCSS(theme: string): string;
applyThemeCSS(theme: string): void;
removeThemeCSS(): void;
saveTheme(theme: string): void;
loadTheme(): string;
}
// ========== 国际化 ==========
export interface LocaleInfo {
code: string;
name: string;
direction: 'ltr' | 'rtl';
isSupported: boolean;
isCurrent: boolean;
isFallback: boolean;
}
export interface I18nUtils {
t(key: string, params?: Record<string, string | number>): string;
plural(key: string, count: number, params?: Record<string, string | number>): string;
getCurrentLocale(): string;
setCurrentLocale(locale: string): void;
switchLocale(locale: string): void;
getFallbackLocale(): string;
setFallbackLocale(locale: string): void;
hasTranslation(key: string): boolean;
getTranslations(locale: string): Record<string, unknown>;
addTranslations(locale: string, translations: Record<string, unknown>): void;
removeTranslation(locale: string, key: string): void;
clearTranslations(locale: string): void;
getSupportedLocales(): string[];
isLocaleSupported(locale: string): boolean;
getLocaleName(locale: string): string;
getLocaleDirection(locale: string): 'ltr' | 'rtl';
getLocaleInfo(locale: string): LocaleInfo;
getAllLocaleInfo(): LocaleInfo[];
formatNumber(number: number, options?: Intl.NumberFormatOptions): string;
formatCurrency(amount: number, currency?: string, options?: Intl.NumberFormatOptions): string;
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
formatDate(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
formatTime(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
formatRelativeTime(date: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
formatList(list: string[], options?: Record<string, unknown>): string;
formatPlural(count: number, options?: Intl.PluralRulesOptions): string;
addLocaleListener(listener: (locale: string) => void): () => void;
removeLocaleListener(listener: (locale: string) => void): void;
clearLocaleListeners(): void;
initI18n(): void;
saveLocale(locale: string): void;
loadLocale(): string;
getDefaultLocale(): string;
}
// ========== 插件 ==========
export interface Plugin {
name: string;
version?: string;
description?: string;
hooks?: Record<string, (...args: unknown[]) => void>;
middleware?: (...args: unknown[]) => unknown;
install?: (manager: PluginManager) => void | Record<string, unknown> | null;
uninstall?: (manager: PluginManager) => void;
init?: (manager: PluginManager) => void;
destroy?: (manager: PluginManager) => void;
installed?: boolean;
enabled?: boolean;
[key: string]: unknown;
}
export interface PluginManager {
register(name: string, plugin: Plugin): PluginManager;
unregister(name: string): PluginManager;
get(name: string): Plugin | null;
has(name: string): boolean;
getAll(): Plugin[];
getNames(): string[];
enable(name: string): PluginManager;
disable(name: string): PluginManager;
isEnabled(name: string): boolean;
destroy(): void;
}
export interface PluginUtils {
createManager(): PluginManager;
register(name: string, plugin: Plugin): PluginManager;
unregister(name: string): PluginManager;
get(name: string): Plugin | null;
has(name: string): boolean;
getAll(): Plugin[];
getNames(): string[];
enable(name: string): PluginManager;
disable(name: string): PluginManager;
isEnabled(name: string): boolean;
getPreset(name: string): Plugin | null;
getAllPresets(): Record<string, Plugin>;
createPlugin(config: Partial<Plugin>): Plugin;
validatePlugin(plugin: Partial<Plugin>): { valid: boolean; errors: string[] };
}
// ========== MeToast 主接口 ==========
export interface MeToast {
version: string;
_toasts: Map<string, ToastInstance>;
_config: ToastConfig;
_destroyed?: boolean;
// 内部方法
_emit(opts: ToastOptions): ToastInstance;
_remove(id: string): void;
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
_groupCount(name: string): number;
// 配置
configure(opts: Partial<ToastConfig>): MeToast;
init(options?: InitOptions): MeToast;
destroy(): void;
getStatus(): StatusInfo;
getConfig(): ToastConfig;
updateConfig(config: Partial<ToastConfig>): MeToast;
resetConfig(): MeToast;
// Toast 方法
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
// 分组
group(name: string): GroupAPI;
dismissGroup(name: string): void;
// Toast 管理
find(id: string): ToastInstance | undefined;
dismiss(id?: string): void;
removeToast(id: string): void;
clear(position?: ToastPosition): void;
getToasts(): ToastInstance[];
count(): number;
hasToasts(): boolean;
getToast(id: string): ToastInstance | null;
closeAll(): void;
clearAll(): void;
pauseAll(): void;
resumeAll(): void;
updateAll(partial: Partial<ToastOptions>): void;
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
findByType(type: ToastType): ToastInstance[];
findByPosition(position: ToastPosition): ToastInstance[];
getAll(): Map<string, ToastInstance>;
// 插件
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
// 子模块
animations: AnimationUtils;
themes: ThemeUtils;
i18n: I18nUtils;
plugins: PluginUtils;
presetPlugins: Record<string, Plugin>;
}
// ========== 钩子名称 ==========
export const HOOK_NAMES = {
BEFORE_INIT: 'beforeInit',
AFTER_INIT: 'afterInit',
BEFORE_DESTROY: 'beforeDestroy',
AFTER_DESTROY: 'afterDestroy',
BEFORE_SHOW: 'beforeShow',
AFTER_SHOW: 'afterShow',
BEFORE_CLOSE: 'beforeClose',
AFTER_CLOSE: 'afterClose',
BEFORE_UPDATE: 'beforeUpdate',
AFTER_UPDATE: 'afterUpdate',
CONFIG_CHANGE: 'configChange',
THEME_CHANGE: 'themeChange',
LOCALE_CHANGE: 'localeChange',
CLICK: 'click',
HOVER: 'hover',
DRAG_START: 'dragStart',
DRAG_END: 'dragEnd',
ANIMATION_START: 'animationStart',
ANIMATION_END: 'animationEnd',
PROGRESS_START: 'progressStart',
PROGRESS_END: 'progressEnd',
ERROR: 'error',
} as const;
export type HookName = (typeof HOOK_NAMES)[keyof typeof HOOK_NAMES];
// ========== 全局声明 ==========
declare global {
interface Window {
MeToast: MeToast;
Met: MeToast;
}
}
+8 -12
View File
@@ -1,24 +1,21 @@
/**
* MetonaToast Utils -
* MetonaToast Utils
* @module utils
* @version 2.0.1
* @description 使
* @version 0.5.0
*/
/**
* ID
* @returns {string} ID
*/
export const generateId = () => {
export const generateId = (): string => {
return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
};
/**
* HTML转义
* @param {string} s -
* @returns {string}
* @param s -
*/
export const escapeHTML = (s) => {
export const escapeHTML = (s: string | null | undefined): string => {
if (typeof document === 'undefined') {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
@@ -35,10 +32,9 @@ export const escapeHTML = (s) => {
/**
*
* @returns {boolean}
*/
export const prefersDark = () => {
export const prefersDark = (): boolean => {
return typeof window !== 'undefined' &&
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
typeof window.matchMedia === 'function' &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
};
File diff suppressed because it is too large Load Diff
+369
View File
@@ -0,0 +1,369 @@
/**
* MetonaToast DOM jsdom DOM
* @module tests
* @version 0.5.0
*/
import MeToast, { Toast } from '../src/index';
import type { ToastInstance } from '../src/types';
// jsdom polyfill: PointerEvent 相关方法
if (typeof Element !== 'undefined') {
if (!(Element.prototype as unknown as { setPointerCapture?: unknown }).setPointerCapture) {
(Element.prototype as unknown as Record<string, unknown>).setPointerCapture = jest.fn();
}
if (!(Element.prototype as unknown as { releasePointerCapture?: unknown }).releasePointerCapture) {
(Element.prototype as unknown as Record<string, unknown>).releasePointerCapture = jest.fn();
}
}
const pointer = (type: string, x: number, y: number): PointerEvent =>
new MouseEvent(type, { clientX: x, clientY: y, bubbles: true }) as unknown as PointerEvent;
beforeEach(() => {
jest.clearAllMocks();
MeToast._toasts.clear();
MeToast.resetConfig();
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
Toast._hooks.clear();
Toast._registry.clear();
const { _containerCache } = require('../src/toast.js');
_containerCache.clear();
MeToast.animations.destroy();
MeToast.animations.reset();
});
describe('toast.ts — DOM 交互', () => {
test('onShow 回调异常被捕获', () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
MeToast.success('x', { onShow: () => { throw new Error('show-err'); } });
expect(consoleError).toHaveBeenCalled();
consoleError.mockRestore();
});
test('onUpdate 回调异常被捕获', () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
const t = MeToast.success('x', { onUpdate: () => { throw new Error('upd-err'); } });
t.update({ message: 'y' });
expect(consoleError).toHaveBeenCalled();
consoleError.mockRestore();
});
test('_applyStyles 字符串 width 生效', () => {
const t = MeToast.info({ message: 'w', width: '80%' });
expect(t.el!.style.width).toBe('80%');
});
test('点击 close 按钮关闭 toast 且不触发 click 钩子', () => {
const fn = jest.fn();
Toast.on('click', fn);
const t = MeToast.info('test');
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
closeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
expect(t.closing).toBe(true);
expect(fn).not.toHaveBeenCalled();
Toast.off('click', fn);
});
test('点击主体触发 onClick 并关闭', () => {
const onClick = jest.fn();
const t = MeToast.info('test', { onClick });
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
expect(onClick).toHaveBeenCalledWith(t);
expect(t.closing).toBe(true);
});
test('onClick 回调异常被捕获', () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
const t = MeToast.info('test', { onClick: () => { throw new Error('click-err'); } });
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
expect(consoleError).toHaveBeenCalled();
consoleError.mockRestore();
});
test('closeOnClick false 时点击不关闭但触发 click 钩子', () => {
const fn = jest.fn();
Toast.on('click', fn);
const t = MeToast.info('test', { closeOnClick: false });
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
expect(t.closing).toBe(false);
expect(fn).toHaveBeenCalled();
Toast.off('click', fn);
});
test('hover 触发暂停/恢复与 hover 钩子', () => {
const fn = jest.fn();
Toast.on('hover', fn);
const t = MeToast.info({ message: 'h', duration: 5000 });
t.el!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
expect(t.paused).toBe(true);
t.el!.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
expect(t.paused).toBe(false);
expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2);
Toast.off('hover', fn);
});
test('animationstart/animationend 触发钩子', () => {
const start = jest.fn();
const end = jest.fn();
Toast.on('animationStart', start);
Toast.on('animationEnd', end);
const t = MeToast.info('a');
t.el!.dispatchEvent(new Event('animationstart'));
t.el!.dispatchEvent(new Event('animationend'));
expect(start).toHaveBeenCalled();
expect(end).toHaveBeenCalled();
Toast.off('animationStart', start);
Toast.off('animationEnd', end);
});
test('拖拽 down→move→up 超过阈值触发关闭', (done) => {
const t = MeToast.info('drag', { duration: 5000 });
const el = t.el!;
el.dispatchEvent(pointer('pointerdown', 100, 50));
el.dispatchEvent(pointer('pointermove', 250, 60));
expect(el.style.transform).toContain('translate(150px');
expect(el.style.opacity).toBe('0.25');
el.dispatchEvent(pointer('pointerup', 250, 60));
expect(el.style.opacity).toBe('0');
setTimeout(() => {
expect(t.closing).toBe(true);
done();
}, 350);
});
test('拖拽未超阈值松手恢复', () => {
const t = MeToast.info('drag2', { duration: 5000 });
const el = t.el!;
el.dispatchEvent(pointer('pointerdown', 100, 50));
el.dispatchEvent(pointer('pointermove', 150, 60));
el.dispatchEvent(pointer('pointerup', 150, 60));
expect(t.closing).toBe(false);
expect(t.paused).toBe(false);
expect(el.style.transform).toBe('');
});
test('拖拽触发 dragStart/dragEnd 钩子', () => {
const start = jest.fn();
const end = jest.fn();
Toast.on('dragStart', start);
Toast.on('dragEnd', end);
const t = MeToast.info('d', { duration: 5000 });
const el = t.el!;
el.dispatchEvent(pointer('pointerdown', 0, 0));
el.dispatchEvent(pointer('pointerup', 0, 0));
expect(start).toHaveBeenCalled();
expect(end).toHaveBeenCalled();
Toast.off('dragStart', start);
Toast.off('dragEnd', end);
});
test('pointercancel 等价于 pointerup', () => {
const t = MeToast.info('pc', { duration: 5000 });
const el = t.el!;
el.dispatchEvent(pointer('pointerdown', 10, 10));
el.dispatchEvent(pointer('pointercancel', 10, 10));
expect(t.closing).toBe(false);
});
test('拖拽从 close 按钮按下时早退', () => {
const t = MeToast.info('dc', { duration: 5000 });
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
closeBtn.dispatchEvent(pointer('pointerdown', 0, 0));
expect(t.paused).toBe(false);
});
test('close 非 immediate 走离场动画并清理', (done) => {
const t = MeToast.info('anim-close');
t.close();
expect(t.el!.classList.contains('met-leaving')).toBe(true);
setTimeout(() => {
expect(t.el).toBeNull();
expect(MeToast.count()).toBe(0);
done();
}, 400);
});
test('updatePosition 移动到新容器', () => {
const t = MeToast.info('move', { position: 'top-left' });
t.updatePosition('bottom-right');
expect(t.config.position).toBe('bottom-right');
expect(t.el!.parentElement!.className).toContain('bottom-right');
});
test('updatePosition 同位置直接返回', () => {
const t = MeToast.info('same');
const container = t.el!.parentElement;
t.updatePosition('top-right');
expect(t.el!.parentElement).toBe(container);
});
test('_limitToasts registry 无实例时走 _removeToast 兜底', () => {
const el = document.createElement('div');
const ghost = document.createElement('div');
ghost.dataset.id = 'ghost-id';
(el.querySelectorAll as unknown) = () => [ghost];
const t = new Toast({ message: 'x', max: 0 });
expect(() => t._limitToasts(el)).not.toThrow();
});
test('remove() 清理注册表与内存', () => {
const t = MeToast.info('rm');
expect(Toast._registry.has(t.id)).toBe(true);
t.remove();
expect(Toast._registry.has(t.id)).toBe(false);
expect(MeToast.count()).toBe(0);
});
});
describe('api.ts — 对话框与按钮交互', () => {
test('confirm 点击确认按钮 resolve true', async () => {
const promise = MeToast.confirm('确定删除?');
await new Promise(r => setTimeout(r, 10));
const toast = MeToast.getToasts()[0];
(toast.el!.querySelector('.met-confirm-btn') as HTMLElement).click();
const result = await promise;
expect(result).toBe(true);
});
test('confirm 点击取消按钮 resolve false', async () => {
const promise = MeToast.confirm('确定删除?');
await new Promise(r => setTimeout(r, 10));
const toast = MeToast.getToasts()[0];
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
const result = await promise;
expect(result).toBe(false);
});
test('prompt Enter 提交输入值', async () => {
const promise = MeToast.prompt('请输入姓名');
await new Promise(r => setTimeout(r, 10));
const toast = MeToast.getToasts()[0];
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
input.value = '张三';
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
const result = await promise;
expect(result).toBe('张三');
});
test('prompt 点击提交按钮', async () => {
const promise = MeToast.prompt('请输入姓名');
await new Promise(r => setTimeout(r, 10));
const toast = MeToast.getToasts()[0];
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
input.value = '李四';
(toast.el!.querySelector('.met-submit-btn') as HTMLElement).click();
const result = await promise;
expect(result).toBe('李四');
});
test('prompt 点击取消按钮 resolve null', async () => {
const promise = MeToast.prompt('请输入姓名');
await new Promise(r => setTimeout(r, 10));
const toast = MeToast.getToasts()[0];
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
const result = await promise;
expect(result).toBeNull();
});
test('action 按钮点击触发回调并关闭', async () => {
const onClick = jest.fn();
const a = MeToast.action('msg', [{ text: 'OK', onClick }]);
await new Promise(r => setTimeout(r, 10));
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
expect(onClick).toHaveBeenCalledWith(a.toast);
expect(a.toast.closing).toBe(true);
});
test('action 按钮 close:false 点击不关闭', async () => {
const onClick = jest.fn();
const a = MeToast.action('msg', [{ text: 'Keep', onClick, close: false }]);
await new Promise(r => setTimeout(r, 10));
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
expect(a.toast.closing).toBe(false);
});
test('action 按钮 onClick 异常被捕获', async () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
const a = MeToast.action('msg', [{ text: 'Boom', onClick: () => { throw new Error('act-err'); } }]);
await new Promise(r => setTimeout(r, 10));
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
expect(consoleError).toHaveBeenCalled();
consoleError.mockRestore();
});
test('countdown onComplete 异常被捕获', (done) => {
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
MeToast.countdown('{seconds} 秒', 1, { onComplete: () => { throw new Error('cd-err'); } });
setTimeout(() => {
expect(consoleError).toHaveBeenCalled();
consoleError.mockRestore();
done();
}, 1600);
});
test('loading control 支持 update 与 dismiss', () => {
const l = MeToast.loading('l1');
l.update({ message: 'l2' });
const toast = MeToast.find(l.id);
expect(toast!.message).toBe('l2');
l.dismiss();
expect(toast!.closing).toBe(true);
});
test('group 单对象参数分支', () => {
const g = MeToast.group('obj-group');
const t = g.show({ message: 'obj-arg' });
expect(t.group).toBe('obj-group');
expect(t.message).toBe('obj-arg');
g.dismiss();
});
test('queue 消息级 onClose 异常被静默捕获', async () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
const q = MeToast.queue([
{ message: 'a', onClose: () => { throw new Error('q-err'); } },
], { delay: 5, duration: 10 });
await q;
expect(consoleError).not.toHaveBeenCalled();
consoleError.mockRestore();
});
test('_remove 从内存移除指定 id', () => {
const t = MeToast.info('x');
MeToast._remove(t.id);
expect(MeToast.count()).toBe(0);
});
});
describe('plugins.ts — 异常与钩子管理', () => {
test('插件 install 抛错被捕获', () => {
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
MeToast.plugins.register('bad-install', { name: 'bad-install', install: () => { throw new Error('install-err'); } });
expect(consoleError).toHaveBeenCalled();
consoleError.mockRestore();
});
test('keyboard 插件 install/uninstall 移除事件监听', () => {
MeToast.use('keyboard');
MeToast.plugins.unregister('keyboard');
expect(MeToast.plugins.has('keyboard')).toBe(false);
});
test('persistence 插件 save 异常被捕获', () => {
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
const pluginUtils = require('../src/plugins.js').pluginUtils;
const preset = pluginUtils.getPreset('persistence');
const saveFn = (preset as Record<string, (c: unknown) => void>).save;
expect(() => saveFn({ duration: 3000 })).not.toThrow();
spy.mockRestore();
});
test('persistence 插件 uninstall removeItem 异常被捕获', () => {
const spy = jest.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => { throw new Error('quota'); });
const pluginUtils = require('../src/plugins.js').pluginUtils;
const preset = pluginUtils.getPreset('persistence');
expect(() => preset?.uninstall?.()).not.toThrow();
spy.mockRestore();
});
});
File diff suppressed because it is too large Load Diff
+149
View File
@@ -0,0 +1,149 @@
/**
* MetonaToast React
* @module tests
* @version 0.5.0
*/
import React from 'react';
import { renderToString } from 'react-dom/server';
import { createRoot } from 'react-dom/client';
import { act } from 'react';
import MeToast, { Toast } from '../src/index';
import { createBoundApi, Toast as ToastComponent, useToast } from '../src/react';
import type { ToastInstance } from '../src/types';
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
beforeEach(() => {
jest.clearAllMocks();
MeToast._toasts.clear();
MeToast.resetConfig();
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
Toast._hooks.clear();
Toast._registry.clear();
const { _containerCache } = require('../src/toast.js');
_containerCache.clear();
MeToast.animations.reset();
});
describe('React 适配器 — createBoundApi', () => {
test('跟踪创建的 toast id', () => {
const cleanup = new Set<string>();
const api = createBoundApi(cleanup);
(api.info as (m: string) => ToastInstance)('hello');
expect(cleanup.size).toBe(1);
});
test('success/error/warning/show 均被跟踪', () => {
const cleanup = new Set<string>();
const api = createBoundApi(cleanup);
(api.success as (m: string) => ToastInstance)('a');
(api.error as (m: string) => ToastInstance)('b');
(api.warning as (m: string) => ToastInstance)('c');
(api.show as (m: string) => ToastInstance)('d');
expect(cleanup.size).toBe(4);
});
test('loading 控制对象也被跟踪', () => {
const cleanup = new Set<string>();
const api = createBoundApi(cleanup);
(api.loading as (m: string) => { id: string })('loading...');
expect(cleanup.size).toBe(1);
});
test('dismiss 同步清理集合', () => {
const cleanup = new Set<string>();
const api = createBoundApi(cleanup);
const t = (api.success as (m: string) => ToastInstance)('hi');
expect(cleanup.has(t.id)).toBe(true);
(api.dismiss as (id?: string) => void)(t.id);
expect(cleanup.has(t.id)).toBe(false);
});
test('removeToast 同步清理集合', () => {
const cleanup = new Set<string>();
const api = createBoundApi(cleanup);
const t = (api.info as (m: string) => ToastInstance)('hi');
(api.removeToast as (id: string) => void)(t.id);
expect(cleanup.size).toBe(0);
});
test('未跟踪的方法直接透传', () => {
const cleanup = new Set<string>();
const api = createBoundApi(cleanup);
expect(typeof (api as Record<string, unknown>).count).toBe('function');
expect((api as { count(): number }).count()).toBe(0);
});
});
describe('React 适配器 — Toast 组件', () => {
test('SSR 渲染为空(effect 不执行)', () => {
const html = renderToString(React.createElement(ToastComponent, { message: 'ssr message' }));
expect(html).toBe('');
});
test('SSR 带 type 不抛异常', () => {
const html = renderToString(React.createElement(ToastComponent, { message: 'x', type: 'success' }));
expect(html).toBe('');
});
test('SSR 未知 type fallback 到 show 不抛异常', () => {
const html = renderToString(React.createElement(ToastComponent, { message: 'y', type: 'custom-type' }));
expect(html).toBe('');
});
test('组件挂载创建 toast,卸载自动移除', () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
act(() => { root.render(React.createElement(ToastComponent, { message: 'mounted', type: 'success' })); });
expect(MeToast.count()).toBe(1);
act(() => { root.unmount(); });
expect(MeToast.count()).toBe(0);
host.remove();
});
test('组件 autoClose:false 卸载不移除', () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
act(() => { root.render(React.createElement(ToastComponent, { message: 'keep', type: 'error', autoClose: false })); });
expect(MeToast.count()).toBe(1);
act(() => { root.unmount(); });
expect(MeToast.count()).toBe(1);
MeToast.dismiss();
host.remove();
});
test('组件 message 变化重新创建实例', () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
act(() => { root.render(React.createElement(ToastComponent, { message: 'v1', type: 'info' })); });
act(() => { root.render(React.createElement(ToastComponent, { message: 'v2', type: 'warning' })); });
expect(MeToast.count()).toBe(1);
const t = MeToast.getToasts()[0];
expect(t.message).toBe('v2');
expect(t.type).toBe('warning');
act(() => { root.unmount(); });
expect(MeToast.count()).toBe(0);
host.remove();
});
test('useToast 组件内创建的 toast 在卸载时清理', () => {
const Harness = (): React.ReactElement => {
const toast = useToast() as Record<string, (m: string) => ToastInstance>;
return React.createElement('button', { onClick: () => toast.info('tracked') });
};
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
act(() => { root.render(React.createElement(Harness)); });
const btn = host.querySelector('button')!;
act(() => { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
expect(MeToast.count()).toBe(1);
act(() => { root.unmount(); });
expect(MeToast.count()).toBe(0);
host.remove();
});
});
+13
View File
@@ -0,0 +1,13 @@
/**
* Jest jsdom Node
*/
import { TextEncoder, TextDecoder } from 'util';
const g = globalThis as Record<string, unknown>;
if (typeof g.TextEncoder === 'undefined') {
g.TextEncoder = TextEncoder;
}
if (typeof g.TextDecoder === 'undefined') {
g.TextDecoder = TextDecoder;
}
+58
View File
@@ -0,0 +1,58 @@
/**
* document/window/localStorage jest.resetModules()
* resetModules
* require import
* jsdom getter document/window defineProperty
* @module tests
* @version 0.5.0
*/
/** 临时移除全局对象,返回恢复函数 */
const hideGlobal = (key: string): (() => void) => {
const desc = Object.getOwnPropertyDescriptor(global, key);
Object.defineProperty(global, key, { value: undefined, configurable: true, writable: true });
return () => {
if (desc) Object.defineProperty(global, key, desc);
else delete (global as Record<string, unknown>)[key];
};
};
describe('环境隔离(SSR / localStorage 异常路径)', () => {
test('escapeHTML SSR 路径(无 document', () => {
const restore = hideGlobal('document');
jest.resetModules();
const { escapeHTML } = require('../src/utils.js');
const result = escapeHTML('<script>alert("xss")</script>');
expect(result).not.toContain('<script>');
expect(result).toContain('&lt;script&gt;');
restore();
});
test('prefersDark 无 window 时返回 false', () => {
const restore = hideGlobal('window');
jest.resetModules();
const { prefersDark } = require('../src/utils.js');
expect(prefersDark()).toBe(false);
restore();
});
test('saveTheme localStorage 异常被捕获', () => {
const orig = (global as Record<string, unknown>).localStorage;
(global as Record<string, unknown>).localStorage = undefined;
jest.resetModules();
const { saveTheme } = require('../src/themes.js');
expect(() => saveTheme('dark')).not.toThrow();
(global as Record<string, unknown>).localStorage = orig;
});
test('loadLocale localStorage 异常返回 default', () => {
const orig = (global as Record<string, unknown>).localStorage;
(global as Record<string, unknown>).localStorage = {
getItem: () => { throw new Error('quota'); },
};
jest.resetModules();
const { loadLocale } = require('../src/i18n.js');
expect(typeof loadLocale()).toBe('string');
(global as Record<string, unknown>).localStorage = orig;
});
});
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": false,
"declarationMap": false,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"baseUrl": ".",
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests", "site"]
}
-1090
View File
File diff suppressed because it is too large Load Diff